@dxos/plugin-thread 0.7.5-main.9cb18ac → 0.7.5-main.9d2a38b

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 (182) 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-OD2IZKRP.mjs → chunk-GQONSZ3J.mjs} +19 -83
  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-6KTMQPL4.mjs → chunk-NO5AZLUZ.mjs} +7 -3
  16. package/dist/lib/browser/chunk-NO5AZLUZ.mjs.map +7 -0
  17. package/dist/lib/browser/index.mjs +149 -939
  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-ZCNMFBAO.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-2CB5AVBL.cjs → chunk-6PUGKCPR.cjs} +69 -135
  36. package/dist/lib/node/chunk-6PUGKCPR.cjs.map +7 -0
  37. package/dist/lib/node/{chunk-VW2P26MZ.cjs → chunk-AUVKOX5Q.cjs} +13 -8
  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-5AYGCXXP.cjs → chunk-ZRN7B26W.cjs} +26 -23
  46. package/dist/lib/node/chunk-ZRN7B26W.cjs.map +7 -0
  47. package/dist/lib/node/index.cjs +147 -933
  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-RMYATBXM.mjs → chunk-B3CF35MY.mjs} +7 -3
  67. package/dist/lib/node-esm/chunk-B3CF35MY.mjs.map +7 -0
  68. package/dist/lib/node-esm/{chunk-AQ74G3GY.mjs → chunk-DYFIKKDT.mjs} +19 -83
  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 +149 -939
  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/CommentContainer.d.ts +1 -1
  112. package/dist/types/src/components/CommentContainer.d.ts.map +1 -1
  113. package/dist/types/src/components/CommentsContainer.d.ts +2 -7
  114. package/dist/types/src/components/CommentsContainer.d.ts.map +1 -1
  115. package/dist/types/src/components/ThreadComplementary.d.ts +1 -5
  116. package/dist/types/src/components/ThreadComplementary.d.ts.map +1 -1
  117. package/dist/types/src/components/ThreadContainer.d.ts +7 -0
  118. package/dist/types/src/components/ThreadContainer.d.ts.map +1 -0
  119. package/dist/types/src/components/index.d.ts +3 -15
  120. package/dist/types/src/components/index.d.ts.map +1 -1
  121. package/dist/types/src/components/types.d.ts +5 -5
  122. package/dist/types/src/components/types.d.ts.map +1 -1
  123. package/dist/types/src/index.d.ts +1 -3
  124. package/dist/types/src/index.d.ts.map +1 -1
  125. package/dist/types/src/meta.d.ts +1 -2
  126. package/dist/types/src/meta.d.ts.map +1 -1
  127. package/dist/types/src/translations.d.ts +3 -0
  128. package/dist/types/src/translations.d.ts.map +1 -1
  129. package/dist/types/src/types.d.ts +10 -14
  130. package/dist/types/src/types.d.ts.map +1 -1
  131. package/dist/types/tsconfig.tsbuildinfo +1 -1
  132. package/package.json +30 -38
  133. package/src/ThreadPlugin.tsx +105 -470
  134. package/src/capabilities/app-graph-builder.ts +120 -0
  135. package/src/capabilities/capabilities.ts +19 -0
  136. package/src/capabilities/index.ts +14 -0
  137. package/src/capabilities/intent-resolver.ts +223 -0
  138. package/src/capabilities/markdown.ts +18 -0
  139. package/src/capabilities/react-surface.tsx +61 -0
  140. package/src/capabilities/settings.ts +14 -0
  141. package/src/capabilities/state.ts +25 -0
  142. package/src/components/Chat.stories.tsx +25 -23
  143. package/src/components/CommentContainer.tsx +16 -30
  144. package/src/components/CommentsContainer.tsx +1 -17
  145. package/src/components/ThreadComplementary.tsx +82 -48
  146. package/src/components/ThreadContainer.tsx +22 -0
  147. package/src/components/index.ts +1 -2
  148. package/src/components/types.ts +5 -5
  149. package/src/index.ts +1 -5
  150. package/src/meta.ts +1 -1
  151. package/src/translations.ts +1 -0
  152. package/src/types.ts +10 -29
  153. package/dist/lib/browser/ThreadArticle-EWLTNU6Z.mjs +0 -18
  154. package/dist/lib/browser/ThreadArticle-EWLTNU6Z.mjs.map +0 -7
  155. package/dist/lib/browser/ThreadMain-ABEDFWHC.mjs +0 -26
  156. package/dist/lib/browser/ThreadMain-ABEDFWHC.mjs.map +0 -7
  157. package/dist/lib/browser/chunk-6KTMQPL4.mjs.map +0 -7
  158. package/dist/lib/browser/chunk-ENERBDYI.mjs.map +0 -7
  159. package/dist/lib/browser/chunk-OD2IZKRP.mjs.map +0 -7
  160. package/dist/lib/browser/meta.mjs +0 -11
  161. package/dist/lib/browser/meta.mjs.map +0 -7
  162. package/dist/lib/node/ThreadArticle-ZCNMFBAO.cjs.map +0 -7
  163. package/dist/lib/node/ThreadMain-5AYGCXXP.cjs.map +0 -7
  164. package/dist/lib/node/chunk-2CB5AVBL.cjs.map +0 -7
  165. package/dist/lib/node/chunk-IC2NICNC.cjs.map +0 -7
  166. package/dist/lib/node/chunk-VW2P26MZ.cjs.map +0 -7
  167. package/dist/lib/node/meta.cjs.map +0 -7
  168. package/dist/lib/node-esm/ThreadArticle-F37HYTOU.mjs +0 -19
  169. package/dist/lib/node-esm/ThreadArticle-F37HYTOU.mjs.map +0 -7
  170. package/dist/lib/node-esm/ThreadMain-OL6ZHNZY.mjs +0 -27
  171. package/dist/lib/node-esm/ThreadMain-OL6ZHNZY.mjs.map +0 -7
  172. package/dist/lib/node-esm/chunk-7QRGG25F.mjs.map +0 -7
  173. package/dist/lib/node-esm/chunk-AQ74G3GY.mjs.map +0 -7
  174. package/dist/lib/node-esm/chunk-RMYATBXM.mjs.map +0 -7
  175. package/dist/lib/node-esm/meta.mjs +0 -12
  176. package/dist/lib/node-esm/meta.mjs.map +0 -7
  177. package/dist/types/src/components/ThreadArticle.d.ts +0 -5
  178. package/dist/types/src/components/ThreadArticle.d.ts.map +0 -1
  179. package/dist/types/src/components/ThreadMain.d.ts +0 -7
  180. package/dist/types/src/components/ThreadMain.d.ts.map +0 -1
  181. package/src/components/ThreadArticle.tsx +0 -18
  182. 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 { createSurface, SurfaceProvider } from '@dxos/app-framework';
10
+ import { Capabilities, contributes, createSurface } from '@dxos/app-framework';
11
+ import { withPluginManager } from '@dxos/app-framework/testing';
11
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,24 +47,10 @@ const Story = () => {
46
47
  }
47
48
 
48
49
  return (
49
- <SurfaceProvider
50
- value={{
51
- surfaces: {
52
- ObjectMessage: createSurface({
53
- id: 'test',
54
- role: 'card',
55
- component: ({ role }) => <span>{JSON.stringify({ role })}</span>,
56
- }),
57
- },
58
- }}
59
- >
60
- <Mosaic.Root debug>
61
- <main className='max-is-prose mli-auto bs-dvh overflow-hidden'>
62
- {space && <ChatContainer thread={thread} />}
63
- </main>
64
- <Mosaic.DragOverlay />
65
- </Mosaic.Root>
66
- </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>
67
54
  );
68
55
  };
69
56
 
@@ -72,9 +59,24 @@ export const Default = {};
72
59
  const meta: Meta = {
73
60
  title: 'plugins/plugin-thread/Chat',
74
61
  component: Thread,
75
- // TODO(burdon): Use decorator.
76
- render: () => <ClientRepeater component={Story} createIdentity createSpace types={[ThreadType, MessageType]} />,
77
- decorators: [withTheme, withLayout({ fullscreen: true, tooltips: 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
+ ],
78
80
  parameters: { translations },
79
81
  };
80
82
 
@@ -5,8 +5,7 @@
5
5
  import { CheckCircle, X } from '@phosphor-icons/react';
6
6
  import React, { useCallback, useMemo, useRef, useState } from 'react';
7
7
 
8
- import { create, makeRef, RefArray } from '@dxos/live-object';
9
- import { MessageType } from '@dxos/plugin-space/types';
8
+ import { RefArray } from '@dxos/live-object';
10
9
  import { fullyQualifiedId, getSpace, useMembers } from '@dxos/react-client/echo';
11
10
  import { useIdentity } from '@dxos/react-client/halo';
12
11
  import { Button, Tag, Tooltip, useThemeContext, useTranslation } from '@dxos/react-ui';
@@ -94,13 +93,12 @@ const DeleteThreadButton = ({ onDelete }: { onDelete: () => void }) => {
94
93
  export const CommentContainer = ({
95
94
  thread,
96
95
  detached,
97
- context,
98
96
  current,
99
97
  onAttend,
100
- onThreadDelete,
101
- onMessageDelete,
102
- onResolve,
103
98
  onComment,
99
+ onResolve,
100
+ onMessageDelete,
101
+ onThreadDelete,
104
102
  }: ThreadContainerProps) => {
105
103
  const identity = useIdentity()!;
106
104
  const space = getSpace(thread);
@@ -129,33 +127,26 @@ export const CommentContainer = ({
129
127
  const scrollToEnd = (behavior: ScrollBehavior) =>
130
128
  setTimeout(() => threadScrollRef.current?.scrollIntoView({ behavior, block: 'end' }), 10);
131
129
 
132
- const handleCreate: MessageTextboxProps['onSend'] = useCallback(() => {
130
+ const handleAttend = useCallback(() => onAttend?.(thread), [onAttend, thread]);
131
+ const handleResolve = useCallback(() => onResolve?.(thread), [onResolve, thread]);
132
+ const handleMessageDelete = useCallback((id: string) => onMessageDelete?.(thread, id), [onMessageDelete, thread]);
133
+ const handleThreadDelete = useCallback(() => onThreadDelete?.(thread), [onThreadDelete, thread]);
134
+
135
+ const handleComment: MessageTextboxProps['onSend'] = useCallback(() => {
133
136
  if (!messageRef.current) {
134
137
  return false;
135
138
  }
136
139
 
137
- thread.messages.push(
138
- makeRef(
139
- create(MessageType, {
140
- sender: { identityKey: identity.identityKey.toHex() },
141
- timestamp: new Date().toISOString(),
142
- text: messageRef.current,
143
- context: context ? makeRef(context) : undefined,
144
- }),
145
- ),
146
- );
147
- onComment?.(thread);
148
-
140
+ onComment?.(thread, messageRef.current);
149
141
  messageRef.current = '';
150
142
  scrollToEnd('instant');
151
143
  rerenderEditor();
152
144
 
153
- // TODO(burdon): Scroll to bottom.
154
145
  return true;
155
146
  }, [thread, identity]);
156
147
 
157
148
  return (
158
- <Thread onClickCapture={onAttend} onFocusCapture={onAttend} current={current} id={fullyQualifiedId(thread)}>
149
+ <Thread onClickCapture={handleAttend} onFocusCapture={handleAttend} current={current} id={fullyQualifiedId(thread)}>
159
150
  <div
160
151
  role='none'
161
152
  className={mx(
@@ -182,19 +173,14 @@ export const CommentContainer = ({
182
173
  <div className='flex flex-row items-center pli-1'>
183
174
  {thread.status === 'staged' && <Tag palette='neutral'>{t('draft button')}</Tag>}
184
175
  {onResolve && !(thread?.status === 'staged') && (
185
- <ToggleResolvedButton isResolved={thread?.status === 'resolved'} onResolve={onResolve} />
176
+ <ToggleResolvedButton isResolved={thread?.status === 'resolved'} onResolve={handleResolve} />
186
177
  )}
187
- {onThreadDelete && <DeleteThreadButton onDelete={onThreadDelete} />}
178
+ {onThreadDelete && <DeleteThreadButton onDelete={handleThreadDelete} />}
188
179
  </div>
189
180
  </div>
190
181
  {/** TODO(dmaretskyi): How's `thread.messages` undefined? */}
191
182
  {RefArray.allResolvedTargets(thread.messages?.filter(nonNullable) ?? []).map((message) => (
192
- <MessageContainer
193
- key={message.id}
194
- message={message}
195
- members={members}
196
- onDelete={(id: string) => onMessageDelete?.(id)}
197
- />
183
+ <MessageContainer key={message.id} message={message} members={members} onDelete={handleMessageDelete} />
198
184
  ))}
199
185
  {/*
200
186
  TODO(wittjosiah): Can't autofocus this generally.
@@ -202,7 +188,7 @@ export const CommentContainer = ({
202
188
  Also, it steals focus from documents when first rendered.
203
189
  Need to find a way to autofocus in one scenario only: when a new thread is created.
204
190
  */}
205
- <MessageTextbox extensions={extensions} onSend={handleCreate} {...textboxMetadata} />
191
+ <MessageTextbox extensions={extensions} onSend={handleComment} {...textboxMetadata} />
206
192
  <ThreadFooter activity={activity}>{t('activity message')}</ThreadFooter>
207
193
  {/* NOTE(thure): This can’t also be the `overflow-anchor` because `ScrollArea` injects an interceding node that contains this necessary ref’d element. */}
208
194
  <div role='none' className='bs-px -mbs-px' ref={threadScrollRef} />