@taskforcehq/taskforce 0.3.305 → 0.3.307

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 (440) hide show
  1. package/README.md +0 -2
  2. package/dist/Taskforce.module.css +603 -107
  3. package/dist/TaskforceCore.js +216 -43
  4. package/dist/TaskforceCore.test.js +611 -28
  5. package/dist/assets/fonts/CourierPrime-Bold.woff2 +0 -0
  6. package/dist/assets/fonts/CourierPrime-BoldItalic.woff2 +0 -0
  7. package/dist/assets/fonts/CourierPrime-Italic.woff2 +0 -0
  8. package/dist/assets/fonts/CourierPrime-Regular.woff2 +0 -0
  9. package/dist/assets/fonts/Noir_medium.woff2 +0 -0
  10. package/dist/assets/fonts/Noir_regular.woff2 +0 -0
  11. package/dist/assets/fonts/RussoOne-Regular.woff2 +0 -0
  12. package/dist/assets/fonts/SpecialElite-Regular.woff2 +0 -0
  13. package/dist/compat/workspaceSyncCompat.js +46 -2
  14. package/dist/compat/workspaceSyncCompat.test.js +38 -1
  15. package/dist/components/activity/EntityActivityTimeline.d.ts +34 -0
  16. package/dist/components/activity/EntityActivityTimeline.js +81 -0
  17. package/dist/components/context/ContextAttachmentManager.d.ts +14 -0
  18. package/dist/components/context/ContextAttachmentManager.js +561 -0
  19. package/dist/components/features/AgentsModule.d.ts +14 -1
  20. package/dist/components/features/AgentsModule.js +366 -18
  21. package/dist/components/features/AgentsModule.test.js +507 -8
  22. package/dist/components/features/AnnotatedAttachmentWorkspace.d.ts +3 -1
  23. package/dist/components/features/AnnotatedAttachmentWorkspace.js +118 -4
  24. package/dist/components/features/AnnotatedAttachmentWorkspace.test.js +52 -0
  25. package/dist/components/features/DocumentIndex.d.ts +2 -2
  26. package/dist/components/features/DocumentIndex.js +3 -4
  27. package/dist/components/features/DocumentViewer.d.ts +7 -1
  28. package/dist/components/features/DocumentViewer.js +62 -4
  29. package/dist/components/features/DocumentWorkspace.d.ts +10 -2
  30. package/dist/components/features/DocumentWorkspace.js +15 -4
  31. package/dist/components/features/DocumentWorkspace.test.js +51 -1
  32. package/dist/components/features/TaskSettings.d.ts +2 -0
  33. package/dist/components/features/TaskSettings.js +83 -78
  34. package/dist/components/features/TaskSettings.test.js +171 -82
  35. package/dist/components/features/documentWorkspaceModel.d.ts +1 -1
  36. package/dist/components/task/TaskActionHeader.js +11 -5
  37. package/dist/components/task/TaskActionHeader.test.js +10 -0
  38. package/dist/components/task/TaskCard.js +24 -29
  39. package/dist/components/task/TaskCard.test.js +38 -3
  40. package/dist/components/task/TaskContextUpload.js +4 -526
  41. package/dist/components/task/TaskContextUpload.test.js +3 -3
  42. package/dist/components/task/TaskForm.js +61 -116
  43. package/dist/components/task/TaskForm.test.js +110 -8
  44. package/dist/components/task/TaskKanban.d.ts +2 -1
  45. package/dist/components/task/TaskKanban.js +94 -21
  46. package/dist/components/task/TaskKanban.test.js +36 -0
  47. package/dist/components/task/TaskList.js +5 -1
  48. package/dist/components/task/TaskList.test.js +4 -0
  49. package/dist/components/ui/EditableAvatarButton.d.ts +18 -0
  50. package/dist/components/ui/EditableAvatarButton.js +18 -0
  51. package/dist/components/ui/HierarchyActionBadge.d.ts +10 -0
  52. package/dist/components/ui/HierarchyActionBadge.js +5 -0
  53. package/dist/components/views/AppShellHeader.d.ts +3 -1
  54. package/dist/components/views/AppShellHeader.js +27 -11
  55. package/dist/components/views/AppShellHeader.test.js +18 -1
  56. package/dist/components/views/PlanComparisonPage.d.ts +9 -1
  57. package/dist/components/views/PlanComparisonPage.js +78 -14
  58. package/dist/components/views/PlanComparisonPage.test.js +257 -0
  59. package/dist/components/views/PlansPage.js +127 -36
  60. package/dist/components/views/PlansPage.test.js +294 -12
  61. package/dist/components/views/StandaloneLayout.js +529 -592
  62. package/dist/components/views/WidgetView.js +10 -8
  63. package/dist/components/views/panels/PlanningDrawer.d.ts +25 -5
  64. package/dist/components/views/panels/PlanningDrawer.js +270 -55
  65. package/dist/components/views/panels/PlanningDrawer.test.d.ts +1 -0
  66. package/dist/components/views/panels/PlanningDrawer.test.js +529 -0
  67. package/dist/components/views/planningScope.d.ts +20 -0
  68. package/dist/components/views/planningScope.js +25 -0
  69. package/dist/components/views/planningScope.test.d.ts +1 -0
  70. package/dist/components/views/planningScope.test.js +60 -0
  71. package/dist/components/views/standalone/modals/AccountHubModal.d.ts +26 -1
  72. package/dist/components/views/standalone/modals/AccountHubModal.js +76 -2
  73. package/dist/components/views/standalone/modals/AccountHubModal.test.js +20 -3
  74. package/dist/components/views/standalone/modals/AvatarImageManagerModal.d.ts +20 -0
  75. package/dist/components/views/standalone/modals/AvatarImageManagerModal.js +127 -0
  76. package/dist/components/views/standalone/modals/EditProfileModal.d.ts +2 -9
  77. package/dist/components/views/standalone/modals/EditProfileModal.js +3 -2
  78. package/dist/components/views/standalone/modals/SyncEnableWarningModal.d.ts +9 -0
  79. package/dist/components/views/standalone/modals/SyncEnableWarningModal.js +6 -0
  80. package/dist/components/views/standalone/modals/SyncEnableWarningModal.test.d.ts +1 -0
  81. package/dist/components/views/standalone/modals/SyncEnableWarningModal.test.js +24 -0
  82. package/dist/components/views/standalone/modals/SyncStatusModal.d.ts +2 -1
  83. package/dist/components/views/standalone/modals/SyncStatusModal.js +2 -2
  84. package/dist/config/envSchema.js +1 -0
  85. package/dist/core/AiProfileService.d.ts +46 -2
  86. package/dist/core/AiProfileService.js +1073 -82
  87. package/dist/core/AiProfileService.test.js +19 -1
  88. package/dist/core/AiProfiles.test.js +1249 -44
  89. package/dist/core/AttachmentLinkService.d.ts +3 -0
  90. package/dist/core/AttachmentLinkService.js +110 -23
  91. package/dist/core/AuthIdentityService.d.ts +107 -0
  92. package/dist/core/AuthIdentityService.js +284 -0
  93. package/dist/core/AuthTokenService.d.ts +4 -0
  94. package/dist/core/AuthTokenService.js +34 -6
  95. package/dist/core/AuthTokenService.test.js +3 -3
  96. package/dist/core/EntitlementsPolicy.test.js +2 -2
  97. package/dist/core/GlobalSettingsService.js +127 -6
  98. package/dist/core/McpTokenService.d.ts +12 -0
  99. package/dist/core/McpTokenService.js +44 -1
  100. package/dist/core/PlanEntitlementService.d.ts +23 -2
  101. package/dist/core/PlanEntitlementService.js +343 -57
  102. package/dist/core/PlanFeatureCatalog.test.d.ts +1 -0
  103. package/dist/core/PlanFeatureCatalog.test.js +84 -0
  104. package/dist/core/PlanVersionPolicy.test.js +35 -2
  105. package/dist/core/PlanningEntities.test.js +418 -4
  106. package/dist/core/SignupMonetizationSettings.test.js +199 -27
  107. package/dist/core/SyncReconciliationService.js +2 -65
  108. package/dist/core/SystemAdmin.test.js +42 -42
  109. package/dist/core/TaskActivityService.d.ts +28 -1
  110. package/dist/core/TaskActivityService.js +227 -46
  111. package/dist/core/TaskActivitySync.test.d.ts +1 -0
  112. package/dist/core/TaskActivitySync.test.js +90 -0
  113. package/dist/core/TaskAttachmentsCanonical.test.js +51 -1
  114. package/dist/core/TaskAuditTimeline.test.js +96 -0
  115. package/dist/core/Taskforce.d.ts +415 -40
  116. package/dist/core/Taskforce.ids.test.js +58 -0
  117. package/dist/core/Taskforce.js +1543 -266
  118. package/dist/core/Taskforce.listTasksSlim.test.d.ts +1 -0
  119. package/dist/core/Taskforce.listTasksSlim.test.js +91 -0
  120. package/dist/core/Taskforce.mcpAuth.test.js +109 -0
  121. package/dist/core/UserProfileAvatarDrafts.test.js +30 -3
  122. package/dist/core/WorkspaceLifecycleService.js +0 -5
  123. package/dist/core/WorkspacePermissions.test.js +25 -1
  124. package/dist/core/WorkspacePlanMode.test.js +117 -16
  125. package/dist/core/shared.d.ts +1 -1
  126. package/dist/core/shared.js +5 -1
  127. package/dist/core/types.d.ts +75 -2
  128. package/dist/hooks/auth/useTaskforceAuthBootstrap.js +28 -2
  129. package/dist/hooks/bootstrapTimeouts.d.ts +4 -0
  130. package/dist/hooks/bootstrapTimeouts.js +8 -0
  131. package/dist/hooks/bootstrapTimeouts.test.d.ts +1 -0
  132. package/dist/hooks/bootstrapTimeouts.test.js +12 -0
  133. package/dist/hooks/sync/bootstrap.js +2 -0
  134. package/dist/hooks/sync/controlPlane.d.ts +37 -0
  135. package/dist/hooks/sync/controlPlane.js +78 -0
  136. package/dist/hooks/sync/controlPlane.test.d.ts +1 -0
  137. package/dist/hooks/sync/controlPlane.test.js +121 -0
  138. package/dist/hooks/sync/lifecyclePolicy.d.ts +45 -0
  139. package/dist/hooks/sync/lifecyclePolicy.js +93 -0
  140. package/dist/hooks/sync/lifecyclePolicy.test.d.ts +1 -0
  141. package/dist/hooks/sync/lifecyclePolicy.test.js +124 -0
  142. package/dist/hooks/sync/orchestratorShared.d.ts +13 -4
  143. package/dist/hooks/sync/orchestratorShared.js +129 -48
  144. package/dist/hooks/sync/orchestratorShared.test.js +50 -1
  145. package/dist/hooks/sync/pullLifecycle.d.ts +31 -0
  146. package/dist/hooks/sync/pullLifecycle.js +24 -0
  147. package/dist/hooks/sync/pullLifecycle.test.d.ts +1 -0
  148. package/dist/hooks/sync/pullLifecycle.test.js +80 -0
  149. package/dist/hooks/sync/recovery.d.ts +16 -2
  150. package/dist/hooks/sync/recovery.js +179 -53
  151. package/dist/hooks/sync/recovery.test.d.ts +1 -0
  152. package/dist/hooks/sync/recovery.test.js +292 -0
  153. package/dist/hooks/sync/transfers.d.ts +16 -3
  154. package/dist/hooks/sync/transfers.js +210 -66
  155. package/dist/hooks/sync/transfers.test.d.ts +1 -0
  156. package/dist/hooks/sync/transfers.test.js +928 -0
  157. package/dist/hooks/sync/useRecentSyncEvents.d.ts +21 -0
  158. package/dist/hooks/sync/useRecentSyncEvents.js +92 -0
  159. package/dist/hooks/sync/useRecentSyncEvents.test.d.ts +1 -0
  160. package/dist/hooks/sync/useRecentSyncEvents.test.js +124 -0
  161. package/dist/hooks/sync/useSyncStatusActions.d.ts +30 -0
  162. package/dist/hooks/sync/useSyncStatusActions.js +88 -0
  163. package/dist/hooks/sync/useSyncStatusActions.test.d.ts +1 -0
  164. package/dist/hooks/sync/useSyncStatusActions.test.js +133 -0
  165. package/dist/hooks/sync/useSyncStatusControls.d.ts +25 -0
  166. package/dist/hooks/sync/useSyncStatusControls.js +60 -0
  167. package/dist/hooks/sync/useSyncStatusControls.test.d.ts +1 -0
  168. package/dist/hooks/sync/useSyncStatusControls.test.js +88 -0
  169. package/dist/hooks/useFilterSort.js +13 -13
  170. package/dist/hooks/useFilterSort.test.js +87 -1
  171. package/dist/hooks/useSyncOrchestrator.aiProfiles.test.js +12 -0
  172. package/dist/hooks/useSyncOrchestrator.d.ts +9 -2
  173. package/dist/hooks/useSyncOrchestrator.js +409 -183
  174. package/dist/hooks/useSyncOrchestrator.retry-closure.test.js +407 -0
  175. package/dist/hooks/useTaskData.js +8 -3
  176. package/dist/hooks/useTaskData.test.js +45 -0
  177. package/dist/hooks/useTaskforce.d.ts +145 -52
  178. package/dist/hooks/useTaskforce.js +592 -253
  179. package/dist/hooks/useTaskforce.runtime-routing.test.d.ts +1 -0
  180. package/dist/hooks/useTaskforce.runtime-routing.test.js +246 -0
  181. package/dist/hooks/useTaskforce.sync-behavior.test.js +2468 -201
  182. package/dist/hooks/useWorkspaceSyncController.d.ts +1 -0
  183. package/dist/hooks/useWorkspaceSyncController.js +13 -2
  184. package/dist/hooks/useWorkspaceSyncController.test.js +48 -0
  185. package/dist/hooks/workspace/useTaskforceWorkspaceBootstrap.d.ts +16 -0
  186. package/dist/hooks/workspace/useTaskforceWorkspaceBootstrap.js +328 -40
  187. package/dist/localization/locales/en-US.d.ts +0 -1
  188. package/dist/localization/locales/en-US.js +0 -1
  189. package/dist/localization/locales/es-419.js +0 -1
  190. package/dist/localization/locales/pt-BR.js +0 -1
  191. package/dist/mcp/canonicalAssetHelpers.d.ts +16 -4
  192. package/dist/mcp/canonicalAssetHelpers.js +18 -10
  193. package/dist/mcp/canonicalAssetHelpers.test.d.ts +1 -0
  194. package/dist/mcp/canonicalAssetHelpers.test.js +67 -0
  195. package/dist/mcp/clientIntegrations.d.ts +1 -0
  196. package/dist/mcp/clientIntegrations.js +25 -0
  197. package/dist/mcp/clientIntegrations.test.js +14 -3
  198. package/dist/mcp/clientProfileDefaults.js +3 -3
  199. package/dist/mcp/collaborationRegistrar.js +7 -7
  200. package/dist/mcp/documentAssetRegistrar.js +116 -10
  201. package/dist/mcp/runtime.d.ts +3 -0
  202. package/dist/mcp/runtime.js +1099 -185
  203. package/dist/mcp/runtime.test.js +1491 -140
  204. package/dist/mcp/taskAttachmentTypes.d.ts +1 -1
  205. package/dist/mcp/taskPlanningRegistrar.js +48 -0
  206. package/dist/mcp/toolRegistry.d.ts +56 -0
  207. package/dist/mcp/toolRegistry.js +8 -0
  208. package/dist/mcp/toolRegistry.test.js +4 -0
  209. package/dist/migrations/taskSchemaMigrations.d.ts +1 -1
  210. package/dist/migrations/taskSchemaMigrations.js +192 -61
  211. package/dist/migrations/taskSchemaMigrations.test.js +15 -0
  212. package/dist/resources/templates/workflow-sources/workflowDocs.mjs +6 -6
  213. package/dist/resources/templates/workflows/collaborate.yaml +1 -1
  214. package/dist/resources/templates/workflows/evaluate.yaml +2 -2
  215. package/dist/resources/templates/workflows/plan.yaml +1 -1
  216. package/dist/resources/templates/workflows/review.yaml +2 -2
  217. package/dist/server/aiProfileAvatarAssets.d.ts +49 -0
  218. package/dist/server/aiProfileAvatarAssets.js +319 -0
  219. package/dist/server/aiProfileAvatarAssets.test.d.ts +1 -0
  220. package/dist/server/aiProfileAvatarAssets.test.js +213 -0
  221. package/dist/server/annotatedAttachmentsRoutes.test.js +69 -10
  222. package/dist/server/auth/providers/apple.d.ts +33 -0
  223. package/dist/server/auth/providers/apple.js +82 -0
  224. package/dist/server/auth/providers/appleClientSecret.d.ts +37 -0
  225. package/dist/server/auth/providers/appleClientSecret.js +65 -0
  226. package/dist/server/auth/providers/github.d.ts +26 -0
  227. package/dist/server/auth/providers/github.js +86 -0
  228. package/dist/server/auth/providers/google.d.ts +21 -0
  229. package/dist/server/auth/providers/google.js +56 -0
  230. package/dist/server/auth/providers/types.d.ts +21 -0
  231. package/dist/server/auth/providers/types.js +1 -0
  232. package/dist/server/auth.d.ts +2 -0
  233. package/dist/server/auth.js +6 -3
  234. package/dist/server/documentReviewRoutes.test.js +36 -3
  235. package/dist/server/index.cookieProxy.test.js +1 -0
  236. package/dist/server/index.d.ts +12 -0
  237. package/dist/server/index.js +124 -9
  238. package/dist/server/index.rateLimit.test.js +3 -0
  239. package/dist/server/index.test.js +111 -3
  240. package/dist/server/routes/admin.d.ts +3 -0
  241. package/dist/server/routes/admin.js +459 -28
  242. package/dist/server/routes/annotatedAttachments.js +51 -0
  243. package/dist/server/routes/auth.d.ts +26 -2
  244. package/dist/server/routes/auth.js +1170 -71
  245. package/dist/server/routes/authSupport.d.ts +5 -1
  246. package/dist/server/routes/authSupport.js +13 -2
  247. package/dist/server/routes/billing.js +372 -35
  248. package/dist/server/routes/billing.test.js +595 -12
  249. package/dist/server/routes/documents.d.ts +1 -0
  250. package/dist/server/routes/documents.js +182 -70
  251. package/dist/server/routes/shared.d.ts +28 -2
  252. package/dist/server/routes/shared.js +69 -14
  253. package/dist/server/routes/sync.integration.test.js +658 -39
  254. package/dist/server/routes/sync.js +74 -608
  255. package/dist/server/routes/syncAuxRoutes.d.ts +15 -0
  256. package/dist/server/routes/syncAuxRoutes.js +91 -0
  257. package/dist/server/routes/syncAuxRoutes.test.d.ts +1 -0
  258. package/dist/server/routes/syncAuxRoutes.test.js +158 -0
  259. package/dist/server/routes/syncPullApplyRoutes.d.ts +36 -0
  260. package/dist/server/routes/syncPullApplyRoutes.js +204 -0
  261. package/dist/server/routes/syncPushRoutes.d.ts +24 -0
  262. package/dist/server/routes/syncPushRoutes.js +212 -0
  263. package/dist/server/routes/syncRouteGuards.d.ts +36 -0
  264. package/dist/server/routes/syncRouteGuards.js +67 -0
  265. package/dist/server/routes/syncRouteGuards.test.d.ts +1 -0
  266. package/dist/server/routes/syncRouteGuards.test.js +94 -0
  267. package/dist/server/routes/syncRouteTypes.d.ts +13 -0
  268. package/dist/server/routes/syncRouteTypes.js +1 -0
  269. package/dist/server/routes/syncSnapshotRoutes.d.ts +66 -0
  270. package/dist/server/routes/syncSnapshotRoutes.js +182 -0
  271. package/dist/server/routes/tasks.js +55 -10
  272. package/dist/server/routes/workspaces.js +36 -5
  273. package/dist/server/routes.js +147 -11
  274. package/dist/server/routes.test.js +2704 -183
  275. package/dist/services/workflowExportSnapshots.test.js +2 -0
  276. package/dist/shared/aiProfileSeatScope.d.ts +5 -0
  277. package/dist/shared/aiProfileSeatScope.js +21 -0
  278. package/dist/shared/aiProfileSurfaceType.d.ts +1 -1
  279. package/dist/shared/aiProfileSurfaceType.js +10 -8
  280. package/dist/shared/passwordPolicy.d.ts +13 -0
  281. package/dist/shared/passwordPolicy.js +84 -0
  282. package/dist/shared/passwordPolicy.test.d.ts +1 -0
  283. package/dist/shared/passwordPolicy.test.js +34 -0
  284. package/dist/storage/objectStorage.d.ts +1 -0
  285. package/dist/storage/objectStorage.js +2 -1
  286. package/dist/storage/objectStorageClient.d.ts +1 -0
  287. package/dist/storage/objectStorageClient.js +41 -8
  288. package/dist/storage/objectStorageClient.test.js +26 -1
  289. package/dist/storage/objectStorageConfig.js +3 -1
  290. package/dist/storage/objectStorageConfig.test.js +13 -1
  291. package/dist/storage/workspaceAssetStore.d.ts +15 -3
  292. package/dist/storage/workspaceAssetStore.js +132 -44
  293. package/dist/storage/workspaceAssetStore.test.js +93 -6
  294. package/dist/styles/fonts.css +70 -0
  295. package/dist/sync/collaborationSyncPayload.d.ts +6 -1
  296. package/dist/sync/collaborationSyncPayload.js +19 -11
  297. package/dist/sync/collaborationSyncPayload.test.js +14 -1
  298. package/dist/sync/collaborationSyncState.d.ts +3 -3
  299. package/dist/sync/collaborationSyncState.js +19 -12
  300. package/dist/sync/contentSyncPayload.d.ts +5 -1
  301. package/dist/sync/contentSyncPayload.js +9 -0
  302. package/dist/sync/contentSyncPayload.test.js +39 -0
  303. package/dist/sync/contentSyncState.d.ts +9 -9
  304. package/dist/sync/contentSyncState.js +248 -107
  305. package/dist/sync/planningSyncPayload.js +5 -0
  306. package/dist/sync/planningSyncPayload.test.js +24 -0
  307. package/dist/sync/syncApplyHandlers.d.ts +11 -4
  308. package/dist/sync/syncApplyHandlers.js +115 -42
  309. package/dist/sync/syncApplyHandlers.test.js +251 -4
  310. package/dist/sync/syncFieldSemantics.d.ts +12 -0
  311. package/dist/sync/syncFieldSemantics.js +55 -0
  312. package/dist/sync/syncResourceAdapters.d.ts +46 -0
  313. package/dist/sync/syncResourceAdapters.js +79 -0
  314. package/dist/sync/syncService.d.ts +6 -0
  315. package/dist/sync/syncService.js +65 -1
  316. package/dist/sync/syncService.test.js +96 -3
  317. package/dist/sync/taskSyncChangeKey.d.ts +2 -0
  318. package/dist/sync/taskSyncChangeKey.js +143 -0
  319. package/dist/sync/taskSyncPayload.js +3 -8
  320. package/dist/sync/workspacePullFeed.d.ts +4 -0
  321. package/dist/sync/workspacePullFeed.js +132 -72
  322. package/dist/sync/workspacePullFeed.test.js +92 -0
  323. package/dist/sync/workspaceRepair.js +5 -2
  324. package/dist/sync/workspaceSyncModel.d.ts +73 -6
  325. package/dist/sync/workspaceSyncModel.js +342 -73
  326. package/dist/sync/workspaceSyncModel.test.js +559 -14
  327. package/dist/sync/workspaceSyncState.d.ts +17 -0
  328. package/dist/sync/workspaceSyncState.js +2 -1
  329. package/dist/sync/workspaceSyncSurface.js +3 -8
  330. package/dist/types.d.ts +50 -2
  331. package/dist/ui/assets/AgentsModule-2y82_3DO.js +1 -0
  332. package/dist/ui/assets/AnnotatedAttachmentWorkspace-BTLZZCiQ.js +3 -0
  333. package/dist/ui/assets/AnnotatedAttachmentWorkspace-BVKew-mH.css +1 -0
  334. package/dist/ui/assets/ContextAttachmentManager-D2epuNnG.js +3 -0
  335. package/dist/ui/assets/CourierPrime-Bold-BuLj_dpw.woff2 +0 -0
  336. package/dist/ui/assets/CourierPrime-BoldItalic-BrK2pUkZ.woff2 +0 -0
  337. package/dist/ui/assets/CourierPrime-Italic-DFUZK5Ey.woff2 +0 -0
  338. package/dist/ui/assets/CourierPrime-Regular-BsKphzVf.woff2 +0 -0
  339. package/dist/ui/assets/DocumentWorkspace-D4rJKXzd.css +1 -0
  340. package/dist/ui/assets/DocumentWorkspace-sVa-3Do6.js +252 -0
  341. package/dist/ui/assets/EntityActivityTimeline-DyqvvhuF.css +1 -0
  342. package/dist/ui/assets/EntityActivityTimeline-Zods27y_.js +1 -0
  343. package/dist/ui/assets/{InitiativesModule-Dhm7M8e7.js → InitiativesModule-D4XseTwW.js} +1 -1
  344. package/dist/ui/assets/Noir_medium-bxQwKGzB.woff2 +0 -0
  345. package/dist/ui/assets/Noir_regular-ojf2kxlG.woff2 +0 -0
  346. package/dist/ui/assets/PlansPage-D5AcbO0L.js +1 -0
  347. package/dist/ui/assets/PlansPage-Dvqsz5zc.css +1 -0
  348. package/dist/ui/assets/RussoOne-Regular-Cu_qq_qC.woff2 +0 -0
  349. package/dist/ui/assets/SpecialElite-Regular-Di6gSvAS.woff2 +0 -0
  350. package/dist/ui/assets/TaskContextUpload-uC5qx4Bv.js +1 -0
  351. package/dist/ui/assets/TaskSettings-CqvPAsTv.js +9 -0
  352. package/dist/ui/assets/{WorkflowsModule-BcS4afRn.js → WorkflowsModule-oOA9bcdK.js} +4 -4
  353. package/dist/ui/assets/documentReferences-DPZuTgHh.js +1 -0
  354. package/dist/ui/assets/index-607WPo_X.js +4 -0
  355. package/dist/ui/assets/index-CYyodXsg.css +1 -0
  356. package/dist/ui/assets/{vendor-icons-BSUaRwF8.js → vendor-icons-B32hGuF2.js} +1 -1
  357. package/dist/ui/fonts/CourierPrime-Bold.ttf +0 -0
  358. package/dist/ui/fonts/CourierPrime-Bold.woff2 +0 -0
  359. package/dist/ui/fonts/CourierPrime-BoldItalic.ttf +0 -0
  360. package/dist/ui/fonts/CourierPrime-BoldItalic.woff2 +0 -0
  361. package/dist/ui/fonts/CourierPrime-Italic.ttf +0 -0
  362. package/dist/ui/fonts/CourierPrime-Italic.woff2 +0 -0
  363. package/dist/ui/fonts/CourierPrime-Regular.ttf +0 -0
  364. package/dist/ui/fonts/CourierPrime-Regular.woff2 +0 -0
  365. package/dist/ui/fonts/Noir_medium.otf +0 -0
  366. package/dist/ui/fonts/Noir_medium.woff2 +0 -0
  367. package/dist/ui/fonts/Noir_regular.otf +0 -0
  368. package/dist/ui/fonts/Noir_regular.woff2 +0 -0
  369. package/dist/ui/fonts/RussoOne-Regular.woff2 +0 -0
  370. package/dist/ui/fonts/SpecialElite-Regular.ttf +0 -0
  371. package/dist/ui/fonts/SpecialElite-Regular.woff2 +0 -0
  372. package/dist/ui/index.html +3 -3
  373. package/dist/ui/product/agent manager.png +0 -0
  374. package/dist/ui/product/agent_manager_02.png +0 -0
  375. package/dist/utils/accountProfileSummaryCache.d.ts +5 -0
  376. package/dist/utils/aiProfileEvents.d.ts +7 -0
  377. package/dist/utils/aiProfileEvents.js +6 -0
  378. package/dist/utils/assignees.d.ts +8 -0
  379. package/dist/utils/assignees.js +10 -1
  380. package/dist/utils/assignees.test.d.ts +1 -0
  381. package/dist/utils/assignees.test.js +25 -0
  382. package/dist/utils/avatarDisplay.d.ts +1 -0
  383. package/dist/utils/avatarDisplay.js +15 -0
  384. package/dist/utils/avatarUpload.d.ts +27 -0
  385. package/dist/utils/avatarUpload.js +132 -0
  386. package/dist/utils/avatarUpload.test.d.ts +1 -0
  387. package/dist/utils/avatarUpload.test.js +40 -0
  388. package/dist/utils/bootstrapTrace.d.ts +7 -0
  389. package/dist/utils/bootstrapTrace.js +29 -0
  390. package/dist/utils/commercialLifecyclePresentation.js +3 -0
  391. package/dist/utils/contextAssetEvents.d.ts +2 -0
  392. package/dist/utils/contextFiles.d.ts +3 -1
  393. package/dist/utils/contextFiles.js +2 -1
  394. package/dist/utils/syncEventDetails.d.ts +3 -0
  395. package/dist/utils/syncEventDetails.js +64 -0
  396. package/dist/utils/syncEventDetails.test.d.ts +1 -0
  397. package/dist/utils/syncEventDetails.test.js +23 -0
  398. package/dist/utils/syncEventPresentation.d.ts +15 -0
  399. package/dist/utils/syncEventPresentation.js +129 -0
  400. package/dist/utils/syncEventPresentation.test.d.ts +1 -0
  401. package/dist/utils/syncEventPresentation.test.js +64 -0
  402. package/dist/utils/syncStatusPresentation.d.ts +72 -0
  403. package/dist/utils/syncStatusPresentation.js +217 -0
  404. package/dist/utils/syncStatusPresentation.test.d.ts +1 -0
  405. package/dist/utils/syncStatusPresentation.test.js +164 -0
  406. package/dist/utils/taskActivity.d.ts +9 -1
  407. package/dist/utils/taskActivity.js +290 -22
  408. package/dist/utils/taskActivity.test.js +141 -8
  409. package/dist/utils/taskSearch.d.ts +22 -0
  410. package/dist/utils/taskSearch.js +87 -0
  411. package/dist/utils/taskSearch.test.d.ts +1 -0
  412. package/dist/utils/taskSearch.test.js +69 -0
  413. package/dist/utils/taskUtils.js +10 -3
  414. package/dist/utils/taskUtils.test.d.ts +1 -0
  415. package/dist/utils/taskUtils.test.js +19 -0
  416. package/dist/utils/workspaceSyncPresentation.d.ts +4 -0
  417. package/dist/utils/workspaceSyncPresentation.js +68 -2
  418. package/dist/utils/workspaceSyncPresentation.test.js +36 -1
  419. package/package.json +3 -2
  420. package/scripts/export-sqlite-to-postgres.mjs +7 -9
  421. package/scripts/run-e2e-realtime.sh +17 -3
  422. package/scripts/run-smoke.sh +17 -5
  423. package/scripts/run-soak.sh +17 -5
  424. package/src/resources/templates/workflow-sources/workflowDocs.mjs +6 -6
  425. package/src/resources/templates/workflows/collaborate.yaml +1 -1
  426. package/src/resources/templates/workflows/evaluate.yaml +2 -2
  427. package/src/resources/templates/workflows/plan.yaml +1 -1
  428. package/src/resources/templates/workflows/review.yaml +2 -2
  429. package/dist/ui/assets/AgentsModule-N2MnQBZk.js +0 -1
  430. package/dist/ui/assets/AnnotatedAttachmentWorkspace-BG6P_GVW.js +0 -3
  431. package/dist/ui/assets/AnnotatedAttachmentWorkspace-BaS2VwIr.css +0 -1
  432. package/dist/ui/assets/DocumentWorkspace-B3nV_Gc5.css +0 -1
  433. package/dist/ui/assets/DocumentWorkspace-DRbDMwh8.js +0 -252
  434. package/dist/ui/assets/PlansPage-C2dd2n1X.css +0 -1
  435. package/dist/ui/assets/PlansPage-Cq0zXj3I.js +0 -1
  436. package/dist/ui/assets/RussoOne-Regular-C3BxZIj7.ttf +0 -0
  437. package/dist/ui/assets/TaskSettings-ln0aF7xc.js +0 -9
  438. package/dist/ui/assets/index-C2-OXZV7.css +0 -1
  439. package/dist/ui/assets/index-DcSI5F86.js +0 -6
  440. package/scripts/migrate-planning-model.ts +0 -233
@@ -1,6 +1,6 @@
1
- import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
1
+ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
2
2
  import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react';
3
- import { ArrowDown, ArrowLeft, ArrowUpLeft, ArrowUp, Check, ClipboardPaste, Copy, FolderOpen, Hand, HelpCircle, Loader2, MessageSquareCode, MessageSquareText, MousePointer2, Pin, Plus, RotateCcw, Save, SquareMousePointer, Trash2, ZoomIn, ZoomOut, } from 'lucide-react';
3
+ import { ArrowDown, ArrowLeft, ArrowUpLeft, ArrowUp, Check, ChevronLeft, ClipboardPaste, Copy, FolderOpen, Hand, HelpCircle, Loader2, MessageSquareCode, MessageSquareText, MousePointer2, Pin, Plus, RotateCcw, Save, Search, SquareMousePointer, Trash2, ZoomIn, ZoomOut, } from 'lucide-react';
4
4
  import sharedStyles from '../../Taskforce.module.css';
5
5
  import { fetchTaskforceApi } from '../../utils/taskforceApiClient';
6
6
  import { logBootstrapDebug } from '../../hooks/workspace/workspaceLocalState';
@@ -132,6 +132,33 @@ function formatTimestamp(value) {
132
132
  return 'Unsaved';
133
133
  return parsed.toLocaleString();
134
134
  }
135
+ function formatFileSize(bytes) {
136
+ const normalized = typeof bytes === 'number' && Number.isFinite(bytes) ? Math.max(0, bytes) : 0;
137
+ if (normalized < 1024)
138
+ return `${normalized}B`;
139
+ if (normalized < 1024 * 1024)
140
+ return `${(normalized / 1024).toFixed(1)}KB`;
141
+ return `${(normalized / (1024 * 1024)).toFixed(1)}MB`;
142
+ }
143
+ function formatRelativeDate(isoDate) {
144
+ if (!isoDate)
145
+ return '';
146
+ const parsed = new Date(isoDate);
147
+ if (Number.isNaN(parsed.getTime()))
148
+ return '';
149
+ const now = new Date();
150
+ const diffMs = now.getTime() - parsed.getTime();
151
+ const diffDays = Math.floor(diffMs / (1000 * 60 * 60 * 24));
152
+ if (diffDays <= 0)
153
+ return 'Today';
154
+ if (diffDays === 1)
155
+ return 'Yesterday';
156
+ if (diffDays < 7)
157
+ return `${diffDays}d ago`;
158
+ if (diffDays < 30)
159
+ return `${Math.floor(diffDays / 7)}w ago`;
160
+ return parsed.toLocaleDateString();
161
+ }
135
162
  function getSessionCreatorLabel(session) {
136
163
  const createdByLabel = String(session.createdByActor?.label || '').trim();
137
164
  if (createdByLabel)
@@ -371,7 +398,7 @@ function buildDraftPayloadFromSession(session, fallbackTitle) {
371
398
  annotations: session?.annotations || [],
372
399
  });
373
400
  }
374
- export function AnnotatedAttachmentWorkspaceShell({ runtimeMode = 'local', apiBaseUrl = '', cloudAuthBaseUrl = '', workspaceId = 'default', sessionLoadReady = true, requestedTarget = null, requestedSessionId = null, requestedOpenVersion = 0, resolveTaskReferenceLabel, resolveImageReferenceLabel, onRequestedTargetHandled, onOpenTarget, onContextChange, onBackToTask, }) {
401
+ export function AnnotatedAttachmentWorkspaceShell({ runtimeMode = 'local', apiBaseUrl = '', cloudAuthBaseUrl = '', workspaceId = 'default', sessionLoadReady = true, requestedTarget = null, requestedSessionId = null, requestedOpenVersion = 0, imageTrayOpen, onCloseImageTray, resolveTaskReferenceLabel, resolveImageReferenceLabel, onRequestedTargetHandled, onOpenTarget, onContextChange, onBackToTask, }) {
375
402
  const resolvedApiBase = runtimeMode === 'cloud'
376
403
  ? (cloudAuthBaseUrl || apiBaseUrl || '')
377
404
  : '';
@@ -403,6 +430,10 @@ export function AnnotatedAttachmentWorkspaceShell({ runtimeMode = 'local', apiBa
403
430
  const [openImageModalOpen, setOpenImageModalOpen] = useState(false);
404
431
  const [openImageReference, setOpenImageReference] = useState('');
405
432
  const [openingImage, setOpeningImage] = useState(false);
433
+ const [imageTrayItems, setImageTrayItems] = useState([]);
434
+ const [imageTraySearchQuery, setImageTraySearchQuery] = useState('');
435
+ const [imageTrayLoading, setImageTrayLoading] = useState(false);
436
+ const [imageTrayError, setImageTrayError] = useState(null);
406
437
  const [imageLoading, setImageLoading] = useState(false);
407
438
  const [imageLoadError, setImageLoadError] = useState(false);
408
439
  const [zoomLevel, setZoomLevel] = useState(1);
@@ -486,6 +517,22 @@ export function AnnotatedAttachmentWorkspaceShell({ runtimeMode = 'local', apiBa
486
517
  hitRadius: 11,
487
518
  }), []);
488
519
  const arrowOverlayViewBox = useMemo(() => `0 0 ${Math.max(canvasMediaSize.width, 1)} ${Math.max(canvasMediaSize.height, 1)}`, [canvasMediaSize.height, canvasMediaSize.width]);
520
+ const showImageTray = typeof imageTrayOpen === 'boolean';
521
+ const filteredImageTrayItems = useMemo(() => {
522
+ const query = imageTraySearchQuery.trim().toLowerCase();
523
+ if (!query)
524
+ return imageTrayItems;
525
+ return imageTrayItems.filter((item) => {
526
+ const haystack = [
527
+ item.displayName,
528
+ item.originalFilename,
529
+ item.imageReferenceLabel,
530
+ item.taskReferenceLabel,
531
+ item.assetId,
532
+ ].filter(Boolean).join(' ').toLowerCase();
533
+ return haystack.includes(query);
534
+ });
535
+ }, [imageTrayItems, imageTraySearchQuery]);
489
536
  const canPan = useMemo(() => {
490
537
  const scroller = canvasScrollerRef.current;
491
538
  if (!scroller)
@@ -889,6 +936,26 @@ export function AnnotatedAttachmentWorkspaceShell({ runtimeMode = 'local', apiBa
889
936
  }
890
937
  }
891
938
  }, [clearPendingAutoSave, createSessionForTarget, requestHeaders, resolvedApiBase, syncDraftFromSession, workspaceId]);
939
+ const fetchImageTrayItems = useCallback(async () => {
940
+ setImageTrayLoading(true);
941
+ setImageTrayError(null);
942
+ try {
943
+ const params = new URLSearchParams();
944
+ params.set('workspaceId', String(workspaceId || 'default').trim() || 'default');
945
+ const res = await fetchTaskforceApi(`/api/taskforce/annotated-attachments/images?${params.toString()}`, { credentials: 'include', headers: requestHeaders, cache: 'no-store' }, resolvedApiBase);
946
+ const data = await res.json().catch(() => ({}));
947
+ if (!res.ok) {
948
+ throw new Error(String(data?.error || 'Failed to load images.'));
949
+ }
950
+ setImageTrayItems(Array.isArray(data?.images) ? data.images : []);
951
+ }
952
+ catch (requestError) {
953
+ setImageTrayError(requestError instanceof Error ? requestError.message : 'Failed to load images.');
954
+ }
955
+ finally {
956
+ setImageTrayLoading(false);
957
+ }
958
+ }, [requestHeaders, resolvedApiBase, workspaceId]);
892
959
  const pasteImageFromClipboard = useCallback(async () => {
893
960
  if (typeof navigator === 'undefined' || !navigator.clipboard || typeof navigator.clipboard.read !== 'function') {
894
961
  setError('Clipboard image paste is not supported in this environment.');
@@ -1016,6 +1083,33 @@ export function AnnotatedAttachmentWorkspaceShell({ runtimeMode = 'local', apiBa
1016
1083
  setOpeningImage(false);
1017
1084
  }
1018
1085
  }, [isTargetControlled, loadSessions, onOpenTarget, openImageReference, requestHeaders, workspaceId]);
1086
+ const openImageFromTray = useCallback(async (item) => {
1087
+ if (!item.assetId || !item.path)
1088
+ return;
1089
+ const flushed = await flushPendingDraft('session-switch');
1090
+ if (!flushed)
1091
+ return;
1092
+ const target = {
1093
+ assetId: item.assetId,
1094
+ path: item.path,
1095
+ displayName: String(item.displayName || item.originalFilename || 'Image attachment').trim() || 'Image attachment',
1096
+ ...(item.taskId ? { taskId: item.taskId } : {}),
1097
+ ...(item.taskReferenceLabel ? { taskReferenceLabel: item.taskReferenceLabel } : {}),
1098
+ ...(item.imageReferenceLabel ? { imageReferenceLabel: item.imageReferenceLabel } : {}),
1099
+ };
1100
+ pendingRequestedSessionIdRef.current = null;
1101
+ setError(null);
1102
+ if (isTargetControlled) {
1103
+ lastLoadedTargetKeyRef.current = '';
1104
+ onOpenTarget?.(target, { sessionId: null });
1105
+ }
1106
+ else {
1107
+ setUncontrolledTarget(target);
1108
+ lastLoadedTargetKeyRef.current = getAnnotatedTargetKey(target);
1109
+ resetForTargetLoad(target);
1110
+ void loadSessions(target, { autoCreateIfEmpty: true });
1111
+ }
1112
+ }, [flushPendingDraft, isTargetControlled, loadSessions, onOpenTarget, resetForTargetLoad]);
1019
1113
  const applySessionLocally = useCallback((session) => {
1020
1114
  setSessions((current) => {
1021
1115
  const existingIndex = current.findIndex((item) => item.id === session.id);
@@ -1207,6 +1301,11 @@ export function AnnotatedAttachmentWorkspaceShell({ runtimeMode = 'local', apiBa
1207
1301
  setLastSaveError(null);
1208
1302
  setSaveState('saved');
1209
1303
  }, [currentDraftSnapshot, saveState, selectedSessionId]);
1304
+ useEffect(() => {
1305
+ if (!showImageTray || !imageTrayOpen)
1306
+ return;
1307
+ void fetchImageTrayItems();
1308
+ }, [fetchImageTrayItems, imageTrayOpen, showImageTray]);
1210
1309
  useEffect(() => {
1211
1310
  if (typeof window === 'undefined')
1212
1311
  return undefined;
@@ -1802,7 +1901,22 @@ export function AnnotatedAttachmentWorkspaceShell({ runtimeMode = 'local', apiBa
1802
1901
  event.stopPropagation();
1803
1902
  }, [panMode, readPoint, toolMode]);
1804
1903
  const activeSessionTimestamp = selectedSession?.updatedAt ? formatTimestamp(selectedSession.updatedAt) : 'Not saved yet';
1805
- return (_jsxs("div", { className: styles.shell, children: [_jsxs("section", { className: `tf-surface-panel ${styles.panel} ${styles.sessionPanel}`, children: [_jsx("div", { className: styles.sessionContextBar, children: activeTarget?.taskId ? (_jsxs(_Fragment, { children: [_jsx("div", { className: styles.sessionContextLeft, children: onBackToTask ? (_jsx("button", { type: "button", className: `tf-button-ghost tf-button-compact ${styles.ghostBtn} ${styles.backToTaskBtn}`, onClick: () => {
1904
+ return (_jsxs("div", { className: `${styles.shell} ${showImageTray ? styles.shellWithImageTray : ''} ${showImageTray && imageTrayOpen ? styles.shellImageTrayOpen : ''}`.trim(), children: [showImageTray ? (_jsxs("aside", { className: `${styles.imageTrayPanel} ${imageTrayOpen ? styles.imageTrayPanelOpen : ''}`.trim(), "aria-label": "Image tray", "aria-hidden": !imageTrayOpen, children: [_jsxs("div", { className: styles.imageTrayHeader, children: [_jsxs("span", { className: styles.imageTrayTitle, children: [_jsx(FolderOpen, { size: 14 }), "Images"] }), _jsx("button", { type: "button", className: "tf-control-icon", onClick: onCloseImageTray, title: "Collapse image tray", "aria-label": "Collapse image tray", children: _jsx(ChevronLeft, { size: 16 }) })] }), _jsxs("div", { className: styles.imageTraySearch, children: [_jsx(Search, { size: 12, className: styles.imageTraySearchIcon }), _jsx("input", { type: "text", placeholder: "Search images...", value: imageTraySearchQuery, onChange: (event) => setImageTraySearchQuery(event.target.value), className: styles.imageTraySearchInput })] }), _jsx("div", { className: `tf-scrollbar ${styles.imageTrayList}`, children: imageTrayLoading ? (_jsx("div", { className: styles.imageTrayState, children: "Loading images..." })) : imageTrayError ? (_jsx("div", { className: styles.imageTrayStateError, children: imageTrayError })) : filteredImageTrayItems.length === 0 ? (_jsx("div", { className: styles.imageTrayState, children: imageTraySearchQuery.trim() ? 'No images match your search.' : 'No images found.' })) : filteredImageTrayItems.map((item) => {
1905
+ const isSelected = activeTarget?.assetId === item.assetId;
1906
+ const attachmentCount = typeof item.attachmentCount === 'number' && Number.isFinite(item.attachmentCount)
1907
+ ? Math.max(0, item.attachmentCount)
1908
+ : 0;
1909
+ const sessionCount = typeof item.sessionCount === 'number' && Number.isFinite(item.sessionCount)
1910
+ ? Math.max(0, item.sessionCount)
1911
+ : 0;
1912
+ const linkedLabel = attachmentCount > 1
1913
+ ? `${attachmentCount} tasks linked`
1914
+ : attachmentCount === 1
1915
+ ? item.taskReferenceLabel || '1 task linked'
1916
+ : 'Unattached';
1917
+ const imageName = String(item.displayName || item.originalFilename || 'Image attachment').trim() || 'Image attachment';
1918
+ return (_jsxs("button", { type: "button", className: `${styles.imageTrayItem} ${isSelected ? styles.imageTrayItemActive : ''}`.trim(), onClick: () => void openImageFromTray(item), title: imageName, children: [_jsx("span", { className: styles.imageTrayThumb, children: _jsx("img", { src: item.path, alt: "", loading: "lazy" }) }), _jsxs("span", { className: styles.imageTrayItemBody, children: [_jsx("span", { className: styles.imageTrayItemTitle, children: imageName }), _jsx("span", { className: styles.imageTrayItemTask, children: linkedLabel }), _jsxs("span", { className: styles.imageTrayItemMeta, children: [_jsxs("span", { className: styles.imageTrayItemMetaDetails, children: [formatFileSize(item.sizeBytes), item.updatedAt ? _jsxs(_Fragment, { children: [" \u00B7 ", formatRelativeDate(item.updatedAt)] }) : null, sessionCount > 0 ? _jsxs(_Fragment, { children: [" \u00B7 ", sessionCount, " session", sessionCount === 1 ? '' : 's'] }) : null] }), item.imageReferenceLabel ? (_jsx("span", { className: styles.imageTrayItemReference, children: item.imageReferenceLabel })) : null] })] })] }, item.assetId));
1919
+ }) })] })) : null, _jsxs("section", { className: `tf-surface-panel ${styles.panel} ${styles.sessionPanel}`, children: [_jsx("div", { className: styles.sessionContextBar, children: activeTarget?.taskId ? (_jsxs(_Fragment, { children: [_jsx("div", { className: styles.sessionContextLeft, children: onBackToTask ? (_jsx("button", { type: "button", className: `tf-button-ghost tf-button-compact ${styles.ghostBtn} ${styles.backToTaskBtn}`, onClick: () => {
1806
1920
  void flushPendingDraft('back-to-task').then((saved) => {
1807
1921
  if (!saved)
1808
1922
  return;
@@ -320,6 +320,58 @@ describe('AnnotatedAttachmentWorkspaceShell', () => {
320
320
  expect(screen.queryByAltText('Homepage shot')).not.toBeInTheDocument();
321
321
  });
322
322
  });
323
+ it('loads images from the image tray and opens the selected image', async () => {
324
+ const user = userEvent.setup();
325
+ fetchTaskforceApi
326
+ .mockResolvedValueOnce({
327
+ ok: true,
328
+ json: async () => ({
329
+ images: [
330
+ {
331
+ assetId: 'asset-image-24',
332
+ imageReferenceLabel: 'I-24',
333
+ path: '/api/taskforce/context/workspaces%2Fworkspace-1%2Fassets%2Fimages%2Fasset-image-24.png',
334
+ displayName: 'Reference image',
335
+ originalFilename: 'reference.png',
336
+ sizeBytes: 2048,
337
+ updatedAt: '2026-03-25T12:00:00.000Z',
338
+ attachmentCount: 0,
339
+ sessionCount: 0,
340
+ },
341
+ ],
342
+ }),
343
+ })
344
+ .mockResolvedValueOnce({
345
+ ok: true,
346
+ json: async () => ({ sessions: [] }),
347
+ })
348
+ .mockResolvedValueOnce({
349
+ ok: true,
350
+ json: async () => ({
351
+ session: buildSessionResponse({
352
+ id: 'session-ref-1',
353
+ taskId: '',
354
+ baseImageAssetId: 'asset-image-24',
355
+ title: 'Reference image review',
356
+ }),
357
+ }),
358
+ });
359
+ function ControlledImageTrayHarness() {
360
+ const [target, setTarget] = React.useState(null);
361
+ const [sessionId, setSessionId] = React.useState(null);
362
+ return (_jsx(AnnotatedAttachmentWorkspaceShell, { workspaceId: "workspace-1", imageTrayOpen: true, requestedTarget: target, requestedSessionId: sessionId, onOpenTarget: (nextTarget, options) => {
363
+ setTarget(nextTarget);
364
+ setSessionId(options?.sessionId ?? null);
365
+ } }));
366
+ }
367
+ render(_jsx(ControlledImageTrayHarness, {}));
368
+ expect(await screen.findByRole('button', { name: /reference image/i })).toBeInTheDocument();
369
+ await user.click(screen.getByRole('button', { name: /reference image/i }));
370
+ await waitFor(() => expect(fetchTaskforceApi).toHaveBeenNthCalledWith(1, '/api/taskforce/annotated-attachments/images?workspaceId=workspace-1', expect.objectContaining({ cache: 'no-store' }), ''));
371
+ await waitFor(() => expect(fetchTaskforceApi).toHaveBeenNthCalledWith(2, '/api/taskforce/annotated-attachments/sessions?workspaceId=workspace-1&imageAssetId=asset-image-24', expect.any(Object), ''));
372
+ expect(await screen.findByAltText('Reference image')).toBeInTheDocument();
373
+ expect(await findSessionCard('Reference image review')).toBeInTheDocument();
374
+ });
323
375
  it('does not revert to the previous image while opening a pasted image in controlled mode', async () => {
324
376
  const user = userEvent.setup();
325
377
  const fetchSpy = vi.spyOn(globalThis, 'fetch').mockResolvedValue({
@@ -11,8 +11,8 @@ interface Props {
11
11
  }>;
12
12
  onSearchChange: (q: string) => void;
13
13
  onSelect: (doc: DocMeta) => void;
14
- onRefresh: () => void;
14
+ onCloseTray?: () => void;
15
15
  hasActiveFilters: boolean;
16
16
  }
17
- export declare function DocumentIndex({ documents, selectedDocId, loading, error, searchQuery, taskById, onSearchChange, onSelect, onRefresh, hasActiveFilters, }: Props): import("react/jsx-runtime").JSX.Element;
17
+ export declare function DocumentIndex({ documents, selectedDocId, loading, error, searchQuery, taskById, onSearchChange, onSelect, onCloseTray, hasActiveFilters, }: Props): import("react/jsx-runtime").JSX.Element;
18
18
  export {};
@@ -1,6 +1,5 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
2
- import { Search, RefreshCw, FileText, ClipboardList, Eye, BookOpen, File } from 'lucide-react';
3
- import styles from '../../Taskforce.module.css';
2
+ import { Search, FileText, ClipboardList, Eye, BookOpen, File, ChevronLeft } from 'lucide-react';
4
3
  import indexStyles from './DocumentIndex.module.css';
5
4
  import { getEffectiveAttachmentState } from './documentWorkspaceModel';
6
5
  import { getReadableDocumentName } from '../../utils/documentNames';
@@ -42,8 +41,8 @@ function handleItemKeyDown(event, onSelect) {
42
41
  event.preventDefault();
43
42
  onSelect();
44
43
  }
45
- export function DocumentIndex({ documents, selectedDocId, loading, error, searchQuery, taskById, onSearchChange, onSelect, onRefresh, hasActiveFilters, }) {
46
- return (_jsxs("div", { className: indexStyles.index, children: [_jsxs("div", { className: indexStyles.header, children: [_jsxs("span", { className: indexStyles.title, children: [_jsx(FileText, { size: 14 }), "Documents"] }), _jsx("button", { className: indexStyles.refreshBtn, onClick: onRefresh, title: "Refresh document list", disabled: loading, children: _jsx(RefreshCw, { size: 13, className: loading ? styles.docSpinning : undefined }) })] }), _jsxs("div", { className: indexStyles.search, children: [_jsx(Search, { size: 12, className: indexStyles.searchIcon }), _jsx("input", { type: "text", placeholder: "Search documents\u2026", value: searchQuery, onChange: e => onSearchChange(e.target.value), className: indexStyles.searchInput })] }), _jsxs("div", { className: `${indexStyles.list} tf-scrollbar`, children: [loading && (_jsx("div", { className: indexStyles.state, children: "Loading documents\u2026" })), !loading && error && (_jsx("div", { className: indexStyles.stateError, children: error })), !loading && !error && documents.length === 0 && (_jsx("div", { className: indexStyles.state, children: hasActiveFilters
44
+ export function DocumentIndex({ documents, selectedDocId, loading, error, searchQuery, taskById, onSearchChange, onSelect, onCloseTray, hasActiveFilters, }) {
45
+ return (_jsxs("div", { className: indexStyles.index, children: [_jsxs("div", { className: indexStyles.header, children: [_jsxs("span", { className: indexStyles.title, children: [_jsx(FileText, { size: 14 }), "Documents"] }), onCloseTray ? (_jsx("button", { type: "button", className: "tf-control-icon", onClick: onCloseTray, title: "Collapse document tray", "aria-label": "Collapse document tray", children: _jsx(ChevronLeft, { size: 16 }) })) : null] }), _jsxs("div", { className: indexStyles.search, children: [_jsx(Search, { size: 12, className: indexStyles.searchIcon }), _jsx("input", { type: "text", placeholder: "Search documents\u2026", value: searchQuery, onChange: e => onSearchChange(e.target.value), className: indexStyles.searchInput })] }), _jsxs("div", { className: `${indexStyles.list} tf-scrollbar`, children: [loading && (_jsx("div", { className: indexStyles.state, children: "Loading documents\u2026" })), !loading && error && (_jsx("div", { className: indexStyles.stateError, children: error })), !loading && !error && documents.length === 0 && (_jsx("div", { className: indexStyles.state, children: hasActiveFilters
47
46
  ? 'No documents match your filters.'
48
47
  : 'No documents found. Documents added as task attachments will appear here.' })), !loading && documents.map(doc => {
49
48
  const task = doc.taskId ? taskById.get(doc.taskId) : null;
@@ -4,10 +4,16 @@ export declare function schedulePrintCleanup(cleanup: () => void): number;
4
4
  interface Props {
5
5
  doc: DocMeta;
6
6
  taskTitle: string | null;
7
+ tasks?: Array<{
8
+ id: string;
9
+ title: string;
10
+ referenceLabel?: string | null;
11
+ }>;
7
12
  apiBaseUrl?: string;
8
13
  onSaved?: (updatedAt: string) => void;
9
14
  onDeleted?: () => void;
15
+ onBackToTask?: (taskId: string) => void;
10
16
  enableTaskGeneration?: boolean;
11
17
  }
12
- export declare function DocumentViewer({ doc, taskTitle, apiBaseUrl, onSaved, onDeleted, enableTaskGeneration }: Props): import("react/jsx-runtime").JSX.Element;
18
+ export declare function DocumentViewer({ doc, taskTitle, tasks, apiBaseUrl, onSaved, onDeleted, onBackToTask, enableTaskGeneration }: Props): import("react/jsx-runtime").JSX.Element;
13
19
  export {};
@@ -4,10 +4,11 @@ import Markdown from 'react-markdown';
4
4
  import { renderToStaticMarkup } from 'react-dom/server';
5
5
  import remarkGfm from 'remark-gfm';
6
6
  import rehypeSanitize from 'rehype-sanitize';
7
- import { Printer, Link, Download, Clock, HardDrive, Tag, Pencil, X, Save, Eye, SplitSquareHorizontal, CheckCircle, AlertCircle, Zap, Trash2, MessageSquareText, Send, Bot, User, RotateCcw, CheckCheck } from 'lucide-react';
7
+ import { Printer, Link, Download, Clock, HardDrive, Tag, Pencil, X, Save, Eye, SplitSquareHorizontal, CheckCircle, AlertCircle, Zap, Trash2, MessageSquareText, Send, Bot, User, RotateCcw, CheckCheck, ArrowLeft } from 'lucide-react';
8
8
  import styles from '../../Taskforce.module.css';
9
9
  import viewerStyles from './DocumentViewer.module.css';
10
10
  import taskFormStyles from '../task/TaskForm.module.css';
11
+ import { Modal } from '../ui/Modal';
11
12
  import { getEffectiveAttachmentState } from './documentWorkspaceModel';
12
13
  import { DocumentGenerateModal } from './DocumentGenerateModal';
13
14
  import { fetchTaskforceApi, resolveTaskforceApiUrl } from '../../utils/taskforceApiClient';
@@ -500,7 +501,7 @@ function toggleNthTaskCheckbox(markdown, targetIndex, nextChecked) {
500
501
  });
501
502
  return updated.join('\n');
502
503
  }
503
- export function DocumentViewer({ doc, taskTitle, apiBaseUrl = '', onSaved, onDeleted, enableTaskGeneration = false }) {
504
+ export function DocumentViewer({ doc, taskTitle, tasks = [], apiBaseUrl = '', onSaved, onDeleted, onBackToTask, enableTaskGeneration = false }) {
504
505
  const fallbackReadableName = getReadableDocumentName(doc);
505
506
  const [currentDocumentName, setCurrentDocumentName] = useState(fallbackReadableName);
506
507
  const readableName = currentDocumentName;
@@ -532,6 +533,10 @@ export function DocumentViewer({ doc, taskTitle, apiBaseUrl = '', onSaved, onDel
532
533
  const [monochromeMode, setMonochromeMode] = useState(false);
533
534
  const [deleting, setDeleting] = useState(false);
534
535
  const [renaming, setRenaming] = useState(false);
536
+ const [attachModalOpen, setAttachModalOpen] = useState(false);
537
+ const [attachSearch, setAttachSearch] = useState('');
538
+ const [attachingTaskId, setAttachingTaskId] = useState(null);
539
+ const [attachError, setAttachError] = useState(null);
535
540
  const [renameValue, setRenameValue] = useState(readableName);
536
541
  const isEditable = doc.editable !== false;
537
542
  const attachmentState = getEffectiveAttachmentState(doc);
@@ -545,6 +550,13 @@ export function DocumentViewer({ doc, taskTitle, apiBaseUrl = '', onSaved, onDel
545
550
  const reviewThreadEndRef = useRef(null);
546
551
  const markdownContainerRef = useRef(null);
547
552
  const activeReviewSession = reviewSessions.find((session) => session.id === selectedReviewSessionId) || reviewSessions[0] || null;
553
+ const filteredTasks = tasks.filter((task) => {
554
+ const query = attachSearch.trim().toLowerCase();
555
+ if (!query)
556
+ return true;
557
+ return task.title.toLowerCase().includes(query)
558
+ || String(task.referenceLabel || '').trim().toLowerCase().includes(query);
559
+ });
548
560
  const fetchContent = useCallback(async () => {
549
561
  setLoadingContent(true);
550
562
  setLoadError(null);
@@ -941,6 +953,44 @@ export function DocumentViewer({ doc, taskTitle, apiBaseUrl = '', onSaved, onDel
941
953
  setDeleting(false);
942
954
  }
943
955
  }, [apiBaseUrl, attachmentState, deleting, doc.assetId, doc.attachmentCount, onDeleted, readableName]);
956
+ const handleOpenAttachModal = useCallback(() => {
957
+ setAttachSearch('');
958
+ setAttachError(null);
959
+ setAttachingTaskId(null);
960
+ setAttachModalOpen(true);
961
+ }, []);
962
+ const handleAttachToTask = useCallback(async (taskId) => {
963
+ if (!doc.assetId)
964
+ return;
965
+ setAttachError(null);
966
+ setAttachingTaskId(taskId);
967
+ try {
968
+ const res = await fetchTaskforceApi(`/api/taskforce/documents/${encodeURIComponent(doc.assetId)}/attach`, {
969
+ method: 'POST',
970
+ credentials: 'include',
971
+ headers: { 'Content-Type': 'application/json' },
972
+ body: JSON.stringify({
973
+ targetType: 'task',
974
+ targetId: taskId,
975
+ }),
976
+ }, apiBaseUrl);
977
+ const data = await res.json().catch(() => ({}));
978
+ if (!res.ok)
979
+ throw new Error(data.error || `Attach failed (${res.status})`);
980
+ if (data.alreadyAttached) {
981
+ setAttachError('This document is already attached to that task.');
982
+ return;
983
+ }
984
+ setAttachModalOpen(false);
985
+ onSaved?.(new Date().toISOString());
986
+ }
987
+ catch (error) {
988
+ setAttachError(error?.message || 'Failed to attach document.');
989
+ }
990
+ finally {
991
+ setAttachingTaskId(null);
992
+ }
993
+ }, [apiBaseUrl, doc.assetId, onSaved]);
944
994
  const handleToggleReviewStatus = useCallback(async () => {
945
995
  if (!activeReviewSession || reviewBusy)
946
996
  return;
@@ -1227,7 +1277,7 @@ export function DocumentViewer({ doc, taskTitle, apiBaseUrl = '', onSaved, onDel
1227
1277
  return;
1228
1278
  setRenameValue(readableName);
1229
1279
  setRenaming(true);
1230
- }, children: readableName })), taskTitle && (_jsx("span", { className: viewerStyles.metaTaskName, title: `From task: ${taskTitle}`, children: taskTitle })), attachmentState === 'unattached' && (_jsx("span", { className: viewerStyles.statusBadge, children: "Unattached" })), doc.updatedAt && (_jsxs("span", { className: viewerStyles.metaDate, title: formatDate(doc.updatedAt), children: [_jsx(Clock, { size: 11 }), formatDate(doc.updatedAt)] })), _jsxs("span", { className: viewerStyles.metaSize, children: [_jsx(HardDrive, { size: 11 }), formatBytes(doc.sizeBytes)] }), documentReferenceLabel && (_jsx(ReferenceBadgeButton, { label: documentReferenceLabel, copied: copiedReference, onClick: () => { void handleCopyReference(); }, title: "Copy document reference" })), saveStatus.type === 'saved' && (_jsxs("span", { className: `${viewerStyles.saveStatus} ${viewerStyles.saveStatusSaved}`, children: [_jsx(CheckCircle, { size: 11 }), "Saved"] })), saveStatus.type === 'error' && (_jsxs("span", { className: `${viewerStyles.saveStatus} ${viewerStyles.saveStatusError}`, title: saveStatus.message, children: [_jsx(AlertCircle, { size: 11 }), saveStatus.message || 'Save failed'] })), isDirty && saveStatus.type !== 'saving' && (_jsx("span", { className: viewerStyles.unsaved, children: "Unsaved changes" }))] }), _jsx("div", { className: viewerStyles.actions, children: editMode ? (_jsxs(_Fragment, { children: [_jsxs("div", { className: viewerStyles.layoutToggle, children: [_jsx("button", { className: `${viewerStyles.layoutBtn} ${layout === 'editor' ? viewerStyles.layoutBtnActive : ''}`, onClick: () => setLayout('editor'), title: "Editor only", children: _jsx(Pencil, { size: 12 }) }), _jsx("button", { className: `${viewerStyles.layoutBtn} ${layout === 'split' ? viewerStyles.layoutBtnActive : ''}`, onClick: () => setLayout('split'), title: "Split view", children: _jsx(SplitSquareHorizontal, { size: 12 }) }), _jsx("button", { className: `${viewerStyles.layoutBtn} ${layout === 'preview' ? viewerStyles.layoutBtnActive : ''}`, onClick: () => setLayout('preview'), title: "Preview only", children: _jsx(Eye, { size: 12 }) })] }), _jsxs("button", { className: viewerStyles.actionBtn, onClick: handleCancelEdit, title: "Discard changes", children: [_jsx(X, { size: 13 }), "Cancel"] }), _jsxs("button", { className: `${viewerStyles.actionBtn} ${viewerStyles.saveBtn}`, onClick: handleSave, disabled: saveStatus.type === 'saving' || !isDirty, title: "Save (Ctrl+S)", children: [_jsx(Save, { size: 13 }), saveStatus.type === 'saving' ? 'Saving…' : 'Save'] })] })) : (_jsxs(_Fragment, { children: [doc.assetId && (_jsxs("button", { className: viewerStyles.actionBtn, onClick: () => setRenaming((prev) => !prev), title: "Rename document", children: [_jsx(Pencil, { size: 13 }), renaming ? 'Close Rename' : 'Rename'] })), isEditable && (_jsxs("button", { className: `${viewerStyles.actionBtn} ${viewerStyles.editBtn}`, onClick: handleEnterEdit, title: "Edit document", children: [_jsx(Pencil, { size: 13 }), "Edit"] })), _jsxs("button", { className: viewerStyles.actionBtn, onClick: handleToggleReviewSidebar, title: showReviewSidebar ? 'Hide review and comments' : 'Show review and comments', children: [_jsx(MessageSquareText, { size: 13 }), showReviewSidebar ? 'Hide Review' : 'Show Review'] }), _jsxs("button", { className: viewerStyles.actionBtn, onClick: handleCopyLink, title: "Copy link", children: [_jsx(Link, { size: 13 }), copied ? 'Copied!' : 'Copy link'] }), _jsxs("button", { className: viewerStyles.actionBtn, onClick: handleDownload, title: "Download .md", children: [_jsx(Download, { size: 13 }), "Download"] }), _jsxs("button", { className: viewerStyles.actionBtn, onClick: handlePrint, title: "Print", children: [_jsx(Printer, { size: 13 }), "Print"] }), _jsxs("button", { className: viewerStyles.actionBtn, onClick: () => setMonochromeMode((prev) => !prev), title: "Toggle black and white style", children: [_jsx(Eye, { size: 13 }), monochromeMode ? 'Color' : 'B/W'] }), enableTaskGeneration && doc.docType === 'implementation-plan' && !editMode && (_jsxs("button", { className: `${viewerStyles.actionBtn} ${viewerStyles.generateBtn}`, onClick: () => setShowGenModal(true), title: "Generate tasks from this plan", children: [_jsx(Zap, { size: 13 }), "Generate Tasks"] })), canDelete && (_jsxs("button", { className: `${viewerStyles.actionBtn} ${viewerStyles.dangerBtn}`, onClick: () => { void handleDelete(); }, disabled: deleting, title: attachmentState === 'attached'
1280
+ }, children: readableName })), taskTitle && (_jsx("span", { className: viewerStyles.metaTaskName, title: `From task: ${taskTitle}`, children: taskTitle })), attachmentState === 'unattached' && (_jsx("span", { className: viewerStyles.statusBadge, children: "Unattached" })), doc.updatedAt && (_jsxs("span", { className: viewerStyles.metaDate, title: formatDate(doc.updatedAt), children: [_jsx(Clock, { size: 11 }), formatDate(doc.updatedAt)] })), _jsxs("span", { className: viewerStyles.metaSize, children: [_jsx(HardDrive, { size: 11 }), formatBytes(doc.sizeBytes)] }), documentReferenceLabel && (_jsx(ReferenceBadgeButton, { label: documentReferenceLabel, copied: copiedReference, onClick: () => { void handleCopyReference(); }, title: "Copy document reference" })), saveStatus.type === 'saved' && (_jsxs("span", { className: `${viewerStyles.saveStatus} ${viewerStyles.saveStatusSaved}`, children: [_jsx(CheckCircle, { size: 11 }), "Saved"] })), saveStatus.type === 'error' && (_jsxs("span", { className: `${viewerStyles.saveStatus} ${viewerStyles.saveStatusError}`, title: saveStatus.message, children: [_jsx(AlertCircle, { size: 11 }), saveStatus.message || 'Save failed'] })), isDirty && saveStatus.type !== 'saving' && (_jsx("span", { className: viewerStyles.unsaved, children: "Unsaved changes" }))] }), _jsx("div", { className: viewerStyles.actions, children: editMode ? (_jsxs(_Fragment, { children: [_jsxs("div", { className: viewerStyles.layoutToggle, children: [_jsx("button", { className: `${viewerStyles.layoutBtn} ${layout === 'editor' ? viewerStyles.layoutBtnActive : ''}`, onClick: () => setLayout('editor'), title: "Editor only", children: _jsx(Pencil, { size: 12 }) }), _jsx("button", { className: `${viewerStyles.layoutBtn} ${layout === 'split' ? viewerStyles.layoutBtnActive : ''}`, onClick: () => setLayout('split'), title: "Split view", children: _jsx(SplitSquareHorizontal, { size: 12 }) }), _jsx("button", { className: `${viewerStyles.layoutBtn} ${layout === 'preview' ? viewerStyles.layoutBtnActive : ''}`, onClick: () => setLayout('preview'), title: "Preview only", children: _jsx(Eye, { size: 12 }) })] }), _jsxs("button", { className: viewerStyles.actionBtn, onClick: handleCancelEdit, title: "Discard changes", children: [_jsx(X, { size: 13 }), "Cancel"] }), _jsxs("button", { className: `${viewerStyles.actionBtn} ${viewerStyles.saveBtn}`, onClick: handleSave, disabled: saveStatus.type === 'saving' || !isDirty, title: "Save (Ctrl+S)", children: [_jsx(Save, { size: 13 }), saveStatus.type === 'saving' ? 'Saving…' : 'Save'] })] })) : (_jsxs(_Fragment, { children: [doc.taskId && onBackToTask && (_jsxs("button", { className: viewerStyles.actionBtn, onClick: () => onBackToTask(doc.taskId), title: "Back to task", "aria-label": "Back to task", children: [_jsx(ArrowLeft, { size: 13 }), "Back to task"] })), doc.assetId && (_jsxs("button", { className: viewerStyles.actionBtn, onClick: () => setRenaming((prev) => !prev), title: "Rename document", children: [_jsx(Pencil, { size: 13 }), renaming ? 'Close Rename' : 'Rename'] })), isEditable && (_jsxs("button", { className: `${viewerStyles.actionBtn} ${viewerStyles.editBtn}`, onClick: handleEnterEdit, title: "Edit document", children: [_jsx(Pencil, { size: 13 }), "Edit"] })), doc.assetId && (_jsxs("button", { className: viewerStyles.actionBtn, onClick: handleOpenAttachModal, title: "Attach document to a task", children: [_jsx(Link, { size: 13 }), "Attach"] })), _jsxs("button", { className: viewerStyles.actionBtn, onClick: handleToggleReviewSidebar, title: showReviewSidebar ? 'Hide review and comments' : 'Show review and comments', children: [_jsx(MessageSquareText, { size: 13 }), showReviewSidebar ? 'Hide Review' : 'Show Review'] }), _jsxs("button", { className: viewerStyles.actionBtn, onClick: handleCopyLink, title: "Copy link", children: [_jsx(Link, { size: 13 }), copied ? 'Copied!' : 'Copy link'] }), _jsxs("button", { className: viewerStyles.actionBtn, onClick: handleDownload, title: "Download .md", children: [_jsx(Download, { size: 13 }), "Download"] }), _jsxs("button", { className: viewerStyles.actionBtn, onClick: handlePrint, title: "Print", children: [_jsx(Printer, { size: 13 }), "Print"] }), _jsxs("button", { className: viewerStyles.actionBtn, onClick: () => setMonochromeMode((prev) => !prev), title: "Toggle black and white style", children: [_jsx(Eye, { size: 13 }), monochromeMode ? 'Color' : 'B/W'] }), enableTaskGeneration && doc.docType === 'implementation-plan' && !editMode && (_jsxs("button", { className: `${viewerStyles.actionBtn} ${viewerStyles.generateBtn}`, onClick: () => setShowGenModal(true), title: "Generate tasks from this plan", children: [_jsx(Zap, { size: 13 }), "Generate Tasks"] })), canDelete && (_jsxs("button", { className: `${viewerStyles.actionBtn} ${viewerStyles.dangerBtn}`, onClick: () => { void handleDelete(); }, disabled: deleting, title: attachmentState === 'attached'
1231
1281
  ? 'Remove this document from linked tasks and delete it'
1232
1282
  : 'Permanently delete this unattached document', children: [_jsx(Trash2, { size: 13 }), deleting
1233
1283
  ? 'Deleting…'
@@ -1254,5 +1304,13 @@ export function DocumentViewer({ doc, taskTitle, apiBaseUrl = '', onSaved, onDel
1254
1304
  event.preventDefault();
1255
1305
  void handleAddReviewComment();
1256
1306
  }
1257
- } })] }), _jsx("button", { type: "button", className: taskFormStyles.sendCommentBtn, onClick: () => { void handleAddReviewComment(); }, disabled: reviewBusy || !reviewDraft.trim() || activeReviewSession?.status === 'resolved', "aria-label": "Send review comment", title: "Send review comment", children: _jsx(Send, { size: 15 }) })] })] })] }) }))] }))), enableTaskGeneration && showGenModal && content !== null && (_jsx(DocumentGenerateModal, { content: content, docTitle: doc.title, apiBaseUrl: apiBaseUrl, onClose: () => setShowGenModal(false), onGenerated: (_count) => setShowGenModal(false) })), !editMode && (_jsx("div", { className: viewerStyles.printHint, children: "For the cleanest output, disable browser Headers and footers in the print dialog." }))] }));
1307
+ } })] }), _jsx("button", { type: "button", className: taskFormStyles.sendCommentBtn, onClick: () => { void handleAddReviewComment(); }, disabled: reviewBusy || !reviewDraft.trim() || activeReviewSession?.status === 'resolved', "aria-label": "Send review comment", title: "Send review comment", children: _jsx(Send, { size: 15 }) })] })] })] }) }))] }))), enableTaskGeneration && showGenModal && content !== null && (_jsx(DocumentGenerateModal, { content: content, docTitle: doc.title, apiBaseUrl: apiBaseUrl, onClose: () => setShowGenModal(false), onGenerated: (_count) => setShowGenModal(false) })), _jsx(Modal, { isOpen: attachModalOpen, onClose: () => {
1308
+ if (attachingTaskId)
1309
+ return;
1310
+ setAttachModalOpen(false);
1311
+ }, title: "Attach document", size: "md", footer: (_jsx("button", { className: viewerStyles.actionBtn, onClick: () => setAttachModalOpen(false), disabled: Boolean(attachingTaskId), children: "Close" })), children: _jsxs("div", { className: viewerStyles.attachModalBody, children: [_jsx("p", { className: viewerStyles.attachModalCopy, children: "Attach this document to a task from the document manager. Initiative and workstream targets can be enabled later on the same attachment model." }), _jsx("input", { type: "text", className: styles.input, placeholder: "Search tasks\u2026", value: attachSearch, onChange: (event) => setAttachSearch(event.target.value) }), attachError && (_jsxs("div", { className: viewerStyles.attachModalError, children: [_jsx(AlertCircle, { size: 14 }), _jsx("span", { children: attachError })] })), _jsx("div", { className: `${viewerStyles.attachTaskList} tf-scrollbar`, children: filteredTasks.length === 0 ? (_jsx("div", { className: viewerStyles.attachEmptyState, children: tasks.length === 0 ? 'No tasks are available in this workspace.' : 'No tasks match your search.' })) : (filteredTasks.map((task) => {
1312
+ const isCurrentPrimaryAttachment = doc.taskId === task.id;
1313
+ const isBusy = attachingTaskId === task.id;
1314
+ return (_jsxs("button", { type: "button", className: viewerStyles.attachTaskRow, disabled: isBusy, onClick: () => { void handleAttachToTask(task.id); }, children: [_jsxs("span", { className: viewerStyles.attachTaskInfo, children: [_jsx("span", { className: viewerStyles.attachTaskTitle, children: task.title }), task.referenceLabel && (_jsx("span", { className: viewerStyles.attachTaskReference, children: task.referenceLabel }))] }), _jsx("span", { className: viewerStyles.attachTaskMeta, children: isBusy ? 'Attaching…' : isCurrentPrimaryAttachment ? 'Attached' : 'Attach' })] }, task.id));
1315
+ })) })] }) }), !editMode && (_jsx("div", { className: viewerStyles.printHint, children: "For the cleanest output, disable browser Headers and footers in the print dialog." }))] }));
1258
1316
  }
@@ -7,6 +7,7 @@ interface Props {
7
7
  tasks: Array<{
8
8
  id: string;
9
9
  title: string;
10
+ referenceLabel?: string | null;
10
11
  }>;
11
12
  apiBaseUrl?: string;
12
13
  documents: DocMeta[];
@@ -15,16 +16,20 @@ interface Props {
15
16
  selectedDoc: DocMeta | null;
16
17
  searchQuery: string;
17
18
  hasActiveFilters: boolean;
19
+ documentTrayOpen?: boolean;
18
20
  onSearchChange: (value: string) => void;
19
21
  onSelectDoc: (doc: DocMeta | null) => void;
20
22
  onRefresh: () => Promise<void> | void;
23
+ onCloseDocumentTray?: () => void;
21
24
  onDeleteSelected: () => Promise<void> | void;
25
+ onBackToTask?: (taskId: string) => void;
22
26
  enableTaskGeneration?: boolean;
23
27
  }
24
28
  interface ShellProps {
25
29
  tasks: Array<{
26
30
  id: string;
27
31
  title: string;
32
+ referenceLabel?: string | null;
28
33
  }>;
29
34
  runtimeMode?: 'local' | 'cloud';
30
35
  apiBaseUrl?: string;
@@ -33,10 +38,13 @@ interface ShellProps {
33
38
  attachmentFilters?: DocumentAttachmentFilter[];
34
39
  onTypeFiltersChange?: (values: DocumentTypeFilter[]) => void;
35
40
  onAttachmentFiltersChange?: (values: DocumentAttachmentFilter[]) => void;
41
+ documentTrayOpen?: boolean;
42
+ onCloseDocumentTray?: () => void;
36
43
  requestedDocPath?: string | null;
37
44
  requestedDocAssetId?: string | null;
38
45
  onRequestedDocHandled?: () => void;
46
+ onBackToTask?: (taskId: string) => void;
39
47
  enableTaskGeneration?: boolean;
40
48
  }
41
- export declare function DocumentWorkspace({ tasks, apiBaseUrl, documents, loading, error, selectedDoc, searchQuery, hasActiveFilters, onSearchChange, onSelectDoc, onRefresh, onDeleteSelected, enableTaskGeneration }: Props): import("react/jsx-runtime").JSX.Element;
42
- export declare function DocumentWorkspaceShell({ tasks, runtimeMode, apiBaseUrl, cloudAuthBaseUrl, typeFilters: controlledTypeFilters, attachmentFilters: controlledAttachmentFilters, onTypeFiltersChange, onAttachmentFiltersChange, requestedDocPath, requestedDocAssetId, onRequestedDocHandled, enableTaskGeneration }: ShellProps): import("react/jsx-runtime").JSX.Element;
49
+ export declare function DocumentWorkspace({ tasks, apiBaseUrl, documents, loading, error, selectedDoc, searchQuery, hasActiveFilters, documentTrayOpen, onSearchChange, onSelectDoc, onRefresh, onCloseDocumentTray, onDeleteSelected, onBackToTask, enableTaskGeneration }: Props): import("react/jsx-runtime").JSX.Element;
50
+ export declare function DocumentWorkspaceShell({ tasks, runtimeMode, apiBaseUrl, cloudAuthBaseUrl, typeFilters: controlledTypeFilters, attachmentFilters: controlledAttachmentFilters, onTypeFiltersChange, onAttachmentFiltersChange, documentTrayOpen, onCloseDocumentTray, requestedDocPath, requestedDocAssetId, onRequestedDocHandled, onBackToTask, enableTaskGeneration }: ShellProps): import("react/jsx-runtime").JSX.Element;
@@ -1,14 +1,17 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import React from 'react';
2
3
  import styles from '../../Taskforce.module.css';
3
4
  import { DocumentIndex } from './DocumentIndex';
4
5
  import { DocumentViewer } from './DocumentViewer';
5
6
  import { useDocumentWorkspaceController } from './useDocumentWorkspaceController';
6
7
  export { DOCUMENT_TYPE_FILTER_OPTIONS, DOCUMENT_ATTACHMENT_FILTER_OPTIONS } from './documentWorkspaceFilters';
7
- export function DocumentWorkspace({ tasks, apiBaseUrl = '', documents, loading, error, selectedDoc, searchQuery, hasActiveFilters, onSearchChange, onSelectDoc, onRefresh, onDeleteSelected, enableTaskGeneration = false }) {
8
+ export function DocumentWorkspace({ tasks, apiBaseUrl = '', documents, loading, error, selectedDoc, searchQuery, hasActiveFilters, documentTrayOpen, onSearchChange, onSelectDoc, onRefresh, onCloseDocumentTray, onDeleteSelected, onBackToTask, enableTaskGeneration = false }) {
8
9
  const taskById = new Map(tasks.map(t => [t.id, t]));
9
- return (_jsxs("div", { className: styles.docWorkspace, children: [_jsx("div", { className: styles.docIndexPanel, children: _jsx(DocumentIndex, { documents: documents, selectedDocId: selectedDoc?.id ?? null, loading: loading, error: error, searchQuery: searchQuery, onSearchChange: onSearchChange, onSelect: (doc) => onSelectDoc(doc), onRefresh: onRefresh, taskById: taskById, hasActiveFilters: hasActiveFilters }) }), _jsx("div", { className: styles.docViewerPanel, children: selectedDoc ? (_jsx(DocumentViewer, { doc: selectedDoc, taskTitle: selectedDoc.taskId ? (taskById.get(selectedDoc.taskId)?.title ?? null) : null, apiBaseUrl: apiBaseUrl, onSaved: (_updatedAt) => onRefresh(), onDeleted: () => void onDeleteSelected(), enableTaskGeneration: enableTaskGeneration })) : !loading && (_jsx("div", { className: styles.docViewerEmpty, children: _jsx("p", { className: "tf-empty-copy", children: documents.length === 0 ? 'No documents match your current filters.' : 'Select a document to view it.' }) })) })] }));
10
+ const useTrayIndex = typeof documentTrayOpen === 'boolean';
11
+ const documentIndex = (_jsx(DocumentIndex, { documents: documents, selectedDocId: selectedDoc?.id ?? null, loading: loading, error: error, searchQuery: searchQuery, onSearchChange: onSearchChange, onSelect: (doc) => onSelectDoc(doc), onCloseTray: useTrayIndex ? onCloseDocumentTray : undefined, taskById: taskById, hasActiveFilters: hasActiveFilters }));
12
+ return (_jsxs("div", { className: `${styles.docWorkspace} ${useTrayIndex ? styles.docWorkspaceTrayMode : ''} ${useTrayIndex && documentTrayOpen ? styles.docWorkspaceTrayOpen : ''}`.trim(), children: [useTrayIndex ? (_jsx("div", { className: `${styles.docTrayPanel} ${documentTrayOpen ? styles.docTrayPanelOpen : ''}`.trim(), "aria-hidden": !documentTrayOpen, children: documentIndex })) : (_jsx("div", { className: styles.docIndexPanel, children: documentIndex })), _jsx("div", { className: styles.docViewerPanel, children: selectedDoc ? (_jsx(DocumentViewer, { doc: selectedDoc, taskTitle: selectedDoc.taskId ? (taskById.get(selectedDoc.taskId)?.title ?? null) : null, tasks: tasks, apiBaseUrl: apiBaseUrl, onSaved: (_updatedAt) => onRefresh(), onDeleted: () => void onDeleteSelected(), onBackToTask: onBackToTask, enableTaskGeneration: enableTaskGeneration })) : !loading && (_jsx("div", { className: styles.docViewerEmpty, children: _jsx("p", { className: "tf-empty-copy", children: documents.length === 0 ? 'No documents match your current filters.' : 'Select a document to view it.' }) })) })] }));
10
13
  }
11
- export function DocumentWorkspaceShell({ tasks, runtimeMode = 'local', apiBaseUrl = '', cloudAuthBaseUrl = '', typeFilters: controlledTypeFilters, attachmentFilters: controlledAttachmentFilters, onTypeFiltersChange, onAttachmentFiltersChange, requestedDocPath = null, requestedDocAssetId = null, onRequestedDocHandled, enableTaskGeneration = false }) {
14
+ export function DocumentWorkspaceShell({ tasks, runtimeMode = 'local', apiBaseUrl = '', cloudAuthBaseUrl = '', typeFilters: controlledTypeFilters, attachmentFilters: controlledAttachmentFilters, onTypeFiltersChange, onAttachmentFiltersChange, documentTrayOpen, onCloseDocumentTray, requestedDocPath = null, requestedDocAssetId = null, onRequestedDocHandled, onBackToTask, enableTaskGeneration = false }) {
12
15
  const { documentApiBaseUrl, filteredDocs, loading, error, selectedDoc, searchQuery, hasActiveFilters, setSearchQuery, fetchDocuments, selectDoc, handleDeleteSelected, } = useDocumentWorkspaceController({
13
16
  runtimeMode,
14
17
  apiBaseUrl,
@@ -21,5 +24,13 @@ export function DocumentWorkspaceShell({ tasks, runtimeMode = 'local', apiBaseUr
21
24
  requestedDocAssetId,
22
25
  onRequestedDocHandled,
23
26
  });
24
- return (_jsx(DocumentWorkspace, { tasks: tasks, apiBaseUrl: documentApiBaseUrl, documents: filteredDocs, loading: loading, error: error, selectedDoc: selectedDoc, searchQuery: searchQuery, hasActiveFilters: hasActiveFilters, onSearchChange: setSearchQuery, onSelectDoc: selectDoc, onRefresh: fetchDocuments, onDeleteSelected: handleDeleteSelected, enableTaskGeneration: enableTaskGeneration }));
27
+ const previousDocumentTrayOpenRef = React.useRef(documentTrayOpen);
28
+ React.useEffect(() => {
29
+ const wasOpen = previousDocumentTrayOpenRef.current === true;
30
+ previousDocumentTrayOpenRef.current = documentTrayOpen;
31
+ if (documentTrayOpen !== true || wasOpen)
32
+ return;
33
+ void fetchDocuments();
34
+ }, [documentTrayOpen, fetchDocuments]);
35
+ return (_jsx(DocumentWorkspace, { tasks: tasks, apiBaseUrl: documentApiBaseUrl, documents: filteredDocs, loading: loading, error: error, selectedDoc: selectedDoc, searchQuery: searchQuery, hasActiveFilters: hasActiveFilters, documentTrayOpen: documentTrayOpen, onSearchChange: setSearchQuery, onSelectDoc: selectDoc, onRefresh: fetchDocuments, onCloseDocumentTray: onCloseDocumentTray, onDeleteSelected: handleDeleteSelected, onBackToTask: onBackToTask, enableTaskGeneration: enableTaskGeneration }));
25
36
  }
@@ -45,7 +45,7 @@ function createEmptyReviewSessionsResponse() {
45
45
  };
46
46
  }
47
47
  function renderDocumentWorkspace(props) {
48
- return render(_jsx(DocumentWorkspace, { tasks: props.tasks ?? [], documents: props.documents, loading: false, error: null, selectedDoc: props.selectedDoc ?? null, searchQuery: props.searchQuery ?? '', hasActiveFilters: props.hasActiveFilters ?? false, onSearchChange: props.onSearchChange ?? (() => { }), onSelectDoc: props.onSelectDoc ?? (() => { }), onRefresh: props.onRefresh ?? (() => { }), onDeleteSelected: props.onDeleteSelected ?? (() => { }) }));
48
+ return render(_jsx(DocumentWorkspace, { tasks: props.tasks ?? [], documents: props.documents, loading: false, error: null, selectedDoc: props.selectedDoc ?? null, searchQuery: props.searchQuery ?? '', hasActiveFilters: props.hasActiveFilters ?? false, documentTrayOpen: props.documentTrayOpen, onSearchChange: props.onSearchChange ?? (() => { }), onSelectDoc: props.onSelectDoc ?? (() => { }), onRefresh: props.onRefresh ?? (() => { }), onDeleteSelected: props.onDeleteSelected ?? (() => { }) }));
49
49
  }
50
50
  describe('Document workspace management', () => {
51
51
  beforeEach(() => {
@@ -97,6 +97,16 @@ describe('Document workspace management', () => {
97
97
  expect(screen.queryByRole('button', { name: /Attached notes/i })).not.toBeInTheDocument();
98
98
  expect(screen.getByText('Unattached')).toBeInTheDocument();
99
99
  });
100
+ it('can render the document index as a collapsible tray', () => {
101
+ const docs = [createDoc({ id: 'tray-doc', displayName: 'Tray Document' })];
102
+ const { container, rerender } = renderDocumentWorkspace({
103
+ documents: docs,
104
+ documentTrayOpen: true,
105
+ });
106
+ expect(container.querySelector('[aria-hidden="false"]')).toHaveTextContent('Tray Document');
107
+ rerender(_jsx(DocumentWorkspace, { tasks: [], documents: docs, loading: false, error: null, selectedDoc: null, searchQuery: "", hasActiveFilters: false, documentTrayOpen: false, onSearchChange: () => { }, onSelectDoc: () => { }, onRefresh: () => { }, onDeleteSelected: () => { } }));
108
+ expect(container.querySelector('[aria-hidden="true"]')).toHaveTextContent('Tray Document');
109
+ });
100
110
  it('keeps document search in the index without duplicate filter controls', () => {
101
111
  renderDocumentWorkspace({ documents: [createDoc()] });
102
112
  expect(screen.getByPlaceholderText('Search documents…')).toBeInTheDocument();
@@ -145,6 +155,46 @@ describe('Document workspace management', () => {
145
155
  confirmSpy.mockRestore();
146
156
  alertSpy.mockRestore();
147
157
  });
158
+ it('opens an attach modal from the document viewer and attaches the document to a task', async () => {
159
+ const user = userEvent.setup();
160
+ fetchTaskforceApi
161
+ .mockResolvedValueOnce({
162
+ ok: true,
163
+ text: async () => '# Example\n',
164
+ })
165
+ .mockResolvedValueOnce(createEmptyReviewSessionsResponse())
166
+ .mockResolvedValueOnce({
167
+ ok: true,
168
+ json: async () => ({ success: true, attached: true, alreadyAttached: false }),
169
+ });
170
+ const onSaved = vi.fn();
171
+ render(_jsx(DocumentViewer, { doc: createDoc({
172
+ id: 'doc-attach',
173
+ assetId: 'asset-doc-attach',
174
+ attachmentCount: 0,
175
+ attachmentState: 'unattached',
176
+ }), taskTitle: null, tasks: [
177
+ { id: 'task-1', title: 'First attach target', referenceLabel: 'T-11' },
178
+ { id: 'task-2', title: 'Second attach target', referenceLabel: 'T-12' },
179
+ ], onSaved: onSaved }));
180
+ await waitFor(() => expect(fetchTaskforceApi).toHaveBeenCalledWith('/api/taskforce/documents/asset-doc-1/content', { credentials: 'same-origin' }, ''));
181
+ await user.click(screen.getByRole('button', { name: /^Attach$/i }));
182
+ expect(screen.getByText(/Attach this document to a task from the document manager/i)).toBeInTheDocument();
183
+ expect(screen.getByText('T-11')).toBeInTheDocument();
184
+ expect(screen.getByText('T-12')).toBeInTheDocument();
185
+ await user.clear(screen.getByPlaceholderText('Search tasks…'));
186
+ await user.type(screen.getByPlaceholderText('Search tasks…'), 'T-12');
187
+ expect(screen.queryByRole('button', { name: /First attach target/i })).not.toBeInTheDocument();
188
+ expect(screen.getByRole('button', { name: /Second attach target/i })).toBeInTheDocument();
189
+ await user.clear(screen.getByPlaceholderText('Search tasks…'));
190
+ await user.type(screen.getByPlaceholderText('Search tasks…'), 'Second');
191
+ await user.click(screen.getByRole('button', { name: /Second attach target/i }));
192
+ await waitFor(() => expect(fetchTaskforceApi).toHaveBeenLastCalledWith('/api/taskforce/documents/asset-doc-attach/attach', expect.objectContaining({
193
+ method: 'POST',
194
+ credentials: 'include',
195
+ }), ''));
196
+ expect(onSaved).toHaveBeenCalled();
197
+ });
148
198
  it('shows delete and unlink for attached docs and sends forceDetach on delete', async () => {
149
199
  const user = userEvent.setup();
150
200
  fetchTaskforceApi
@@ -208,11 +208,13 @@ export interface TaskSettingsModel {
208
208
  cleanupWarnings?: string[];
209
209
  }>;
210
210
  onMcpHostRootChange?: (value: string) => void;
211
+ onOpenCloudAuth?: (mode: 'login' | 'register') => void;
211
212
  isAuthenticated?: boolean;
212
213
  }
213
214
  interface TaskSettingsProps {
214
215
  settingsModel: TaskSettingsModel;
215
216
  onSectionChange?: (section: any) => void;
217
+ renderMode?: 'full' | 'mcp-only';
216
218
  }
217
219
  export declare function TaskSettings(props: TaskSettingsProps): import("react/jsx-runtime").JSX.Element;
218
220
  export {};