@taskforcehq/taskforce 0.3.306 → 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 (215) hide show
  1. package/README.md +0 -2
  2. package/dist/Taskforce.module.css +317 -27
  3. package/dist/TaskforceCore.js +1 -0
  4. package/dist/TaskforceCore.test.js +178 -0
  5. package/dist/components/activity/EntityActivityTimeline.d.ts +34 -0
  6. package/dist/components/activity/EntityActivityTimeline.js +81 -0
  7. package/dist/components/context/ContextAttachmentManager.js +15 -3
  8. package/dist/components/features/AgentsModule.d.ts +5 -1
  9. package/dist/components/features/AgentsModule.js +142 -39
  10. package/dist/components/features/AgentsModule.test.js +260 -11
  11. package/dist/components/features/AnnotatedAttachmentWorkspace.d.ts +3 -1
  12. package/dist/components/features/AnnotatedAttachmentWorkspace.js +118 -4
  13. package/dist/components/features/AnnotatedAttachmentWorkspace.test.js +52 -0
  14. package/dist/components/features/DocumentIndex.d.ts +2 -2
  15. package/dist/components/features/DocumentIndex.js +3 -4
  16. package/dist/components/features/DocumentViewer.d.ts +3 -1
  17. package/dist/components/features/DocumentViewer.js +6 -5
  18. package/dist/components/features/DocumentWorkspace.d.ts +10 -2
  19. package/dist/components/features/DocumentWorkspace.js +15 -4
  20. package/dist/components/features/DocumentWorkspace.test.js +20 -3
  21. package/dist/components/features/TaskSettings.d.ts +2 -0
  22. package/dist/components/features/TaskSettings.js +83 -78
  23. package/dist/components/features/TaskSettings.test.js +171 -82
  24. package/dist/components/task/TaskActionHeader.js +11 -5
  25. package/dist/components/task/TaskActionHeader.test.js +10 -0
  26. package/dist/components/task/TaskCard.js +6 -2
  27. package/dist/components/task/TaskContextUpload.test.js +3 -3
  28. package/dist/components/task/TaskForm.js +14 -64
  29. package/dist/components/task/TaskKanban.js +0 -8
  30. package/dist/components/task/TaskList.js +5 -1
  31. package/dist/components/task/TaskList.test.js +4 -0
  32. package/dist/components/ui/HierarchyActionBadge.d.ts +10 -0
  33. package/dist/components/ui/HierarchyActionBadge.js +5 -0
  34. package/dist/components/views/AppShellHeader.d.ts +3 -1
  35. package/dist/components/views/AppShellHeader.js +27 -11
  36. package/dist/components/views/AppShellHeader.test.js +18 -1
  37. package/dist/components/views/PlanComparisonPage.js +9 -1
  38. package/dist/components/views/PlanComparisonPage.test.js +19 -0
  39. package/dist/components/views/PlansPage.js +1 -1
  40. package/dist/components/views/StandaloneLayout.js +256 -69
  41. package/dist/components/views/WidgetView.js +10 -8
  42. package/dist/components/views/panels/PlanningDrawer.d.ts +19 -6
  43. package/dist/components/views/panels/PlanningDrawer.js +259 -51
  44. package/dist/components/views/panels/PlanningDrawer.test.js +392 -4
  45. package/dist/config/envSchema.js +1 -0
  46. package/dist/core/AiProfileService.d.ts +7 -0
  47. package/dist/core/AiProfileService.js +460 -35
  48. package/dist/core/AiProfiles.test.js +648 -2
  49. package/dist/core/AttachmentLinkService.d.ts +3 -0
  50. package/dist/core/AttachmentLinkService.js +58 -13
  51. package/dist/core/GlobalSettingsService.js +12 -0
  52. package/dist/core/McpTokenService.d.ts +12 -0
  53. package/dist/core/McpTokenService.js +44 -1
  54. package/dist/core/PlanningEntities.test.js +366 -4
  55. package/dist/core/TaskActivityService.d.ts +28 -1
  56. package/dist/core/TaskActivityService.js +227 -46
  57. package/dist/core/TaskActivitySync.test.d.ts +1 -0
  58. package/dist/core/TaskActivitySync.test.js +90 -0
  59. package/dist/core/TaskAuditTimeline.test.js +96 -0
  60. package/dist/core/Taskforce.d.ts +190 -26
  61. package/dist/core/Taskforce.ids.test.js +58 -0
  62. package/dist/core/Taskforce.js +775 -71
  63. package/dist/core/Taskforce.mcpAuth.test.js +109 -0
  64. package/dist/core/types.d.ts +7 -0
  65. package/dist/hooks/auth/useTaskforceAuthBootstrap.js +2 -1
  66. package/dist/hooks/bootstrapTimeouts.d.ts +4 -0
  67. package/dist/hooks/bootstrapTimeouts.js +8 -0
  68. package/dist/hooks/bootstrapTimeouts.test.d.ts +1 -0
  69. package/dist/hooks/bootstrapTimeouts.test.js +12 -0
  70. package/dist/hooks/sync/orchestratorShared.d.ts +3 -3
  71. package/dist/hooks/sync/orchestratorShared.js +61 -1
  72. package/dist/hooks/sync/recovery.js +8 -0
  73. package/dist/hooks/sync/recovery.test.js +5 -5
  74. package/dist/hooks/sync/transfers.d.ts +2 -1
  75. package/dist/hooks/sync/transfers.js +36 -9
  76. package/dist/hooks/sync/transfers.test.js +534 -2
  77. package/dist/hooks/useFilterSort.js +13 -13
  78. package/dist/hooks/useFilterSort.test.js +87 -1
  79. package/dist/hooks/useSyncOrchestrator.js +50 -5
  80. package/dist/hooks/useTaskforce.d.ts +85 -50
  81. package/dist/hooks/useTaskforce.js +44 -14
  82. package/dist/hooks/useTaskforce.runtime-routing.test.js +94 -0
  83. package/dist/hooks/useTaskforce.sync-behavior.test.js +99 -0
  84. package/dist/hooks/useWorkspaceSyncController.js +7 -1
  85. package/dist/hooks/useWorkspaceSyncController.test.js +48 -0
  86. package/dist/hooks/workspace/useTaskforceWorkspaceBootstrap.js +14 -7
  87. package/dist/mcp/canonicalAssetHelpers.d.ts +16 -4
  88. package/dist/mcp/canonicalAssetHelpers.js +18 -10
  89. package/dist/mcp/canonicalAssetHelpers.test.d.ts +1 -0
  90. package/dist/mcp/canonicalAssetHelpers.test.js +67 -0
  91. package/dist/mcp/clientIntegrations.d.ts +1 -0
  92. package/dist/mcp/clientIntegrations.js +25 -0
  93. package/dist/mcp/clientIntegrations.test.js +14 -3
  94. package/dist/mcp/clientProfileDefaults.js +3 -3
  95. package/dist/mcp/collaborationRegistrar.js +7 -7
  96. package/dist/mcp/documentAssetRegistrar.js +113 -7
  97. package/dist/mcp/runtime.d.ts +1 -0
  98. package/dist/mcp/runtime.js +969 -122
  99. package/dist/mcp/runtime.test.js +1083 -11
  100. package/dist/mcp/taskAttachmentTypes.d.ts +1 -1
  101. package/dist/mcp/taskPlanningRegistrar.js +48 -0
  102. package/dist/mcp/toolRegistry.d.ts +56 -0
  103. package/dist/mcp/toolRegistry.js +8 -0
  104. package/dist/mcp/toolRegistry.test.js +4 -0
  105. package/dist/migrations/taskSchemaMigrations.js +21 -0
  106. package/dist/server/aiProfileAvatarAssets.d.ts +49 -0
  107. package/dist/server/aiProfileAvatarAssets.js +319 -0
  108. package/dist/server/aiProfileAvatarAssets.test.d.ts +1 -0
  109. package/dist/server/aiProfileAvatarAssets.test.js +213 -0
  110. package/dist/server/annotatedAttachmentsRoutes.test.js +66 -7
  111. package/dist/server/index.js +4 -0
  112. package/dist/server/index.test.js +6 -1
  113. package/dist/server/routes/admin.js +187 -9
  114. package/dist/server/routes/annotatedAttachments.js +51 -0
  115. package/dist/server/routes/auth.d.ts +11 -0
  116. package/dist/server/routes/auth.js +189 -27
  117. package/dist/server/routes/authSupport.d.ts +4 -0
  118. package/dist/server/routes/authSupport.js +12 -1
  119. package/dist/server/routes/billing.js +11 -0
  120. package/dist/server/routes/billing.test.js +3 -0
  121. package/dist/server/routes/documents.js +10 -0
  122. package/dist/server/routes/shared.js +0 -10
  123. package/dist/server/routes/sync.integration.test.js +221 -0
  124. package/dist/server/routes/sync.js +13 -3
  125. package/dist/server/routes/syncSnapshotRoutes.js +2 -0
  126. package/dist/server/routes/tasks.js +12 -9
  127. package/dist/server/routes.js +87 -8
  128. package/dist/server/routes.test.js +890 -22
  129. package/dist/shared/aiProfileSurfaceType.d.ts +1 -1
  130. package/dist/shared/aiProfileSurfaceType.js +10 -8
  131. package/dist/storage/objectStorage.d.ts +1 -0
  132. package/dist/storage/objectStorage.js +2 -1
  133. package/dist/storage/objectStorageClient.d.ts +1 -0
  134. package/dist/storage/objectStorageClient.js +41 -8
  135. package/dist/storage/objectStorageClient.test.js +26 -1
  136. package/dist/storage/objectStorageConfig.js +3 -1
  137. package/dist/storage/objectStorageConfig.test.js +13 -1
  138. package/dist/sync/collaborationSyncPayload.js +2 -0
  139. package/dist/sync/collaborationSyncPayload.test.js +1 -0
  140. package/dist/sync/collaborationSyncState.d.ts +3 -3
  141. package/dist/sync/collaborationSyncState.js +19 -12
  142. package/dist/sync/contentSyncPayload.d.ts +5 -1
  143. package/dist/sync/contentSyncPayload.js +9 -0
  144. package/dist/sync/contentSyncPayload.test.js +39 -0
  145. package/dist/sync/contentSyncState.d.ts +5 -1
  146. package/dist/sync/contentSyncState.js +231 -37
  147. package/dist/sync/planningSyncPayload.js +5 -0
  148. package/dist/sync/planningSyncPayload.test.js +24 -0
  149. package/dist/sync/syncApplyHandlers.d.ts +4 -4
  150. package/dist/sync/syncApplyHandlers.js +89 -31
  151. package/dist/sync/syncApplyHandlers.test.js +184 -4
  152. package/dist/sync/syncResourceAdapters.d.ts +2 -0
  153. package/dist/sync/syncResourceAdapters.js +2 -0
  154. package/dist/sync/syncService.d.ts +1 -0
  155. package/dist/sync/syncService.js +13 -1
  156. package/dist/sync/syncService.test.js +39 -0
  157. package/dist/sync/workspacePullFeed.d.ts +4 -0
  158. package/dist/sync/workspacePullFeed.js +129 -70
  159. package/dist/sync/workspacePullFeed.test.js +49 -0
  160. package/dist/sync/workspaceSyncModel.d.ts +28 -6
  161. package/dist/sync/workspaceSyncModel.js +159 -45
  162. package/dist/sync/workspaceSyncModel.test.js +269 -3
  163. package/dist/types.d.ts +33 -2
  164. package/dist/ui/assets/AgentsModule-2y82_3DO.js +1 -0
  165. package/dist/ui/assets/AnnotatedAttachmentWorkspace-BTLZZCiQ.js +3 -0
  166. package/dist/ui/assets/AnnotatedAttachmentWorkspace-BVKew-mH.css +1 -0
  167. package/dist/ui/assets/ContextAttachmentManager-D2epuNnG.js +3 -0
  168. package/dist/ui/assets/DocumentWorkspace-D4rJKXzd.css +1 -0
  169. package/dist/ui/assets/DocumentWorkspace-sVa-3Do6.js +252 -0
  170. package/dist/ui/assets/EntityActivityTimeline-DyqvvhuF.css +1 -0
  171. package/dist/ui/assets/EntityActivityTimeline-Zods27y_.js +1 -0
  172. package/dist/ui/assets/{InitiativesModule-9E6ParrY.js → InitiativesModule-D4XseTwW.js} +1 -1
  173. package/dist/ui/assets/PlansPage-D5AcbO0L.js +1 -0
  174. package/dist/ui/assets/PlansPage-Dvqsz5zc.css +1 -0
  175. package/dist/ui/assets/TaskContextUpload-uC5qx4Bv.js +1 -0
  176. package/dist/ui/assets/TaskSettings-CqvPAsTv.js +9 -0
  177. package/dist/ui/assets/{WorkflowsModule-BA7jcEpH.js → WorkflowsModule-oOA9bcdK.js} +4 -4
  178. package/dist/ui/assets/documentReferences-DPZuTgHh.js +1 -0
  179. package/dist/ui/assets/index-607WPo_X.js +4 -0
  180. package/dist/ui/assets/index-CYyodXsg.css +1 -0
  181. package/dist/ui/assets/{vendor-icons-DuEd_65c.js → vendor-icons-B32hGuF2.js} +1 -1
  182. package/dist/ui/index.html +3 -3
  183. package/dist/ui/product/agent manager.png +0 -0
  184. package/dist/ui/product/agent_manager_02.png +0 -0
  185. package/dist/utils/aiProfileEvents.d.ts +7 -0
  186. package/dist/utils/aiProfileEvents.js +6 -0
  187. package/dist/utils/assignees.d.ts +6 -0
  188. package/dist/utils/assignees.js +8 -1
  189. package/dist/utils/assignees.test.d.ts +1 -0
  190. package/dist/utils/assignees.test.js +25 -0
  191. package/dist/utils/avatarDisplay.d.ts +1 -0
  192. package/dist/utils/avatarDisplay.js +15 -0
  193. package/dist/utils/contextFiles.d.ts +3 -1
  194. package/dist/utils/contextFiles.js +2 -1
  195. package/dist/utils/taskActivity.d.ts +7 -1
  196. package/dist/utils/taskActivity.js +63 -1
  197. package/dist/utils/taskActivity.test.js +23 -1
  198. package/dist/utils/taskSearch.d.ts +22 -0
  199. package/dist/utils/taskSearch.js +87 -0
  200. package/dist/utils/taskSearch.test.d.ts +1 -0
  201. package/dist/utils/taskSearch.test.js +69 -0
  202. package/dist/utils/taskUtils.js +10 -3
  203. package/dist/utils/taskUtils.test.d.ts +1 -0
  204. package/dist/utils/taskUtils.test.js +19 -0
  205. package/package.json +1 -1
  206. package/dist/ui/assets/AgentsModule-DrLawwNl.js +0 -1
  207. package/dist/ui/assets/AnnotatedAttachmentWorkspace-BaS2VwIr.css +0 -1
  208. package/dist/ui/assets/AnnotatedAttachmentWorkspace-Z9_whf7F.js +0 -3
  209. package/dist/ui/assets/DocumentWorkspace-B03MaSkw.js +0 -252
  210. package/dist/ui/assets/DocumentWorkspace-Dhdso07p.css +0 -1
  211. package/dist/ui/assets/PlansPage-CvfnMiWq.css +0 -1
  212. package/dist/ui/assets/PlansPage-DRXscYxH.js +0 -1
  213. package/dist/ui/assets/TaskSettings-DZX4jk7e.js +0 -9
  214. package/dist/ui/assets/index-9eT8e0Lu.css +0 -1
  215. package/dist/ui/assets/index-C6k75jr8.js +0 -6
@@ -32,11 +32,11 @@ describe('TaskContextUpload drag and drop', () => {
32
32
  const renderUpload = (contextFiles = [], taskId, extraProps) => render(_jsx(TaskContextUpload, { taskId: taskId, contextFiles: contextFiles, onAddContextFile: onAddContextFile, onRemoveContextFile: onRemoveContextFile, onUpdateContextCaption: onUpdateContextCaption, ...extraProps }));
33
33
  const openSection = async (user) => {
34
34
  const localUser = user ?? userEvent.setup();
35
- const showToggle = screen.queryByTitle('Show context documents');
36
- if (showToggle) {
35
+ if (!screen.queryByText('Drop files or URLs here')) {
36
+ const showToggle = await screen.findByTitle('Show context documents');
37
37
  await localUser.click(showToggle);
38
38
  }
39
- return screen.getByText('Drop files or URLs here');
39
+ return screen.findByText('Drop files or URLs here');
40
40
  };
41
41
  beforeEach(() => {
42
42
  vi.clearAllMocks();
@@ -14,12 +14,21 @@ import { Markdown } from "../ui/Markdown";
14
14
  import { IconMap, resolveColor } from "../../utils/constants";
15
15
  import { formatDate } from "../../utils/formatting";
16
16
  import { t } from "../../localization";
17
- import { TaskContextUpload } from "./TaskContextUpload";
18
- import { getTaskActivityItems, getTaskEventPrimaryChangeField, summarizeTaskChange, summarizeTaskEvent, } from "../../utils/taskActivity";
17
+ import { getTaskActivityItems } from "../../utils/taskActivity";
19
18
  import { ensureAssigneeOptionPresent, getAssigneeKind, getAssigneeLabel, } from "../../utils/assignees";
20
19
  import { getTaskReferenceLabel } from "../../utils/taskReferences";
21
20
  import { getVisibleTaskFormTaxonomies } from "../../utils/taxonomyFormDefaults.js";
22
21
  import { getInitiativeReferenceLabel, getWorkstreamReferenceLabel, } from "../../utils/planningReferences";
22
+ const DeferredEntityActivityTimeline = React.lazy(() => (import("../activity/EntityActivityTimeline").then((module) => ({
23
+ default: module.EntityActivityTimeline,
24
+ }))));
25
+ const DeferredTaskContextUpload = React.lazy(() => (import("./TaskContextUpload").then((module) => ({
26
+ default: module.TaskContextUpload,
27
+ }))));
28
+ function ActivityTimelineFallback({ count }) {
29
+ const placeholderCount = Math.max(2, Math.min(count || 3, 4));
30
+ return (_jsx("div", { className: `${formStyles.commentThread} ${formStyles.activityLoadingThread}`.trim(), "aria-busy": "true", "aria-live": "polite", "aria-label": "Loading activity", children: Array.from({ length: placeholderCount }, (_, index) => (_jsxs("div", { className: formStyles.activitySkeletonItem, children: [_jsx("div", { className: formStyles.activitySkeletonMeta }), _jsx("div", { className: formStyles.activitySkeletonBody })] }, index))) }));
31
+ }
23
32
  function SortableChecklistRow({ id, item, onToggle, onRemove, }) {
24
33
  const { attributes, listeners, setNodeRef, transform, transition, isDragging, } = useSortable({ id });
25
34
  const style = {
@@ -31,12 +40,6 @@ function SortableChecklistRow({ id, item, onToggle, onRemove, }) {
31
40
  ? "Mark checklist item incomplete"
32
41
  : "Mark checklist item complete", children: item.isCompleted ? _jsx(Check, { size: 14, strokeWidth: 2.1 }) : null }), _jsx("span", { className: `${formStyles.checklistItemText} ${item.isCompleted ? formStyles.checklistItemTextCompleted : ""}`.trim(), children: item.title }), _jsx("button", { type: "button", className: formStyles.checklistRemoveBtn, onClick: onRemove, title: "Remove checklist item", "aria-label": "Remove checklist item", children: _jsx(X, { size: 15, strokeWidth: 1.9 }) })] }));
33
42
  }
34
- function hexToRgba(hex, opacity) {
35
- const result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex);
36
- if (!result)
37
- return "";
38
- return `rgba(${parseInt(result[1], 16)}, ${parseInt(result[2], 16)}, ${parseInt(result[3], 16)}, ${opacity})`;
39
- }
40
43
  export function TaskForm(props) {
41
44
  const { editingTaskId, error, title, description, category, type, priority, complexity, manualComplexityEnabled = false, assignee, scheduledDate, dueDate, workstreamInput = "", checklistItems, comments, newCommentText, contextFiles, currentWorkspaceId, apiBaseUrl, descriptionFocused, showMarkdownHelp, showChecklist = false, checklistEnabled = true, formTaxonomies, onTaxonomyChange, onOpenSettings, categories, types, priorities, taxonomyDisplayLabels, assigneeOptions, taxonomies, workstreams = [], initiatives = [], copiedId, onTitleChange, onDescriptionChange, onCategoryChange, onTypeChange, onPriorityChange, onComplexityChange, onAssigneeChange, onScheduledDateChange, onDueDateChange, onWorkstreamInputChange = () => { }, onChecklistItemsChange, onNewCommentTextChange, onDescriptionFocusedChange, onShowMarkdownHelpChange, onShowChecklistChange = () => { }, onSubmit, onAddComment, onAddContextFile, onRemoveContextFile, onUpdateContextCaption, onCopyId, onToggleInProgress, onToggleReview, onToggleComplete, onToggleCancel, onSetStatus, onArchiveTask, onUnarchive, onOpenTaskById, currentTask, commentsEndRef, } = props;
42
45
  const currentTaskReferenceLabel = getTaskReferenceLabel(currentTask);
@@ -381,65 +384,12 @@ export function TaskForm(props) {
381
384
  const frame = window.requestAnimationFrame(scrollToLatest);
382
385
  return () => window.cancelAnimationFrame(frame);
383
386
  }, [activityCount, commentsEndRef, currentTask?.id, editingTaskId]);
384
- const activitySidebar = editingTaskId ? (_jsxs("aside", { className: formStyles.activitySidebar, "aria-label": t("taskForm.commentsSectionTitle"), children: [_jsxs("div", { className: formStyles.activitySidebarHeader, children: [_jsx("h2", { className: formStyles.activitySidebarTitle, children: t("taskForm.commentsSectionTitle") }), _jsx("span", { className: formStyles.activitySidebarCount, children: activityCount })] }), _jsxs("div", { className: `${formStyles.commentPanel} ${formStyles.activitySidebarPanel}`.trim(), children: [_jsxs("div", { className: formStyles.commentThread, children: [activityCount === 0 && (_jsx("div", { className: formStyles.emptyComments, children: t("taskForm.noComments") })), taskActivityItems.map((item) => {
385
- if (item.type === "comment") {
386
- const c = item.comment;
387
- const actorPresentation = resolveActivityActorPresentation({
388
- actorId: c.author,
389
- actorType: c.actor?.kind === "ai"
390
- ? "ai"
391
- : c.actor?.kind === "human"
392
- ? "human"
393
- : null,
394
- actorProfile: c.actor,
395
- });
396
- const commentKind = actorPresentation.kind === "agent" ? "agent" : "member";
397
- const { ActorIcon: CommentActorIcon } = actorPresentation;
398
- const authorColor = actorPresentation.color;
399
- const authorLabel = actorPresentation.label;
400
- const commentAccentStyle = authorColor && hexToRgba(authorColor, 0.35)
401
- ? {
402
- background: `linear-gradient(135deg, ${hexToRgba(authorColor, 0.18)} 0%, ${hexToRgba(authorColor, 0.1)} 100%)`,
403
- borderColor: hexToRgba(authorColor, 0.35),
404
- }
405
- : undefined;
406
- return (_jsxs("div", { className: `${formStyles.comment} ${commentKind === "agent" ? formStyles.commentAi : formStyles.commentUser}`, children: [_jsxs("div", { className: formStyles.commentHeader, children: [_jsx("span", { className: formStyles.commentAuthor, style: authorColor ? { color: authorColor } : undefined, children: _jsxs(_Fragment, { children: [_jsx(CommentActorIcon, { size: 12, strokeWidth: 1.5 }), " ", authorLabel] }) }), _jsx("span", { className: formStyles.commentTime, children: formatDate(c.timestamp, {
407
- month: "short",
408
- day: "numeric",
409
- hour: "2-digit",
410
- minute: "2-digit",
411
- }) })] }), _jsx("div", { className: formStyles.commentText, style: commentAccentStyle, children: _jsx(Markdown, { onTaskIdClick: onOpenTaskById, children: c.text }) })] }, c.id));
412
- }
413
- const event = item.event;
414
- const actorPresentation = resolveActivityActorPresentation({
415
- actorId: event.actor,
416
- actorType: event.actorType,
417
- actorProfile: event.actorProfile,
418
- fallbackKind: "system",
419
- });
420
- const { ActorIcon: EventActorIcon } = actorPresentation;
421
- const authorColor = actorPresentation.color;
422
- const authorLabel = actorPresentation.label;
423
- const eventKind = actorPresentation.kind;
424
- const changeEntries = Object.entries(event.details?.changes || {});
425
- const primaryChangeField = getTaskEventPrimaryChangeField(event);
426
- const displayedChangeEntries = changeEntries.length > 1 && primaryChangeField
427
- ? changeEntries.filter(([field]) => field !== primaryChangeField)
428
- : changeEntries.length > 1
429
- ? changeEntries
430
- : [];
431
- return (_jsxs("div", { className: `${formStyles.comment} ${formStyles.activityEvent} ${eventKind === "system" ? formStyles.activityEventSystem : ""}`, children: [_jsxs("div", { className: `${formStyles.commentHeader} ${formStyles.activityEventHeader}`, children: [_jsx("span", { className: formStyles.commentAuthor, style: authorColor ? { color: authorColor } : undefined, children: _jsxs(_Fragment, { children: [_jsx(EventActorIcon, { size: 12, strokeWidth: 1.5 }), " ", authorLabel] }) }), _jsx("span", { className: formStyles.commentTime, children: formatDate(event.createdAt, {
432
- month: "short",
433
- day: "numeric",
434
- hour: "2-digit",
435
- minute: "2-digit",
436
- }) })] }), _jsxs("div", { className: formStyles.activityEventText, children: [_jsx("div", { children: summarizeTaskEvent(event, formatActivityFieldValue) }), displayedChangeEntries.length > 0 && (_jsx("div", { className: formStyles.activityEventChanges, children: displayedChangeEntries.map(([field, change]) => (_jsx("div", { className: formStyles.activityEventChange, children: summarizeTaskChange(field, change, formatActivityFieldValue) }, field))) }))] })] }, item.id));
437
- }), _jsx("div", { ref: commentsEndRef })] }), _jsxs("div", { className: formStyles.commentInputArea, children: [_jsx("textarea", { className: formStyles.commentInput, placeholder: t("taskForm.commentPlaceholder"), value: newCommentText, onChange: (e) => onNewCommentTextChange(e.target.value), onKeyDown: (e) => {
387
+ const activitySidebar = editingTaskId ? (_jsxs("aside", { className: formStyles.activitySidebar, "aria-label": t("taskForm.commentsSectionTitle"), children: [_jsxs("div", { className: formStyles.activitySidebarHeader, children: [_jsx("h2", { className: formStyles.activitySidebarTitle, children: t("taskForm.commentsSectionTitle") }), _jsx("span", { className: formStyles.activitySidebarCount, children: activityCount })] }), _jsxs("div", { className: `${formStyles.commentPanel} ${formStyles.activitySidebarPanel}`.trim(), children: [_jsx(React.Suspense, { fallback: _jsx(ActivityTimelineFallback, { count: activityCount }), children: _jsx(DeferredEntityActivityTimeline, { activity: currentTask?.activity, comments: comments, formatFieldValue: formatActivityFieldValue, resolveActorPresentation: resolveActivityActorPresentation, onOpenTaskById: onOpenTaskById, emptyMessage: t("taskForm.noComments"), listClassName: formStyles.commentThread, emptyClassName: formStyles.emptyComments, endRef: commentsEndRef }) }), _jsxs("div", { className: formStyles.commentInputArea, children: [_jsx("textarea", { className: formStyles.commentInput, placeholder: t("taskForm.commentPlaceholder"), value: newCommentText, onChange: (e) => onNewCommentTextChange(e.target.value), onKeyDown: (e) => {
438
388
  if (e.key === "Enter" && !e.shiftKey) {
439
389
  e.preventDefault();
440
390
  onAddComment();
441
391
  }
442
- }, rows: 1 }), _jsx("button", { type: "button", className: formStyles.sendCommentBtn, onClick: onAddComment, disabled: !newCommentText.trim(), "aria-label": "Send comment", title: "Send comment", children: _jsx(Send, { size: 16 }) })] })] })] })) : null;
392
+ }, rows: 4 }), _jsx("button", { type: "button", className: formStyles.sendCommentBtn, onClick: onAddComment, disabled: !newCommentText.trim(), "aria-label": "Send comment", title: "Send comment", children: _jsx(Send, { size: 16 }) })] })] })] })) : null;
443
393
  return (_jsxs("form", { ref: formRef, onSubmit: onSubmit, className: `${styles.form} ${styles.appScrollbar} tf-scrollbar`, children: [error && _jsx("div", { className: formStyles.error, children: error }), isReadOnly && (_jsx("div", { className: `${formStyles.formNotice} tf-text-helper`, children: isDeletedReadOnly
444
394
  ? "Deleted tasks are read-only. Restore the task to make changes."
445
395
  : "Archived tasks are read-only. Unarchive the task to make changes." })), _jsxs("fieldset", { disabled: isReadOnly, className: `${formStyles.formFieldsetReset} ${editingTaskId ? formStyles.formLayoutWithSidebar : formStyles.formLayout}`.trim(), children: [_jsxs("div", { className: formStyles.formMainColumn, children: [_jsxs("div", { className: styles.field, children: [_jsxs("div", { className: styles.labelRow, children: [_jsx("label", { className: styles.label, children: "Title" }), assignee && (_jsx("span", { title: `Assigned to: ${currentAssigneeLabel}`, className: [
@@ -560,7 +510,7 @@ export function TaskForm(props) {
560
510
  .map((entry, entryIndex) => ({
561
511
  ...entry,
562
512
  order: entryIndex,
563
- }))) }, item.id || `checklist-${index}`))) }) })) : (_jsx("div", { className: `${formStyles.settingsHint} tf-text-helper`, children: "No checklist items yet." }))] }))] }))] }), _jsxs("div", { children: [_jsx(TaskContextUpload, { taskId: editingTaskId, taskReferenceLabel: currentTaskReferenceLabel, workspaceId: currentWorkspaceId, apiBaseUrl: apiBaseUrl, contextFiles: contextFiles, onAddContextFile: onAddContextFile, onRemoveContextFile: onRemoveContextFile, onUpdateContextCaption: onUpdateContextCaption }), _jsx("div", { className: `${formStyles.taskFormLifecycle} ${formStyles.taskFormMetaDivider}`, children: _jsx("div", { className: formStyles.taskFormMetaGrid, children: currentTask && (_jsxs(_Fragment, { children: [_jsxs("div", { className: formStyles.taskFormDateRow, children: [_jsx("span", { className: formStyles.taskFormDateLabel, children: t("taskForm.createdLabel") }), _jsxs("span", { className: formStyles.taskFormMetaStack, children: [_jsx("span", { className: formStyles.taskFormDateValue, children: createdLabel || t("taskForm.emptyValue") }), _jsx("span", { className: formStyles.taskFormMetaActor, children: createdByLabel
513
+ }))) }, item.id || `checklist-${index}`))) }) })) : (_jsx("div", { className: `${formStyles.settingsHint} tf-text-helper`, children: "No checklist items yet." }))] }))] }))] }), _jsxs("div", { children: [_jsx(React.Suspense, { fallback: null, children: _jsx(DeferredTaskContextUpload, { taskId: editingTaskId, taskReferenceLabel: currentTaskReferenceLabel, workspaceId: currentWorkspaceId, apiBaseUrl: apiBaseUrl, contextFiles: contextFiles, onAddContextFile: onAddContextFile, onRemoveContextFile: onRemoveContextFile, onUpdateContextCaption: onUpdateContextCaption }) }), _jsx("div", { className: `${formStyles.taskFormLifecycle} ${formStyles.taskFormMetaDivider}`, children: _jsx("div", { className: formStyles.taskFormMetaGrid, children: currentTask && (_jsxs(_Fragment, { children: [_jsxs("div", { className: formStyles.taskFormDateRow, children: [_jsx("span", { className: formStyles.taskFormDateLabel, children: t("taskForm.createdLabel") }), _jsxs("span", { className: formStyles.taskFormMetaStack, children: [_jsx("span", { className: formStyles.taskFormDateValue, children: createdLabel || t("taskForm.emptyValue") }), _jsx("span", { className: formStyles.taskFormMetaActor, children: createdByLabel
564
514
  ? `by ${createdByLabel}`
565
515
  : t("taskForm.emptyValue") })] })] }), _jsxs("div", { className: formStyles.taskFormDateRow, children: [_jsx("span", { className: formStyles.taskFormDateLabel, children: t("taskForm.updatedLabel") }), _jsxs("span", { className: formStyles.taskFormMetaStack, children: [_jsx("span", { className: formStyles.taskFormDateValue, children: updatedLabel || t("taskForm.emptyValue") }), _jsx("span", { className: formStyles.taskFormMetaActor, children: updatedByLabel
566
516
  ? `by ${updatedByLabel}`
@@ -520,20 +520,12 @@ export function TaskKanban({ tasks, columns: kanbanColumns, groupBy, searchQuery
520
520
  void onAssignTaskToWorkstream?.(activeTaskDataId, overWorkstreamDataId);
521
521
  return;
522
522
  }
523
- if (overType === 'planning-task-unlink' && activeTaskDataId) {
524
- void onAssignTaskToWorkstream?.(activeTaskDataId, null);
525
- return;
526
- }
527
523
  }
528
524
  if (activeType === 'planning-workstream') {
529
525
  if (overType === 'planning-initiative-target' && activeWorkstreamDataId && overInitiativeDataId) {
530
526
  void onAssignWorkstreamToInitiative?.(activeWorkstreamDataId, overInitiativeDataId);
531
527
  return;
532
528
  }
533
- if (overType === 'planning-workstream-unlink' && activeWorkstreamDataId) {
534
- void onAssignWorkstreamToInitiative?.(activeWorkstreamDataId, null);
535
- return;
536
- }
537
529
  return;
538
530
  }
539
531
  const activeId = String(active.id);
@@ -11,6 +11,7 @@ import { getVisibleTaxonomiesForTasks } from '../../utils/taxonomyLifecycle.js';
11
11
  import { TaskCard } from './TaskCard';
12
12
  import { t } from '../../localization';
13
13
  import { DeleteAllDeletedAction, TaskScopeToggle } from '../views/panels/FilterToolbarControls';
14
+ import { normalizeTaskReferenceToken } from '../../utils/taskSearch';
14
15
  export const TaskList = forwardRef((props, ref) => {
15
16
  const { tasks, archivedTasks, categories, types, priorities, taxonomyDisplayLabels, assigneeOptions = [], workstreams = [], initiatives = [], searchQuery, filterCategories, filterTypes, filterPriorities, filterStatus, filterAssignees = [], filterTaxonomies, sortBy, sortOrder, showArchive, taskScope, collapsedCategories, loadingTasks, filteredTasks, filteredArchive, groupedTasks, filteredDeletedTasks = [], groupedDeletedTasks = {}, copiedId, recentlyChangedTaskIds = [], showTaskCardStatusLabel = true, onSearchChange, onFilterCategoriesChange, onFilterTypesChange, onFilterPrioritiesChange, onFilterStatusChange, onFilterAssigneesChange, onTaxonomyFilterChange, onSortByChange, onSortOrderChange, onShowArchiveChange, onTaskScopeChange, onClearFilters, onToggleCategory, onEditTask, onOpenTaskById, onCopyId, onToggleInProgress, onToggleReview, onToggleComplete, onToggleCancel, onSetStatus, onArchiveTask, onBulkArchive, onUnarchive, onDelete, onDeleteAllDeleted, onFetchArchive, onAddTaskToCategory, supplementalTasks = [], taxonomies, } = props;
16
17
  const changedTaskIdSet = new Set(recentlyChangedTaskIds);
@@ -46,8 +47,11 @@ export const TaskList = forwardRef((props, ref) => {
46
47
  : effectiveTaskScope === 'archived'
47
48
  ? 'archived'
48
49
  : null;
50
+ const searchedTaskReference = normalizeTaskReferenceToken(searchQuery);
49
51
  const emptyLabel = searchQuery
50
- ? t('taskList.noMatchingTasks')
52
+ ? searchedTaskReference
53
+ ? `No task found for ${searchedTaskReference.toUpperCase()}`
54
+ : t('taskList.noMatchingTasks')
51
55
  : effectiveTaskScope === 'archived'
52
56
  ? 'No archived tasks found.'
53
57
  : effectiveTaskScope === 'deleted'
@@ -390,6 +390,10 @@ describe('TaskList Component', () => {
390
390
  renderComponent({ filteredTasks: [], loadingTasks: false });
391
391
  expect(screen.getByText('No active tasks')).toBeInTheDocument();
392
392
  });
393
+ it('shows a task-reference-specific empty state for unmatched searches', () => {
394
+ renderComponent({ filteredTasks: [], loadingTasks: false, searchQuery: ' T - 123 ' });
395
+ expect(screen.getByText('No task found for T-123')).toBeInTheDocument();
396
+ });
393
397
  it('2.13 Loading state', () => {
394
398
  renderComponent({ loadingTasks: true });
395
399
  expect(screen.queryByText('No active tasks')).not.toBeInTheDocument();
@@ -0,0 +1,10 @@
1
+ import React from 'react';
2
+ interface HierarchyActionBadgeProps {
3
+ ariaLabel: string;
4
+ children: React.ReactNode;
5
+ disabled?: boolean;
6
+ onClick: (event: React.MouseEvent<HTMLButtonElement>) => void;
7
+ title: string;
8
+ }
9
+ export declare function HierarchyActionBadge({ ariaLabel, children, disabled, onClick, title, }: HierarchyActionBadgeProps): import("react/jsx-runtime").JSX.Element;
10
+ export {};
@@ -0,0 +1,5 @@
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
+ import styles from '../../Taskforce.module.css';
3
+ export function HierarchyActionBadge({ ariaLabel, children, disabled = false, onClick, title, }) {
4
+ return (_jsx("button", { type: "button", className: `${styles.taskIdBadge} ${styles.taskHierarchyActionBtn}`, onClick: onClick, disabled: disabled, title: title, "aria-label": ariaLabel, children: children }));
5
+ }
@@ -4,10 +4,12 @@ interface AppShellHeaderProps {
4
4
  projectName?: string | null;
5
5
  currentWorkspaceId?: string | null;
6
6
  brandLabel?: string;
7
+ brandHomeLabel?: string;
8
+ onBrandClick?: () => void;
7
9
  runtimeMode?: 'local' | 'cloud';
8
10
  theme?: TaskforceTheme;
9
11
  meta?: React.ReactNode;
10
12
  actions?: React.ReactNode;
11
13
  }
12
- export declare function AppShellHeader({ projectName, currentWorkspaceId, brandLabel, runtimeMode, theme, meta, actions }: AppShellHeaderProps): import("react/jsx-runtime").JSX.Element;
14
+ export declare function AppShellHeader({ projectName, currentWorkspaceId, brandLabel, brandHomeLabel, onBrandClick, runtimeMode, theme, meta, actions }: AppShellHeaderProps): import("react/jsx-runtime").JSX.Element;
13
15
  export {};
@@ -5,19 +5,35 @@ import logoLight from '../../assets/branding/logos/logo_light.svg';
5
5
  import logoDark from '../../assets/branding/logos/logo_dark.svg';
6
6
  import favicon from '../../assets/favicon/favicon.svg';
7
7
  import { DEFAULT_TASKFORCE_THEME, isLightThemeFamily } from '../../utils/theme';
8
- export function AppShellHeader({ projectName, currentWorkspaceId, brandLabel = 'TaskForce', runtimeMode = 'local', theme = DEFAULT_TASKFORCE_THEME, meta, actions }) {
8
+ export function AppShellHeader({ projectName, currentWorkspaceId, brandLabel = 'TaskForce', brandHomeLabel = 'Go to Kanban', onBrandClick, runtimeMode = 'local', theme = DEFAULT_TASKFORCE_THEME, meta, actions }) {
9
9
  const logo = isLightThemeFamily(theme) ? logoDark : logoLight;
10
10
  const normalizedProjectName = String(projectName || '').trim();
11
- const normalizedWorkspaceId = String(currentWorkspaceId || '').trim().toLowerCase();
11
+ const workspaceIdForCopy = String(currentWorkspaceId || '').trim();
12
+ const normalizedWorkspaceId = workspaceIdForCopy.toLowerCase();
12
13
  const showProjectName = Boolean(normalizedProjectName
13
14
  && (runtimeMode !== 'cloud' || (normalizedWorkspaceId && normalizedWorkspaceId !== 'default')));
14
- return (_jsxs("div", { className: `${styles.header} ${shellStyles.standaloneHeader}`, children: [_jsxs("div", { className: `${styles.headerTitle} ${shellStyles.standaloneTitle}`, children: [_jsx("img", { src: logo, alt: "Taskforce Logo", className: styles.brandIcon, onError: (event) => {
15
- const target = event.currentTarget;
16
- if (target.src !== favicon) {
17
- target.src = favicon;
18
- }
19
- else {
20
- target.style.display = 'none';
21
- }
22
- } }), _jsx("span", { children: brandLabel }), runtimeMode === 'cloud' && (_jsx("span", { className: styles.brandCloudSuffix, children: "HQ" })), showProjectName && (_jsxs(_Fragment, { children: [_jsx("span", { className: styles.projectSlash, children: "/" }), _jsx("span", { className: styles.projectName, title: currentWorkspaceId ? `Workspace ID: ${currentWorkspaceId}` : undefined, children: normalizedProjectName })] })), meta] }), _jsx("div", { className: styles.headerActions, style: { gap: '16px' }, children: actions })] }));
15
+ const copyWorkspaceId = () => {
16
+ if (!workspaceIdForCopy || typeof navigator === 'undefined')
17
+ return;
18
+ const clipboard = navigator.clipboard;
19
+ if (!clipboard?.writeText)
20
+ return;
21
+ void clipboard.writeText(workspaceIdForCopy).catch(() => undefined);
22
+ };
23
+ const handleProjectNameKeyDown = (event) => {
24
+ if (event.key !== 'Enter' && event.key !== ' ')
25
+ return;
26
+ event.preventDefault();
27
+ copyWorkspaceId();
28
+ };
29
+ const brandContent = (_jsxs(_Fragment, { children: [_jsx("img", { src: logo, alt: "Taskforce Logo", className: styles.brandIcon, onError: (event) => {
30
+ const target = event.currentTarget;
31
+ if (target.src !== favicon) {
32
+ target.src = favicon;
33
+ }
34
+ else {
35
+ target.style.display = 'none';
36
+ }
37
+ } }), _jsx("span", { children: brandLabel }), runtimeMode === 'cloud' && (_jsx("span", { className: styles.brandCloudSuffix, children: "HQ" }))] }));
38
+ return (_jsxs("div", { className: `${styles.header} ${shellStyles.standaloneHeader}`, children: [_jsxs("div", { className: `${styles.headerTitle} ${shellStyles.standaloneTitle}`, children: [onBrandClick ? (_jsx("button", { type: "button", className: styles.brandHomeButton, onClick: onBrandClick, title: brandHomeLabel, "aria-label": brandHomeLabel, children: brandContent })) : (_jsx("span", { className: styles.brandHomeStatic, children: brandContent })), showProjectName && (_jsxs(_Fragment, { children: [_jsx("span", { className: styles.projectSlash, children: "/" }), _jsx("span", { className: styles.projectName, title: workspaceIdForCopy ? `Workspace ID: ${workspaceIdForCopy}` : undefined, role: workspaceIdForCopy ? 'button' : undefined, tabIndex: workspaceIdForCopy ? 0 : undefined, onClick: copyWorkspaceId, onMouseDown: (event) => event.preventDefault(), onKeyDown: handleProjectNameKeyDown, children: normalizedProjectName })] })), meta] }), _jsx("div", { className: styles.headerActions, style: { gap: '16px' }, children: actions })] }));
23
39
  }
@@ -1,6 +1,7 @@
1
1
  import { jsx as _jsx } from "react/jsx-runtime";
2
2
  import { render, screen } from '@testing-library/react';
3
- import { describe, expect, it } from 'vitest';
3
+ import userEvent from '@testing-library/user-event';
4
+ import { describe, expect, it, vi } from 'vitest';
4
5
  import { AppShellHeader } from './AppShellHeader';
5
6
  import logoDark from '../../assets/branding/logos/logo_dark.svg';
6
7
  describe('AppShellHeader', () => {
@@ -18,6 +19,22 @@ describe('AppShellHeader', () => {
18
19
  render(_jsx(AppShellHeader, { runtimeMode: "local", theme: "dawn" }));
19
20
  expect(screen.getByAltText('Taskforce Logo')).toHaveAttribute('src', logoDark);
20
21
  });
22
+ it('calls the brand click handler from the logo button', async () => {
23
+ const onBrandClick = vi.fn();
24
+ render(_jsx(AppShellHeader, { runtimeMode: "local", onBrandClick: onBrandClick }));
25
+ await userEvent.click(screen.getByRole('button', { name: 'Go to Kanban' }));
26
+ expect(onBrandClick).toHaveBeenCalledTimes(1);
27
+ });
28
+ it('copies the workspace id from the project badge', async () => {
29
+ const writeText = vi.fn().mockResolvedValue(undefined);
30
+ Object.defineProperty(navigator, 'clipboard', {
31
+ configurable: true,
32
+ value: { writeText }
33
+ });
34
+ render(_jsx(AppShellHeader, { runtimeMode: "local", projectName: "Roadmap", currentWorkspaceId: "workspace-alpha" }));
35
+ await userEvent.click(screen.getByRole('button', { name: 'Roadmap' }));
36
+ expect(writeText).toHaveBeenCalledWith('workspace-alpha');
37
+ });
21
38
  it('hides a stale project label while cloud runtime is still on the default workspace', () => {
22
39
  render(_jsx(AppShellHeader, { runtimeMode: "cloud", projectName: "Old Workspace", currentWorkspaceId: "default" }));
23
40
  expect(screen.queryByText('Old Workspace')).not.toBeInTheDocument();
@@ -106,6 +106,7 @@ function getFeatureDisplayParts(feature, seatLimit) {
106
106
  export function PlanComparisonPage({ heading, subtitle, variant = 'app', chrome, footer, isAuthenticated, authSessionResolved = true, currentPlanId, currentPlanVersionId, currentEntitlementState, markCurrentPlan, canManageCurrentPlan = true, pricingEndpoint, statusBanner, onBack, backLabel, backDisabled = false, topActions, shellOwnsScroll = false, showPageBackButton = true, showHeaderBarWithChrome = false, currentPlanCardActionMode = 'manage', fallbackPricingSource = null, onSelectPlan }) {
107
107
  const [plans, setPlans] = useState([]);
108
108
  const [pricingSource, setPricingSource] = useState(null);
109
+ const [pricingPageCopy, setPricingPageCopy] = useState({ pageTitle: null, pageDescription: null });
109
110
  const [loading, setLoading] = useState(true);
110
111
  const [error, setError] = useState(null);
111
112
  const [retryNonce, setRetryNonce] = useState(0);
@@ -124,6 +125,10 @@ export function PlanComparisonPage({ heading, subtitle, variant = 'app', chrome,
124
125
  if (active) {
125
126
  setPlans(Array.isArray(body?.plans) ? body.plans : []);
126
127
  setPricingSource(body?.pricingSource && typeof body.pricingSource === 'object' ? body.pricingSource : null);
128
+ setPricingPageCopy({
129
+ pageTitle: typeof body?.pageTitle === 'string' ? (body.pageTitle.trim() || null) : null,
130
+ pageDescription: typeof body?.pageDescription === 'string' ? (body.pageDescription.trim() || null) : null
131
+ });
127
132
  }
128
133
  return;
129
134
  }
@@ -134,6 +139,7 @@ export function PlanComparisonPage({ heading, subtitle, variant = 'app', chrome,
134
139
  const message = err instanceof Error ? err.message : 'Failed to load pricing';
135
140
  setError(message || 'Failed to load pricing');
136
141
  setPricingSource(null);
142
+ setPricingPageCopy({ pageTitle: null, pageDescription: null });
137
143
  }
138
144
  }
139
145
  finally {
@@ -156,6 +162,8 @@ export function PlanComparisonPage({ heading, subtitle, variant = 'app', chrome,
156
162
  return String(a.displayName || a.planId).localeCompare(String(b.displayName || b.planId));
157
163
  }), [plans]);
158
164
  const pricingSourceLabel = useMemo(() => formatPricingSource(pricingSource || fallbackPricingSource), [fallbackPricingSource, pricingSource]);
165
+ const resolvedHeading = String(pricingPageCopy.pageTitle || heading).trim() || heading;
166
+ const resolvedSubtitle = String(pricingPageCopy.pageDescription || subtitle || '').trim();
159
167
  const planSelectionEnabled = authSessionResolved;
160
168
  const contentClassName = `${styles.plansWrapper} ${shellOwnsScroll ? styles.shellOwnsScroll : 'tf-scrollbar'} ${variant === 'site' ? styles.siteVariant : ''} ${chrome ? styles.withChrome : ''}`.trim();
161
169
  const wrapPage = (content) => {
@@ -170,7 +178,7 @@ export function PlanComparisonPage({ heading, subtitle, variant = 'app', chrome,
170
178
  if (error) {
171
179
  return wrapPage(_jsx(_Fragment, { children: _jsx("div", { className: contentClassName, children: _jsx("div", { className: styles.contentFrame, children: _jsxs("div", { className: styles.emptyState, children: [_jsx("h2", { children: "Connecting to plan pricing..." }), _jsx("p", { children: error }), _jsx("button", { className: styles.backBtn, onClick: () => setRetryNonce((value) => value + 1), children: "Retry" })] }) }) }) }));
172
180
  }
173
- return wrapPage(_jsx(_Fragment, { children: _jsx("div", { className: contentClassName, children: _jsxs("div", { className: styles.contentFrame, children: [_jsxs("div", { className: styles.header, children: [(!chrome || showHeaderBarWithChrome) && (showPageBackButton || topActions) && (_jsxs("div", { className: styles.headerBar, children: [_jsx("div", { children: showPageBackButton ? (_jsx("button", { className: styles.backBtn, onClick: onBack, disabled: backDisabled, children: backLabel })) : null }), _jsx("div", { className: styles.headerActions, children: topActions })] })), _jsx("h1", { className: styles.title, children: heading }), subtitle && _jsx("p", { className: styles.subtitle, children: subtitle }), isDebugModeEnabled() && pricingSourceLabel ? (_jsxs("p", { className: styles.debugMeta, children: ["Pricing source: ", pricingSourceLabel] })) : null, statusBanner && (_jsx("div", { className: `${styles.statusBanner} ${statusBanner.type === 'success'
181
+ return wrapPage(_jsx(_Fragment, { children: _jsx("div", { className: contentClassName, children: _jsxs("div", { className: styles.contentFrame, children: [_jsxs("div", { className: styles.header, children: [(!chrome || showHeaderBarWithChrome) && (showPageBackButton || topActions) && (_jsxs("div", { className: styles.headerBar, children: [_jsx("div", { children: showPageBackButton ? (_jsx("button", { className: styles.backBtn, onClick: onBack, disabled: backDisabled, children: backLabel })) : null }), _jsx("div", { className: styles.headerActions, children: topActions })] })), _jsx("h1", { className: styles.title, children: resolvedHeading }), resolvedSubtitle && _jsx("p", { className: styles.subtitle, children: resolvedSubtitle }), isDebugModeEnabled() && pricingSourceLabel ? (_jsxs("p", { className: styles.debugMeta, children: ["Pricing source: ", pricingSourceLabel] })) : null, statusBanner && (_jsx("div", { className: `${styles.statusBanner} ${statusBanner.type === 'success'
174
182
  ? styles.successBanner
175
183
  : (statusBanner.type === 'info' ? styles.infoBanner : styles.errorBanner)}`, children: statusBanner.message }))] }), sortedPlans.length === 0 ? (_jsxs("div", { className: `${styles.emptyState} ${styles.marketingEmptyState}`, children: [_jsx("span", { className: styles.emptyBadge, children: "Coming Soon" }), _jsx("h2", { children: "Paid plans are getting their final polish." }), _jsx("p", { children: "Taskforce pricing is on the way with flexible options for solo operators, teams, and larger rollouts. Check back soon for launch tiers, feature bundles, and early access details." }), _jsxs("div", { className: styles.emptyHighlights, "aria-label": "Upcoming pricing highlights", children: [_jsx("span", { children: "Launch-ready tiers" }), _jsx("span", { children: "Team billing controls" }), _jsx("span", { children: "Feature-based packaging" })] })] })) : (_jsx("div", { className: styles.grid, children: sortedPlans.map((plan) => {
176
184
  const monthPrice = plan.pricing.month;
@@ -9,6 +9,8 @@ describe('PlanComparisonPage', () => {
9
9
  vi.stubGlobal('fetch', vi.fn(async () => ({
10
10
  ok: true,
11
11
  json: async () => ({
12
+ pageTitle: 'Subscription Plans',
13
+ pageDescription: 'Manage your subscription and explore available features. Your current plan is highlighted below.',
12
14
  pricingSource: {
13
15
  environment: 'production',
14
16
  runtimeMode: 'cloud'
@@ -66,6 +68,23 @@ describe('PlanComparisonPage', () => {
66
68
  expect(await screen.findByText('Start using Taskforce at no cost.')).toBeInTheDocument();
67
69
  expect(screen.getByText('For small collaborative teams.')).toBeInTheDocument();
68
70
  });
71
+ it('prefers pricing payload header copy when provided', async () => {
72
+ vi.stubGlobal('fetch', vi.fn(async () => ({
73
+ ok: true,
74
+ json: async () => ({
75
+ pageTitle: 'Choose Your Taskforce Plan',
76
+ pageDescription: 'Compare plans and pick the right fit for your team.',
77
+ pricingSource: {
78
+ environment: 'production',
79
+ runtimeMode: 'cloud'
80
+ },
81
+ plans: []
82
+ })
83
+ })));
84
+ render(_jsx(PlanComparisonPage, { heading: "Subscription Plans", subtitle: "Fallback subtitle", isAuthenticated: false, onBack: () => { }, backLabel: "Back", onSelectPlan: () => { } }));
85
+ expect(await screen.findByRole('heading', { name: 'Choose Your Taskforce Plan' })).toBeInTheDocument();
86
+ expect(screen.getByText('Compare plans and pick the right fit for your team.')).toBeInTheDocument();
87
+ });
69
88
  it('falls back to provided pricing source details when the pricing response omits them', async () => {
70
89
  globalThis.__DEBUG_MODE__ = true;
71
90
  vi.stubGlobal('fetch', vi.fn(async () => ({
@@ -570,6 +570,6 @@ export function PlansPage({ isAuthenticated, authUserId, runtimeMode, authSessio
570
570
  navigate('/');
571
571
  }, [closeEmbeddedPlansScreen, embedded, handleContinueToTaskforce, hasResolvedActiveEntitlement, navigate, onContinueToTaskforce]);
572
572
  const headerActions = embedded ? null : (_jsxs(_Fragment, { children: [_jsx("button", { className: styles.navAction, onClick: handleBack, disabled: continueBusy || continueActionBusy || (isAuthenticated && !hasResolvedActiveEntitlement), children: hasResolvedActiveEntitlement ? 'Take Me to Taskforce' : 'Back' }), isAuthenticated && accountProfileSummary?.stripeCustomerId && (_jsx("button", { className: styles.navAction, onClick: () => { void openBillingPortal(); }, disabled: actionBusy, children: "Manage billing" })), !isAuthenticated && (_jsxs("button", { className: styles.navAction, onClick: () => navigate(`/login?mode=login&next=${encodeURIComponent(buildEmbeddedPlansPath())}`), children: [_jsx(User, { size: 16, style: { marginRight: '8px', verticalAlign: 'text-bottom' } }), "Sign In"] }))] }));
573
- const chrome = embedded ? null : (_jsx(AppShellHeader, { projectName: projectName, currentWorkspaceId: currentWorkspaceId, runtimeMode: runtimeMode === 'cloud' ? 'cloud' : 'local', theme: currentTheme, actions: headerActions }));
573
+ const chrome = embedded ? null : (_jsx(AppShellHeader, { projectName: projectName, currentWorkspaceId: currentWorkspaceId, runtimeMode: runtimeMode === 'cloud' ? 'cloud' : 'local', theme: currentTheme, onBrandClick: handleBack, actions: headerActions }));
574
574
  return (_jsx(PlanComparisonPage, { chrome: chrome, heading: "Subscription Plans", subtitle: "Manage your subscription and explore available features. Your current plan is highlighted below.", isAuthenticated: isAuthenticated, authSessionResolved: authSessionResolved, currentPlanId: String(accountProfileSummary?.planId || queryPlanId || '').trim() || null, currentPlanVersionId: String(accountProfileSummary?.planVersionId || queryPlanVersionId || '').trim() || null, currentEntitlementState: String(accountProfileSummary?.entitlementState || '').trim() || null, markCurrentPlan: lifecyclePresentation.markCurrentPlan, canManageCurrentPlan: Boolean(accountProfileSummary?.stripeCustomerId), pricingEndpoint: pricingEndpoint, statusBanner: feedback, onBack: handleBack, backLabel: hasResolvedActiveEntitlement ? 'Take Me to Taskforce' : (embedded ? 'Back to Board' : 'Back to App'), backDisabled: continueBusy || continueActionBusy || (isAuthenticated && !hasResolvedActiveEntitlement), topActions: null, shellOwnsScroll: shellOwnsScroll, showPageBackButton: false, showHeaderBarWithChrome: false, currentPlanCardActionMode: "manage", fallbackPricingSource: fallbackPricingSource, onSelectPlan: handleSelectPlan }));
575
575
  }