@taskforcehq/taskforce 0.3.306 → 0.3.308

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 (232) hide show
  1. package/README.md +0 -2
  2. package/dist/Taskforce.module.css +317 -27
  3. package/dist/TaskforceCore.js +1 -0
  4. package/dist/TaskforceCore.test.js +178 -0
  5. package/dist/components/activity/EntityActivityTimeline.d.ts +34 -0
  6. package/dist/components/activity/EntityActivityTimeline.js +81 -0
  7. package/dist/components/context/ContextAttachmentManager.js +15 -3
  8. package/dist/components/features/AgentsModule.d.ts +5 -1
  9. package/dist/components/features/AgentsModule.js +142 -39
  10. package/dist/components/features/AgentsModule.test.js +260 -11
  11. package/dist/components/features/AnnotatedAttachmentWorkspace.d.ts +3 -1
  12. package/dist/components/features/AnnotatedAttachmentWorkspace.js +118 -4
  13. package/dist/components/features/AnnotatedAttachmentWorkspace.test.js +52 -0
  14. package/dist/components/features/DocumentIndex.d.ts +2 -2
  15. package/dist/components/features/DocumentIndex.js +3 -4
  16. package/dist/components/features/DocumentViewer.d.ts +3 -1
  17. package/dist/components/features/DocumentViewer.js +6 -5
  18. package/dist/components/features/DocumentWorkspace.d.ts +10 -2
  19. package/dist/components/features/DocumentWorkspace.js +15 -4
  20. package/dist/components/features/DocumentWorkspace.test.js +20 -3
  21. package/dist/components/features/TaskSettings.d.ts +2 -0
  22. package/dist/components/features/TaskSettings.js +83 -78
  23. package/dist/components/features/TaskSettings.test.js +171 -82
  24. package/dist/components/task/TaskActionHeader.js +11 -5
  25. package/dist/components/task/TaskActionHeader.test.js +10 -0
  26. package/dist/components/task/TaskCard.js +6 -2
  27. package/dist/components/task/TaskContextUpload.test.js +3 -3
  28. package/dist/components/task/TaskForm.js +14 -64
  29. package/dist/components/task/TaskKanban.js +0 -8
  30. package/dist/components/task/TaskList.js +5 -1
  31. package/dist/components/task/TaskList.test.js +4 -0
  32. package/dist/components/ui/HierarchyActionBadge.d.ts +10 -0
  33. package/dist/components/ui/HierarchyActionBadge.js +5 -0
  34. package/dist/components/views/AppShellHeader.d.ts +3 -1
  35. package/dist/components/views/AppShellHeader.js +27 -11
  36. package/dist/components/views/AppShellHeader.test.js +18 -1
  37. package/dist/components/views/PlanComparisonPage.d.ts +20 -15
  38. package/dist/components/views/PlanComparisonPage.js +221 -82
  39. package/dist/components/views/PlanComparisonPage.test.js +268 -14
  40. package/dist/components/views/PlansPage.js +16 -10
  41. package/dist/components/views/PlansPage.test.js +4 -2
  42. package/dist/components/views/StandaloneLayout.js +256 -69
  43. package/dist/components/views/WidgetView.js +10 -8
  44. package/dist/components/views/panels/PlanningDrawer.d.ts +19 -6
  45. package/dist/components/views/panels/PlanningDrawer.js +259 -51
  46. package/dist/components/views/panels/PlanningDrawer.test.js +392 -4
  47. package/dist/config/envSchema.js +1 -0
  48. package/dist/core/AiProfileService.d.ts +7 -0
  49. package/dist/core/AiProfileService.js +460 -35
  50. package/dist/core/AiProfiles.test.js +648 -2
  51. package/dist/core/AttachmentLinkService.d.ts +3 -0
  52. package/dist/core/AttachmentLinkService.js +58 -13
  53. package/dist/core/GlobalSettingsService.js +30 -0
  54. package/dist/core/GlobalSettingsService.test.d.ts +1 -0
  55. package/dist/core/GlobalSettingsService.test.js +45 -0
  56. package/dist/core/McpTokenService.d.ts +12 -0
  57. package/dist/core/McpTokenService.js +44 -1
  58. package/dist/core/PlanEntitlementService.d.ts +20 -0
  59. package/dist/core/PlanEntitlementService.js +215 -19
  60. package/dist/core/PlanFeatureCatalog.test.js +55 -0
  61. package/dist/core/PlanningEntities.test.js +366 -4
  62. package/dist/core/TaskActivityService.d.ts +28 -1
  63. package/dist/core/TaskActivityService.js +227 -46
  64. package/dist/core/TaskActivitySync.test.d.ts +1 -0
  65. package/dist/core/TaskActivitySync.test.js +90 -0
  66. package/dist/core/TaskAuditTimeline.test.js +96 -0
  67. package/dist/core/Taskforce.d.ts +204 -26
  68. package/dist/core/Taskforce.ids.test.js +58 -0
  69. package/dist/core/Taskforce.js +781 -71
  70. package/dist/core/Taskforce.mcpAuth.test.js +109 -0
  71. package/dist/core/types.d.ts +10 -0
  72. package/dist/hooks/auth/useTaskforceAuthBootstrap.js +2 -1
  73. package/dist/hooks/bootstrapTimeouts.d.ts +4 -0
  74. package/dist/hooks/bootstrapTimeouts.js +8 -0
  75. package/dist/hooks/bootstrapTimeouts.test.d.ts +1 -0
  76. package/dist/hooks/bootstrapTimeouts.test.js +12 -0
  77. package/dist/hooks/sync/orchestratorShared.d.ts +3 -3
  78. package/dist/hooks/sync/orchestratorShared.js +61 -1
  79. package/dist/hooks/sync/recovery.js +8 -0
  80. package/dist/hooks/sync/recovery.test.js +5 -5
  81. package/dist/hooks/sync/transfers.d.ts +2 -1
  82. package/dist/hooks/sync/transfers.js +36 -9
  83. package/dist/hooks/sync/transfers.test.js +534 -2
  84. package/dist/hooks/useFilterSort.js +13 -13
  85. package/dist/hooks/useFilterSort.test.js +87 -1
  86. package/dist/hooks/useSyncOrchestrator.js +50 -5
  87. package/dist/hooks/useTaskforce.d.ts +85 -50
  88. package/dist/hooks/useTaskforce.js +44 -14
  89. package/dist/hooks/useTaskforce.runtime-routing.test.js +94 -0
  90. package/dist/hooks/useTaskforce.sync-behavior.test.js +99 -0
  91. package/dist/hooks/useWorkspaceSyncController.js +7 -1
  92. package/dist/hooks/useWorkspaceSyncController.test.js +48 -0
  93. package/dist/hooks/workspace/useTaskforceWorkspaceBootstrap.js +14 -7
  94. package/dist/mcp/canonicalAssetHelpers.d.ts +16 -4
  95. package/dist/mcp/canonicalAssetHelpers.js +18 -10
  96. package/dist/mcp/canonicalAssetHelpers.test.d.ts +1 -0
  97. package/dist/mcp/canonicalAssetHelpers.test.js +67 -0
  98. package/dist/mcp/clientIntegrations.d.ts +1 -0
  99. package/dist/mcp/clientIntegrations.js +25 -0
  100. package/dist/mcp/clientIntegrations.test.js +14 -3
  101. package/dist/mcp/clientProfileDefaults.js +3 -3
  102. package/dist/mcp/collaborationRegistrar.js +7 -7
  103. package/dist/mcp/documentAssetRegistrar.js +113 -7
  104. package/dist/mcp/runtime.d.ts +1 -0
  105. package/dist/mcp/runtime.js +969 -122
  106. package/dist/mcp/runtime.test.js +1083 -11
  107. package/dist/mcp/taskAttachmentTypes.d.ts +1 -1
  108. package/dist/mcp/taskPlanningRegistrar.js +48 -0
  109. package/dist/mcp/toolRegistry.d.ts +56 -0
  110. package/dist/mcp/toolRegistry.js +8 -0
  111. package/dist/mcp/toolRegistry.test.js +4 -0
  112. package/dist/migrations/taskSchemaMigrations.js +58 -15
  113. package/dist/server/aiProfileAvatarAssets.d.ts +49 -0
  114. package/dist/server/aiProfileAvatarAssets.js +319 -0
  115. package/dist/server/aiProfileAvatarAssets.test.d.ts +1 -0
  116. package/dist/server/aiProfileAvatarAssets.test.js +213 -0
  117. package/dist/server/annotatedAttachmentsRoutes.test.js +66 -7
  118. package/dist/server/index.js +4 -0
  119. package/dist/server/index.test.js +6 -1
  120. package/dist/server/routes/admin.js +328 -9
  121. package/dist/server/routes/annotatedAttachments.js +51 -0
  122. package/dist/server/routes/auth.d.ts +11 -0
  123. package/dist/server/routes/auth.js +189 -27
  124. package/dist/server/routes/authSupport.d.ts +5 -0
  125. package/dist/server/routes/authSupport.js +13 -1
  126. package/dist/server/routes/billing.d.ts +1 -0
  127. package/dist/server/routes/billing.js +116 -63
  128. package/dist/server/routes/billing.test.js +162 -11
  129. package/dist/server/routes/documents.js +10 -0
  130. package/dist/server/routes/shared.js +0 -10
  131. package/dist/server/routes/sync.integration.test.js +221 -0
  132. package/dist/server/routes/sync.js +13 -3
  133. package/dist/server/routes/syncSnapshotRoutes.js +2 -0
  134. package/dist/server/routes/tasks.js +12 -9
  135. package/dist/server/routes.js +87 -8
  136. package/dist/server/routes.test.js +972 -23
  137. package/dist/shared/aiProfileSurfaceType.d.ts +1 -1
  138. package/dist/shared/aiProfileSurfaceType.js +10 -8
  139. package/dist/storage/objectStorage.d.ts +1 -0
  140. package/dist/storage/objectStorage.js +2 -1
  141. package/dist/storage/objectStorageClient.d.ts +1 -0
  142. package/dist/storage/objectStorageClient.js +41 -8
  143. package/dist/storage/objectStorageClient.test.js +26 -1
  144. package/dist/storage/objectStorageConfig.js +3 -1
  145. package/dist/storage/objectStorageConfig.test.js +13 -1
  146. package/dist/sync/collaborationSyncPayload.js +2 -0
  147. package/dist/sync/collaborationSyncPayload.test.js +1 -0
  148. package/dist/sync/collaborationSyncState.d.ts +3 -3
  149. package/dist/sync/collaborationSyncState.js +19 -12
  150. package/dist/sync/contentSyncPayload.d.ts +5 -1
  151. package/dist/sync/contentSyncPayload.js +9 -0
  152. package/dist/sync/contentSyncPayload.test.js +39 -0
  153. package/dist/sync/contentSyncState.d.ts +5 -1
  154. package/dist/sync/contentSyncState.js +231 -37
  155. package/dist/sync/planningSyncPayload.js +5 -0
  156. package/dist/sync/planningSyncPayload.test.js +24 -0
  157. package/dist/sync/syncApplyHandlers.d.ts +4 -4
  158. package/dist/sync/syncApplyHandlers.js +89 -31
  159. package/dist/sync/syncApplyHandlers.test.js +184 -4
  160. package/dist/sync/syncResourceAdapters.d.ts +2 -0
  161. package/dist/sync/syncResourceAdapters.js +2 -0
  162. package/dist/sync/syncService.d.ts +1 -0
  163. package/dist/sync/syncService.js +13 -1
  164. package/dist/sync/syncService.test.js +39 -0
  165. package/dist/sync/workspacePullFeed.d.ts +4 -0
  166. package/dist/sync/workspacePullFeed.js +129 -70
  167. package/dist/sync/workspacePullFeed.test.js +49 -0
  168. package/dist/sync/workspaceSyncModel.d.ts +28 -6
  169. package/dist/sync/workspaceSyncModel.js +159 -45
  170. package/dist/sync/workspaceSyncModel.test.js +269 -3
  171. package/dist/types.d.ts +33 -2
  172. package/dist/ui/agent-logos/ChatGPT.png +0 -0
  173. package/dist/ui/agent-logos/Gemini CLI.jpeg +0 -0
  174. package/dist/ui/agent-logos/antigravity.jpeg +0 -0
  175. package/dist/ui/agent-logos/claude-code.jpeg +0 -0
  176. package/dist/ui/agent-logos/codex.jpeg +0 -0
  177. package/dist/ui/agent-logos/cursor.png +0 -0
  178. package/dist/ui/agent-logos/openclaw.jpeg +0 -0
  179. package/dist/ui/agent-logos/windsurf.png +0 -0
  180. package/dist/ui/assets/AgentsModule-BS4Pi_nC.js +1 -0
  181. package/dist/ui/assets/AnnotatedAttachmentWorkspace-BVKew-mH.css +1 -0
  182. package/dist/ui/assets/AnnotatedAttachmentWorkspace-CCckzWYZ.js +3 -0
  183. package/dist/ui/assets/ContextAttachmentManager-CfG_ER7C.js +3 -0
  184. package/dist/ui/assets/DocumentWorkspace-BVaWPA25.js +252 -0
  185. package/dist/ui/assets/DocumentWorkspace-D4rJKXzd.css +1 -0
  186. package/dist/ui/assets/EntityActivityTimeline-DSj0ThaK.js +1 -0
  187. package/dist/ui/assets/EntityActivityTimeline-DyqvvhuF.css +1 -0
  188. package/dist/ui/assets/{InitiativesModule-9E6ParrY.js → InitiativesModule-AxMkWtxH.js} +1 -1
  189. package/dist/ui/assets/PlansPage-B_z-D6Nh.css +1 -0
  190. package/dist/ui/assets/PlansPage-Ndt7mYfo.js +1 -0
  191. package/dist/ui/assets/TaskContextUpload-Dw4rTF4i.js +1 -0
  192. package/dist/ui/assets/TaskSettings-f2ga42_V.js +9 -0
  193. package/dist/ui/assets/{WorkflowsModule-BA7jcEpH.js → WorkflowsModule-E4UL3mov.js} +4 -4
  194. package/dist/ui/assets/documentReferences-BOUcJm6-.js +1 -0
  195. package/dist/ui/assets/index-BvAbqx5Q.css +1 -0
  196. package/dist/ui/assets/index-MAHKvFqp.js +4 -0
  197. package/dist/ui/assets/{vendor-icons-DuEd_65c.js → vendor-icons-B32hGuF2.js} +1 -1
  198. package/dist/ui/index.html +3 -3
  199. package/dist/ui/og-image.png +0 -0
  200. package/dist/ui/product/agent manager.png +0 -0
  201. package/dist/ui/product/agent_manager_02.png +0 -0
  202. package/dist/utils/aiProfileEvents.d.ts +7 -0
  203. package/dist/utils/aiProfileEvents.js +6 -0
  204. package/dist/utils/assignees.d.ts +6 -0
  205. package/dist/utils/assignees.js +8 -1
  206. package/dist/utils/assignees.test.d.ts +1 -0
  207. package/dist/utils/assignees.test.js +25 -0
  208. package/dist/utils/avatarDisplay.d.ts +1 -0
  209. package/dist/utils/avatarDisplay.js +15 -0
  210. package/dist/utils/contextFiles.d.ts +3 -1
  211. package/dist/utils/contextFiles.js +2 -1
  212. package/dist/utils/taskActivity.d.ts +7 -1
  213. package/dist/utils/taskActivity.js +63 -1
  214. package/dist/utils/taskActivity.test.js +23 -1
  215. package/dist/utils/taskSearch.d.ts +22 -0
  216. package/dist/utils/taskSearch.js +87 -0
  217. package/dist/utils/taskSearch.test.d.ts +1 -0
  218. package/dist/utils/taskSearch.test.js +69 -0
  219. package/dist/utils/taskUtils.js +10 -3
  220. package/dist/utils/taskUtils.test.d.ts +1 -0
  221. package/dist/utils/taskUtils.test.js +19 -0
  222. package/package.json +1 -1
  223. package/dist/ui/assets/AgentsModule-DrLawwNl.js +0 -1
  224. package/dist/ui/assets/AnnotatedAttachmentWorkspace-BaS2VwIr.css +0 -1
  225. package/dist/ui/assets/AnnotatedAttachmentWorkspace-Z9_whf7F.js +0 -3
  226. package/dist/ui/assets/DocumentWorkspace-B03MaSkw.js +0 -252
  227. package/dist/ui/assets/DocumentWorkspace-Dhdso07p.css +0 -1
  228. package/dist/ui/assets/PlansPage-CvfnMiWq.css +0 -1
  229. package/dist/ui/assets/PlansPage-DRXscYxH.js +0 -1
  230. package/dist/ui/assets/TaskSettings-DZX4jk7e.js +0 -9
  231. package/dist/ui/assets/index-9eT8e0Lu.css +0 -1
  232. package/dist/ui/assets/index-C6k75jr8.js +0 -6
@@ -3,7 +3,7 @@ import React from 'react';
3
3
  import { fireEvent, render, screen, waitFor } from '@testing-library/react';
4
4
  import userEvent from '@testing-library/user-event';
5
5
  import { describe, expect, it, vi } from 'vitest';
6
- import { PlanningDrawer, } from './PlanningDrawer';
6
+ import { normalizePlanningOwnerOptions, PlanningDrawer, } from './PlanningDrawer';
7
7
  vi.mock('@dnd-kit/core', async () => {
8
8
  const actual = await vi.importActual('@dnd-kit/core');
9
9
  return {
@@ -56,14 +56,16 @@ function renderPlanningDrawerHarness(options) {
56
56
  });
57
57
  }
58
58
  };
59
- return (_jsx(PlanningDrawer, { open: true, initiatives: [initiativeSummary], standaloneWorkstreams: [], activeInitiativeId: "", activeWorkstreamId: "", expandedInitiativeIds: new Set(['initiative-1']), detail: options?.detail ?? null, editor: editor, secondaryPane: secondaryPane, assigneeOptions: [], draftInitiativeSummary: draftInitiativeId === 'IN-12' ? initiativeSummary : null, draftTitle: draftTitle, draftDescription: draftDescription, draftOwner: draftOwner, draftInitiativeId: draftInitiativeId, onChangeDraftTitle: setDraftTitle, onChangeDraftDescription: setDraftDescription, onChangeDraftOwner: setDraftOwner, onChangeDraftInitiativeId: setDraftInitiativeId, onCollapseTreePane: () => { }, onExpandTreePane: () => { }, onCollapsePrimaryPane: () => { }, onExpandPrimaryPane: () => { }, onCollapseSecondaryPane: () => { }, onExpandSecondaryPane: () => { }, onBackFromSecondary: () => setSecondaryPane(null), treeCollapsed: false, primaryCollapsed: false, secondaryCollapsed: false, onCancelEditor: () => setEditor(null), onSubmitEditor: handleSubmitEditor, isSubmittingEditor: false, onCreateInitiative: () => { }, onCreateWorkstream: () => {
59
+ return (_jsx(PlanningDrawer, { open: true, initiatives: options?.initiatives ?? [initiativeSummary], standaloneWorkstreams: options?.standaloneWorkstreams ?? [], allStandaloneWorkstreams: options?.allStandaloneWorkstreams, activeInitiativeId: "", activeWorkstreamId: "", expandedInitiativeIds: new Set(['initiative-1']), detail: options?.detail ?? null, editor: editor, secondaryPane: secondaryPane, assigneeOptions: [], draftInitiativeSummary: draftInitiativeId === 'IN-12' ? initiativeSummary : null, draftTitle: draftTitle, draftDescription: draftDescription, draftOwner: draftOwner, draftInitiativeId: draftInitiativeId, onChangeDraftTitle: setDraftTitle, onChangeDraftDescription: setDraftDescription, onChangeDraftOwner: setDraftOwner, onChangeDraftInitiativeId: setDraftInitiativeId, onCollapseTreePane: () => { }, onExpandTreePane: () => { }, onCollapsePrimaryPane: () => { }, onExpandPrimaryPane: () => { }, onCollapseSecondaryPane: () => { }, onExpandSecondaryPane: () => { }, onBackFromSecondary: () => setSecondaryPane(null), treeCollapsed: false, primaryCollapsed: false, secondaryCollapsed: false, onCancelEditor: () => setEditor(null), onSubmitEditor: handleSubmitEditor, isSubmittingEditor: false, onCreateInitiative: () => { }, onCreateWorkstream: () => {
60
60
  setSecondaryPane(null);
61
61
  setDraftTitle('');
62
62
  setDraftDescription('');
63
63
  setDraftOwner('');
64
64
  setDraftInitiativeId('');
65
65
  setEditor({ mode: 'create', entityType: 'workstream' });
66
- }, onToggleInitiative: () => { }, onSelectInitiative: () => { }, onSelectWorkstream: () => { }, onOpenInitiativeDetails: () => { }, onOpenWorkstreamDetails: () => { }, onOpenNestedWorkstreamDetails: () => { }, onEditInitiative: () => { }, onEditWorkstream: () => { }, onArchiveInitiative: () => { }, onUnarchiveInitiative: () => { }, onArchiveWorkstream: () => { }, onUnarchiveWorkstream: () => { }, onCreateTaskInWorkstream: () => { }, onCreateWorkstreamInInitiative: (initiativeId) => {
66
+ }, onToggleInitiative: () => { }, onSelectInitiative: () => { }, onSelectWorkstream: () => { }, onOpenInitiativeDetails: () => { }, onOpenWorkstreamDetails: () => { }, onOpenNestedWorkstreamDetails: () => { }, onEditInitiative: () => { }, onEditWorkstream: (workstreamId) => {
67
+ setEditor({ mode: 'edit', entityType: 'workstream', targetId: workstreamId });
68
+ }, onArchiveInitiative: () => { }, onUnarchiveInitiative: () => { }, onArchiveWorkstream: () => { }, onUnarchiveWorkstream: () => { }, onCreateTaskInWorkstream: () => { }, onCreateWorkstreamInInitiative: (initiativeId) => {
67
69
  setDraftTitle('');
68
70
  setDraftDescription('');
69
71
  setDraftOwner('');
@@ -72,11 +74,40 @@ function renderPlanningDrawerHarness(options) {
72
74
  kind: 'editor',
73
75
  editor: { mode: 'create', entityType: 'workstream' },
74
76
  });
75
- }, onAssignInitiativeToWorkstream: () => { }, onRemovePlanningContextFile: removePlanningContextFile, onUpdatePlanningContextCaption: updatePlanningContextCaption }));
77
+ }, onAssignInitiativeToWorkstream: options?.onAssignInitiativeToWorkstream || (() => { }), onRemoveTaskFromWorkstream: options?.onRemoveTaskFromWorkstream, onAttachTaskToWorkstreamByReference: options?.onAttachTaskToWorkstreamByReference, onAttachWorkstreamToInitiativeByReference: options?.onAttachWorkstreamToInitiativeByReference, onRemovePlanningContextFile: removePlanningContextFile, onUpdatePlanningContextCaption: updatePlanningContextCaption }));
76
78
  }
77
79
  render(_jsx(Harness, {}));
78
80
  return { createWorkstream, removePlanningContextFile, submitEditor, updatePlanningContextCaption };
79
81
  }
82
+ describe('normalizePlanningOwnerOptions', () => {
83
+ it('omits the shared unassigned option because planning stores empty owner as unassigned', () => {
84
+ expect(normalizePlanningOwnerOptions([
85
+ { value: 'unassigned', label: 'Unassigned', kind: 'unassigned' },
86
+ { value: 'member-1', label: 'Member One', kind: 'member' },
87
+ ], '')).toEqual([
88
+ { value: 'member-1', label: 'Member One', kind: 'member' },
89
+ ]);
90
+ });
91
+ it('hides archived agents and collapses active duplicate agent profiles by username', () => {
92
+ expect(normalizePlanningOwnerOptions([
93
+ { value: 'ai-profile-archived', label: 'Cascade', kind: 'agent', username: 'cascade', archivedAt: '2026-04-24T15:28:13.577Z' },
94
+ { value: 'ai-profile-new', label: 'Codex', kind: 'agent', username: 'codex' },
95
+ { value: 'ai-profile-old', label: 'Codex', kind: 'agent', username: 'codex' },
96
+ { value: 'member-1', label: 'Member One', kind: 'member' },
97
+ ], '')).toEqual([
98
+ { value: 'ai-profile-new', label: 'Codex', kind: 'agent', username: 'codex' },
99
+ { value: 'member-1', label: 'Member One', kind: 'member' },
100
+ ]);
101
+ });
102
+ it('keeps the selected duplicate profile as the visible option while editing', () => {
103
+ expect(normalizePlanningOwnerOptions([
104
+ { value: 'ai-profile-new', label: 'Codex', kind: 'agent', username: 'codex' },
105
+ { value: 'ai-profile-old', label: 'Codex', kind: 'agent', username: 'codex' },
106
+ ], 'ai-profile-old')).toEqual([
107
+ { value: 'ai-profile-old', label: 'Codex', kind: 'agent', username: 'codex' },
108
+ ]);
109
+ });
110
+ });
80
111
  describe('PlanningDrawer workstream create submit', () => {
81
112
  it('submits the top-level workstream create editor with Enter', async () => {
82
113
  const user = userEvent.setup();
@@ -108,6 +139,363 @@ describe('PlanningDrawer workstream create submit', () => {
108
139
  });
109
140
  });
110
141
  describe('PlanningDrawer context attachments', () => {
142
+ it('removes a workstream from an initiative from the initiative detail row action', async () => {
143
+ const user = userEvent.setup();
144
+ const onAssignInitiativeToWorkstream = vi.fn();
145
+ const workstreamSummary = {
146
+ id: 'workstream-1',
147
+ referenceNumber: 44,
148
+ title: 'Execution Lane',
149
+ description: 'Ship the work',
150
+ initiativeId: 'initiative-1',
151
+ ownerLabel: null,
152
+ progressPercent: 0,
153
+ taskCount: 2,
154
+ completedTaskCount: 1,
155
+ tasks: [],
156
+ };
157
+ renderPlanningDrawerHarness({
158
+ detail: {
159
+ type: 'initiative',
160
+ item: {
161
+ ...initiativeSummary,
162
+ workstreamCount: 1,
163
+ workstreams: [workstreamSummary],
164
+ },
165
+ },
166
+ onAssignInitiativeToWorkstream,
167
+ });
168
+ await user.click(screen.getByRole('button', { name: /remove from initiative/i }));
169
+ expect(onAssignInitiativeToWorkstream).toHaveBeenCalledWith('workstream-1', null);
170
+ });
171
+ it('removes a linked workstream from its initiative from the detail header', async () => {
172
+ const user = userEvent.setup();
173
+ const onAssignInitiativeToWorkstream = vi.fn();
174
+ const workstreamSummary = {
175
+ id: 'workstream-1',
176
+ referenceNumber: 44,
177
+ title: 'Execution Lane',
178
+ description: 'Ship the work',
179
+ initiativeId: 'initiative-1',
180
+ ownerLabel: null,
181
+ progressPercent: 0,
182
+ taskCount: 0,
183
+ completedTaskCount: 0,
184
+ tasks: [],
185
+ };
186
+ renderPlanningDrawerHarness({
187
+ detail: { type: 'workstream', item: workstreamSummary },
188
+ onAssignInitiativeToWorkstream,
189
+ });
190
+ expect(screen.getAllByText('IN-12').length).toBeGreaterThan(0);
191
+ expect(screen.getByText('WS-44')).toBeInTheDocument();
192
+ await user.click(screen.getByRole('button', { name: /remove from initiative/i }));
193
+ expect(onAssignInitiativeToWorkstream).toHaveBeenCalledWith('workstream-1', null);
194
+ });
195
+ it('opens the workstream editor from the standalone workstream link action', async () => {
196
+ const user = userEvent.setup();
197
+ const workstreamSummary = {
198
+ id: 'workstream-1',
199
+ referenceNumber: 44,
200
+ title: 'Execution Lane',
201
+ description: 'Ship the work',
202
+ initiativeId: null,
203
+ ownerLabel: null,
204
+ progressPercent: 0,
205
+ taskCount: 0,
206
+ completedTaskCount: 0,
207
+ tasks: [],
208
+ };
209
+ renderPlanningDrawerHarness({
210
+ detail: { type: 'workstream', item: workstreamSummary },
211
+ });
212
+ await user.click(screen.getByRole('button', { name: /attach to initiative/i }));
213
+ expect(screen.getByPlaceholderText('Paste initiative reference (e.g. IN-123)')).toBeInTheDocument();
214
+ });
215
+ it('sets a workstream initiative from the editor link action', async () => {
216
+ const user = userEvent.setup();
217
+ const onAssignInitiativeToWorkstream = vi.fn();
218
+ const workstreamSummary = {
219
+ id: 'workstream-1',
220
+ referenceNumber: 44,
221
+ title: 'Execution Lane',
222
+ description: 'Ship the work',
223
+ initiativeId: null,
224
+ ownerLabel: null,
225
+ progressPercent: 0,
226
+ taskCount: 0,
227
+ completedTaskCount: 0,
228
+ tasks: [],
229
+ };
230
+ renderPlanningDrawerHarness({
231
+ detail: { type: 'workstream', item: workstreamSummary },
232
+ onAssignInitiativeToWorkstream,
233
+ });
234
+ await user.click(screen.getByRole('button', { name: /attach to initiative/i }));
235
+ await user.type(screen.getByPlaceholderText('Paste initiative reference (e.g. IN-123)'), 'IN-12');
236
+ await user.click(screen.getByRole('button', { name: /attach workstream to initiative/i }));
237
+ expect(onAssignInitiativeToWorkstream).toHaveBeenCalledWith('workstream-1');
238
+ expect(screen.queryByRole('button', { name: /clear initiative/i })).not.toBeInTheDocument();
239
+ });
240
+ it('attaches a task reference to the current workstream from the link action', async () => {
241
+ const user = userEvent.setup();
242
+ const onAttachTaskToWorkstreamByReference = vi.fn();
243
+ const workstreamSummary = {
244
+ id: 'workstream-1',
245
+ referenceNumber: 44,
246
+ title: 'Execution Lane',
247
+ description: 'Ship the work',
248
+ ownerLabel: null,
249
+ progressPercent: 0,
250
+ taskCount: 0,
251
+ completedTaskCount: 0,
252
+ tasks: [],
253
+ };
254
+ renderPlanningDrawerHarness({
255
+ detail: { type: 'workstream', item: workstreamSummary },
256
+ onAttachTaskToWorkstreamByReference,
257
+ });
258
+ await user.type(screen.getByPlaceholderText('Paste task reference (e.g. T-123)'), 'T-863');
259
+ await user.click(screen.getByRole('button', { name: /attach task to workstream/i }));
260
+ expect(onAttachTaskToWorkstreamByReference).toHaveBeenCalledWith('workstream-1', 'T-863');
261
+ expect(screen.getByPlaceholderText('Paste task reference (e.g. T-123)')).toHaveValue('');
262
+ });
263
+ it('removes a task from the current workstream from the task row hierarchy action', async () => {
264
+ const user = userEvent.setup();
265
+ const onRemoveTaskFromWorkstream = vi.fn();
266
+ const workstreamSummary = {
267
+ id: 'workstream-1',
268
+ referenceNumber: 44,
269
+ title: 'Execution Lane',
270
+ description: 'Ship the work',
271
+ ownerLabel: null,
272
+ progressPercent: 0,
273
+ taskCount: 1,
274
+ completedTaskCount: 0,
275
+ tasks: [{
276
+ id: 'task-1',
277
+ referenceNumber: 863,
278
+ title: 'Investigate bootstrap timeouts',
279
+ status: 'task',
280
+ }],
281
+ };
282
+ renderPlanningDrawerHarness({
283
+ detail: { type: 'workstream', item: workstreamSummary },
284
+ onRemoveTaskFromWorkstream,
285
+ });
286
+ await user.click(screen.getByRole('button', { name: /remove from workstream/i }));
287
+ expect(onRemoveTaskFromWorkstream).toHaveBeenCalledWith('task-1', 'workstream-1');
288
+ });
289
+ it('hides archived workstreams in initiative detail until the archive toggle is enabled', async () => {
290
+ const user = userEvent.setup();
291
+ const activeWorkstream = {
292
+ id: 'workstream-1',
293
+ referenceNumber: 44,
294
+ title: 'Execution Lane',
295
+ description: 'Ship the work',
296
+ initiativeId: 'initiative-1',
297
+ ownerLabel: null,
298
+ progressPercent: 0,
299
+ taskCount: 2,
300
+ completedTaskCount: 1,
301
+ tasks: [],
302
+ };
303
+ const archivedWorkstream = {
304
+ id: 'workstream-archived',
305
+ referenceNumber: 45,
306
+ title: 'Retired Lane',
307
+ description: 'Completed work',
308
+ initiativeId: 'initiative-1',
309
+ ownerLabel: null,
310
+ progressPercent: 100,
311
+ taskCount: 1,
312
+ completedTaskCount: 1,
313
+ isArchived: true,
314
+ tasks: [],
315
+ };
316
+ renderPlanningDrawerHarness({
317
+ detail: {
318
+ type: 'initiative',
319
+ item: {
320
+ ...initiativeSummary,
321
+ workstreamCount: 1,
322
+ workstreams: [activeWorkstream],
323
+ allWorkstreams: [activeWorkstream, archivedWorkstream],
324
+ },
325
+ },
326
+ });
327
+ expect(screen.getByText('Execution Lane')).toBeInTheDocument();
328
+ expect(screen.queryByText('Retired Lane')).not.toBeInTheDocument();
329
+ await user.click(screen.getByRole('button', { name: /show archived workstreams/i }));
330
+ expect(screen.getByText('Retired Lane')).toBeInTheDocument();
331
+ expect(screen.getByText(/1 tasks • 100% complete • archived/i)).toBeInTheDocument();
332
+ });
333
+ it('hides archived workstreams in the planning tray until the tray toggle is enabled', async () => {
334
+ const user = userEvent.setup();
335
+ const activeWorkstream = {
336
+ id: 'workstream-1',
337
+ referenceNumber: 44,
338
+ title: 'Execution Lane',
339
+ description: 'Ship the work',
340
+ initiativeId: 'initiative-1',
341
+ ownerLabel: null,
342
+ progressPercent: 0,
343
+ taskCount: 2,
344
+ completedTaskCount: 1,
345
+ tasks: [],
346
+ };
347
+ const archivedWorkstream = {
348
+ id: 'workstream-archived',
349
+ referenceNumber: 45,
350
+ title: 'Retired Lane',
351
+ description: 'Completed work',
352
+ initiativeId: 'initiative-1',
353
+ ownerLabel: null,
354
+ progressPercent: 100,
355
+ taskCount: 1,
356
+ completedTaskCount: 1,
357
+ isArchived: true,
358
+ tasks: [],
359
+ };
360
+ const standaloneWorkstream = {
361
+ id: 'workstream-standalone',
362
+ referenceNumber: 46,
363
+ title: 'Standalone Lane',
364
+ description: 'Independent work',
365
+ initiativeId: null,
366
+ ownerLabel: null,
367
+ progressPercent: 0,
368
+ taskCount: 0,
369
+ completedTaskCount: 0,
370
+ tasks: [],
371
+ };
372
+ const archivedStandaloneWorkstream = {
373
+ id: 'workstream-standalone-archived',
374
+ referenceNumber: 47,
375
+ title: 'Retired Standalone Lane',
376
+ description: 'Old independent work',
377
+ initiativeId: null,
378
+ ownerLabel: null,
379
+ progressPercent: 100,
380
+ taskCount: 1,
381
+ completedTaskCount: 1,
382
+ isArchived: true,
383
+ tasks: [],
384
+ };
385
+ renderPlanningDrawerHarness({
386
+ initiatives: [{
387
+ ...initiativeSummary,
388
+ workstreamCount: 1,
389
+ workstreams: [activeWorkstream],
390
+ allWorkstreams: [activeWorkstream, archivedWorkstream],
391
+ }],
392
+ standaloneWorkstreams: [standaloneWorkstream],
393
+ allStandaloneWorkstreams: [standaloneWorkstream, archivedStandaloneWorkstream],
394
+ });
395
+ expect(screen.getByText('Execution Lane')).toBeInTheDocument();
396
+ expect(screen.getByText('Standalone Lane')).toBeInTheDocument();
397
+ expect(screen.queryByText('Retired Lane')).not.toBeInTheDocument();
398
+ expect(screen.queryByText('Retired Standalone Lane')).not.toBeInTheDocument();
399
+ await user.click(screen.getByRole('button', { name: /show archived workstreams in tray/i }));
400
+ expect(screen.getByText('Retired Lane')).toBeInTheDocument();
401
+ expect(screen.getByText('Retired Standalone Lane')).toBeInTheDocument();
402
+ expect(screen.getAllByText(/archived/i).length).toBeGreaterThan(0);
403
+ });
404
+ it('hides archived tasks in workstream detail until the archive toggle is enabled', async () => {
405
+ const user = userEvent.setup();
406
+ const workstreamSummary = {
407
+ id: 'workstream-1',
408
+ referenceNumber: 44,
409
+ title: 'Execution Lane',
410
+ description: 'Ship the work',
411
+ ownerLabel: null,
412
+ progressPercent: 50,
413
+ taskCount: 1,
414
+ completedTaskCount: 0,
415
+ tasks: [
416
+ {
417
+ id: 'task-1',
418
+ referenceNumber: 863,
419
+ title: 'Investigate bootstrap timeouts',
420
+ status: 'task',
421
+ },
422
+ {
423
+ id: 'task-archived',
424
+ referenceNumber: 864,
425
+ title: 'Retired investigation',
426
+ status: 'done',
427
+ isArchived: true,
428
+ },
429
+ ],
430
+ };
431
+ renderPlanningDrawerHarness({
432
+ detail: { type: 'workstream', item: workstreamSummary },
433
+ });
434
+ expect(screen.getByText('Investigate bootstrap timeouts')).toBeInTheDocument();
435
+ expect(screen.queryByText('Retired investigation')).not.toBeInTheDocument();
436
+ await user.click(screen.getByRole('button', { name: /show archived tasks/i }));
437
+ expect(screen.getByText('Retired investigation')).toBeInTheDocument();
438
+ expect(screen.getByText(/Done • archived/i)).toBeInTheDocument();
439
+ });
440
+ it('renders workstream activity in the detail timeline', async () => {
441
+ const user = userEvent.setup();
442
+ const workstreamSummary = {
443
+ id: 'workstream-1',
444
+ referenceNumber: 44,
445
+ title: 'Execution Lane',
446
+ description: 'Ship the work',
447
+ ownerLabel: null,
448
+ progressPercent: 50,
449
+ taskCount: 1,
450
+ completedTaskCount: 0,
451
+ tasks: [
452
+ {
453
+ id: 'task-1',
454
+ referenceNumber: 863,
455
+ title: 'Investigate bootstrap timeouts',
456
+ status: 'task',
457
+ },
458
+ ],
459
+ activity: [
460
+ {
461
+ id: 'event:event-1',
462
+ type: 'event',
463
+ timestamp: '2026-04-28T09:00:00.000Z',
464
+ event: {
465
+ id: 'event-1',
466
+ workspaceId: 'workspace-1',
467
+ entityType: 'workstream',
468
+ entityId: 'workstream-1',
469
+ action: 'workstream-task-added',
470
+ actor: 'user',
471
+ actorType: 'human',
472
+ createdAt: '2026-04-28T09:00:00.000Z',
473
+ details: {
474
+ taskId: 'task-1',
475
+ taskTitle: 'Investigate bootstrap timeouts',
476
+ changes: {
477
+ workstreamId: {
478
+ from: null,
479
+ to: 'workstream-1',
480
+ },
481
+ },
482
+ },
483
+ },
484
+ },
485
+ ],
486
+ attachmentCount: 1,
487
+ attachments: [{ path: '/api/taskforce/context/spec.pdf', caption: '', timestamp: '' }],
488
+ };
489
+ renderPlanningDrawerHarness({
490
+ detail: { type: 'workstream', item: workstreamSummary },
491
+ });
492
+ const contextHeading = await screen.findByText('Context Documents (1)');
493
+ const activityHeading = screen.getByText('Recent activity (1)');
494
+ expect(Boolean(contextHeading.compareDocumentPosition(activityHeading) & Node.DOCUMENT_POSITION_FOLLOWING)).toBe(true);
495
+ expect(await screen.findByText('Task added: Investigate bootstrap timeouts')).toBeInTheDocument();
496
+ await user.click(screen.getByRole('button', { name: 'Recent activity (1)' }));
497
+ expect(screen.queryByText('Task added: Investigate bootstrap timeouts')).not.toBeInTheDocument();
498
+ });
111
499
  it('wires workstream context caption and remove actions through planning callbacks', async () => {
112
500
  const user = userEvent.setup();
113
501
  const workstreamSummary = {
@@ -40,6 +40,7 @@ const SPECS = [
40
40
  { key: 'R2_KEY_PREFIX', description: 'Optional R2 key prefix.', category: 'Storage', defaultValue: '', targets: ['server', 'all'] },
41
41
  { key: 'R2_SIGNED_UPLOAD_TTL_SECONDS', description: 'Upload signed URL TTL.', category: 'Storage', defaultValue: '600', targets: ['server', 'all'] },
42
42
  { key: 'R2_SIGNED_DOWNLOAD_TTL_SECONDS', description: 'Download signed URL TTL.', category: 'Storage', defaultValue: '300', targets: ['server', 'all'] },
43
+ { key: 'R2_REQUEST_TIMEOUT_MS', description: 'R2 request timeout in ms.', category: 'Storage', defaultValue: '15000', targets: ['server', 'all'] },
43
44
  { key: 'TASKFORCE_VIRUS_SCAN_PROVIDER', description: 'Virus scanner provider (none|clamav).', category: 'Security', defaultValue: 'none', targets: ['server', 'all'] },
44
45
  { key: 'TASKFORCE_VIRUS_SCAN_FAIL_CLOSED', description: 'Fail uploads closed on scan failures.', category: 'Security', defaultValue: 'false', targets: ['server', 'all'] },
45
46
  { key: 'TASKFORCE_VIRUS_SCAN_TIMEOUT_MS', description: 'Virus scan timeout in ms.', category: 'Security', defaultValue: '15000', targets: ['server', 'all'] },
@@ -11,6 +11,8 @@ export interface AiProfileRecordLike {
11
11
  color: string;
12
12
  avatarUrl?: string | null;
13
13
  avatarSourceUrl?: string | null;
14
+ avatarRevision?: number;
15
+ avatarUpdatedAt?: string | null;
14
16
  description?: string | null;
15
17
  role?: string | null;
16
18
  provider?: string | null;
@@ -98,6 +100,11 @@ export declare function updateAiProfileAvatarService(deps: AiProfileServiceDeps,
98
100
  avatarUrl?: string | null;
99
101
  avatarSourceUrl?: string | null;
100
102
  }): AiProfileRecordLike;
103
+ export declare function updateAiProfileSurfaceTypeService(deps: AiProfileServiceDeps, input: {
104
+ workspaceId: string;
105
+ profileId: string;
106
+ surfaceType?: AiProfileSurfaceType | null;
107
+ }): AiProfileRecordLike;
101
108
  export declare function archiveAiProfileService(deps: AiProfileServiceDeps, input: {
102
109
  workspaceId: string;
103
110
  profileId: string;