@dxos/plugin-thread 0.7.4 → 0.7.5-labs.5f04cf6

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 (197) hide show
  1. package/dist/lib/browser/ThreadContainer-4VLBSBI7.mjs +19 -0
  2. package/dist/lib/browser/ThreadContainer-4VLBSBI7.mjs.map +7 -0
  3. package/dist/lib/browser/app-graph-builder-Q7A2ZRUA.mjs +121 -0
  4. package/dist/lib/browser/app-graph-builder-Q7A2ZRUA.mjs.map +7 -0
  5. package/dist/lib/browser/chunk-2B3EXYZ7.mjs +18 -0
  6. package/dist/lib/browser/chunk-2B3EXYZ7.mjs.map +7 -0
  7. package/dist/lib/browser/{chunk-ENERBDYI.mjs → chunk-636QDHE3.mjs} +3 -3
  8. package/dist/lib/browser/chunk-636QDHE3.mjs.map +7 -0
  9. package/dist/lib/browser/chunk-BNEWNQBO.mjs +71 -0
  10. package/dist/lib/browser/chunk-BNEWNQBO.mjs.map +7 -0
  11. package/dist/lib/browser/{chunk-52HWDLBR.mjs → chunk-GQONSZ3J.mjs} +31 -117
  12. package/dist/lib/browser/chunk-GQONSZ3J.mjs.map +7 -0
  13. package/dist/lib/browser/chunk-NAO3LZMZ.mjs +16 -0
  14. package/dist/lib/browser/chunk-NAO3LZMZ.mjs.map +7 -0
  15. package/dist/lib/browser/chunk-NO5AZLUZ.mjs +81 -0
  16. package/dist/lib/browser/chunk-NO5AZLUZ.mjs.map +7 -0
  17. package/dist/lib/browser/index.mjs +147 -1094
  18. package/dist/lib/browser/index.mjs.map +4 -4
  19. package/dist/lib/browser/intent-resolver-PIEZ3LKQ.mjs +275 -0
  20. package/dist/lib/browser/intent-resolver-PIEZ3LKQ.mjs.map +7 -0
  21. package/dist/lib/browser/markdown-2NBXKXCY.mjs +112 -0
  22. package/dist/lib/browser/markdown-2NBXKXCY.mjs.map +7 -0
  23. package/dist/lib/browser/meta.json +1 -1
  24. package/dist/lib/browser/react-surface-CHCZO4TI.mjs +412 -0
  25. package/dist/lib/browser/react-surface-CHCZO4TI.mjs.map +7 -0
  26. package/dist/lib/browser/settings-TKOPRXSU.mjs +22 -0
  27. package/dist/lib/browser/settings-TKOPRXSU.mjs.map +7 -0
  28. package/dist/lib/browser/state-GNFUURIC.mjs +33 -0
  29. package/dist/lib/browser/state-GNFUURIC.mjs.map +7 -0
  30. package/dist/lib/browser/types.mjs +6 -4
  31. package/dist/lib/node/{ThreadArticle-I7ADXDW3.cjs → ThreadContainer-VOYLV4UL.cjs} +14 -13
  32. package/dist/lib/node/ThreadContainer-VOYLV4UL.cjs.map +7 -0
  33. package/dist/lib/node/app-graph-builder-ZXW5KUDK.cjs +135 -0
  34. package/dist/lib/node/app-graph-builder-ZXW5KUDK.cjs.map +7 -0
  35. package/dist/lib/node/{chunk-YCDM2SZM.cjs → chunk-6PUGKCPR.cjs} +76 -165
  36. package/dist/lib/node/chunk-6PUGKCPR.cjs.map +7 -0
  37. package/dist/lib/node/chunk-AUVKOX5Q.cjs +101 -0
  38. package/dist/lib/node/chunk-AUVKOX5Q.cjs.map +7 -0
  39. package/dist/lib/node/chunk-AWSTZ2KN.cjs +93 -0
  40. package/dist/lib/node/chunk-AWSTZ2KN.cjs.map +7 -0
  41. package/dist/lib/node/{chunk-IC2NICNC.cjs → chunk-F735VMTN.cjs} +7 -7
  42. package/dist/lib/node/chunk-F735VMTN.cjs.map +7 -0
  43. package/dist/lib/node/{meta.cjs → chunk-FPWNZ6TB.cjs} +13 -10
  44. package/dist/lib/node/chunk-FPWNZ6TB.cjs.map +7 -0
  45. package/dist/lib/node/{ThreadMain-MRNJC3TI.cjs → chunk-ZRN7B26W.cjs} +26 -23
  46. package/dist/lib/node/chunk-ZRN7B26W.cjs.map +7 -0
  47. package/dist/lib/node/index.cjs +142 -1083
  48. package/dist/lib/node/index.cjs.map +4 -4
  49. package/dist/lib/node/intent-resolver-X5O3SVLF.cjs +287 -0
  50. package/dist/lib/node/intent-resolver-X5O3SVLF.cjs.map +7 -0
  51. package/dist/lib/node/markdown-FWRKMSKV.cjs +122 -0
  52. package/dist/lib/node/markdown-FWRKMSKV.cjs.map +7 -0
  53. package/dist/lib/node/meta.json +1 -1
  54. package/dist/lib/node/react-surface-LO7S2HPR.cjs +414 -0
  55. package/dist/lib/node/react-surface-LO7S2HPR.cjs.map +7 -0
  56. package/dist/lib/node/settings-TH4YSUVU.cjs +36 -0
  57. package/dist/lib/node/settings-TH4YSUVU.cjs.map +7 -0
  58. package/dist/lib/node/state-TQWEZ6B3.cjs +49 -0
  59. package/dist/lib/node/state-TQWEZ6B3.cjs.map +7 -0
  60. package/dist/lib/node/types.cjs +6 -4
  61. package/dist/lib/node/types.cjs.map +2 -2
  62. package/dist/lib/node-esm/ThreadContainer-7DZY2MWA.mjs +20 -0
  63. package/dist/lib/node-esm/ThreadContainer-7DZY2MWA.mjs.map +7 -0
  64. package/dist/lib/node-esm/app-graph-builder-ATS6HGE6.mjs +122 -0
  65. package/dist/lib/node-esm/app-graph-builder-ATS6HGE6.mjs.map +7 -0
  66. package/dist/lib/node-esm/chunk-B3CF35MY.mjs +82 -0
  67. package/dist/lib/node-esm/chunk-B3CF35MY.mjs.map +7 -0
  68. package/dist/lib/node-esm/{chunk-JG7MXH6O.mjs → chunk-DYFIKKDT.mjs} +31 -117
  69. package/dist/lib/node-esm/chunk-DYFIKKDT.mjs.map +7 -0
  70. package/dist/lib/node-esm/{chunk-7QRGG25F.mjs → chunk-FXUATCLW.mjs} +3 -3
  71. package/dist/lib/node-esm/chunk-FXUATCLW.mjs.map +7 -0
  72. package/dist/lib/node-esm/chunk-GS2OILYF.mjs +20 -0
  73. package/dist/lib/node-esm/chunk-GS2OILYF.mjs.map +7 -0
  74. package/dist/lib/node-esm/chunk-HLWJTUWZ.mjs +73 -0
  75. package/dist/lib/node-esm/chunk-HLWJTUWZ.mjs.map +7 -0
  76. package/dist/lib/node-esm/chunk-ZJITX3TO.mjs +17 -0
  77. package/dist/lib/node-esm/chunk-ZJITX3TO.mjs.map +7 -0
  78. package/dist/lib/node-esm/index.mjs +147 -1094
  79. package/dist/lib/node-esm/index.mjs.map +4 -4
  80. package/dist/lib/node-esm/intent-resolver-XJWT5X6T.mjs +276 -0
  81. package/dist/lib/node-esm/intent-resolver-XJWT5X6T.mjs.map +7 -0
  82. package/dist/lib/node-esm/markdown-6AQFDAW3.mjs +113 -0
  83. package/dist/lib/node-esm/markdown-6AQFDAW3.mjs.map +7 -0
  84. package/dist/lib/node-esm/meta.json +1 -1
  85. package/dist/lib/node-esm/react-surface-LAF3QIPB.mjs +413 -0
  86. package/dist/lib/node-esm/react-surface-LAF3QIPB.mjs.map +7 -0
  87. package/dist/lib/node-esm/settings-KEDRR7RM.mjs +23 -0
  88. package/dist/lib/node-esm/settings-KEDRR7RM.mjs.map +7 -0
  89. package/dist/lib/node-esm/state-7MLR27YG.mjs +34 -0
  90. package/dist/lib/node-esm/state-7MLR27YG.mjs.map +7 -0
  91. package/dist/lib/node-esm/types.mjs +6 -4
  92. package/dist/types/src/ThreadPlugin.d.ts +1 -3
  93. package/dist/types/src/ThreadPlugin.d.ts.map +1 -1
  94. package/dist/types/src/capabilities/app-graph-builder.d.ts +180 -0
  95. package/dist/types/src/capabilities/app-graph-builder.d.ts.map +1 -0
  96. package/dist/types/src/capabilities/capabilities.d.ts +15 -0
  97. package/dist/types/src/capabilities/capabilities.d.ts.map +1 -0
  98. package/dist/types/src/capabilities/index.d.ts +187 -0
  99. package/dist/types/src/capabilities/index.d.ts.map +1 -0
  100. package/dist/types/src/capabilities/intent-resolver.d.ts +4 -0
  101. package/dist/types/src/capabilities/intent-resolver.d.ts.map +1 -0
  102. package/dist/types/src/capabilities/markdown.d.ts +4 -0
  103. package/dist/types/src/capabilities/markdown.d.ts.map +1 -0
  104. package/dist/types/src/capabilities/react-surface.d.ts +4 -0
  105. package/dist/types/src/capabilities/react-surface.d.ts.map +1 -0
  106. package/dist/types/src/capabilities/settings.d.ts +4 -0
  107. package/dist/types/src/capabilities/settings.d.ts.map +1 -0
  108. package/dist/types/src/capabilities/state.d.ts +7 -0
  109. package/dist/types/src/capabilities/state.d.ts.map +1 -0
  110. package/dist/types/src/components/Chat.stories.d.ts.map +1 -1
  111. package/dist/types/src/components/ChatContainer.d.ts.map +1 -1
  112. package/dist/types/src/components/CommentContainer.d.ts +1 -1
  113. package/dist/types/src/components/CommentContainer.d.ts.map +1 -1
  114. package/dist/types/src/components/CommentsContainer.d.ts +2 -7
  115. package/dist/types/src/components/CommentsContainer.d.ts.map +1 -1
  116. package/dist/types/src/components/MessageContainer.d.ts.map +1 -1
  117. package/dist/types/src/components/ThreadComplementary.d.ts +1 -6
  118. package/dist/types/src/components/ThreadComplementary.d.ts.map +1 -1
  119. package/dist/types/src/components/ThreadContainer.d.ts +7 -0
  120. package/dist/types/src/components/ThreadContainer.d.ts.map +1 -0
  121. package/dist/types/src/components/index.d.ts +3 -15
  122. package/dist/types/src/components/index.d.ts.map +1 -1
  123. package/dist/types/src/components/testing.d.ts +50 -66
  124. package/dist/types/src/components/testing.d.ts.map +1 -1
  125. package/dist/types/src/components/types.d.ts +5 -5
  126. package/dist/types/src/components/types.d.ts.map +1 -1
  127. package/dist/types/src/extensions/threads.d.ts +2 -2
  128. package/dist/types/src/extensions/threads.d.ts.map +1 -1
  129. package/dist/types/src/hooks/analytics.d.ts.map +1 -1
  130. package/dist/types/src/index.d.ts +1 -3
  131. package/dist/types/src/index.d.ts.map +1 -1
  132. package/dist/types/src/meta.d.ts +1 -2
  133. package/dist/types/src/meta.d.ts.map +1 -1
  134. package/dist/types/src/translations.d.ts +3 -0
  135. package/dist/types/src/translations.d.ts.map +1 -1
  136. package/dist/types/src/types.d.ts +86 -22
  137. package/dist/types/src/types.d.ts.map +1 -1
  138. package/dist/types/tsconfig.tsbuildinfo +1 -0
  139. package/package.json +30 -38
  140. package/src/ThreadPlugin.tsx +109 -620
  141. package/src/capabilities/app-graph-builder.ts +120 -0
  142. package/src/capabilities/capabilities.ts +19 -0
  143. package/src/capabilities/index.ts +14 -0
  144. package/src/capabilities/intent-resolver.ts +223 -0
  145. package/src/capabilities/markdown.ts +18 -0
  146. package/src/capabilities/react-surface.tsx +61 -0
  147. package/src/capabilities/settings.ts +14 -0
  148. package/src/capabilities/state.ts +25 -0
  149. package/src/components/Chat.stories.tsx +26 -22
  150. package/src/components/ChatContainer.tsx +13 -11
  151. package/src/components/CommentContainer.tsx +21 -32
  152. package/src/components/CommentsContainer.tsx +1 -17
  153. package/src/components/MessageContainer.tsx +7 -5
  154. package/src/components/ThreadComplementary.tsx +88 -68
  155. package/src/components/ThreadContainer.tsx +22 -0
  156. package/src/components/index.ts +1 -2
  157. package/src/components/testing.ts +26 -22
  158. package/src/components/types.ts +5 -5
  159. package/src/extensions/threads.ts +16 -25
  160. package/src/hooks/analytics.ts +6 -10
  161. package/src/index.ts +1 -5
  162. package/src/meta.ts +1 -1
  163. package/src/translations.ts +1 -0
  164. package/src/types.ts +70 -38
  165. package/dist/lib/browser/ThreadArticle-6IXTL7CH.mjs +0 -18
  166. package/dist/lib/browser/ThreadArticle-6IXTL7CH.mjs.map +0 -7
  167. package/dist/lib/browser/ThreadMain-4IAS67KE.mjs +0 -26
  168. package/dist/lib/browser/ThreadMain-4IAS67KE.mjs.map +0 -7
  169. package/dist/lib/browser/chunk-52HWDLBR.mjs.map +0 -7
  170. package/dist/lib/browser/chunk-ENERBDYI.mjs.map +0 -7
  171. package/dist/lib/browser/chunk-XUHNZID5.mjs +0 -20
  172. package/dist/lib/browser/chunk-XUHNZID5.mjs.map +0 -7
  173. package/dist/lib/browser/meta.mjs +0 -11
  174. package/dist/lib/browser/meta.mjs.map +0 -7
  175. package/dist/lib/node/ThreadArticle-I7ADXDW3.cjs.map +0 -7
  176. package/dist/lib/node/ThreadMain-MRNJC3TI.cjs.map +0 -7
  177. package/dist/lib/node/chunk-IC2NICNC.cjs.map +0 -7
  178. package/dist/lib/node/chunk-XFO6D2LG.cjs +0 -39
  179. package/dist/lib/node/chunk-XFO6D2LG.cjs.map +0 -7
  180. package/dist/lib/node/chunk-YCDM2SZM.cjs.map +0 -7
  181. package/dist/lib/node/meta.cjs.map +0 -7
  182. package/dist/lib/node-esm/ThreadArticle-CJHYMF2W.mjs +0 -19
  183. package/dist/lib/node-esm/ThreadArticle-CJHYMF2W.mjs.map +0 -7
  184. package/dist/lib/node-esm/ThreadMain-VISGW4IF.mjs +0 -27
  185. package/dist/lib/node-esm/ThreadMain-VISGW4IF.mjs.map +0 -7
  186. package/dist/lib/node-esm/chunk-7QRGG25F.mjs.map +0 -7
  187. package/dist/lib/node-esm/chunk-I5MU54RT.mjs +0 -21
  188. package/dist/lib/node-esm/chunk-I5MU54RT.mjs.map +0 -7
  189. package/dist/lib/node-esm/chunk-JG7MXH6O.mjs.map +0 -7
  190. package/dist/lib/node-esm/meta.mjs +0 -12
  191. package/dist/lib/node-esm/meta.mjs.map +0 -7
  192. package/dist/types/src/components/ThreadArticle.d.ts +0 -5
  193. package/dist/types/src/components/ThreadArticle.d.ts.map +0 -1
  194. package/dist/types/src/components/ThreadMain.d.ts +0 -7
  195. package/dist/types/src/components/ThreadMain.d.ts.map +0 -1
  196. package/src/components/ThreadArticle.tsx +0 -18
  197. package/src/components/ThreadMain.tsx +0 -27
@@ -0,0 +1,120 @@
1
+ //
2
+ // Copyright 2025 DXOS.org
3
+ //
4
+
5
+ import { Capabilities, contributes, createIntent, type PluginsContext } from '@dxos/app-framework';
6
+ import { ClientCapabilities } from '@dxos/plugin-client';
7
+ import { createExtension, toSignal } from '@dxos/plugin-graph';
8
+ import { ObservabilityAction } from '@dxos/plugin-observability/types';
9
+ import { memoizeQuery } from '@dxos/plugin-space';
10
+ import { getTypename, parseId, SpaceState } from '@dxos/react-client/echo';
11
+
12
+ import { ThreadCapabilities } from './capabilities';
13
+ import { THREAD_PLUGIN } from '../meta';
14
+
15
+ const type = 'orphan-comments-for-subject';
16
+ const icon = 'ph--chat-text--regular';
17
+
18
+ export default (context: PluginsContext) => {
19
+ const resolver = (typename: string) =>
20
+ context.requestCapabilities(Capabilities.Metadata).find(({ id }) => id === typename)?.metadata ?? {};
21
+
22
+ return contributes(
23
+ Capabilities.AppGraphBuilder,
24
+ createExtension({
25
+ id: `${THREAD_PLUGIN}/comments-for-subject`,
26
+ resolver: ({ id }) => {
27
+ // TODO(Zan): Find util (or make one).
28
+ if (!id.endsWith('~comments')) {
29
+ return;
30
+ }
31
+
32
+ const client = context.requestCapability(ClientCapabilities.Client);
33
+ const [subjectId] = id.split('~');
34
+ const { spaceId, objectId } = parseId(subjectId);
35
+ const spaces = toSignal(
36
+ (onChange) => client.spaces.subscribe(() => onChange()).unsubscribe,
37
+ () => client.spaces.get(),
38
+ );
39
+ const space = spaces?.find((space) => space.id === spaceId && space.state.get() === SpaceState.SPACE_READY);
40
+ if (!objectId) {
41
+ // TODO(wittjosiah): Support comments for arbitrary subjects.
42
+ // This is to ensure that the comments panel is not stuck on an old object.
43
+ return {
44
+ id,
45
+ type,
46
+ data: null,
47
+ properties: {
48
+ icon,
49
+ label: ['unnamed object threads label', { ns: THREAD_PLUGIN }],
50
+ showResolvedThreads: false,
51
+ object: null,
52
+ space,
53
+ },
54
+ };
55
+ }
56
+
57
+ const [object] = memoizeQuery(space, { id: objectId });
58
+ if (!object || !subjectId) {
59
+ return;
60
+ }
61
+
62
+ const meta = resolver(getTypename(object) ?? '');
63
+ const label = meta.label?.(object) ||
64
+ object.name ||
65
+ meta.placeholder || ['unnamed object threads label', { ns: THREAD_PLUGIN }];
66
+
67
+ const { getViewState } = context.requestCapability(ThreadCapabilities.State);
68
+ const viewState = getViewState(subjectId);
69
+
70
+ return {
71
+ id,
72
+ type,
73
+ data: null,
74
+ properties: {
75
+ icon,
76
+ label,
77
+ showResolvedThreads: viewState.showResolvedThreads,
78
+ object,
79
+ },
80
+ };
81
+ },
82
+ actions: ({ node }) => {
83
+ const dataId = node.id.split('~').at(0);
84
+ if (!node.id.endsWith('~comments') || !dataId) {
85
+ return;
86
+ }
87
+
88
+ const [spaceId, objectId] = dataId.split(':');
89
+
90
+ const { getViewState } = context.requestCapability(ThreadCapabilities.State);
91
+ const viewState = getViewState(dataId);
92
+ const toggle = async () => {
93
+ const newToggleState = !viewState.showResolvedThreads;
94
+ viewState.showResolvedThreads = newToggleState;
95
+ const { dispatchPromise: dispatch } = context.requestCapability(Capabilities.IntentDispatcher);
96
+ await dispatch(
97
+ createIntent(ObservabilityAction.SendEvent, {
98
+ name: 'threads.toggle-show-resolved',
99
+ properties: { spaceId, threadId: objectId, showResolved: newToggleState },
100
+ }),
101
+ );
102
+ };
103
+
104
+ return [
105
+ {
106
+ id: `${THREAD_PLUGIN}/toggle-show-resolved/${node.id}`,
107
+ data: toggle,
108
+ properties: {
109
+ label: ['toggle show resolved', { ns: THREAD_PLUGIN }],
110
+ menuItemType: 'toggle',
111
+ isChecked: viewState.showResolvedThreads,
112
+ testId: 'threadPlugin.toggleShowResolved',
113
+ icon: viewState.showResolvedThreads ? 'ph--eye-slash--regular' : 'ph--eye--regular',
114
+ },
115
+ },
116
+ ];
117
+ },
118
+ }),
119
+ );
120
+ };
@@ -0,0 +1,19 @@
1
+ //
2
+ // Copyright 2025 DXOS.org
3
+ //
4
+
5
+ import { defineCapability } from '@dxos/app-framework';
6
+ import { type DeepReadonly } from '@dxos/util';
7
+
8
+ import { THREAD_PLUGIN } from '../meta';
9
+ import { type ViewState, type ThreadState } from '../types';
10
+
11
+ export namespace ThreadCapabilities {
12
+ type GetViewState = (subjectId: string) => ViewState;
13
+ export const State = defineCapability<{ state: DeepReadonly<ThreadState>; getViewState: GetViewState }>(
14
+ `${THREAD_PLUGIN}/capability/state`,
15
+ );
16
+ export const MutableState = defineCapability<{ state: ThreadState; getViewState: GetViewState }>(
17
+ `${THREAD_PLUGIN}/capability/state`,
18
+ );
19
+ }
@@ -0,0 +1,14 @@
1
+ //
2
+ // Copyright 2025 DXOS.org
3
+ //
4
+
5
+ import { lazy } from '@dxos/app-framework';
6
+
7
+ export const AppGraphBuilder = lazy(() => import('./app-graph-builder'));
8
+ export const IntentResolver = lazy(() => import('./intent-resolver'));
9
+ export const Markdown = lazy(() => import('./markdown'));
10
+ export const ReactSurface = lazy(() => import('./react-surface'));
11
+ export const ThreadSettings = lazy(() => import('./settings'));
12
+ export const ThreadState = lazy(() => import('./state'));
13
+
14
+ export * from './capabilities';
@@ -0,0 +1,223 @@
1
+ //
2
+ // Copyright 2025 DXOS.org
3
+ //
4
+
5
+ import {
6
+ Capabilities,
7
+ contributes,
8
+ createIntent,
9
+ createResolver,
10
+ LayoutAction,
11
+ NavigationAction,
12
+ type PluginsContext,
13
+ } from '@dxos/app-framework';
14
+ import { Ref } from '@dxos/echo-schema';
15
+ import { log } from '@dxos/log';
16
+ import { ObservabilityAction } from '@dxos/plugin-observability/types';
17
+ import { ChannelType, ThreadType } from '@dxos/plugin-space/types';
18
+ import { create, fullyQualifiedId, getSpace, makeRef } from '@dxos/react-client/echo';
19
+
20
+ import { ThreadCapabilities } from './capabilities';
21
+ import { THREAD_PLUGIN } from '../meta';
22
+ import { ThreadAction } from '../types';
23
+
24
+ export default (context: PluginsContext) =>
25
+ contributes(Capabilities.IntentResolver, [
26
+ createResolver(ThreadAction.Create, ({ name, cursor, subject }) => {
27
+ if (cursor && subject) {
28
+ // Seed the threads array if it does not exist.
29
+ if (subject?.threads === undefined) {
30
+ try {
31
+ // Static schema will throw an error if subject does not support threads array property.
32
+ subject.threads = [];
33
+ } catch (err) {
34
+ log.error('Subject does not support threads array', { typename: subject?.typename });
35
+ return;
36
+ }
37
+ }
38
+
39
+ const { state } = context.requestCapability(ThreadCapabilities.MutableState);
40
+ const subjectId = fullyQualifiedId(subject);
41
+ const thread = create(ThreadType, { name, anchor: cursor, messages: [], status: 'staged' });
42
+ const draft = state.drafts[subjectId];
43
+ if (draft) {
44
+ draft.push(thread);
45
+ } else {
46
+ state.drafts[subjectId] = [thread];
47
+ }
48
+
49
+ return {
50
+ data: { object: thread },
51
+ intents: [
52
+ createIntent(ThreadAction.Select, { current: fullyQualifiedId(thread) }),
53
+ createIntent(NavigationAction.Open, { activeParts: { complementary: 'comments' } }),
54
+ createIntent(LayoutAction.SetLayout, { element: 'complementary', state: true }),
55
+ ],
56
+ };
57
+ } else {
58
+ // NOTE: This is the standalone thread creation case.
59
+ return {
60
+ data: { object: create(ChannelType, { threads: [makeRef(create(ThreadType, { messages: [] }))] }) },
61
+ };
62
+ }
63
+ }),
64
+ createResolver(ThreadAction.Select, ({ current, skipOpen }) => {
65
+ const { state } = context.requestCapability(ThreadCapabilities.MutableState);
66
+ state.current = current;
67
+
68
+ return {
69
+ intents: !skipOpen
70
+ ? [createIntent(NavigationAction.Open, { activeParts: { complementary: 'comments' } })]
71
+ : undefined,
72
+ };
73
+ }),
74
+ createResolver(ThreadAction.ToggleResolved, ({ thread }) => {
75
+ if (thread.status === 'active' || thread.status === undefined) {
76
+ thread.status = 'resolved';
77
+ } else if (thread.status === 'resolved') {
78
+ thread.status = 'active';
79
+ }
80
+
81
+ const space = getSpace(thread);
82
+ const spaceId = space?.id;
83
+
84
+ return {
85
+ intents: [
86
+ createIntent(ObservabilityAction.SendEvent, {
87
+ name: 'threads.toggle-resolved',
88
+ properties: { threadId: thread.id, spaceId },
89
+ }),
90
+ ],
91
+ };
92
+ }),
93
+ createResolver(ThreadAction.Delete, ({ subject, thread }, undo) => {
94
+ const { state } = context.requestCapability(ThreadCapabilities.MutableState);
95
+ const subjectId = fullyQualifiedId(subject);
96
+ const draft = state.drafts[subjectId];
97
+ if (draft) {
98
+ // Check if we're deleting a draft; if so, remove it.
99
+ const index = draft.findIndex((t) => t.id === thread.id);
100
+ if (index !== -1) {
101
+ draft.splice(index, 1);
102
+ return;
103
+ }
104
+ }
105
+
106
+ const space = getSpace(thread);
107
+ if (!space || !subject.threads) {
108
+ return;
109
+ }
110
+
111
+ if (!undo) {
112
+ const index = subject.threads.findIndex((t: any) => t?.id === thread.id);
113
+ const cursor = subject.threads[index]?.anchor;
114
+ if (index !== -1) {
115
+ subject.threads?.splice(index, 1);
116
+ }
117
+
118
+ space.db.remove(thread);
119
+
120
+ return {
121
+ undoable: {
122
+ message: ['thread deleted label', { ns: THREAD_PLUGIN }],
123
+ data: { cursor },
124
+ },
125
+ intents: [
126
+ createIntent(ObservabilityAction.SendEvent, {
127
+ name: 'threads.delete',
128
+ properties: { threadId: thread.id, spaceId: space.id },
129
+ }),
130
+ ],
131
+ };
132
+ } else {
133
+ // TODO(wittjosiah): SDK should do this automatically.
134
+ const savedThread = space.db.add(thread);
135
+ subject.threads.push(savedThread);
136
+
137
+ return {
138
+ intents: [
139
+ createIntent(ObservabilityAction.SendEvent, {
140
+ name: 'threads.undo-delete',
141
+ properties: { threadId: thread.id, spaceId: space.id },
142
+ }),
143
+ ],
144
+ };
145
+ }
146
+ }),
147
+ createResolver(ThreadAction.OnMessageAdd, ({ thread, subject }) => {
148
+ const { state } = context.requestCapability(ThreadCapabilities.MutableState);
149
+ const subjectId = fullyQualifiedId(subject);
150
+ const space = getSpace(subject);
151
+ const intents = [];
152
+ const analyticsProperties = { threadId: thread.id, spaceId: space?.id };
153
+
154
+ if (state.drafts[subjectId]?.find((t) => t === thread)) {
155
+ // Move draft to document.
156
+ thread.status = 'active';
157
+ subject.threads ? subject.threads.push(makeRef(thread)) : (subject.threads = [makeRef(thread)]);
158
+ state.drafts[subjectId] = state.drafts[subjectId]?.filter(({ id }) => id !== thread.id);
159
+ intents.push(
160
+ createIntent(ObservabilityAction.SendEvent, {
161
+ name: 'threads.thread-created',
162
+ properties: analyticsProperties,
163
+ }),
164
+ );
165
+ }
166
+
167
+ intents.push(
168
+ createIntent(ObservabilityAction.SendEvent, {
169
+ name: 'threads.message-added',
170
+ properties: { ...analyticsProperties, threadLength: thread.messages.length },
171
+ }),
172
+ );
173
+
174
+ return { intents };
175
+ }),
176
+ createResolver(ThreadAction.DeleteMessage, ({ subject, thread, messageId, message, messageIndex }, undo) => {
177
+ const space = getSpace(subject);
178
+
179
+ if (!undo) {
180
+ const messageIndex = thread.messages.findIndex(Ref.hasObjectId(messageId));
181
+ const message = thread.messages[messageIndex]?.target;
182
+ if (!message) {
183
+ return;
184
+ }
185
+
186
+ if (messageIndex === 0 && thread.messages.length === 1) {
187
+ // If the message is the only message in the thread, delete the thread.
188
+ return {
189
+ intents: [createIntent(ThreadAction.Delete, { subject, thread })],
190
+ };
191
+ } else {
192
+ thread.messages.splice(messageIndex, 1);
193
+ }
194
+
195
+ return {
196
+ undoable: {
197
+ message: ['message deleted label', { ns: THREAD_PLUGIN }],
198
+ data: { message, messageIndex },
199
+ },
200
+ intents: [
201
+ createIntent(ObservabilityAction.SendEvent, {
202
+ name: 'threads.message.delete',
203
+ properties: { threadId: thread.id, spaceId: space?.id },
204
+ }),
205
+ ],
206
+ };
207
+ } else {
208
+ if (!messageIndex || !message) {
209
+ return;
210
+ }
211
+
212
+ thread.messages.splice(messageIndex, 0, makeRef(message));
213
+ return {
214
+ intents: [
215
+ createIntent(ObservabilityAction.SendEvent, {
216
+ name: 'threads.message.undo-delete',
217
+ properties: { threadId: thread.id, spaceId: space?.id },
218
+ }),
219
+ ],
220
+ };
221
+ }
222
+ }),
223
+ ]);
@@ -0,0 +1,18 @@
1
+ //
2
+ // Copyright 2025 DXOS.org
3
+ //
4
+
5
+ import { Capabilities, contributes, type PluginsContext } from '@dxos/app-framework';
6
+ import { MarkdownCapabilities } from '@dxos/plugin-markdown';
7
+
8
+ import { ThreadCapabilities } from './capabilities';
9
+ import { threads } from '../extensions';
10
+
11
+ export default (context: PluginsContext) =>
12
+ contributes(MarkdownCapabilities.Extensions, [
13
+ ({ document: doc }) => {
14
+ const { dispatchPromise: dispatch } = context.requestCapability(Capabilities.IntentDispatcher);
15
+ const { state } = context.requestCapability(ThreadCapabilities.MutableState);
16
+ return threads(state, doc, dispatch);
17
+ },
18
+ ]);
@@ -0,0 +1,61 @@
1
+ //
2
+ // Copyright 2025 DXOS.org
3
+ //
4
+
5
+ import React from 'react';
6
+
7
+ import { Capabilities, contributes, createSurface, isLayoutParts, useCapability } from '@dxos/app-framework';
8
+ import { type Ref } from '@dxos/echo-schema';
9
+ import { ChannelType, type ThreadType } from '@dxos/plugin-space/types';
10
+ import { getSpace } from '@dxos/react-client/echo';
11
+
12
+ import { ThreadContainer, ThreadComplementary, ThreadSettings } from '../components';
13
+ import { THREAD_PLUGIN } from '../meta';
14
+ import { type ThreadSettingsProps } from '../types';
15
+
16
+ export default () =>
17
+ contributes(Capabilities.ReactSurface, [
18
+ createSurface({
19
+ id: `${THREAD_PLUGIN}/channel`,
20
+ role: 'article',
21
+ filter: (data): data is { subject: ChannelType } =>
22
+ data.subject instanceof ChannelType && !!data.subject.threads[0],
23
+ component: ({ data, role }) => {
24
+ const location = useCapability(Capabilities.Location);
25
+ const channel = data.subject;
26
+ const thread = channel.threads[0].target!;
27
+ // TODO(zan): Maybe we should have utility for positional main object ids.
28
+ if (isLayoutParts(location?.active) && location.active.main) {
29
+ const layoutEntries = location.active.main;
30
+ const currentPosition = layoutEntries.findIndex((entry) => channel.id === entry.id);
31
+ if (currentPosition > 0) {
32
+ const objectToTheLeft = layoutEntries[currentPosition - 1];
33
+ const context = getSpace(channel)?.db.getObjectById(objectToTheLeft.id);
34
+ return <ThreadContainer role={role} thread={thread} context={context} />;
35
+ }
36
+ }
37
+
38
+ return <ThreadContainer role={role} thread={thread} />;
39
+ },
40
+ }),
41
+ createSurface({
42
+ id: `${THREAD_PLUGIN}/thread`,
43
+ role: 'complementary--comments',
44
+ filter: (data): data is { subject: { threads: Ref<ThreadType>[] } } =>
45
+ !!data.subject &&
46
+ typeof data.subject === 'object' &&
47
+ 'threads' in data.subject &&
48
+ Array.isArray(data.subject.threads) &&
49
+ !(data.subject instanceof ChannelType),
50
+ component: ({ data }) => <ThreadComplementary subject={data.subject} />,
51
+ }),
52
+ createSurface({
53
+ id: `${THREAD_PLUGIN}/settings`,
54
+ role: 'settings',
55
+ filter: (data): data is any => data.subject === THREAD_PLUGIN,
56
+ component: () => {
57
+ const settings = useCapability(Capabilities.SettingsStore).getStore<ThreadSettingsProps>(THREAD_PLUGIN)!.value;
58
+ return <ThreadSettings settings={settings} />;
59
+ },
60
+ }),
61
+ ]);
@@ -0,0 +1,14 @@
1
+ //
2
+ // Copyright 2025 DXOS.org
3
+ //
4
+
5
+ import { Capabilities, contributes } from '@dxos/app-framework';
6
+ import { create } from '@dxos/live-object';
7
+
8
+ import { THREAD_PLUGIN } from '../meta';
9
+ import { type ThreadSettingsProps, ThreadSettingsSchema } from '../types';
10
+
11
+ export default () => {
12
+ const settings = create<ThreadSettingsProps>({});
13
+ return contributes(Capabilities.Settings, { schema: ThreadSettingsSchema, prefix: THREAD_PLUGIN, value: settings });
14
+ };
@@ -0,0 +1,25 @@
1
+ //
2
+ // Copyright 2025 DXOS.org
3
+ //
4
+
5
+ import { contributes } from '@dxos/app-framework';
6
+ import { create } from '@dxos/live-object';
7
+
8
+ import { ThreadCapabilities } from './capabilities';
9
+ import { type ThreadState, type ViewStore } from '../types';
10
+
11
+ const initialViewState = { showResolvedThreads: false };
12
+
13
+ export default () => {
14
+ const state = create<ThreadState>({ drafts: {} });
15
+ const viewStore = create<ViewStore>({});
16
+
17
+ const getViewState = (subjectId: string) => {
18
+ if (!viewStore[subjectId]) {
19
+ viewStore[subjectId] = { ...initialViewState };
20
+ }
21
+ return viewStore[subjectId];
22
+ };
23
+
24
+ return contributes(ThreadCapabilities.State, { state, getViewState });
25
+ };
@@ -7,13 +7,14 @@ import '@dxos-theme';
7
7
  import { type Meta } from '@storybook/react';
8
8
  import React, { useEffect, useState } from 'react';
9
9
 
10
- import { SurfaceProvider } from '@dxos/app-framework';
11
- import { ThreadType } from '@dxos/plugin-space/types';
10
+ import { Capabilities, contributes, createSurface } from '@dxos/app-framework';
11
+ import { withPluginManager } from '@dxos/app-framework/testing';
12
+ import { MessageType, ThreadType } from '@dxos/plugin-space/types';
12
13
  import { faker } from '@dxos/random';
13
14
  import { useClient } from '@dxos/react-client';
14
15
  import { type Space } from '@dxos/react-client/echo';
15
16
  import { useIdentity } from '@dxos/react-client/halo';
16
- import { ClientRepeater } from '@dxos/react-client/testing';
17
+ import { withClientProvider } from '@dxos/react-client/testing';
17
18
  import { Mosaic } from '@dxos/react-ui-mosaic';
18
19
  import { Thread } from '@dxos/react-ui-thread';
19
20
  import { withLayout, withTheme } from '@dxos/storybook-utils';
@@ -46,22 +47,10 @@ const Story = () => {
46
47
  }
47
48
 
48
49
  return (
49
- <SurfaceProvider
50
- value={{
51
- components: {
52
- ObjectMessage: ({ role }) => {
53
- return <span>{JSON.stringify({ role })}</span>;
54
- },
55
- },
56
- }}
57
- >
58
- <Mosaic.Root debug>
59
- <main className='max-is-prose mli-auto bs-dvh overflow-hidden'>
60
- {space && <ChatContainer thread={thread} />}
61
- </main>
62
- <Mosaic.DragOverlay />
63
- </Mosaic.Root>
64
- </SurfaceProvider>
50
+ <Mosaic.Root debug>
51
+ <main className='max-is-prose mli-auto bs-dvh overflow-hidden'>{space && <ChatContainer thread={thread} />}</main>
52
+ <Mosaic.DragOverlay />
53
+ </Mosaic.Root>
65
54
  );
66
55
  };
67
56
 
@@ -70,9 +59,24 @@ export const Default = {};
70
59
  const meta: Meta = {
71
60
  title: 'plugins/plugin-thread/Chat',
72
61
  component: Thread,
73
- // TODO(burdon): Use decorator.
74
- render: () => <ClientRepeater component={Story} createIdentity createSpace types={[ThreadType]} />,
75
- decorators: [withTheme, withLayout({ fullscreen: true })],
62
+ render: () => <Story />,
63
+ decorators: [
64
+ withPluginManager({
65
+ capabilities: [
66
+ contributes(
67
+ Capabilities.ReactSurface,
68
+ createSurface({
69
+ id: 'test',
70
+ role: 'card',
71
+ component: ({ role }) => <span>{JSON.stringify({ role })}</span>,
72
+ }),
73
+ ),
74
+ ],
75
+ }),
76
+ withTheme,
77
+ withLayout({ fullscreen: true, tooltips: true }),
78
+ withClientProvider({ createSpace: true, types: [ThreadType, MessageType] }),
79
+ ],
76
80
  parameters: { translations },
77
81
  };
78
82
 
@@ -4,16 +4,16 @@
4
4
 
5
5
  import React, { useLayoutEffect, useMemo, useRef, useState } from 'react';
6
6
 
7
- import { create } from '@dxos/live-object';
7
+ import { Ref } from '@dxos/echo-schema';
8
+ import { create, makeRef, RefArray } from '@dxos/live-object';
8
9
  import { MessageType } from '@dxos/plugin-space/types';
9
- import { fullyQualifiedId, getSpace, useMembers } from '@dxos/react-client/echo';
10
+ import { fullyQualifiedId, getSpace, useMembers, type Expando } from '@dxos/react-client/echo';
10
11
  import { useIdentity } from '@dxos/react-client/halo';
11
12
  import { Icon, ScrollArea, useThemeContext, useTranslation } from '@dxos/react-ui';
12
13
  import { createBasicExtensions, createThemeExtensions, listener } from '@dxos/react-ui-editor';
13
14
  import { StackItem } from '@dxos/react-ui-stack';
14
15
  import { mx } from '@dxos/react-ui-theme';
15
16
  import { MessageTextbox, type MessageTextboxProps, Thread, ThreadFooter, threadLayout } from '@dxos/react-ui-thread';
16
- import { nonNullable } from '@dxos/util';
17
17
 
18
18
  import { MessageContainer } from './MessageContainer';
19
19
  import { command } from './command-extension';
@@ -77,12 +77,14 @@ export const ChatContainer = ({ thread, context, current, autoFocusTextbox }: Th
77
77
  }
78
78
 
79
79
  thread.messages.push(
80
- create(MessageType, {
81
- sender: { identityKey: identity.identityKey.toHex() },
82
- timestamp: new Date().toISOString(),
83
- text: messageRef.current,
84
- context,
85
- }),
80
+ makeRef(
81
+ create(MessageType, {
82
+ sender: { identityKey: identity.identityKey.toHex() },
83
+ timestamp: new Date().toISOString(),
84
+ text: messageRef.current,
85
+ context: makeRef(context as Expando),
86
+ }),
87
+ ),
86
88
  );
87
89
 
88
90
  messageRef.current = '';
@@ -93,7 +95,7 @@ export const ChatContainer = ({ thread, context, current, autoFocusTextbox }: Th
93
95
  };
94
96
 
95
97
  const handleDelete = (id: string) => {
96
- const messageIndex = thread.messages.filter(nonNullable).findIndex((message) => message.id === id);
98
+ const messageIndex = thread.messages.findIndex(Ref.hasObjectId(id));
97
99
  if (messageIndex !== -1) {
98
100
  thread.messages.splice(messageIndex, 1);
99
101
  }
@@ -108,7 +110,7 @@ export const ChatContainer = ({ thread, context, current, autoFocusTextbox }: Th
108
110
  <ScrollArea.Root classNames='col-span-2'>
109
111
  <ScrollArea.Viewport classNames='overflow-anchored after:overflow-anchor after:block after:bs-px after:-mbs-px [&>div]:min-bs-full [&>div]:!grid [&>div]:grid-rows-[1fr_0]'>
110
112
  <div role='none' className={mx(threadLayout, 'place-self-end')}>
111
- {thread.messages.filter(nonNullable).map((message) => (
113
+ {RefArray.allResolvedTargets(thread.messages ?? []).map((message) => (
112
114
  <MessageContainer key={message.id} message={message} members={members} onDelete={handleDelete} />
113
115
  ))}
114
116
  </div>