@taskforcehq/taskforce 0.3.305 → 0.3.306
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.
- package/dist/Taskforce.module.css +295 -89
- package/dist/TaskforceCore.js +215 -43
- package/dist/TaskforceCore.test.js +433 -28
- package/dist/assets/fonts/CourierPrime-Bold.woff2 +0 -0
- package/dist/assets/fonts/CourierPrime-BoldItalic.woff2 +0 -0
- package/dist/assets/fonts/CourierPrime-Italic.woff2 +0 -0
- package/dist/assets/fonts/CourierPrime-Regular.woff2 +0 -0
- package/dist/assets/fonts/Noir_medium.woff2 +0 -0
- package/dist/assets/fonts/Noir_regular.woff2 +0 -0
- package/dist/assets/fonts/RussoOne-Regular.woff2 +0 -0
- package/dist/assets/fonts/SpecialElite-Regular.woff2 +0 -0
- package/dist/compat/workspaceSyncCompat.js +46 -2
- package/dist/compat/workspaceSyncCompat.test.js +38 -1
- package/dist/components/context/ContextAttachmentManager.d.ts +14 -0
- package/dist/components/context/ContextAttachmentManager.js +549 -0
- package/dist/components/features/AgentsModule.d.ts +10 -1
- package/dist/components/features/AgentsModule.js +260 -15
- package/dist/components/features/AgentsModule.test.js +255 -5
- package/dist/components/features/DocumentViewer.d.ts +5 -1
- package/dist/components/features/DocumentViewer.js +60 -3
- package/dist/components/features/DocumentWorkspace.js +1 -1
- package/dist/components/features/DocumentWorkspace.test.js +33 -0
- package/dist/components/features/documentWorkspaceModel.d.ts +1 -1
- package/dist/components/task/TaskCard.js +19 -28
- package/dist/components/task/TaskCard.test.js +38 -3
- package/dist/components/task/TaskContextUpload.js +4 -526
- package/dist/components/task/TaskForm.js +85 -90
- package/dist/components/task/TaskForm.test.js +110 -8
- package/dist/components/task/TaskKanban.d.ts +2 -1
- package/dist/components/task/TaskKanban.js +94 -13
- package/dist/components/task/TaskKanban.test.js +36 -0
- package/dist/components/ui/EditableAvatarButton.d.ts +18 -0
- package/dist/components/ui/EditableAvatarButton.js +18 -0
- package/dist/components/views/PlanComparisonPage.d.ts +9 -1
- package/dist/components/views/PlanComparisonPage.js +70 -14
- package/dist/components/views/PlanComparisonPage.test.js +238 -0
- package/dist/components/views/PlansPage.js +127 -36
- package/dist/components/views/PlansPage.test.js +294 -12
- package/dist/components/views/StandaloneLayout.js +283 -533
- package/dist/components/views/panels/PlanningDrawer.d.ts +8 -1
- package/dist/components/views/panels/PlanningDrawer.js +18 -11
- package/dist/components/views/panels/PlanningDrawer.test.d.ts +1 -0
- package/dist/components/views/panels/PlanningDrawer.test.js +141 -0
- package/dist/components/views/planningScope.d.ts +20 -0
- package/dist/components/views/planningScope.js +25 -0
- package/dist/components/views/planningScope.test.d.ts +1 -0
- package/dist/components/views/planningScope.test.js +60 -0
- package/dist/components/views/standalone/modals/AccountHubModal.d.ts +26 -1
- package/dist/components/views/standalone/modals/AccountHubModal.js +76 -2
- package/dist/components/views/standalone/modals/AccountHubModal.test.js +20 -3
- package/dist/components/views/standalone/modals/AvatarImageManagerModal.d.ts +20 -0
- package/dist/components/views/standalone/modals/AvatarImageManagerModal.js +127 -0
- package/dist/components/views/standalone/modals/EditProfileModal.d.ts +2 -9
- package/dist/components/views/standalone/modals/EditProfileModal.js +3 -2
- package/dist/components/views/standalone/modals/SyncEnableWarningModal.d.ts +9 -0
- package/dist/components/views/standalone/modals/SyncEnableWarningModal.js +6 -0
- package/dist/components/views/standalone/modals/SyncEnableWarningModal.test.d.ts +1 -0
- package/dist/components/views/standalone/modals/SyncEnableWarningModal.test.js +24 -0
- package/dist/components/views/standalone/modals/SyncStatusModal.d.ts +2 -1
- package/dist/components/views/standalone/modals/SyncStatusModal.js +2 -2
- package/dist/core/AiProfileService.d.ts +39 -2
- package/dist/core/AiProfileService.js +639 -73
- package/dist/core/AiProfileService.test.js +19 -1
- package/dist/core/AiProfiles.test.js +566 -7
- package/dist/core/AttachmentLinkService.js +57 -15
- package/dist/core/AuthIdentityService.d.ts +107 -0
- package/dist/core/AuthIdentityService.js +284 -0
- package/dist/core/AuthTokenService.d.ts +4 -0
- package/dist/core/AuthTokenService.js +34 -6
- package/dist/core/AuthTokenService.test.js +3 -3
- package/dist/core/EntitlementsPolicy.test.js +2 -2
- package/dist/core/GlobalSettingsService.js +115 -6
- package/dist/core/PlanEntitlementService.d.ts +23 -2
- package/dist/core/PlanEntitlementService.js +343 -57
- package/dist/core/PlanFeatureCatalog.test.d.ts +1 -0
- package/dist/core/PlanFeatureCatalog.test.js +84 -0
- package/dist/core/PlanVersionPolicy.test.js +35 -2
- package/dist/core/PlanningEntities.test.js +52 -0
- package/dist/core/SignupMonetizationSettings.test.js +199 -27
- package/dist/core/SyncReconciliationService.js +2 -65
- package/dist/core/SystemAdmin.test.js +42 -42
- package/dist/core/TaskAttachmentsCanonical.test.js +51 -1
- package/dist/core/Taskforce.d.ts +225 -14
- package/dist/core/Taskforce.js +765 -192
- package/dist/core/Taskforce.listTasksSlim.test.d.ts +1 -0
- package/dist/core/Taskforce.listTasksSlim.test.js +91 -0
- package/dist/core/UserProfileAvatarDrafts.test.js +30 -3
- package/dist/core/WorkspaceLifecycleService.js +0 -5
- package/dist/core/WorkspacePermissions.test.js +25 -1
- package/dist/core/WorkspacePlanMode.test.js +117 -16
- package/dist/core/shared.d.ts +1 -1
- package/dist/core/shared.js +5 -1
- package/dist/core/types.d.ts +68 -2
- package/dist/hooks/auth/useTaskforceAuthBootstrap.js +26 -1
- package/dist/hooks/sync/bootstrap.js +2 -0
- package/dist/hooks/sync/controlPlane.d.ts +37 -0
- package/dist/hooks/sync/controlPlane.js +78 -0
- package/dist/hooks/sync/controlPlane.test.d.ts +1 -0
- package/dist/hooks/sync/controlPlane.test.js +121 -0
- package/dist/hooks/sync/lifecyclePolicy.d.ts +45 -0
- package/dist/hooks/sync/lifecyclePolicy.js +93 -0
- package/dist/hooks/sync/lifecyclePolicy.test.d.ts +1 -0
- package/dist/hooks/sync/lifecyclePolicy.test.js +124 -0
- package/dist/hooks/sync/orchestratorShared.d.ts +10 -1
- package/dist/hooks/sync/orchestratorShared.js +68 -47
- package/dist/hooks/sync/orchestratorShared.test.js +50 -1
- package/dist/hooks/sync/pullLifecycle.d.ts +31 -0
- package/dist/hooks/sync/pullLifecycle.js +24 -0
- package/dist/hooks/sync/pullLifecycle.test.d.ts +1 -0
- package/dist/hooks/sync/pullLifecycle.test.js +80 -0
- package/dist/hooks/sync/recovery.d.ts +16 -2
- package/dist/hooks/sync/recovery.js +171 -53
- package/dist/hooks/sync/recovery.test.d.ts +1 -0
- package/dist/hooks/sync/recovery.test.js +292 -0
- package/dist/hooks/sync/transfers.d.ts +15 -3
- package/dist/hooks/sync/transfers.js +179 -62
- package/dist/hooks/sync/transfers.test.d.ts +1 -0
- package/dist/hooks/sync/transfers.test.js +396 -0
- package/dist/hooks/sync/useRecentSyncEvents.d.ts +21 -0
- package/dist/hooks/sync/useRecentSyncEvents.js +92 -0
- package/dist/hooks/sync/useRecentSyncEvents.test.d.ts +1 -0
- package/dist/hooks/sync/useRecentSyncEvents.test.js +124 -0
- package/dist/hooks/sync/useSyncStatusActions.d.ts +30 -0
- package/dist/hooks/sync/useSyncStatusActions.js +88 -0
- package/dist/hooks/sync/useSyncStatusActions.test.d.ts +1 -0
- package/dist/hooks/sync/useSyncStatusActions.test.js +133 -0
- package/dist/hooks/sync/useSyncStatusControls.d.ts +25 -0
- package/dist/hooks/sync/useSyncStatusControls.js +60 -0
- package/dist/hooks/sync/useSyncStatusControls.test.d.ts +1 -0
- package/dist/hooks/sync/useSyncStatusControls.test.js +88 -0
- package/dist/hooks/useSyncOrchestrator.aiProfiles.test.js +12 -0
- package/dist/hooks/useSyncOrchestrator.d.ts +9 -2
- package/dist/hooks/useSyncOrchestrator.js +359 -178
- package/dist/hooks/useSyncOrchestrator.retry-closure.test.js +407 -0
- package/dist/hooks/useTaskData.js +8 -3
- package/dist/hooks/useTaskData.test.js +45 -0
- package/dist/hooks/useTaskforce.d.ts +60 -2
- package/dist/hooks/useTaskforce.js +549 -240
- package/dist/hooks/useTaskforce.runtime-routing.test.d.ts +1 -0
- package/dist/hooks/useTaskforce.runtime-routing.test.js +152 -0
- package/dist/hooks/useTaskforce.sync-behavior.test.js +2380 -212
- package/dist/hooks/useWorkspaceSyncController.d.ts +1 -0
- package/dist/hooks/useWorkspaceSyncController.js +6 -1
- package/dist/hooks/workspace/useTaskforceWorkspaceBootstrap.d.ts +16 -0
- package/dist/hooks/workspace/useTaskforceWorkspaceBootstrap.js +316 -35
- package/dist/localization/locales/en-US.d.ts +0 -1
- package/dist/localization/locales/en-US.js +0 -1
- package/dist/localization/locales/es-419.js +0 -1
- package/dist/localization/locales/pt-BR.js +0 -1
- package/dist/mcp/documentAssetRegistrar.js +3 -3
- package/dist/mcp/runtime.d.ts +2 -0
- package/dist/mcp/runtime.js +131 -64
- package/dist/mcp/runtime.test.js +291 -12
- package/dist/migrations/taskSchemaMigrations.d.ts +1 -1
- package/dist/migrations/taskSchemaMigrations.js +171 -61
- package/dist/migrations/taskSchemaMigrations.test.js +15 -0
- package/dist/resources/templates/workflow-sources/workflowDocs.mjs +6 -6
- package/dist/resources/templates/workflows/collaborate.yaml +1 -1
- package/dist/resources/templates/workflows/evaluate.yaml +2 -2
- package/dist/resources/templates/workflows/plan.yaml +1 -1
- package/dist/resources/templates/workflows/review.yaml +2 -2
- package/dist/server/annotatedAttachmentsRoutes.test.js +3 -3
- package/dist/server/auth/providers/apple.d.ts +33 -0
- package/dist/server/auth/providers/apple.js +82 -0
- package/dist/server/auth/providers/appleClientSecret.d.ts +37 -0
- package/dist/server/auth/providers/appleClientSecret.js +65 -0
- package/dist/server/auth/providers/github.d.ts +26 -0
- package/dist/server/auth/providers/github.js +86 -0
- package/dist/server/auth/providers/google.d.ts +21 -0
- package/dist/server/auth/providers/google.js +56 -0
- package/dist/server/auth/providers/types.d.ts +21 -0
- package/dist/server/auth/providers/types.js +1 -0
- package/dist/server/auth.d.ts +2 -0
- package/dist/server/auth.js +6 -3
- package/dist/server/documentReviewRoutes.test.js +36 -3
- package/dist/server/index.cookieProxy.test.js +1 -0
- package/dist/server/index.d.ts +12 -0
- package/dist/server/index.js +120 -9
- package/dist/server/index.rateLimit.test.js +3 -0
- package/dist/server/index.test.js +106 -3
- package/dist/server/routes/admin.d.ts +3 -0
- package/dist/server/routes/admin.js +272 -19
- package/dist/server/routes/auth.d.ts +15 -2
- package/dist/server/routes/auth.js +994 -57
- package/dist/server/routes/authSupport.d.ts +1 -1
- package/dist/server/routes/authSupport.js +1 -1
- package/dist/server/routes/billing.js +361 -35
- package/dist/server/routes/billing.test.js +592 -12
- package/dist/server/routes/documents.d.ts +1 -0
- package/dist/server/routes/documents.js +172 -70
- package/dist/server/routes/shared.d.ts +28 -2
- package/dist/server/routes/shared.js +69 -4
- package/dist/server/routes/sync.integration.test.js +437 -39
- package/dist/server/routes/sync.js +61 -605
- package/dist/server/routes/syncAuxRoutes.d.ts +15 -0
- package/dist/server/routes/syncAuxRoutes.js +91 -0
- package/dist/server/routes/syncAuxRoutes.test.d.ts +1 -0
- package/dist/server/routes/syncAuxRoutes.test.js +158 -0
- package/dist/server/routes/syncPullApplyRoutes.d.ts +36 -0
- package/dist/server/routes/syncPullApplyRoutes.js +204 -0
- package/dist/server/routes/syncPushRoutes.d.ts +24 -0
- package/dist/server/routes/syncPushRoutes.js +212 -0
- package/dist/server/routes/syncRouteGuards.d.ts +36 -0
- package/dist/server/routes/syncRouteGuards.js +67 -0
- package/dist/server/routes/syncRouteGuards.test.d.ts +1 -0
- package/dist/server/routes/syncRouteGuards.test.js +94 -0
- package/dist/server/routes/syncRouteTypes.d.ts +13 -0
- package/dist/server/routes/syncRouteTypes.js +1 -0
- package/dist/server/routes/syncSnapshotRoutes.d.ts +66 -0
- package/dist/server/routes/syncSnapshotRoutes.js +180 -0
- package/dist/server/routes/tasks.js +43 -1
- package/dist/server/routes/workspaces.js +36 -5
- package/dist/server/routes.js +60 -3
- package/dist/server/routes.test.js +1824 -171
- package/dist/services/workflowExportSnapshots.test.js +2 -0
- package/dist/shared/aiProfileSeatScope.d.ts +5 -0
- package/dist/shared/aiProfileSeatScope.js +21 -0
- package/dist/shared/passwordPolicy.d.ts +13 -0
- package/dist/shared/passwordPolicy.js +84 -0
- package/dist/shared/passwordPolicy.test.d.ts +1 -0
- package/dist/shared/passwordPolicy.test.js +34 -0
- package/dist/storage/workspaceAssetStore.d.ts +15 -3
- package/dist/storage/workspaceAssetStore.js +132 -44
- package/dist/storage/workspaceAssetStore.test.js +93 -6
- package/dist/styles/fonts.css +70 -0
- package/dist/sync/collaborationSyncPayload.d.ts +6 -1
- package/dist/sync/collaborationSyncPayload.js +17 -11
- package/dist/sync/collaborationSyncPayload.test.js +13 -1
- package/dist/sync/contentSyncState.d.ts +4 -8
- package/dist/sync/contentSyncState.js +17 -70
- package/dist/sync/syncApplyHandlers.d.ts +7 -0
- package/dist/sync/syncApplyHandlers.js +28 -13
- package/dist/sync/syncApplyHandlers.test.js +70 -3
- package/dist/sync/syncFieldSemantics.d.ts +12 -0
- package/dist/sync/syncFieldSemantics.js +55 -0
- package/dist/sync/syncResourceAdapters.d.ts +44 -0
- package/dist/sync/syncResourceAdapters.js +77 -0
- package/dist/sync/syncService.d.ts +5 -0
- package/dist/sync/syncService.js +52 -0
- package/dist/sync/syncService.test.js +57 -3
- package/dist/sync/taskSyncChangeKey.d.ts +2 -0
- package/dist/sync/taskSyncChangeKey.js +143 -0
- package/dist/sync/taskSyncPayload.js +3 -8
- package/dist/sync/workspacePullFeed.js +4 -3
- package/dist/sync/workspacePullFeed.test.js +43 -0
- package/dist/sync/workspaceRepair.js +5 -2
- package/dist/sync/workspaceSyncModel.d.ts +45 -0
- package/dist/sync/workspaceSyncModel.js +183 -28
- package/dist/sync/workspaceSyncModel.test.js +291 -12
- package/dist/sync/workspaceSyncState.d.ts +17 -0
- package/dist/sync/workspaceSyncState.js +2 -1
- package/dist/sync/workspaceSyncSurface.js +3 -8
- package/dist/types.d.ts +17 -0
- package/dist/ui/assets/AgentsModule-DrLawwNl.js +1 -0
- package/dist/ui/assets/{AnnotatedAttachmentWorkspace-BG6P_GVW.js → AnnotatedAttachmentWorkspace-Z9_whf7F.js} +1 -1
- package/dist/ui/assets/CourierPrime-Bold-BuLj_dpw.woff2 +0 -0
- package/dist/ui/assets/CourierPrime-BoldItalic-BrK2pUkZ.woff2 +0 -0
- package/dist/ui/assets/CourierPrime-Italic-DFUZK5Ey.woff2 +0 -0
- package/dist/ui/assets/CourierPrime-Regular-BsKphzVf.woff2 +0 -0
- package/dist/ui/assets/DocumentWorkspace-B03MaSkw.js +252 -0
- package/dist/ui/assets/DocumentWorkspace-Dhdso07p.css +1 -0
- package/dist/ui/assets/{InitiativesModule-Dhm7M8e7.js → InitiativesModule-9E6ParrY.js} +1 -1
- package/dist/ui/assets/Noir_medium-bxQwKGzB.woff2 +0 -0
- package/dist/ui/assets/Noir_regular-ojf2kxlG.woff2 +0 -0
- package/dist/ui/assets/PlansPage-CvfnMiWq.css +1 -0
- package/dist/ui/assets/PlansPage-DRXscYxH.js +1 -0
- package/dist/ui/assets/RussoOne-Regular-Cu_qq_qC.woff2 +0 -0
- package/dist/ui/assets/SpecialElite-Regular-Di6gSvAS.woff2 +0 -0
- package/dist/ui/assets/{TaskSettings-ln0aF7xc.js → TaskSettings-DZX4jk7e.js} +1 -1
- package/dist/ui/assets/{WorkflowsModule-BcS4afRn.js → WorkflowsModule-BA7jcEpH.js} +1 -1
- package/dist/ui/assets/index-9eT8e0Lu.css +1 -0
- package/dist/ui/assets/index-C6k75jr8.js +6 -0
- package/dist/ui/assets/{vendor-icons-BSUaRwF8.js → vendor-icons-DuEd_65c.js} +1 -1
- package/dist/ui/fonts/CourierPrime-Bold.ttf +0 -0
- package/dist/ui/fonts/CourierPrime-Bold.woff2 +0 -0
- package/dist/ui/fonts/CourierPrime-BoldItalic.ttf +0 -0
- package/dist/ui/fonts/CourierPrime-BoldItalic.woff2 +0 -0
- package/dist/ui/fonts/CourierPrime-Italic.ttf +0 -0
- package/dist/ui/fonts/CourierPrime-Italic.woff2 +0 -0
- package/dist/ui/fonts/CourierPrime-Regular.ttf +0 -0
- package/dist/ui/fonts/CourierPrime-Regular.woff2 +0 -0
- package/dist/ui/fonts/Noir_medium.otf +0 -0
- package/dist/ui/fonts/Noir_medium.woff2 +0 -0
- package/dist/ui/fonts/Noir_regular.otf +0 -0
- package/dist/ui/fonts/Noir_regular.woff2 +0 -0
- package/dist/ui/fonts/RussoOne-Regular.woff2 +0 -0
- package/dist/ui/fonts/SpecialElite-Regular.ttf +0 -0
- package/dist/ui/fonts/SpecialElite-Regular.woff2 +0 -0
- package/dist/ui/index.html +3 -3
- package/dist/utils/accountProfileSummaryCache.d.ts +5 -0
- package/dist/utils/assignees.d.ts +2 -0
- package/dist/utils/assignees.js +2 -0
- package/dist/utils/avatarUpload.d.ts +27 -0
- package/dist/utils/avatarUpload.js +132 -0
- package/dist/utils/avatarUpload.test.d.ts +1 -0
- package/dist/utils/avatarUpload.test.js +40 -0
- package/dist/utils/bootstrapTrace.d.ts +7 -0
- package/dist/utils/bootstrapTrace.js +29 -0
- package/dist/utils/commercialLifecyclePresentation.js +3 -0
- package/dist/utils/contextAssetEvents.d.ts +2 -0
- package/dist/utils/syncEventDetails.d.ts +3 -0
- package/dist/utils/syncEventDetails.js +64 -0
- package/dist/utils/syncEventDetails.test.d.ts +1 -0
- package/dist/utils/syncEventDetails.test.js +23 -0
- package/dist/utils/syncEventPresentation.d.ts +15 -0
- package/dist/utils/syncEventPresentation.js +129 -0
- package/dist/utils/syncEventPresentation.test.d.ts +1 -0
- package/dist/utils/syncEventPresentation.test.js +64 -0
- package/dist/utils/syncStatusPresentation.d.ts +72 -0
- package/dist/utils/syncStatusPresentation.js +217 -0
- package/dist/utils/syncStatusPresentation.test.d.ts +1 -0
- package/dist/utils/syncStatusPresentation.test.js +164 -0
- package/dist/utils/taskActivity.d.ts +2 -0
- package/dist/utils/taskActivity.js +228 -22
- package/dist/utils/taskActivity.test.js +119 -8
- package/dist/utils/workspaceSyncPresentation.d.ts +4 -0
- package/dist/utils/workspaceSyncPresentation.js +68 -2
- package/dist/utils/workspaceSyncPresentation.test.js +36 -1
- package/package.json +3 -2
- package/scripts/export-sqlite-to-postgres.mjs +7 -9
- package/scripts/run-e2e-realtime.sh +17 -3
- package/scripts/run-smoke.sh +17 -5
- package/scripts/run-soak.sh +17 -5
- package/src/resources/templates/workflow-sources/workflowDocs.mjs +6 -6
- package/src/resources/templates/workflows/collaborate.yaml +1 -1
- package/src/resources/templates/workflows/evaluate.yaml +2 -2
- package/src/resources/templates/workflows/plan.yaml +1 -1
- package/src/resources/templates/workflows/review.yaml +2 -2
- package/dist/ui/assets/AgentsModule-N2MnQBZk.js +0 -1
- package/dist/ui/assets/DocumentWorkspace-B3nV_Gc5.css +0 -1
- package/dist/ui/assets/DocumentWorkspace-DRbDMwh8.js +0 -252
- package/dist/ui/assets/PlansPage-C2dd2n1X.css +0 -1
- package/dist/ui/assets/PlansPage-Cq0zXj3I.js +0 -1
- package/dist/ui/assets/RussoOne-Regular-C3BxZIj7.ttf +0 -0
- package/dist/ui/assets/index-C2-OXZV7.css +0 -1
- package/dist/ui/assets/index-DcSI5F86.js +0 -6
- package/scripts/migrate-planning-model.ts +0 -233
|
@@ -30,7 +30,7 @@ const baseInitiative = (overrides = {}) => ({
|
|
|
30
30
|
createdAt: '2026-02-13T00:00:00.000Z',
|
|
31
31
|
...overrides,
|
|
32
32
|
});
|
|
33
|
-
describe('TaskCard
|
|
33
|
+
describe('TaskCard planning hierarchy references', () => {
|
|
34
34
|
it('renders initiative/workstream/task hierarchy references when planning context is provided', () => {
|
|
35
35
|
const task = baseTask({
|
|
36
36
|
id: 'task-hierarchy',
|
|
@@ -105,7 +105,7 @@ describe('TaskCard parent/child progress', () => {
|
|
|
105
105
|
});
|
|
106
106
|
render(_jsx(TaskCard, { task: task, onClick: () => { }, onCopyId: () => { } }));
|
|
107
107
|
expect(screen.getByText(/Latest Activity:/)).toBeInTheDocument();
|
|
108
|
-
expect(screen.getByText(/Status:
|
|
108
|
+
expect(screen.getByText(/Status changed: Task -> Review/)).toBeInTheDocument();
|
|
109
109
|
expect(screen.getByTitle('2 activity items')).toBeInTheDocument();
|
|
110
110
|
});
|
|
111
111
|
it('uses the current workstream reference in latest activity summaries', () => {
|
|
@@ -136,7 +136,7 @@ describe('TaskCard parent/child progress', () => {
|
|
|
136
136
|
]
|
|
137
137
|
});
|
|
138
138
|
render(_jsx(TaskCard, { task: task, taskWorkstream: baseWorkstream({ id: 'workstream-1', referenceNumber: 12 }), onClick: () => { }, onCopyId: () => { } }));
|
|
139
|
-
expect(screen.getByText(/Workstream: none -> WS-12/)).toBeInTheDocument();
|
|
139
|
+
expect(screen.getByText(/Workstream changed: none -> WS-12/)).toBeInTheDocument();
|
|
140
140
|
expect(screen.queryByText(/workstream-1/i)).not.toBeInTheDocument();
|
|
141
141
|
});
|
|
142
142
|
it('renders named AI assignee labels from assignee options', () => {
|
|
@@ -151,6 +151,41 @@ describe('TaskCard parent/child progress', () => {
|
|
|
151
151
|
], onClick: () => { }, onCopyId: () => { } }));
|
|
152
152
|
expect(screen.getByLabelText('Assigned to Codex')).toBeInTheDocument();
|
|
153
153
|
});
|
|
154
|
+
it('renders AI profile photos in the assignee indicator when available', () => {
|
|
155
|
+
const task = baseTask({
|
|
156
|
+
id: 'task-ai-avatar',
|
|
157
|
+
title: 'AI-owned task',
|
|
158
|
+
assignee: 'ai-profile-123'
|
|
159
|
+
});
|
|
160
|
+
render(_jsx(TaskCard, { task: task, assigneeOptions: [
|
|
161
|
+
{ value: 'ai-profile-123', label: 'Codex', icon: 'Bot', color: '#0ea5e9', kind: 'agent', avatarUrl: '/api/taskforce/context/ai-avatar.webp' }
|
|
162
|
+
], onClick: () => { }, onCopyId: () => { } }));
|
|
163
|
+
const badge = screen.getByLabelText('Assigned to Codex');
|
|
164
|
+
expect(badge.querySelector('img')).toHaveAttribute('src', '/api/taskforce/context/ai-avatar.webp');
|
|
165
|
+
});
|
|
166
|
+
it('renders member profile photos in the assignee indicator when available', () => {
|
|
167
|
+
const task = baseTask({
|
|
168
|
+
id: 'task-member-avatar',
|
|
169
|
+
title: 'Member-owned task',
|
|
170
|
+
assignee: 'user-123'
|
|
171
|
+
});
|
|
172
|
+
render(_jsx(TaskCard, { task: task, assigneeOptions: [
|
|
173
|
+
{ value: 'user-123', label: 'Ross Burke', icon: 'User', color: '#22c55e', kind: 'member', avatarUrl: '/api/taskforce/context/member-avatar.webp' }
|
|
174
|
+
], onClick: () => { }, onCopyId: () => { } }));
|
|
175
|
+
const badge = screen.getByLabelText('Assigned to Ross Burke');
|
|
176
|
+
expect(badge.querySelector('img')).toHaveAttribute('src', '/api/taskforce/context/member-avatar.webp');
|
|
177
|
+
});
|
|
178
|
+
it('renders an empty avatar circle for unassigned tasks', () => {
|
|
179
|
+
const task = baseTask({
|
|
180
|
+
id: 'task-unassigned-avatar',
|
|
181
|
+
title: 'Unassigned task',
|
|
182
|
+
assignee: 'unassigned'
|
|
183
|
+
});
|
|
184
|
+
render(_jsx(TaskCard, { task: task, onClick: () => { }, onCopyId: () => { } }));
|
|
185
|
+
const indicator = screen.getByLabelText('Unassigned');
|
|
186
|
+
expect(indicator.querySelector('img')).toBeNull();
|
|
187
|
+
expect(indicator.querySelector('svg')).toBeNull();
|
|
188
|
+
});
|
|
154
189
|
it('prefers provided type metadata over hardcoded defaults', () => {
|
|
155
190
|
const task = baseTask({
|
|
156
191
|
id: 'task-type-metadata',
|
|
@@ -1,528 +1,6 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
3
|
-
import { AlertTriangle, Copy, Download, File, FileUp, Loader2, PencilLine, Trash2, ChevronDown, ChevronRight } from 'lucide-react';
|
|
4
|
-
import styles from '../../Taskforce.module.css';
|
|
5
|
-
import { dispatchOpenMarkdownDocument } from '../../utils/contextFiles';
|
|
6
|
-
import { canOpenAnnotatedAttachment, dispatchOpenAnnotatedAttachment } from '../../utils/annotatedAttachments';
|
|
7
|
-
import { dispatchTaskforceContextAssetsMutated } from '../../utils/contextAssetEvents';
|
|
8
|
-
import { getDocumentReferenceLabel } from '../../utils/documentReferences';
|
|
9
|
-
const ACCEPTED_CONTEXT_FILE_TYPES = [
|
|
10
|
-
'.png', '.jpg', '.jpeg', '.webp', '.gif',
|
|
11
|
-
'.pdf', '.txt', '.md', '.csv', '.json', '.doc', '.docx',
|
|
12
|
-
'.html', '.js', '.ts', '.tsx', '.css', '.py', '.java', '.go', '.rs', '.sh'
|
|
13
|
-
].join(',');
|
|
14
|
-
const SUPPORTED_CLIPBOARD_IMAGE_TYPES = new Set(['image/png', 'image/jpeg', 'image/webp']);
|
|
15
|
-
const ACCEPTED_CONTEXT_EXTENSIONS = new Set(ACCEPTED_CONTEXT_FILE_TYPES.split(',').map((ext) => ext.trim().toLowerCase()));
|
|
16
|
-
const MAX_CONTEXT_FILE_SIZE_BYTES = 10 * 1024 * 1024; // 10 MB per file
|
|
17
|
-
const CONTEXT_NOTICE_TIMEOUT_MS = 3500;
|
|
18
|
-
function getFileExtension(fileName) {
|
|
19
|
-
const trimmed = fileName.trim().toLowerCase();
|
|
20
|
-
const dotIdx = trimmed.lastIndexOf('.');
|
|
21
|
-
if (dotIdx === -1)
|
|
22
|
-
return '';
|
|
23
|
-
return trimmed.slice(dotIdx);
|
|
24
|
-
}
|
|
25
|
-
function formatFileList(fileNames) {
|
|
26
|
-
if (fileNames.length === 0)
|
|
27
|
-
return '';
|
|
28
|
-
const shown = fileNames.slice(0, 3).join(', ');
|
|
29
|
-
const extra = fileNames.length - 3;
|
|
30
|
-
return extra > 0 ? `${shown}, +${extra} more` : shown;
|
|
31
|
-
}
|
|
32
|
-
function getLocalFileFingerprint(file) {
|
|
33
|
-
return `${file.name.trim().toLowerCase()}::${file.size}::${file.lastModified}`;
|
|
34
|
-
}
|
|
35
|
-
function readFileAsDataUrl(file) {
|
|
36
|
-
return new Promise((resolve, reject) => {
|
|
37
|
-
const reader = new FileReader();
|
|
38
|
-
reader.onload = () => resolve(String(reader.result || ''));
|
|
39
|
-
reader.onerror = () => reject(reader.error || new Error('Failed to read file'));
|
|
40
|
-
reader.readAsDataURL(file);
|
|
41
|
-
});
|
|
42
|
-
}
|
|
43
|
-
function isImagePath(path) {
|
|
44
|
-
return /\.(png|jpe?g|webp|gif)(\?|#|$)/i.test(path);
|
|
45
|
-
}
|
|
46
|
-
function getClipboardImageType(item) {
|
|
47
|
-
const rawTypes = item.types;
|
|
48
|
-
if (!rawTypes)
|
|
49
|
-
return null;
|
|
50
|
-
for (const type of Array.from(rawTypes)) {
|
|
51
|
-
const normalizedType = String(type || '').trim().toLowerCase();
|
|
52
|
-
if (SUPPORTED_CLIPBOARD_IMAGE_TYPES.has(normalizedType)) {
|
|
53
|
-
return normalizedType;
|
|
54
|
-
}
|
|
55
|
-
}
|
|
56
|
-
return null;
|
|
57
|
-
}
|
|
58
|
-
function buildDownloadUrl(pathValue, displayName) {
|
|
59
|
-
const separator = pathValue.includes('?') ? '&' : '?';
|
|
60
|
-
return `${pathValue}${separator}download=1&filename=${encodeURIComponent(displayName)}`;
|
|
61
|
-
}
|
|
62
|
-
function getAttachmentDisplayName(item) {
|
|
63
|
-
if (typeof item === 'string')
|
|
64
|
-
return item.split('/').pop() || 'Context file';
|
|
65
|
-
return item.displayName || item.originalFilename || item.caption || item.fsPath || item.path.split('/').pop() || 'Context file';
|
|
66
|
-
}
|
|
67
|
-
function getAttachmentTypeLabel(item) {
|
|
68
|
-
const candidates = typeof item === 'string'
|
|
69
|
-
? [item]
|
|
70
|
-
: [item.originalFilename, item.displayName, item.caption, item.fsPath, item.path];
|
|
71
|
-
for (const candidate of candidates) {
|
|
72
|
-
if (!candidate)
|
|
73
|
-
continue;
|
|
74
|
-
const ext = getFileExtension(candidate);
|
|
75
|
-
if (ext)
|
|
76
|
-
return ext.replace('.', '').slice(0, 5).toUpperCase();
|
|
77
|
-
}
|
|
78
|
-
const path = typeof item === 'string' ? item : item.path;
|
|
79
|
-
return /^https?:\/\//i.test(path) ? 'LINK' : 'FILE';
|
|
80
|
-
}
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import { ContextAttachmentManager } from '../context/ContextAttachmentManager';
|
|
81
3
|
export function TaskContextUpload(props) {
|
|
82
|
-
const { taskId, taskReferenceLabel, workspaceId, contextFiles, onAddContextFile, onRemoveContextFile, onUpdateContextCaption } = props;
|
|
83
|
-
|
|
84
|
-
const [uploading, setUploading] = useState(false);
|
|
85
|
-
const [uploadError, setUploadError] = useState(null);
|
|
86
|
-
const [linkValue, setLinkValue] = useState('');
|
|
87
|
-
const [linkError, setLinkError] = useState(null);
|
|
88
|
-
const [contextNotice, setContextNotice] = useState(null);
|
|
89
|
-
const [showContextUpload, setShowContextUpload] = useState(false);
|
|
90
|
-
const [isDropActive, setIsDropActive] = useState(false);
|
|
91
|
-
const [dragDepth, setDragDepth] = useState(0);
|
|
92
|
-
const contextKeyRef = useRef(new Set());
|
|
93
|
-
const localUploadFingerprintRef = useRef(new Set());
|
|
94
|
-
const normalizeKey = (value) => (value || '').trim();
|
|
95
|
-
const getContextKeys = (path, fsPath) => {
|
|
96
|
-
const keys = [];
|
|
97
|
-
const normalizedPath = normalizeKey(path);
|
|
98
|
-
const normalizedFsPath = normalizeKey(fsPath);
|
|
99
|
-
if (normalizedPath)
|
|
100
|
-
keys.push(`path:${normalizedPath}`);
|
|
101
|
-
if (normalizedFsPath)
|
|
102
|
-
keys.push(`fs:${normalizedFsPath}`);
|
|
103
|
-
return keys;
|
|
104
|
-
};
|
|
105
|
-
const keySetFromFiles = useMemo(() => {
|
|
106
|
-
const next = new Set();
|
|
107
|
-
contextFiles.forEach((item) => {
|
|
108
|
-
if (typeof item === 'string') {
|
|
109
|
-
getContextKeys(item).forEach((key) => next.add(key));
|
|
110
|
-
return;
|
|
111
|
-
}
|
|
112
|
-
getContextKeys(item.path, item.fsPath).forEach((key) => next.add(key));
|
|
113
|
-
});
|
|
114
|
-
return next;
|
|
115
|
-
}, [contextFiles]);
|
|
116
|
-
useEffect(() => {
|
|
117
|
-
contextKeyRef.current = new Set(keySetFromFiles);
|
|
118
|
-
}, [keySetFromFiles]);
|
|
119
|
-
const hasAnyKey = (set, keys) => keys.some((key) => set.has(key));
|
|
120
|
-
const addKeys = (set, keys) => keys.forEach((key) => set.add(key));
|
|
121
|
-
const trimmedWorkspaceId = String(workspaceId || '').trim();
|
|
122
|
-
const buildApiHeaders = (contentType = 'application/json') => {
|
|
123
|
-
const headers = { 'Content-Type': contentType };
|
|
124
|
-
if (trimmedWorkspaceId)
|
|
125
|
-
headers['x-taskforce-workspace-id'] = trimmedWorkspaceId;
|
|
126
|
-
return headers;
|
|
127
|
-
};
|
|
128
|
-
const addLink = (rawValue, clearInput = true, contextKeySet = contextKeyRef.current) => {
|
|
129
|
-
const trimmed = rawValue.trim();
|
|
130
|
-
if (!trimmed)
|
|
131
|
-
return 'invalid';
|
|
132
|
-
const isHttp = /^https?:\/\//i.test(trimmed);
|
|
133
|
-
if (!isHttp)
|
|
134
|
-
return 'invalid';
|
|
135
|
-
const normalizedPath = trimmed;
|
|
136
|
-
const keys = getContextKeys(normalizedPath);
|
|
137
|
-
if (hasAnyKey(contextKeySet, keys))
|
|
138
|
-
return 'duplicate';
|
|
139
|
-
const nameFromPath = trimmed.split(/[\\/]/).pop() || trimmed;
|
|
140
|
-
onAddContextFile({
|
|
141
|
-
path: normalizedPath,
|
|
142
|
-
caption: nameFromPath,
|
|
143
|
-
timestamp: new Date().toISOString()
|
|
144
|
-
});
|
|
145
|
-
addKeys(contextKeySet, keys);
|
|
146
|
-
if (clearInput)
|
|
147
|
-
setLinkValue('');
|
|
148
|
-
setLinkError(null);
|
|
149
|
-
return 'added';
|
|
150
|
-
};
|
|
151
|
-
useEffect(() => {
|
|
152
|
-
if (!contextNotice)
|
|
153
|
-
return;
|
|
154
|
-
const timeoutId = window.setTimeout(() => {
|
|
155
|
-
setContextNotice(null);
|
|
156
|
-
}, CONTEXT_NOTICE_TIMEOUT_MS);
|
|
157
|
-
return () => window.clearTimeout(timeoutId);
|
|
158
|
-
}, [contextNotice]);
|
|
159
|
-
const handleSelectedFiles = async (selectedFilesInput) => {
|
|
160
|
-
if (selectedFilesInput.length === 0)
|
|
161
|
-
return;
|
|
162
|
-
setUploadError(null);
|
|
163
|
-
setContextNotice(null);
|
|
164
|
-
const selectedFiles = selectedFilesInput;
|
|
165
|
-
const unsupportedFiles = [];
|
|
166
|
-
const oversizedFiles = [];
|
|
167
|
-
const localDuplicateFiles = [];
|
|
168
|
-
const uploadCandidates = [];
|
|
169
|
-
const localFingerprintSet = new Set(localUploadFingerprintRef.current);
|
|
170
|
-
selectedFiles.forEach((file) => {
|
|
171
|
-
const ext = getFileExtension(file.name);
|
|
172
|
-
if (!ACCEPTED_CONTEXT_EXTENSIONS.has(ext)) {
|
|
173
|
-
unsupportedFiles.push(file.name);
|
|
174
|
-
return;
|
|
175
|
-
}
|
|
176
|
-
if (file.size > MAX_CONTEXT_FILE_SIZE_BYTES) {
|
|
177
|
-
oversizedFiles.push(file.name);
|
|
178
|
-
return;
|
|
179
|
-
}
|
|
180
|
-
const fingerprint = getLocalFileFingerprint(file);
|
|
181
|
-
if (localFingerprintSet.has(fingerprint)) {
|
|
182
|
-
localDuplicateFiles.push(file.name);
|
|
183
|
-
return;
|
|
184
|
-
}
|
|
185
|
-
localFingerprintSet.add(fingerprint);
|
|
186
|
-
uploadCandidates.push({ file, fingerprint });
|
|
187
|
-
});
|
|
188
|
-
const validationNotices = [];
|
|
189
|
-
if (unsupportedFiles.length > 0) {
|
|
190
|
-
validationNotices.push(`${unsupportedFiles.length} unsupported file${unsupportedFiles.length === 1 ? '' : 's'} skipped: ${formatFileList(unsupportedFiles)}.`);
|
|
191
|
-
}
|
|
192
|
-
if (oversizedFiles.length > 0) {
|
|
193
|
-
validationNotices.push(`${oversizedFiles.length} file${oversizedFiles.length === 1 ? '' : 's'} over 10 MB skipped: ${formatFileList(oversizedFiles)}.`);
|
|
194
|
-
}
|
|
195
|
-
if (localDuplicateFiles.length > 0) {
|
|
196
|
-
validationNotices.push(`${localDuplicateFiles.length} local duplicate file${localDuplicateFiles.length === 1 ? '' : 's'} skipped before upload: ${formatFileList(localDuplicateFiles)}.`);
|
|
197
|
-
}
|
|
198
|
-
if (uploadCandidates.length === 0) {
|
|
199
|
-
if (validationNotices.length > 0)
|
|
200
|
-
setContextNotice(validationNotices.join(' '));
|
|
201
|
-
if (inputRef.current)
|
|
202
|
-
inputRef.current.value = '';
|
|
203
|
-
return;
|
|
204
|
-
}
|
|
205
|
-
setUploading(true);
|
|
206
|
-
const contextKeySet = new Set(contextKeyRef.current);
|
|
207
|
-
let duplicateCount = 0;
|
|
208
|
-
try {
|
|
209
|
-
for (const { file, fingerprint } of uploadCandidates) {
|
|
210
|
-
let data = null;
|
|
211
|
-
// Keep uploads same-origin so local runtime never routes attachments through the cloud API base.
|
|
212
|
-
const initRes = await fetch('/api/taskforce/context-upload/init', {
|
|
213
|
-
method: 'POST',
|
|
214
|
-
headers: buildApiHeaders(),
|
|
215
|
-
body: JSON.stringify({
|
|
216
|
-
originalName: file.name,
|
|
217
|
-
mimeType: file.type || 'application/octet-stream',
|
|
218
|
-
size: file.size,
|
|
219
|
-
taskId: taskId || null,
|
|
220
|
-
workspaceId: trimmedWorkspaceId || null
|
|
221
|
-
})
|
|
222
|
-
});
|
|
223
|
-
if (initRes.ok) {
|
|
224
|
-
const initData = await initRes.json();
|
|
225
|
-
if (initData?.success && typeof initData?.uploadUrl === 'string' && typeof initData?.path === 'string') {
|
|
226
|
-
const uploadRes = await fetch(initData.uploadUrl, {
|
|
227
|
-
method: String(initData.method || 'PUT'),
|
|
228
|
-
headers: initData.headers || { 'Content-Type': file.type || 'application/octet-stream' },
|
|
229
|
-
body: file
|
|
230
|
-
});
|
|
231
|
-
if (!uploadRes.ok) {
|
|
232
|
-
throw new Error(`Upload failed for ${file.name}`);
|
|
233
|
-
}
|
|
234
|
-
const finalizeRes = await fetch('/api/taskforce/context-upload/finalize', {
|
|
235
|
-
method: 'POST',
|
|
236
|
-
headers: buildApiHeaders(),
|
|
237
|
-
body: JSON.stringify({
|
|
238
|
-
relativePath: initData.relativePath,
|
|
239
|
-
mimeType: file.type || 'application/octet-stream',
|
|
240
|
-
originalName: file.name,
|
|
241
|
-
size: file.size,
|
|
242
|
-
taskId: taskId || null,
|
|
243
|
-
workspaceId: trimmedWorkspaceId || null
|
|
244
|
-
})
|
|
245
|
-
});
|
|
246
|
-
if (!finalizeRes.ok) {
|
|
247
|
-
const finalizeError = await finalizeRes.json().catch(() => ({}));
|
|
248
|
-
throw new Error(finalizeError?.error || `Upload failed for ${file.name}`);
|
|
249
|
-
}
|
|
250
|
-
data = await finalizeRes.json().catch(() => null);
|
|
251
|
-
}
|
|
252
|
-
else if (initData?.success && typeof initData?.path === 'string') {
|
|
253
|
-
data = initData;
|
|
254
|
-
}
|
|
255
|
-
}
|
|
256
|
-
if (!data) {
|
|
257
|
-
const encoded = await readFileAsDataUrl(file);
|
|
258
|
-
const res = await fetch('/api/taskforce/context-upload', {
|
|
259
|
-
method: 'POST',
|
|
260
|
-
headers: buildApiHeaders(),
|
|
261
|
-
body: JSON.stringify({
|
|
262
|
-
file: encoded,
|
|
263
|
-
originalName: file.name,
|
|
264
|
-
taskId: taskId || null,
|
|
265
|
-
workspaceId: trimmedWorkspaceId || null
|
|
266
|
-
})
|
|
267
|
-
});
|
|
268
|
-
if (!res.ok) {
|
|
269
|
-
throw new Error(`Upload failed for ${file.name}`);
|
|
270
|
-
}
|
|
271
|
-
data = await res.json();
|
|
272
|
-
}
|
|
273
|
-
if (!data?.success || !data?.path) {
|
|
274
|
-
throw new Error(`Upload failed for ${file.name}`);
|
|
275
|
-
}
|
|
276
|
-
const keys = getContextKeys(data.path, data.fsPath);
|
|
277
|
-
if (hasAnyKey(contextKeySet, keys)) {
|
|
278
|
-
duplicateCount += 1;
|
|
279
|
-
continue;
|
|
280
|
-
}
|
|
281
|
-
onAddContextFile({
|
|
282
|
-
path: data.path,
|
|
283
|
-
fsPath: data.fsPath,
|
|
284
|
-
caption: typeof data.caption === 'string' && data.caption.trim().length > 0 ? data.caption : file.name,
|
|
285
|
-
displayName: typeof data.displayName === 'string' ? data.displayName : undefined,
|
|
286
|
-
originalFilename: typeof data.originalFilename === 'string' ? data.originalFilename : file.name,
|
|
287
|
-
assetId: typeof data.assetId === 'string' ? data.assetId : undefined,
|
|
288
|
-
referenceNumber: typeof data.referenceNumber === 'number' ? data.referenceNumber : null,
|
|
289
|
-
referenceLabel: typeof data.referenceLabel === 'string' ? data.referenceLabel : undefined,
|
|
290
|
-
taskId: typeof data.taskId === 'string' ? data.taskId : null,
|
|
291
|
-
timestamp: new Date().toISOString()
|
|
292
|
-
});
|
|
293
|
-
dispatchTaskforceContextAssetsMutated({
|
|
294
|
-
workspaceId: trimmedWorkspaceId || 'default',
|
|
295
|
-
taskId: taskId || null,
|
|
296
|
-
reason: 'upload'
|
|
297
|
-
});
|
|
298
|
-
addKeys(contextKeySet, keys);
|
|
299
|
-
localUploadFingerprintRef.current.add(fingerprint);
|
|
300
|
-
}
|
|
301
|
-
const notices = [...validationNotices];
|
|
302
|
-
if (duplicateCount > 0) {
|
|
303
|
-
notices.push(`${duplicateCount} duplicate file${duplicateCount === 1 ? '' : 's'} skipped.`);
|
|
304
|
-
}
|
|
305
|
-
setContextNotice(notices.length > 0 ? notices.join(' ') : null);
|
|
306
|
-
contextKeyRef.current = contextKeySet;
|
|
307
|
-
}
|
|
308
|
-
catch (error) {
|
|
309
|
-
setUploadError(error instanceof Error ? error.message : 'Failed to upload context file');
|
|
310
|
-
}
|
|
311
|
-
finally {
|
|
312
|
-
setUploading(false);
|
|
313
|
-
if (inputRef.current)
|
|
314
|
-
inputRef.current.value = '';
|
|
315
|
-
}
|
|
316
|
-
};
|
|
317
|
-
const handleFilesSelected = async (files) => {
|
|
318
|
-
if (!files || files.length === 0)
|
|
319
|
-
return;
|
|
320
|
-
await handleSelectedFiles(Array.from(files));
|
|
321
|
-
};
|
|
322
|
-
const handlePasteImage = async () => {
|
|
323
|
-
setUploadError(null);
|
|
324
|
-
setContextNotice(null);
|
|
325
|
-
if (!navigator.clipboard || typeof navigator.clipboard.read !== 'function') {
|
|
326
|
-
setUploadError('Clipboard image paste is not available in this browser.');
|
|
327
|
-
return;
|
|
328
|
-
}
|
|
329
|
-
try {
|
|
330
|
-
const clipboardItems = await navigator.clipboard.read();
|
|
331
|
-
const imageFiles = [];
|
|
332
|
-
for (const item of clipboardItems) {
|
|
333
|
-
const imageType = getClipboardImageType(item);
|
|
334
|
-
if (!imageType)
|
|
335
|
-
continue;
|
|
336
|
-
const blob = await item.getType(imageType);
|
|
337
|
-
const extension = imageType.toLowerCase() === 'image/png'
|
|
338
|
-
? 'png'
|
|
339
|
-
: imageType.toLowerCase() === 'image/webp'
|
|
340
|
-
? 'webp'
|
|
341
|
-
: 'jpg';
|
|
342
|
-
imageFiles.push(new globalThis.File([blob], `pasted-image-${Date.now()}.${extension}`, { type: imageType }));
|
|
343
|
-
}
|
|
344
|
-
if (imageFiles.length === 0) {
|
|
345
|
-
setUploadError('Clipboard does not currently contain a supported image.');
|
|
346
|
-
return;
|
|
347
|
-
}
|
|
348
|
-
await handleSelectedFiles(imageFiles);
|
|
349
|
-
}
|
|
350
|
-
catch (error) {
|
|
351
|
-
setUploadError(error?.message || 'Failed to read an image from the clipboard.');
|
|
352
|
-
}
|
|
353
|
-
};
|
|
354
|
-
const parseDroppedLinks = (event) => {
|
|
355
|
-
const droppedLinks = new Set();
|
|
356
|
-
const rawUriList = event.dataTransfer.getData('text/uri-list') || '';
|
|
357
|
-
const rawText = event.dataTransfer.getData('text/plain') || '';
|
|
358
|
-
const candidates = `${rawUriList}\n${rawText}`
|
|
359
|
-
.split('\n')
|
|
360
|
-
.map((line) => line.trim())
|
|
361
|
-
.filter((line) => !!line && !line.startsWith('#'));
|
|
362
|
-
for (const candidate of candidates) {
|
|
363
|
-
if (/^file:\/\//i.test(candidate)) {
|
|
364
|
-
try {
|
|
365
|
-
const fileUrl = new URL(candidate);
|
|
366
|
-
const fsPath = decodeURIComponent(fileUrl.pathname || '').trim();
|
|
367
|
-
if (fsPath)
|
|
368
|
-
droppedLinks.add(fsPath);
|
|
369
|
-
}
|
|
370
|
-
catch {
|
|
371
|
-
// Ignore malformed file URL drops
|
|
372
|
-
}
|
|
373
|
-
continue;
|
|
374
|
-
}
|
|
375
|
-
droppedLinks.add(candidate);
|
|
376
|
-
}
|
|
377
|
-
return Array.from(droppedLinks);
|
|
378
|
-
};
|
|
379
|
-
const handleDrop = (event) => {
|
|
380
|
-
event.preventDefault();
|
|
381
|
-
event.stopPropagation();
|
|
382
|
-
setIsDropActive(false);
|
|
383
|
-
setDragDepth(0);
|
|
384
|
-
setUploadError(null);
|
|
385
|
-
setLinkError(null);
|
|
386
|
-
setContextNotice(null);
|
|
387
|
-
if (event.dataTransfer.files && event.dataTransfer.files.length > 0) {
|
|
388
|
-
void handleFilesSelected(event.dataTransfer.files);
|
|
389
|
-
return;
|
|
390
|
-
}
|
|
391
|
-
const droppedLinks = parseDroppedLinks(event);
|
|
392
|
-
if (droppedLinks.length === 0) {
|
|
393
|
-
setLinkError('Drop a file or URL.');
|
|
394
|
-
return;
|
|
395
|
-
}
|
|
396
|
-
const contextKeySet = new Set(contextKeyRef.current);
|
|
397
|
-
let duplicateCount = 0;
|
|
398
|
-
let addedCount = 0;
|
|
399
|
-
let invalidCount = 0;
|
|
400
|
-
droppedLinks.forEach((link) => {
|
|
401
|
-
const result = addLink(link, false, contextKeySet);
|
|
402
|
-
if (result === 'duplicate')
|
|
403
|
-
duplicateCount += 1;
|
|
404
|
-
if (result === 'added')
|
|
405
|
-
addedCount += 1;
|
|
406
|
-
if (result === 'invalid')
|
|
407
|
-
invalidCount += 1;
|
|
408
|
-
});
|
|
409
|
-
const notices = [];
|
|
410
|
-
if (duplicateCount > 0) {
|
|
411
|
-
notices.push(`${duplicateCount} duplicate link${duplicateCount === 1 ? '' : 's'} skipped.`);
|
|
412
|
-
}
|
|
413
|
-
if (invalidCount > 0) {
|
|
414
|
-
notices.push(`${invalidCount} local path${invalidCount === 1 ? '' : 's'} skipped. Upload files to attach them, or drop an http(s) URL to link.`);
|
|
415
|
-
}
|
|
416
|
-
setContextNotice(notices.length > 0 ? notices.join(' ') : (addedCount > 0 ? null : contextNotice));
|
|
417
|
-
contextKeyRef.current = contextKeySet;
|
|
418
|
-
};
|
|
419
|
-
const handleDragEnter = (event) => {
|
|
420
|
-
event.preventDefault();
|
|
421
|
-
event.stopPropagation();
|
|
422
|
-
setDragDepth((depth) => depth + 1);
|
|
423
|
-
setIsDropActive(true);
|
|
424
|
-
};
|
|
425
|
-
const handleDragLeave = (event) => {
|
|
426
|
-
event.preventDefault();
|
|
427
|
-
event.stopPropagation();
|
|
428
|
-
setDragDepth((depth) => {
|
|
429
|
-
const nextDepth = Math.max(0, depth - 1);
|
|
430
|
-
if (nextDepth === 0)
|
|
431
|
-
setIsDropActive(false);
|
|
432
|
-
return nextDepth;
|
|
433
|
-
});
|
|
434
|
-
};
|
|
435
|
-
const handleDragOver = (event) => {
|
|
436
|
-
event.preventDefault();
|
|
437
|
-
event.stopPropagation();
|
|
438
|
-
event.dataTransfer.dropEffect = 'copy';
|
|
439
|
-
};
|
|
440
|
-
const imageFiles = contextFiles.filter((item) => {
|
|
441
|
-
const path = typeof item === 'string' ? item : item.path;
|
|
442
|
-
return isImagePath(path);
|
|
443
|
-
});
|
|
444
|
-
const documentFiles = contextFiles.filter((item) => {
|
|
445
|
-
const path = typeof item === 'string' ? item : item.path;
|
|
446
|
-
return !isImagePath(path);
|
|
447
|
-
});
|
|
448
|
-
return (_jsxs("div", { className: styles.specialistSection, children: [_jsxs("div", { className: styles.toggleHeading, onClick: () => setShowContextUpload(!showContextUpload), title: showContextUpload ? 'Hide context documents' : 'Show context documents', children: [_jsxs("label", { className: styles.label, children: ["Context Documents ", contextFiles.length > 0 && `(${contextFiles.length})`] }), showContextUpload ? _jsx(ChevronDown, { size: 14 }) : _jsx(ChevronRight, { size: 14 })] }), showContextUpload && (_jsxs(_Fragment, { children: [_jsx("p", { className: styles.settingsHint, children: "Upload files into task context storage, or add an external http(s) URL." }), _jsxs("div", { className: styles.contextUploadPanel, children: [_jsxs("div", { className: styles.contextUploadActions, children: [_jsxs("button", { type: "button", className: styles.secondaryHeaderBtn, onClick: () => inputRef.current?.click(), disabled: uploading, children: [uploading ? _jsx(Loader2, { size: 12, className: styles.spinner }) : _jsx(FileUp, { size: 12 }), uploading ? 'Uploading...' : 'Upload File'] }), _jsxs("button", { type: "button", className: styles.secondaryHeaderBtn, onClick: () => {
|
|
449
|
-
void handlePasteImage();
|
|
450
|
-
}, disabled: uploading, children: [_jsx(Copy, { size: 12 }), "Paste Image from Clipboard"] })] }), _jsx("input", { ref: inputRef, type: "file", accept: ACCEPTED_CONTEXT_FILE_TYPES, multiple: true, className: styles.hiddenInput, onChange: (e) => {
|
|
451
|
-
void handleFilesSelected(e.target.files);
|
|
452
|
-
} }), uploadError && _jsx("div", { className: styles.error, children: uploadError }), _jsxs("div", { className: styles.contextLinkRow, children: [_jsx("input", { type: "text", className: styles.input, placeholder: "Add external URL (https://...)", value: linkValue, onChange: (e) => {
|
|
453
|
-
setLinkValue(e.target.value);
|
|
454
|
-
if (linkError)
|
|
455
|
-
setLinkError(null);
|
|
456
|
-
if (contextNotice)
|
|
457
|
-
setContextNotice(null);
|
|
458
|
-
} }), _jsx("button", { type: "button", className: styles.secondaryHeaderBtn, onClick: () => {
|
|
459
|
-
if (!linkValue.trim()) {
|
|
460
|
-
setLinkError('Enter an http(s) URL to link.');
|
|
461
|
-
return;
|
|
462
|
-
}
|
|
463
|
-
const result = addLink(linkValue, true);
|
|
464
|
-
if (result === 'invalid') {
|
|
465
|
-
setLinkError('Only external http(s) URLs can be linked here. Upload files to attach them.');
|
|
466
|
-
return;
|
|
467
|
-
}
|
|
468
|
-
if (result === 'duplicate') {
|
|
469
|
-
setContextNotice('Link already exists in task context.');
|
|
470
|
-
}
|
|
471
|
-
else if (result === 'added') {
|
|
472
|
-
setContextNotice(null);
|
|
473
|
-
}
|
|
474
|
-
}, children: "Add URL" })] }), _jsxs("div", { className: `${styles.contextDropzone} ${isDropActive ? styles.contextDropzoneActive : ''}`, onDragEnter: handleDragEnter, onDragLeave: handleDragLeave, onDragOver: handleDragOver, onDrop: handleDrop, children: [_jsx(FileUp, { size: 14 }), _jsx("span", { children: "Drop files or URLs here" })] })] }), contextNotice && _jsx("div", { className: styles.contextNotice, children: contextNotice }), linkError && _jsx("div", { className: styles.error, children: linkError }), imageFiles.length > 0 && (_jsx("div", { className: styles.attachmentGrid, children: contextFiles.map((item, idx) => {
|
|
475
|
-
const path = typeof item === 'string' ? item : item.path;
|
|
476
|
-
if (!isImagePath(path))
|
|
477
|
-
return null;
|
|
478
|
-
const fsPath = typeof item === 'string' ? undefined : item.fsPath;
|
|
479
|
-
const caption = typeof item === 'string' ? '' : item.caption || '';
|
|
480
|
-
const displayName = getAttachmentDisplayName(item);
|
|
481
|
-
const isImage = isImagePath(path);
|
|
482
|
-
const isExternalUrl = /^https?:\/\//i.test(path);
|
|
483
|
-
const canAnnotate = canOpenAnnotatedAttachment(item, { taskId });
|
|
484
|
-
return (_jsxs("div", { className: styles.attachmentCard, children: [_jsxs("div", { className: styles.attachmentPreviewContainer, children: [isImage ? (_jsx("img", { src: path, alt: caption || `Context file ${idx}`, onClick: () => {
|
|
485
|
-
if (dispatchOpenAnnotatedAttachment(item, { taskId, taskReferenceLabel }))
|
|
486
|
-
return;
|
|
487
|
-
window.open(path, '_blank');
|
|
488
|
-
}, onError: (e) => {
|
|
489
|
-
e.target.style.display = 'none';
|
|
490
|
-
e.target.parentElement.classList.add(styles.brokenImage);
|
|
491
|
-
} })) : (_jsxs("button", { type: "button", className: styles.contextFileLink, onClick: () => {
|
|
492
|
-
if (dispatchOpenMarkdownDocument(item, fsPath))
|
|
493
|
-
return;
|
|
494
|
-
window.open(path, '_blank');
|
|
495
|
-
}, title: "Open file", children: [_jsx(FileUp, { size: 16 }), _jsx("span", { children: caption || fsPath || path.split('/').pop() || 'Context file' })] })), _jsxs("div", { className: styles.brokenImagePlaceholder, children: [_jsx(AlertTriangle, { size: 16 }), _jsx("span", { children: "Preview unavailable" })] }), _jsxs("div", { className: styles.attachmentActions, children: [canAnnotate && (_jsx("button", { type: "button", className: styles.attachmentActionBtn, onClick: (e) => {
|
|
496
|
-
e.stopPropagation();
|
|
497
|
-
dispatchOpenAnnotatedAttachment(item, { taskId, taskReferenceLabel });
|
|
498
|
-
}, title: "Annotate", children: _jsx(PencilLine, { size: 12 }) })), fsPath && (_jsx("button", { type: "button", className: styles.attachmentActionBtn, onClick: (e) => {
|
|
499
|
-
e.stopPropagation();
|
|
500
|
-
navigator.clipboard.writeText(fsPath);
|
|
501
|
-
}, title: `Copy path: ${fsPath}`, children: _jsx(Copy, { size: 12 }) })), !isExternalUrl && (_jsx("button", { type: "button", className: styles.attachmentActionBtn, onClick: (e) => {
|
|
502
|
-
e.stopPropagation();
|
|
503
|
-
window.open(buildDownloadUrl(path, caption || displayName), '_blank');
|
|
504
|
-
}, title: "Download", children: _jsx(Download, { size: 12 }) })), _jsx("button", { type: "button", className: styles.attachmentActionBtn, onClick: (e) => {
|
|
505
|
-
e.stopPropagation();
|
|
506
|
-
onRemoveContextFile(idx);
|
|
507
|
-
}, title: "Delete", children: _jsx(Trash2, { size: 12 }) })] })] }), _jsx("input", { type: "text", className: styles.attachmentCaptionInput, placeholder: "Add a label...", value: caption, onChange: (e) => onUpdateContextCaption(idx, e.target.value) })] }, idx));
|
|
508
|
-
}) })), documentFiles.length > 0 && (_jsx("div", { className: styles.documentAttachmentList, children: contextFiles.map((item, idx) => {
|
|
509
|
-
const path = typeof item === 'string' ? item : item.path;
|
|
510
|
-
if (isImagePath(path))
|
|
511
|
-
return null;
|
|
512
|
-
const fsPath = typeof item === 'string' ? undefined : item.fsPath;
|
|
513
|
-
const caption = typeof item === 'string' ? '' : item.caption || '';
|
|
514
|
-
const displayName = getAttachmentDisplayName(item);
|
|
515
|
-
const fileType = getAttachmentTypeLabel(item);
|
|
516
|
-
const documentReferenceLabel = typeof item === 'string' ? '' : getDocumentReferenceLabel(item);
|
|
517
|
-
const isExternalUrl = /^https?:\/\//i.test(path);
|
|
518
|
-
return (_jsxs("div", { className: styles.documentAttachmentItem, children: [_jsxs("div", { className: styles.documentAttachmentRow, children: [_jsxs("button", { type: "button", className: styles.documentAttachmentLink, onClick: () => {
|
|
519
|
-
if (dispatchOpenMarkdownDocument(item, fsPath))
|
|
520
|
-
return;
|
|
521
|
-
window.open(path, '_blank');
|
|
522
|
-
}, title: displayName, children: [_jsxs("span", { className: styles.documentAttachmentMain, children: [_jsx("span", { className: styles.documentAttachmentIcon, "aria-hidden": "true", children: _jsx(File, { size: 13 }) }), _jsxs("span", { className: styles.documentAttachmentText, children: [_jsx("span", { className: styles.documentAttachmentName, children: displayName }), documentReferenceLabel && (_jsx("span", { className: styles.taskIdBadge, children: documentReferenceLabel }))] })] }), _jsx("span", { className: styles.documentAttachmentType, children: fileType })] }), _jsxs("div", { className: styles.documentAttachmentActions, children: [fsPath && (_jsx("button", { type: "button", className: styles.documentAttachmentActionBtn, onClick: () => {
|
|
523
|
-
navigator.clipboard.writeText(fsPath);
|
|
524
|
-
}, title: `Copy path: ${fsPath}`, children: _jsx(Copy, { size: 12 }) })), !isExternalUrl && (_jsx("button", { type: "button", className: styles.documentAttachmentActionBtn, onClick: () => {
|
|
525
|
-
window.open(buildDownloadUrl(path, caption || displayName), '_blank');
|
|
526
|
-
}, title: "Download", children: _jsx(Download, { size: 12 }) })), _jsx("button", { type: "button", className: styles.documentAttachmentActionBtn, onClick: () => onRemoveContextFile(idx), title: "Delete", children: _jsx(Trash2, { size: 12 }) })] })] }), _jsx("input", { type: "text", className: styles.documentAttachmentCaptionInput, placeholder: "Add a label...", value: caption, onChange: (e) => onUpdateContextCaption(idx, e.target.value) })] }, idx));
|
|
527
|
-
}) }))] }))] }));
|
|
4
|
+
const { taskId, taskReferenceLabel, workspaceId, apiBaseUrl, contextFiles, onAddContextFile, onRemoveContextFile, onUpdateContextCaption, } = props;
|
|
5
|
+
return (_jsx(ContextAttachmentManager, { ownerType: "task", ownerId: taskId, ownerReferenceLabel: taskReferenceLabel, workspaceId: workspaceId, apiBaseUrl: apiBaseUrl, attachments: contextFiles, onAddAttachment: onAddContextFile, onRemoveAttachment: onRemoveContextFile, onUpdateAttachmentCaption: onUpdateContextCaption }));
|
|
528
6
|
}
|