@taskforcehq/taskforce 0.3.300 → 0.3.302

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 (153) hide show
  1. package/README.md +19 -0
  2. package/dist/Taskforce.module.css +26 -2
  3. package/dist/TaskforceCore.filter-persistence.test.js +1 -1
  4. package/dist/TaskforceCore.js +13 -24
  5. package/dist/TaskforceCore.test.js +167 -152
  6. package/dist/components/features/TaskSettings.js +14 -82
  7. package/dist/components/features/TaskSettings.test.js +33 -70
  8. package/dist/components/features/settings/TaxonomyLibraryManager.js +8 -2
  9. package/dist/components/features/settings/TaxonomyLibraryManager.test.js +1 -1
  10. package/dist/components/task/TaskActionHeader.js +6 -3
  11. package/dist/components/task/TaskCard.js +6 -3
  12. package/dist/components/task/TaskForm.js +2 -2
  13. package/dist/components/task/TaskForm.test.js +41 -2
  14. package/dist/components/ui/ReferenceBadgeButton.js +1 -1
  15. package/dist/components/views/PlansPage.js +9 -8
  16. package/dist/components/views/StandaloneLayout.js +490 -207
  17. package/dist/components/views/panels/FilterToolbar.d.ts +2 -0
  18. package/dist/components/views/panels/FilterToolbarControls.d.ts +1 -0
  19. package/dist/components/views/panels/FilterToolbarControls.js +1 -1
  20. package/dist/components/views/panels/PlanningDrawer.js +19 -2
  21. package/dist/components/views/standalone/modals/AccountHubModal.d.ts +43 -0
  22. package/dist/components/views/standalone/modals/AccountHubModal.js +22 -0
  23. package/dist/components/views/standalone/modals/CreateWorkspaceConfirmModal.d.ts +9 -0
  24. package/dist/components/views/standalone/modals/CreateWorkspaceConfirmModal.js +8 -0
  25. package/dist/components/views/standalone/modals/EditProfileModal.d.ts +26 -0
  26. package/dist/components/views/standalone/modals/EditProfileModal.js +10 -0
  27. package/dist/components/views/standalone/modals/HelpModal.d.ts +9 -0
  28. package/dist/components/views/standalone/modals/HelpModal.js +8 -0
  29. package/dist/components/views/standalone/modals/ScheduleWarningModal.d.ts +13 -0
  30. package/dist/components/views/standalone/modals/ScheduleWarningModal.js +6 -0
  31. package/dist/components/views/standalone/modals/SyncStatusModal.d.ts +61 -0
  32. package/dist/components/views/standalone/modals/SyncStatusModal.js +38 -0
  33. package/dist/components/views/standalone/modals/TeamManagementModal.d.ts +64 -0
  34. package/dist/components/views/standalone/modals/TeamManagementModal.js +10 -0
  35. package/dist/components/views/standalone/modals/types.d.ts +2 -0
  36. package/dist/components/views/standalone/modals/types.js +1 -0
  37. package/dist/components/views/teamManagementAccess.d.ts +1 -1
  38. package/dist/components/views/teamManagementAccess.js +2 -2
  39. package/dist/components/views/teamManagementAccess.test.js +3 -3
  40. package/dist/config/deployment.js +2 -2
  41. package/dist/config/deployment.test.js +5 -3
  42. package/dist/core/AiProfileService.js +7 -2
  43. package/dist/core/AiProfiles.test.js +19 -0
  44. package/dist/core/SyncReconciliationService.d.ts +2 -0
  45. package/dist/core/SyncReconciliationService.js +3 -0
  46. package/dist/core/TaskAuditTimeline.test.js +40 -0
  47. package/dist/core/Taskforce.d.ts +49 -0
  48. package/dist/core/Taskforce.ids.test.js +133 -0
  49. package/dist/core/Taskforce.js +241 -78
  50. package/dist/core/UserProfileAvatarDrafts.test.d.ts +1 -0
  51. package/dist/core/UserProfileAvatarDrafts.test.js +102 -0
  52. package/dist/core/taskReferencePolicy.d.ts +28 -0
  53. package/dist/core/taskReferencePolicy.js +189 -0
  54. package/dist/core/types.d.ts +2 -0
  55. package/dist/hooks/tasks/useTaskFormActions.js +3 -0
  56. package/dist/hooks/useTaskData.js +16 -11
  57. package/dist/hooks/useTaskData.test.js +127 -0
  58. package/dist/hooks/useTaskforce.d.ts +5 -0
  59. package/dist/hooks/useTaskforce.js +63 -15
  60. package/dist/hooks/useTaskforce.sync-behavior.test.js +185 -6
  61. package/dist/mcp/aiProfileBootstrap.d.ts +29 -0
  62. package/dist/mcp/aiProfileBootstrap.js +80 -0
  63. package/dist/mcp/aiProfileBootstrap.test.d.ts +1 -0
  64. package/dist/mcp/aiProfileBootstrap.test.js +68 -0
  65. package/dist/mcp/clientIntegrations.d.ts +2 -8
  66. package/dist/mcp/clientIntegrations.js +21 -40
  67. package/dist/mcp/clientIntegrations.test.js +28 -2
  68. package/dist/mcp/clientProfileDefaults.d.ts +12 -0
  69. package/dist/mcp/clientProfileDefaults.js +37 -0
  70. package/dist/mcp/runtime.d.ts +10 -7
  71. package/dist/mcp/runtime.js +45 -23
  72. package/dist/mcp/runtime.test.js +56 -3
  73. package/dist/migrations/taskSchemaMigrations.js +53 -1
  74. package/dist/qaOpen.test.d.ts +1 -0
  75. package/dist/qaOpen.test.js +56 -0
  76. package/dist/server/index.cookieProxy.test.js +1 -0
  77. package/dist/server/index.d.ts +1 -0
  78. package/dist/server/index.js +15 -1
  79. package/dist/server/index.test.js +14 -1
  80. package/dist/server/routes/admin.js +25 -22
  81. package/dist/server/routes/auth.d.ts +5 -0
  82. package/dist/server/routes/auth.js +350 -16
  83. package/dist/server/routes/billing.js +11 -1
  84. package/dist/server/routes/billing.test.js +32 -2
  85. package/dist/server/routes/documents.js +6 -1
  86. package/dist/server/routes/shared.d.ts +1 -1
  87. package/dist/server/routes/shared.js +11 -3
  88. package/dist/server/routes/sync.integration.test.js +130 -0
  89. package/dist/server/routes/sync.js +226 -1924
  90. package/dist/server/routes/tasks.js +18 -4
  91. package/dist/server/routes.js +26 -19
  92. package/dist/server/routes.test.js +352 -6
  93. package/dist/shared/runtimeContract.d.ts +1 -1
  94. package/dist/sync/collaborationSyncPayload.d.ts +17 -0
  95. package/dist/sync/collaborationSyncPayload.js +131 -0
  96. package/dist/sync/collaborationSyncPayload.test.d.ts +1 -0
  97. package/dist/sync/collaborationSyncPayload.test.js +104 -0
  98. package/dist/sync/collaborationSyncState.d.ts +15 -0
  99. package/dist/sync/collaborationSyncState.js +60 -0
  100. package/dist/sync/contentSyncPayload.d.ts +45 -0
  101. package/dist/sync/contentSyncPayload.js +104 -0
  102. package/dist/sync/contentSyncPayload.test.d.ts +1 -0
  103. package/dist/sync/contentSyncPayload.test.js +104 -0
  104. package/dist/sync/contentSyncState.d.ts +88 -0
  105. package/dist/sync/contentSyncState.js +743 -0
  106. package/dist/sync/planningSyncPayload.d.ts +9 -0
  107. package/dist/sync/planningSyncPayload.js +82 -0
  108. package/dist/sync/planningSyncPayload.test.d.ts +1 -0
  109. package/dist/sync/planningSyncPayload.test.js +68 -0
  110. package/dist/sync/syncApplyHandlers.d.ts +83 -0
  111. package/dist/sync/syncApplyHandlers.js +445 -0
  112. package/dist/sync/taskSyncPayload.d.ts +33 -0
  113. package/dist/sync/taskSyncPayload.js +126 -0
  114. package/dist/sync/taskSyncPayload.test.d.ts +1 -0
  115. package/dist/sync/taskSyncPayload.test.js +54 -0
  116. package/dist/sync/workspacePullFeed.d.ts +28 -0
  117. package/dist/sync/workspacePullFeed.js +301 -0
  118. package/dist/sync/workspacePullFeed.test.d.ts +1 -0
  119. package/dist/sync/workspacePullFeed.test.js +166 -0
  120. package/dist/sync/workspaceSyncModel.d.ts +4 -1
  121. package/dist/sync/workspaceSyncModel.js +9 -2
  122. package/dist/sync/workspaceSyncModel.test.js +29 -0
  123. package/dist/test/setup.js +1 -1
  124. package/dist/types.d.ts +1 -0
  125. package/dist/ui/assets/{AgentsModule-TVmCgizS.js → AgentsModule-JG5jc3he.js} +1 -1
  126. package/dist/ui/assets/{AnnotatedAttachmentWorkspace-CAyhnPeR.js → AnnotatedAttachmentWorkspace-Dvi02Y7q.js} +1 -1
  127. package/dist/ui/assets/{DocumentWorkspace-BOCw5e4D.js → DocumentWorkspace-iys3zfca.js} +2 -2
  128. package/dist/ui/assets/{InitiativesModule-CN0r02Oi.js → InitiativesModule-DJvLJI3A.js} +1 -1
  129. package/dist/ui/assets/PlansPage-BMPlpFpw.js +1 -0
  130. package/dist/ui/assets/TaskSettings-Bqd4cCzW.js +8 -0
  131. package/dist/ui/assets/{TaskSettings-BwB9NtmJ.css → TaskSettings-CnIBL_Eb.css} +1 -1
  132. package/dist/ui/assets/{WorkflowsModule-CY9R2zPB.js → WorkflowsModule-BguHeQaV.js} +1 -1
  133. package/dist/ui/assets/index-MXeWoebC.css +1 -0
  134. package/dist/ui/assets/index-YJ4k5QNa.js +6 -0
  135. package/dist/ui/assets/{vendor-icons-QZyhEwgT.js → vendor-icons-C8EED3Oh.js} +1 -1
  136. package/dist/ui/index.html +3 -3
  137. package/dist/utils/billingStatusCache.d.ts +21 -0
  138. package/dist/utils/billingStatusCache.js +70 -0
  139. package/dist/utils/taskActivity.js +17 -4
  140. package/dist/utils/taskActivity.test.js +27 -0
  141. package/dist/utils/taskNormalization.js +10 -2
  142. package/dist/utils/taskReferences.d.ts +11 -1
  143. package/dist/utils/taskReferences.js +33 -1
  144. package/package.json +4 -1
  145. package/scripts/deploy-prod.sh +41 -0
  146. package/scripts/deploy-staging.sh +23 -0
  147. package/scripts/qa-open-lib.mjs +175 -0
  148. package/scripts/qa-open-lib.mjs.d.ts +49 -0
  149. package/scripts/qa-open.mjs +352 -0
  150. package/dist/ui/assets/PlansPage-FcQSNLZd.js +0 -1
  151. package/dist/ui/assets/TaskSettings-Cw5zYD1L.js +0 -8
  152. package/dist/ui/assets/index-Bzqy5QGm.js +0 -6
  153. package/dist/ui/assets/index-DxRiI3tY.css +0 -1
@@ -172,8 +172,6 @@ export function TaskSettings(props) {
172
172
  const [mcpTokenNotice, setMcpTokenNotice] = useState(null);
173
173
  const [generatedTokenValue, setGeneratedTokenValue] = useState(null);
174
174
  const [selectedMcpClient, setSelectedMcpClient] = useState('cursor');
175
- const [automationAiName, setAutomationAiName] = useState('');
176
- const [automationAiProfileToken, setAutomationAiProfileToken] = useState('');
177
175
  const [mcpConnectionToken, setMcpConnectionToken] = useState('');
178
176
  const [mcpConnectionBusy, setMcpConnectionBusy] = useState(false);
179
177
  const [mcpConnectionResult, setMcpConnectionResult] = useState(null);
@@ -227,77 +225,16 @@ export function TaskSettings(props) {
227
225
  }, [mcpTokens]);
228
226
  const cloudMcpServerId = useMemo(() => buildCloudMcpServerId(currentWorkspaceName || null, projectName || null), [currentWorkspaceName, projectName]);
229
227
  const cloudMcpTokenValue = generatedTokenValue || '<<GENERATE_TOKEN>>';
230
- const selectedAutomationProfileId = useMemo(() => {
231
- if (automationAiProfileToken) {
232
- const byToken = aiProfiles.find((profile) => String(profile.profileToken || '').trim() === automationAiProfileToken.trim());
233
- if (byToken)
234
- return byToken.id;
235
- }
236
- if (automationAiName) {
237
- const byName = aiProfiles.find((profile) => profile.name === automationAiName.trim());
238
- if (byName)
239
- return byName.id;
240
- }
241
- return '';
242
- }, [aiProfiles, automationAiName, automationAiProfileToken]);
243
- const selectedAutomationProfile = useMemo(() => aiProfiles.find((profile) => profile.id === selectedAutomationProfileId) || null, [aiProfiles, selectedAutomationProfileId]);
244
- const inferredAutomationProfileMetadata = useMemo(() => {
245
- switch (selectedMcpClient) {
246
- case 'codex':
247
- return { description: 'OpenAI Codex coding agent connected through MCP.', role: 'Coding Agent', provider: 'OpenAI', model: 'Codex', surfaceType: 'coding_tool' };
248
- case 'chatgpt-desktop':
249
- return { description: 'ChatGPT desktop app connected through the Taskforce MCP connector.', role: 'AI Assistant', provider: 'OpenAI', model: 'ChatGPT', surfaceType: 'chat_app' };
250
- case 'claude-code':
251
- return { description: 'Claude Code coding agent connected through MCP.', role: 'Coding Agent', provider: 'Anthropic', model: 'Claude Code', surfaceType: 'coding_tool' };
252
- case 'claude-desktop':
253
- return { description: 'Claude Desktop connected through the Taskforce MCP connector.', role: 'AI Assistant', provider: 'Anthropic', model: 'Claude', surfaceType: 'chat_app' };
254
- case 'gemini-cli':
255
- return { description: 'Gemini CLI coding agent connected through MCP.', role: 'Coding Agent', provider: 'Google', model: 'Gemini', surfaceType: 'coding_tool' };
256
- case 'vscode':
257
- return { description: 'VS Code MCP-connected coding agent.', role: 'Coding Agent', provider: null, model: null, surfaceType: 'ide' };
258
- case 'cline':
259
- case 'windsurf':
260
- case 'antigravity':
261
- case 'openclaw':
262
- return {
263
- description: `${MCP_CLIENT_OPTIONS.find((option) => option.id === selectedMcpClient)?.label || 'MCP'} coding agent connected through MCP.`,
264
- role: 'Coding Agent',
265
- provider: null,
266
- model: null,
267
- surfaceType: 'coding_tool'
268
- };
269
- default:
270
- return { description: null, role: null, provider: null, model: null, surfaceType: null };
271
- }
272
- }, [selectedMcpClient]);
228
+ // Launch decision: keep generated Cloud MCP config identity-neutral for shared connections.
273
229
  const cloudClientIntegration = useMemo(() => buildCloudMcpClientIntegration({
274
230
  clientId: selectedMcpClient,
275
231
  endpoint: cloudMcpEndpoint,
276
232
  tokenValue: cloudMcpTokenValue,
277
233
  serverId: cloudMcpServerId,
278
- aiProfileName: automationAiName,
279
- aiProfileToken: automationAiProfileToken,
280
- aiProfileDescription: selectedAutomationProfile?.description ?? inferredAutomationProfileMetadata.description,
281
- aiProfileRole: selectedAutomationProfile?.role ?? inferredAutomationProfileMetadata.role,
282
- aiProfileProvider: selectedAutomationProfile?.provider ?? inferredAutomationProfileMetadata.provider,
283
- aiProfileModel: selectedAutomationProfile?.model ?? inferredAutomationProfileMetadata.model,
284
- aiProfileSurfaceType: selectedAutomationProfile?.surfaceType ?? inferredAutomationProfileMetadata.surfaceType,
285
234
  }), [
286
- automationAiName,
287
- automationAiProfileToken,
288
235
  cloudMcpEndpoint,
289
236
  cloudMcpServerId,
290
237
  cloudMcpTokenValue,
291
- inferredAutomationProfileMetadata.description,
292
- inferredAutomationProfileMetadata.model,
293
- inferredAutomationProfileMetadata.provider,
294
- inferredAutomationProfileMetadata.role,
295
- inferredAutomationProfileMetadata.surfaceType,
296
- selectedAutomationProfile?.description,
297
- selectedAutomationProfile?.model,
298
- selectedAutomationProfile?.provider,
299
- selectedAutomationProfile?.role,
300
- selectedAutomationProfile?.surfaceType,
301
238
  selectedMcpClient
302
239
  ]);
303
240
  const cloudMcpServerConfig = cloudClientIntegration.kind === 'json'
@@ -903,22 +840,14 @@ export function TaskSettings(props) {
903
840
  ? new Date(selectedConnectorGrant.lastUsedAt).toLocaleString()
904
841
  : 'not yet'] })] })] })) : ('No active connector authorization for this workspace yet.') }), _jsx("div", { className: styles.settingDescription, style: { marginTop: '12px' }, children: "Use the server URL above in your client, then finish the OAuth authorization flow from the Integrations tab." }), _jsx("div", { style: { display: 'flex', justifyContent: 'flex-end', width: '100%', marginTop: '12px' }, children: _jsx("button", { className: styles.copyBtn, onClick: () => setActiveTab('integrations'), children: "Open Integrations" }) })] }), _jsxs("div", { className: `${styles.settingGroup} ${styles.marginTop16}`, children: [_jsxs("div", { className: `${styles.codeBlockWrapper} ${styles.codeBlockWrapperCustom} ${styles.marginTop12}`, style: { minHeight: '260px' }, children: [_jsxs("div", { className: styles.codeHeader, children: [_jsx("span", { className: styles.codeLabel, children: "Cloud MCP Configuration" }), _jsxs("div", { className: styles.codeHeaderActions, children: [_jsx("select", { className: styles.settingInput, value: selectedMcpClient, onChange: (e) => setSelectedMcpClient(e.target.value), "aria-label": "MCP client", style: { width: 'auto', minWidth: '170px', padding: '6px 12px', fontSize: '12px', borderRadius: '8px' }, children: MCP_CLIENT_OPTIONS.map((option) => (_jsx("option", { value: option.id, disabled: option.disabled, children: option.label }, option.id))) }), _jsx("button", { className: `${styles.copyBtn} ${copiedConfig === 'full' ? styles.copyBtnActive : ''}`, onClick: handleCopyCloudMcpConfig, title: "Copy full configuration file", children: copiedConfig === 'full' ? _jsx(Check, { size: 14 }) : _jsx(Copy, { size: 14 }) })] })] }), _jsx("pre", { className: styles.codeBlock, children: _jsx("code", { children: cloudMcpServerConfig
905
842
  ? renderMcpConfigBlock(cloudMcpServerId, cloudMcpServerConfig, cloudClientIntegration.rootKey || 'mcpServers', cloudClientIntegration.wrapperKeys || [])
906
- : renderTextConfigBlock(cloudClientIntegration.renderedText || '') }) })] }), _jsxs("div", { className: `${styles.codeBlockWrapper} ${styles.marginTop12}`, children: [_jsx("div", { className: styles.codeHeader, children: _jsx("span", { className: styles.codeLabel, children: cloudClientIntegration.instructionLabel }) }), _jsx("div", { style: { padding: '16px 20px' }, children: _jsx("div", { className: styles.settingDescription, children: cloudClientIntegration.instructionText }) })] })] })] })) : (_jsxs(_Fragment, { children: [_jsxs("div", { className: `${styles.settingGroup} ${styles.marginTop16}`, children: [_jsxs("div", { className: styles.settingLabelGroup, children: [_jsx("h3", { className: styles.settingTitle, children: "Generate Token" }), _jsx("span", { className: styles.settingDescription, children: "Tokens are shown once. Default scopes allow task read/write. Add workspace read if you want config and skills metadata in the client." })] }), _jsxs("div", { style: { display: 'grid', gridTemplateColumns: 'minmax(0, 2fr) minmax(220px, 1fr)', gap: '12px', alignItems: 'end' }, children: [_jsxs("div", { className: styles.settingsItem, children: [_jsx("div", { className: styles.settingLabelGroup, children: _jsx("span", { className: styles.settingDescription, children: "Token Name" }) }), _jsx("input", { type: "text", className: styles.settingInput, placeholder: "Cursor on MacBook", value: newTokenName, onChange: (e) => setNewTokenName(e.target.value) })] }), _jsxs("div", { className: styles.settingsItem, children: [_jsx("div", { className: styles.settingLabelGroup, children: _jsx("span", { className: styles.settingDescription, children: "Expiry (Optional)" }) }), _jsx("input", { type: "date", className: `${styles.settingInput} ${styles.taskFormDateInput}`, value: newTokenExpiresAt, onChange: (e) => setNewTokenExpiresAt(e.target.value) })] })] }), _jsxs("div", { className: `${styles.settingsItem} ${styles.marginTop12}`, children: [_jsx("div", { className: styles.settingLabelGroup, children: _jsx("span", { className: styles.settingDescription, children: "Scopes" }) }), _jsxs("div", { style: { display: 'grid', gridTemplateColumns: '96px repeat(3, minmax(110px, 1fr))', gap: '10px 16px', alignItems: 'center' }, children: [_jsx("span", {}), _jsx("span", { className: styles.settingDescription, children: "Tasks" }), _jsx("span", { className: styles.settingDescription, children: "Workspace" }), _jsx("span", { className: styles.settingDescription, style: { opacity: 0.55 }, children: "Attachments" }), _jsx("span", { className: styles.settingDescription, children: "Read" }), _jsx("label", { className: styles.checkboxLabel, children: _jsx("input", { type: "checkbox", className: styles.checkboxSmall, checked: newTokenScopes.includes('tasks:read'), onChange: () => toggleTokenScope('tasks:read') }) }), _jsx("label", { className: styles.checkboxLabel, children: _jsx("input", { type: "checkbox", className: styles.checkboxSmall, checked: newTokenScopes.includes('workspace:read'), onChange: () => toggleTokenScope('workspace:read') }) }), _jsx("label", { className: styles.checkboxLabel, style: { opacity: 0.55 }, children: _jsx("input", { type: "checkbox", className: styles.checkboxSmall, checked: false, disabled: true }) }), _jsx("span", { className: styles.settingDescription, children: "Write" }), _jsx("label", { className: styles.checkboxLabel, children: _jsx("input", { type: "checkbox", className: styles.checkboxSmall, checked: newTokenScopes.includes('tasks:write'), onChange: () => toggleTokenScope('tasks:write') }) }), _jsx("label", { className: styles.checkboxLabel, children: _jsx("input", { type: "checkbox", className: styles.checkboxSmall, checked: false, disabled: true }) }), _jsx("label", { className: styles.checkboxLabel, style: { opacity: 0.55 }, children: _jsx("input", { type: "checkbox", className: styles.checkboxSmall, checked: false, disabled: true }) })] })] })] }), _jsxs("div", { className: `${styles.settingGroup} ${styles.marginTop16}`, children: [_jsxs("div", { className: styles.settingLabelGroup, children: [_jsx("h3", { className: styles.settingTitle, children: "Automation Identity (Optional)" }), _jsx("span", { className: styles.settingDescription, children: "For recurring or background AI workers, set an AI name here so the generated MCP config includes Taskforce automation headers. After the first run, paste the returned profile token too so the same AI profile is reused across runs without extra bootstrap steps." })] }), aiProfiles.length > 0 && (_jsxs("div", { className: `${styles.settingsItem} ${styles.marginTop12}`, children: [_jsx("div", { className: styles.settingLabelGroup, children: _jsx("span", { className: styles.settingDescription, children: "Use Existing AI Profile" }) }), _jsxs("select", { className: styles.settingInput, "aria-label": "Use existing AI profile", value: selectedAutomationProfileId, onChange: (e) => {
907
- const nextId = e.target.value;
908
- if (!nextId) {
909
- setAutomationAiName('');
910
- setAutomationAiProfileToken('');
911
- return;
912
- }
913
- const profile = aiProfiles.find((entry) => entry.id === nextId);
914
- if (!profile)
915
- return;
916
- setAutomationAiName(profile.name);
917
- setAutomationAiProfileToken(String(profile.profileToken || ''));
918
- }, children: [_jsx("option", { value: "", children: "Custom\u2026" }), aiProfiles.map((profile) => (_jsxs("option", { value: profile.id, children: [profile.name, " (@", profile.username, ")"] }, profile.id)))] })] })), _jsxs("div", { style: { display: 'grid', gridTemplateColumns: 'minmax(0, 1fr) minmax(0, 1fr)', gap: '12px', alignItems: 'end' }, children: [_jsxs("div", { className: styles.settingsItem, children: [_jsx("div", { className: styles.settingLabelGroup, children: _jsx("span", { className: styles.settingDescription, children: "AI Name Header" }) }), _jsx("input", { type: "text", className: styles.settingInput, placeholder: "Automation Codex", value: automationAiName, onChange: (e) => setAutomationAiName(e.target.value) })] }), _jsxs("div", { className: styles.settingsItem, children: [_jsx("div", { className: styles.settingLabelGroup, children: _jsx("span", { className: styles.settingDescription, children: "AI Profile Token Header" }) }), _jsx("input", { type: "text", className: styles.settingInput, placeholder: "tfp_...", value: automationAiProfileToken, onChange: (e) => setAutomationAiProfileToken(e.target.value) })] })] })] }), _jsxs("div", { className: `${styles.settingGroup} ${styles.marginTop16}`, children: [_jsx("div", { className: styles.settingHelper, children: "Generated tokens are only shown once. Copy the token before leaving this page." }), (() => {
919
- const displayedGeneratedToken = copiedConfig === 'token'
920
- ? 'Copied to clipboard'
921
- : (generatedTokenValue || '');
843
+ : renderTextConfigBlock(cloudClientIntegration.renderedText || '') }) })] }), _jsxs("div", { className: `${styles.codeBlockWrapper} ${styles.marginTop12}`, children: [_jsx("div", { className: styles.codeHeader, children: _jsx("span", { className: styles.codeLabel, children: cloudClientIntegration.instructionLabel }) }), _jsx("div", { style: { padding: '16px 20px' }, children: _jsx("div", { className: styles.settingDescription, children: cloudClientIntegration.instructionText }) })] })] })] })) : (_jsxs(_Fragment, { children: [_jsxs("div", { className: `${styles.settingGroup} ${styles.marginTop16}`, children: [_jsxs("div", { className: styles.settingLabelGroup, children: [_jsx("h3", { className: styles.settingTitle, children: "Generate Token" }), _jsx("span", { className: styles.settingDescription, children: "Tokens are shown once. Default scopes allow task read/write. Add workspace read if you want config and skills metadata in the client." })] }), _jsxs("div", { style: { display: 'grid', gridTemplateColumns: 'minmax(0, 2fr) minmax(220px, 1fr)', gap: '12px', alignItems: 'end' }, children: [_jsxs("div", { className: styles.settingsItem, children: [_jsx("div", { className: styles.settingLabelGroup, children: _jsx("span", { className: styles.settingDescription, children: "Token Name" }) }), _jsx("input", { type: "text", className: styles.settingInput, placeholder: "Cursor on MacBook", value: newTokenName, onChange: (e) => setNewTokenName(e.target.value) })] }), _jsxs("div", { className: styles.settingsItem, children: [_jsx("div", { className: styles.settingLabelGroup, children: _jsx("span", { className: styles.settingDescription, children: "Expiry (Optional)" }) }), _jsx("input", { type: "date", className: `${styles.settingInput} ${styles.taskFormDateInput}`, value: newTokenExpiresAt, onChange: (e) => setNewTokenExpiresAt(e.target.value) })] })] }), _jsxs("div", { className: `${styles.settingsItem} ${styles.marginTop12}`, children: [_jsx("div", { className: styles.settingLabelGroup, children: _jsx("span", { className: styles.settingDescription, children: "Scopes" }) }), _jsxs("div", { style: { display: 'grid', gridTemplateColumns: '96px repeat(3, minmax(110px, 1fr))', gap: '10px 16px', alignItems: 'center' }, children: [_jsx("span", {}), _jsx("span", { className: styles.settingDescription, children: "Tasks" }), _jsx("span", { className: styles.settingDescription, children: "Workspace" }), _jsx("span", { className: styles.settingDescription, style: { opacity: 0.55 }, children: "Attachments" }), _jsx("span", { className: styles.settingDescription, children: "Read" }), _jsx("label", { className: styles.checkboxLabel, children: _jsx("input", { type: "checkbox", className: styles.checkboxSmall, checked: newTokenScopes.includes('tasks:read'), onChange: () => toggleTokenScope('tasks:read') }) }), _jsx("label", { className: styles.checkboxLabel, children: _jsx("input", { type: "checkbox", className: styles.checkboxSmall, checked: newTokenScopes.includes('workspace:read'), onChange: () => toggleTokenScope('workspace:read') }) }), _jsx("label", { className: styles.checkboxLabel, style: { opacity: 0.55 }, children: _jsx("input", { type: "checkbox", className: styles.checkboxSmall, checked: false, disabled: true }) }), _jsx("span", { className: styles.settingDescription, children: "Write" }), _jsx("label", { className: styles.checkboxLabel, children: _jsx("input", { type: "checkbox", className: styles.checkboxSmall, checked: newTokenScopes.includes('tasks:write'), onChange: () => toggleTokenScope('tasks:write') }) }), _jsx("label", { className: styles.checkboxLabel, children: _jsx("input", { type: "checkbox", className: styles.checkboxSmall, checked: false, disabled: true }) }), _jsx("label", { className: styles.checkboxLabel, style: { opacity: 0.55 }, children: _jsx("input", { type: "checkbox", className: styles.checkboxSmall, checked: false, disabled: true }) })] })] })] }), _jsxs("div", { className: `${styles.settingGroup} ${styles.marginTop16}`, children: [_jsx("div", { className: styles.settingHelper, children: "Generated tokens are only shown once. Copy the token before leaving this page." }), (() => {
844
+ const tokenBarError = mcpTokenNotice?.type === 'error'
845
+ ? mcpTokenNotice.message
846
+ : null;
847
+ const displayedGeneratedToken = tokenBarError
848
+ || (copiedConfig === 'token'
849
+ ? 'Copied to clipboard'
850
+ : (generatedTokenValue || ''));
922
851
  return (_jsx("div", { style: { display: 'flex', gap: '8px', alignItems: 'stretch' }, children: _jsx("div", { className: styles.codeBlockWrapper, style: { flex: 1, cursor: generatedTokenValue ? 'pointer' : 'default' }, onClick: () => generatedTokenValue && copyToClipboard(generatedTokenValue, 'token'), role: generatedTokenValue ? 'button' : undefined, tabIndex: generatedTokenValue ? 0 : -1, onKeyDown: (e) => {
923
852
  if (!generatedTokenValue)
924
853
  return;
@@ -926,8 +855,11 @@ export function TaskSettings(props) {
926
855
  e.preventDefault();
927
856
  copyToClipboard(generatedTokenValue, 'token');
928
857
  }
929
- }, title: generatedTokenValue ? (copiedConfig === 'token' ? 'Token copied' : 'Click to copy token') : undefined, children: _jsx("pre", { className: styles.codeBlock, style: { textAlign: 'center' }, children: _jsx("code", { children: displayedGeneratedToken }) }) }) }));
930
- })()] }), _jsxs("div", { className: `${styles.settingGroup} ${styles.marginTop16}`, children: [mcpTokenNotice?.type === 'error' && (_jsxs("div", { className: `${styles.errorMessage} ${styles.marginTop12}`, children: [_jsx(AlertTriangle, { size: 14 }), mcpTokenNotice.message] })), _jsxs("div", { className: `${styles.codeBlockWrapper} ${styles.codeBlockWrapperCustom} ${styles.marginTop12}`, style: { minHeight: '260px' }, children: [_jsxs("div", { className: styles.codeHeader, children: [_jsx("span", { className: styles.codeLabel, children: "Cloud MCP Configuration" }), _jsxs("div", { className: styles.codeHeaderActions, children: [_jsx("select", { className: styles.settingInput, value: selectedMcpClient, onChange: (e) => setSelectedMcpClient(e.target.value), "aria-label": "MCP client", style: { width: 'auto', minWidth: '170px', padding: '6px 12px', fontSize: '12px', borderRadius: '8px' }, children: MCP_CLIENT_OPTIONS.map((option) => (_jsx("option", { value: option.id, disabled: option.disabled, children: option.label }, option.id))) }), _jsxs("button", { className: styles.copyBtn, onClick: () => void handleCreateMcpToken(), disabled: mcpActionBusyId === 'create', title: "Generate Token", "aria-label": "Generate Token", children: [mcpActionBusyId === 'create' ? _jsx(Loader2, { size: 14, className: styles.spinIcon }) : _jsx(Zap, { size: 14 }), _jsx("span", { children: "GENERATE TOKEN" })] }), _jsx("button", { className: `${styles.copyBtn} ${copiedConfig === 'full' ? styles.copyBtnActive : ''}`, onClick: handleCopyCloudMcpConfig, title: "Copy full configuration file", children: copiedConfig === 'full' ? _jsx(Check, { size: 14 }) : _jsx(Copy, { size: 14 }) })] })] }), _jsx("pre", { className: styles.codeBlock, children: _jsx("code", { children: cloudMcpServerConfig
858
+ }, title: generatedTokenValue ? (copiedConfig === 'token' ? 'Token copied' : 'Click to copy token') : undefined, children: _jsx("pre", { className: styles.codeBlock, style: {
859
+ textAlign: 'center',
860
+ color: tokenBarError ? 'var(--status-danger-strong, #f87171)' : undefined,
861
+ }, children: _jsx("code", { children: displayedGeneratedToken }) }) }) }));
862
+ })()] }), _jsxs("div", { className: `${styles.settingGroup} ${styles.marginTop16}`, children: [_jsxs("div", { className: `${styles.codeBlockWrapper} ${styles.codeBlockWrapperCustom} ${styles.marginTop12}`, style: { minHeight: '260px' }, children: [_jsxs("div", { className: styles.codeHeader, children: [_jsx("span", { className: styles.codeLabel, children: "Cloud MCP Configuration" }), _jsxs("div", { className: styles.codeHeaderActions, children: [_jsx("select", { className: styles.settingInput, value: selectedMcpClient, onChange: (e) => setSelectedMcpClient(e.target.value), "aria-label": "MCP client", style: { width: 'auto', minWidth: '170px', padding: '6px 12px', fontSize: '12px', borderRadius: '8px' }, children: MCP_CLIENT_OPTIONS.map((option) => (_jsx("option", { value: option.id, disabled: option.disabled, children: option.label }, option.id))) }), _jsxs("button", { className: styles.copyBtn, onClick: () => void handleCreateMcpToken(), disabled: mcpActionBusyId === 'create', title: "Generate Token", "aria-label": "Generate Token", children: [mcpActionBusyId === 'create' ? _jsx(Loader2, { size: 14, className: styles.spinIcon }) : _jsx(Zap, { size: 14 }), _jsx("span", { children: "GENERATE TOKEN" })] }), _jsx("button", { className: `${styles.copyBtn} ${copiedConfig === 'full' ? styles.copyBtnActive : ''}`, onClick: handleCopyCloudMcpConfig, title: "Copy full configuration file", children: copiedConfig === 'full' ? _jsx(Check, { size: 14 }) : _jsx(Copy, { size: 14 }) })] })] }), _jsx("pre", { className: styles.codeBlock, children: _jsx("code", { children: cloudMcpServerConfig
931
863
  ? renderMcpConfigBlock(cloudMcpServerId, cloudMcpServerConfig, cloudClientIntegration.rootKey || 'mcpServers', cloudClientIntegration.wrapperKeys || [])
932
864
  : renderTextConfigBlock(cloudClientIntegration.renderedText || '') }) })] }), _jsxs("div", { className: `${styles.codeBlockWrapper} ${styles.marginTop12}`, children: [_jsx("div", { className: styles.codeHeader, children: _jsx("span", { className: styles.codeLabel, children: cloudClientIntegration.instructionLabel }) }), _jsx("div", { style: { padding: '16px 20px' }, children: _jsx("div", { className: styles.settingDescription, children: cloudClientIntegration.instructionText }) })] })] }), _jsxs("div", { className: `${styles.settingGroup} ${styles.marginTop16}`, children: [_jsxs("div", { className: styles.toggleHeading, onClick: () => setShowMcpConnectionTest((prev) => !prev), onKeyDown: (e) => {
933
865
  if (e.key === 'Enter' || e.key === ' ') {
@@ -5,6 +5,8 @@ import { describe, it, expect, vi } from 'vitest';
5
5
  import { TaskSettings } from './TaskSettings';
6
6
  import { TASKFORCE_THEME_OPTIONS } from '../../utils/theme';
7
7
  describe('TaskSettings Component', () => {
8
+ // Launch decision: Cloud MCP settings must stay identity-neutral for shared connections.
9
+ // Do not reintroduce per-connection AI identity controls here unless explicitly requested.
8
10
  // Default mock props
9
11
  const defaultSettingsModel = {
10
12
  currentTheme: 'dark',
@@ -1128,76 +1130,6 @@ describe('TaskSettings Component', () => {
1128
1130
  expect(screen.getByText(/http_headers = \{ Authorization = "Bearer/i)).toBeInTheDocument();
1129
1131
  expect(screen.getAllByText(/https:\/\/mcp\.taskforcehq\.ai\/mcp/).length).toBeGreaterThan(0);
1130
1132
  }, 10000);
1131
- it('4.23e3a Codex MCP output includes optional automation identity headers', async () => {
1132
- const fetchCloudAuthApi = vi.fn(async () => ({
1133
- ok: true,
1134
- json: async () => ({ items: [] })
1135
- }));
1136
- const user = userEvent.setup();
1137
- renderComponent({
1138
- settingsModel: {
1139
- runtimeMode: 'local',
1140
- isAuthenticated: true,
1141
- currentWorkspaceName: 'Workspace Alpha',
1142
- cloudMcpBaseUrl: 'https://mcp.taskforcehq.ai',
1143
- fetchCloudAuthApi
1144
- }
1145
- });
1146
- await user.click(screen.getByRole('button', { name: /MCP/i }));
1147
- await user.selectOptions(screen.getByRole('combobox', { name: 'MCP client' }), 'codex');
1148
- await user.type(screen.getByPlaceholderText('Automation Codex'), 'Automation Codex');
1149
- await user.type(screen.getByPlaceholderText('tfp_...'), 'tfp_automation_codex');
1150
- expect(await screen.findByText(/X-Taskforce-AI-Name = "Automation Codex"/i)).toBeInTheDocument();
1151
- expect(screen.getByText(/X-Taskforce-AI-Profile-Token = "tfp_automation_codex"/i)).toBeInTheDocument();
1152
- }, 10000);
1153
- it('4.23e3aa MCP tab can prefill automation identity from an existing AI profile', async () => {
1154
- const fetchCloudAuthApi = vi.fn(async () => ({
1155
- ok: true,
1156
- json: async () => ({ items: [] })
1157
- }));
1158
- const fetchMock = vi.fn(async (input) => {
1159
- const url = String(input);
1160
- if (url.includes('/api/taskforce/workspace/assignee-options')) {
1161
- return {
1162
- ok: true,
1163
- json: async () => ({
1164
- assignees: [
1165
- {
1166
- value: 'ai-profile-123',
1167
- label: 'Automation Codex',
1168
- profileToken: 'tfp_automation_codex',
1169
- username: 'automation-codex',
1170
- icon: 'Bot',
1171
- color: '#6B7280',
1172
- kind: 'agent',
1173
- }
1174
- ]
1175
- })
1176
- };
1177
- }
1178
- throw new Error(`Unexpected fetch call: ${url}`);
1179
- });
1180
- global.fetch = fetchMock;
1181
- const user = userEvent.setup();
1182
- renderComponent({
1183
- settingsModel: {
1184
- runtimeMode: 'local',
1185
- isAuthenticated: true,
1186
- workspaceId: 'workspace-alpha',
1187
- currentWorkspaceName: 'Workspace Alpha',
1188
- cloudMcpBaseUrl: 'https://mcp.taskforcehq.ai',
1189
- fetchCloudAuthApi
1190
- }
1191
- });
1192
- await user.click(screen.getByRole('button', { name: /MCP/i }));
1193
- await user.selectOptions(await screen.findByRole('combobox', { name: 'Use existing AI profile' }), 'ai-profile-123');
1194
- await user.selectOptions(screen.getByRole('combobox', { name: 'MCP client' }), 'codex');
1195
- expect(await screen.findByDisplayValue('Automation Codex')).toBeInTheDocument();
1196
- expect(screen.getByDisplayValue('tfp_automation_codex')).toBeInTheDocument();
1197
- expect(screen.getByText(/X-Taskforce-AI-Name = "Automation Codex"/i)).toBeInTheDocument();
1198
- expect(screen.getByText(/X-Taskforce-AI-Profile-Token = "tfp_automation_codex"/i)).toBeInTheDocument();
1199
- global.fetch = originalFetch;
1200
- }, 10000);
1201
1133
  it('4.23e3b Claude Code shows a remote MCP add command', async () => {
1202
1134
  const fetchCloudAuthApi = vi.fn(async () => ({
1203
1135
  ok: true,
@@ -1396,6 +1328,37 @@ describe('TaskSettings Component', () => {
1396
1328
  await user.click(screen.getByTitle('Copy full configuration file'));
1397
1329
  expect(screen.getByText('Generate a token before copying the Cloud MCP configuration.')).toBeInTheDocument();
1398
1330
  }, 10000);
1331
+ it('4.23g2 Cloud MCP settings do not surface automation identity controls', async () => {
1332
+ const user = userEvent.setup();
1333
+ renderComponent({
1334
+ settingsModel: {
1335
+ runtimeMode: 'local',
1336
+ isAuthenticated: true,
1337
+ currentWorkspaceName: 'Workspace Alpha',
1338
+ cloudMcpBaseUrl: 'https://mcp.taskforcehq.ai',
1339
+ }
1340
+ });
1341
+ await user.click(screen.getByRole('button', { name: /MCP/i }));
1342
+ await screen.findByText('Cloud MCP Configuration');
1343
+ expect(screen.queryByText('Automation Identity (Optional)')).not.toBeInTheDocument();
1344
+ expect(screen.queryByRole('combobox', { name: 'Use existing AI profile' })).not.toBeInTheDocument();
1345
+ }, 10000);
1346
+ it('4.23g3 Cloud MCP config does not include AI identity headers by default', async () => {
1347
+ const user = userEvent.setup();
1348
+ renderComponent({
1349
+ settingsModel: {
1350
+ runtimeMode: 'local',
1351
+ isAuthenticated: true,
1352
+ currentWorkspaceName: 'Workspace Alpha',
1353
+ cloudMcpBaseUrl: 'https://mcp.taskforcehq.ai',
1354
+ }
1355
+ });
1356
+ await user.click(screen.getByRole('button', { name: /MCP/i }));
1357
+ await screen.findByText('Cloud MCP Configuration');
1358
+ await user.selectOptions(screen.getByRole('combobox', { name: 'MCP client' }), 'codex');
1359
+ expect(screen.queryByText(/X-Taskforce-AI-Name/i)).not.toBeInTheDocument();
1360
+ expect(screen.queryByText(/X-Taskforce-AI-Profile-Token/i)).not.toBeInTheDocument();
1361
+ }, 10000);
1399
1362
  it('4.23h Active tokens sort first and revoked tokens do not show revoke or regenerate actions', async () => {
1400
1363
  const user = userEvent.setup();
1401
1364
  const fetchCloudAuthApi = vi.fn(async () => ({
@@ -65,6 +65,12 @@ function cloneCustomTaxonomyDefinition(taxonomy) {
65
65
  : []
66
66
  };
67
67
  }
68
+ function handleCardKeyDown(event, onSelect) {
69
+ if (event.key !== 'Enter' && event.key !== ' ')
70
+ return;
71
+ event.preventDefault();
72
+ onSelect();
73
+ }
68
74
  function SystemTaxonomyLibrarySurface(props) {
69
75
  const { onAnalyzeSystemTaxonomyPack, onApplySystemTaxonomyPack } = props;
70
76
  const fallbackPacks = useMemo(() => listBuiltInTaxonomyLibraryPacks(), []);
@@ -158,7 +164,7 @@ function SystemTaxonomyLibrarySurface(props) {
158
164
  }
159
165
  return (_jsxs("div", { className: libraryStyles.libraryContainer, children: [_jsxs("section", { className: libraryStyles.introCard, children: [_jsxs("span", { className: libraryStyles.eyebrow, children: [_jsx(BookMarked, { size: 14 }), "System Taxonomy Library"] }), _jsx("h3", { className: libraryStyles.introTitle, children: "Browse starter packs before you replace anything." }), _jsx("p", { className: libraryStyles.introCopy, children: "Library packs give you coordinated categories, task types, and priority scales. Apply replaces only the sections you choose." }), libraryLoading ? _jsx("p", { className: libraryStyles.prototypeNotice, children: "Loading available packs\u2026" }) : null] }), _jsxs("div", { className: libraryStyles.libraryGrid, children: [_jsx("div", { className: libraryStyles.packList, children: packs.map((pack) => {
160
166
  const isActive = pack.id === selectedPackId;
161
- return (_jsxs("article", { className: `${libraryStyles.packCard} ${isActive ? libraryStyles.packCardActive : ''}`, children: [_jsx("div", { className: libraryStyles.packHeader, children: _jsxs("div", { className: libraryStyles.packHeaderText, children: [_jsx("div", { className: libraryStyles.packTitleRow, children: _jsx("h4", { className: libraryStyles.packTitle, children: pack.label }) }), _jsx("p", { className: libraryStyles.packDescription, children: pack.description })] }) }), _jsxs("div", { className: libraryStyles.packCounts, children: [_jsxs("span", { className: libraryStyles.packCountChip, children: [_jsx(Layers, { size: 13 }), pack.categories.length, " categories"] }), _jsxs("span", { className: libraryStyles.packCountChip, children: [_jsx(PackageOpen, { size: 13 }), pack.types.length, " types"] }), _jsxs("span", { className: libraryStyles.packCountChip, children: [_jsx(Check, { size: 13 }), pack.priorities.length, " priority levels"] })] }), _jsxs("div", { className: libraryStyles.packActions, children: [_jsx("button", { type: "button", className: styles.cancelBtn, onClick: () => handleSelectPack(pack.id), children: isActive ? 'Previewing' : 'Preview' }), _jsx("button", { type: "button", className: styles.submitBtn, onClick: () => handleSelectPack(pack.id), children: "Apply" })] })] }, pack.id));
167
+ return (_jsxs("article", { className: `${libraryStyles.packCard} ${libraryStyles.packCardInteractive} ${isActive ? libraryStyles.packCardActive : ''}`, role: "button", tabIndex: 0, "aria-pressed": isActive, onClick: () => handleSelectPack(pack.id), onKeyDown: (event) => handleCardKeyDown(event, () => handleSelectPack(pack.id)), children: [_jsx("div", { className: libraryStyles.packHeader, children: _jsxs("div", { className: libraryStyles.packHeaderText, children: [_jsx("div", { className: libraryStyles.packTitleRow, children: _jsx("h4", { className: libraryStyles.packTitle, children: pack.label }) }), _jsx("p", { className: libraryStyles.packDescription, children: pack.description })] }) }), _jsxs("div", { className: libraryStyles.packCounts, children: [_jsxs("span", { className: libraryStyles.packCountChip, children: [_jsx(Layers, { size: 13 }), pack.categories.length, " categories"] }), _jsxs("span", { className: libraryStyles.packCountChip, children: [_jsx(PackageOpen, { size: 13 }), pack.types.length, " types"] }), _jsxs("span", { className: libraryStyles.packCountChip, children: [_jsx(Check, { size: 13 }), pack.priorities.length, " priority levels"] })] }), _jsx("div", { className: libraryStyles.packActions, children: _jsx("button", { type: "button", className: styles.submitBtn, onClick: () => handleSelectPack(pack.id), children: "Apply" }) })] }, pack.id));
162
168
  }) }), _jsxs("aside", { className: libraryStyles.previewPanel, children: [_jsxs("div", { className: libraryStyles.previewHeader, children: [_jsx("h3", { className: libraryStyles.previewTitle, children: selectedPack.label }), _jsx("p", { className: libraryStyles.previewSubtext, children: "Read-only preview. Applying this pack will replace only the sections you choose." })] }), _jsx(PreviewListSection, { title: "Categories", items: selectedPack.categories.map((category) => ({
163
169
  key: category.value,
164
170
  label: category.label,
@@ -283,7 +289,7 @@ function CustomTaxonomyLibrarySurface(props) {
283
289
  const isActive = pack.id === selectedPackId;
284
290
  const summary = summarizeCustomPack(pack);
285
291
  const taxonomyNames = pack.taxonomies.slice(0, 3).map((taxonomy) => taxonomy.label);
286
- return (_jsxs("article", { className: `${libraryStyles.packCard} ${isActive ? libraryStyles.packCardActive : ''}`, children: [_jsx("div", { className: libraryStyles.packHeader, children: _jsxs("div", { className: libraryStyles.packHeaderText, children: [_jsxs("div", { className: libraryStyles.packTitleRow, children: [_jsx("h4", { className: libraryStyles.packTitle, children: pack.label }), pack.domainTag ? _jsx("span", { className: libraryStyles.packBadge, children: pack.domainTag }) : null] }), _jsx("p", { className: libraryStyles.packDescription, children: pack.description }), pack.bestFor ? (_jsxs("p", { className: libraryStyles.packBestFor, children: [_jsx("strong", { children: "Best for:" }), " ", pack.bestFor] })) : null] }) }), _jsxs("div", { className: libraryStyles.packCounts, children: [_jsxs("span", { className: libraryStyles.packCountChip, children: [_jsx(Shapes, { size: 13 }), pack.taxonomies.length, " taxonomies"] }), summary.multiSelectCount > 0 ? (_jsx("span", { className: libraryStyles.packCountChip, children: "Multi-select" })) : null, summary.numericCount > 0 ? (_jsx("span", { className: libraryStyles.packCountChip, children: "Numeric" })) : null, summary.requiredCount > 0 ? (_jsx("span", { className: libraryStyles.packCountChip, children: "Required" })) : null] }), _jsxs("div", { className: libraryStyles.packTaxonomyPreview, children: [taxonomyNames.map((name) => (_jsx("span", { className: libraryStyles.packTaxonomyChip, children: name }, name))), pack.taxonomies.length > taxonomyNames.length ? (_jsxs("span", { className: libraryStyles.packTaxonomyMore, children: ["+", pack.taxonomies.length - taxonomyNames.length, " more"] })) : null] }), _jsx("div", { className: libraryStyles.packActions, children: _jsx("button", { type: "button", className: styles.cancelBtn, onClick: () => setSelectedPackId(pack.id), children: isActive ? 'Previewing' : 'Preview' }) })] }, pack.id));
292
+ return (_jsxs("article", { className: `${libraryStyles.packCard} ${libraryStyles.packCardInteractive} ${isActive ? libraryStyles.packCardActive : ''}`, role: "button", tabIndex: 0, "aria-pressed": isActive, onClick: () => setSelectedPackId(pack.id), onKeyDown: (event) => handleCardKeyDown(event, () => setSelectedPackId(pack.id)), children: [_jsx("div", { className: libraryStyles.packHeader, children: _jsxs("div", { className: libraryStyles.packHeaderText, children: [_jsxs("div", { className: libraryStyles.packTitleRow, children: [_jsx("h4", { className: libraryStyles.packTitle, children: pack.label }), pack.domainTag ? _jsx("span", { className: libraryStyles.packBadge, children: pack.domainTag }) : null] }), _jsx("p", { className: libraryStyles.packDescription, children: pack.description }), pack.bestFor ? (_jsxs("p", { className: libraryStyles.packBestFor, children: [_jsx("strong", { children: "Best for:" }), " ", pack.bestFor] })) : null] }) }), _jsxs("div", { className: libraryStyles.packCounts, children: [_jsxs("span", { className: libraryStyles.packCountChip, children: [_jsx(Shapes, { size: 13 }), pack.taxonomies.length, " taxonomies"] }), summary.multiSelectCount > 0 ? (_jsx("span", { className: libraryStyles.packCountChip, children: "Multi-select" })) : null, summary.numericCount > 0 ? (_jsx("span", { className: libraryStyles.packCountChip, children: "Numeric" })) : null, summary.requiredCount > 0 ? (_jsx("span", { className: libraryStyles.packCountChip, children: "Required" })) : null] }), _jsxs("div", { className: libraryStyles.packTaxonomyPreview, children: [taxonomyNames.map((name) => (_jsx("span", { className: libraryStyles.packTaxonomyChip, children: name }, name))), pack.taxonomies.length > taxonomyNames.length ? (_jsxs("span", { className: libraryStyles.packTaxonomyMore, children: ["+", pack.taxonomies.length - taxonomyNames.length, " more"] })) : null] }), _jsx("div", { className: libraryStyles.packActions, children: _jsx("span", { className: libraryStyles.packActionHint, children: isActive ? 'Previewing' : 'Click to Preview' }) })] }, pack.id));
287
293
  }) }), _jsxs("aside", { className: libraryStyles.previewPanel, children: [_jsxs("div", { className: libraryStyles.previewHeader, children: [_jsx("h3", { className: libraryStyles.previewTitle, children: selectedPack.label }), selectedPack.bestFor ? (_jsxs("p", { className: libraryStyles.previewBestFor, children: [_jsx("strong", { children: "Best for:" }), " ", selectedPack.bestFor] })) : null, _jsx("p", { className: libraryStyles.previewSubtext, children: "Review each taxonomy before installing it into this workspace. Installed taxonomies become normal workspace-owned custom taxonomies." })] }), _jsxs("div", { className: libraryStyles.previewSummaryRow, children: [_jsxs("span", { className: libraryStyles.packCountChip, children: [selectedPack.taxonomies.length, " taxonomies"] }), (selectedPackSummary?.formEnabledCount || 0) > 0 ? (_jsxs("span", { className: libraryStyles.packCountChip, children: [selectedPackSummary.formEnabledCount, " show in forms"] })) : null, (selectedPackSummary?.numericCount || 0) > 0 ? (_jsxs("span", { className: libraryStyles.packCountChip, children: [selectedPackSummary.numericCount, " numeric"] })) : null, (selectedPackSummary?.requiredCount || 0) > 0 ? (_jsxs("span", { className: libraryStyles.packCountChip, children: [selectedPackSummary.requiredCount, " required"] })) : null] }), _jsxs("section", { className: libraryStyles.applyPanel, children: [_jsx("h4", { className: libraryStyles.applyTitle, children: "Choose Taxonomies to Install" }), _jsx("p", { className: libraryStyles.applyCopy, children: "Select only the taxonomies you want to add. This first pass installs new custom taxonomies only and asks you to skip conflicts rather than replace them." }), _jsx("div", { className: libraryStyles.checkboxList, children: selectedPack.taxonomies.map((taxonomy) => (_jsxs("label", { className: libraryStyles.checkboxLabel, children: [_jsx("input", { type: "checkbox", checked: selectedTaxonomyIds.includes(taxonomy.id), onChange: () => handleToggleTaxonomySelection(taxonomy.id) }), _jsxs("span", { children: [taxonomy.label, taxonomies.some((existingTaxonomy) => existingTaxonomy.id === taxonomy.id) ? ' (Already installed)' : ''] })] }, taxonomy.id))) }), _jsxs("div", { className: libraryStyles.analysisPanel, children: [_jsxs("p", { className: libraryStyles.analysisCopy, children: [selectedTaxonomyIds.length, " ", selectedTaxonomyIds.length === 1 ? 'taxonomy is' : 'taxonomies are', " currently selected from this pack."] }), installableTaxonomies.length > 0 ? (_jsxs("p", { className: libraryStyles.analysisCopy, children: [installableTaxonomies.length, " selected ", installableTaxonomies.length === 1 ? 'taxonomy is' : 'taxonomies are', " ready to install right now."] })) : (_jsx("p", { className: libraryStyles.analysisCopy, children: "No selected taxonomies are currently installable." })), conflictIds.length > 0 ? (_jsxs("p", { className: libraryStyles.analysisWarning, children: [conflictIds.length, " selected ", conflictIds.length === 1 ? 'taxonomy conflicts' : 'taxonomies conflict', " with existing workspace ids: ", conflictIds.join(', '), "."] })) : null] }), _jsxs("div", { className: libraryStyles.packActions, children: [conflictIds.length > 0 ? (_jsx("button", { type: "button", className: styles.cancelBtn, onClick: handleSelectInstallableOnly, children: "Select Installable Only" })) : null, _jsx("button", { type: "button", className: styles.submitBtn, onClick: handleApply, disabled: selectedTaxonomyIds.length === 0 || installableTaxonomies.length === 0, children: "Install Selected Taxonomies" })] }), libraryNotice ? (_jsx("p", { className: `${libraryStyles.prototypeResult} ${libraryNotice.type === 'error' ? libraryStyles.prototypeError : ''}`, children: libraryNotice.message })) : null] }), selectedPack.taxonomies.map((taxonomy) => (_jsxs("section", { className: libraryStyles.customTaxonomyCard, children: [_jsx("div", { className: libraryStyles.customTaxonomyHeader, children: _jsxs("div", { children: [_jsx("h4", { className: libraryStyles.previewSectionTitle, children: taxonomy.label }), taxonomy.description ? _jsx("p", { className: libraryStyles.customTaxonomyDescription, children: taxonomy.description }) : null] }) }), conflictIds.includes(taxonomy.id) ? (_jsx("p", { className: libraryStyles.analysisWarning, children: "Conflicts with an existing taxonomy id in this workspace." })) : null, _jsx("div", { className: libraryStyles.customTaxonomyBadges, children: taxonomyTraitBadges(taxonomy).map((badge) => (_jsx("span", { className: libraryStyles.customTaxonomyBadge, children: badge }, badge))) }), _jsx(PreviewListSection, { title: taxonomy.widgetType === 'level' ? 'Levels' : 'Options', items: taxonomy.options.map((option) => ({
288
294
  key: `${taxonomy.id}:${option.value}`,
289
295
  label: option.label,
@@ -70,7 +70,7 @@ describe('TaxonomyLibraryManager', () => {
70
70
  });
71
71
  expect(screen.getAllByText('Client Delivery')).toHaveLength(2);
72
72
  expect(screen.getAllByText('2 taxonomies').length).toBeGreaterThan(0);
73
- await user.click(screen.getByRole('button', { name: /previewing/i }));
73
+ await user.click(screen.getByRole('button', { name: /client delivery/i }));
74
74
  expect(screen.getAllByText('Approval Stage').length).toBeGreaterThan(0);
75
75
  expect(screen.getByText('Track approvals')).toBeInTheDocument();
76
76
  expect(screen.getAllByText('Required').length).toBeGreaterThan(0);
@@ -4,7 +4,7 @@ import { Check, Loader2, RotateCcw, Plus, Trash2, X } from 'lucide-react';
4
4
  import styles from '../../Taskforce.module.css';
5
5
  import { t } from '../../localization';
6
6
  import { TaskStatusActions } from './TaskStatusActions';
7
- import { getTaskReferenceLabel } from '../../utils/taskReferences';
7
+ import { getTaskReferenceDisplayState } from '../../utils/taskReferences';
8
8
  import { TaskReferenceBadge } from './TaskReferenceBadge';
9
9
  import { formatInitiativeReference, getWorkstreamReferenceLabel } from '../../utils/planningReferences';
10
10
  export function TaskActionHeader({ editingTaskId, loading, autoSaveState = 'idle', title, handleSubmit, resetForm, handleCopyId, copiedId, tasks, currentTask: currentTaskProp, currentTaskWorkstream, currentTaskInitiative, workstreamInput = '', onWorkstreamInputChange, onSetWorkstreamForCurrentTask, handleToggleInProgress, handleToggleReview, handleToggleComplete, handleToggleCancel, handleSetStatus, handleArchiveTask, handleRestoreDeletedTask, handlePermanentlyDeleteDeletedTask }) {
@@ -12,7 +12,10 @@ export function TaskActionHeader({ editingTaskId, loading, autoSaveState = 'idle
12
12
  const currentTask = currentTaskProp ?? (editingTaskId ? tasks.find(t => t.id === editingTaskId) || null : null);
13
13
  const isArchivedReadOnly = Boolean(currentTask?.isArchived);
14
14
  const isDeletedReadOnly = Boolean(currentTask?.isDeleted);
15
- const currentTaskReferenceLabel = getTaskReferenceLabel(currentTask) || editingTaskId || '';
15
+ const currentTaskReference = getTaskReferenceDisplayState(currentTask);
16
+ const currentTaskReferenceLabel = currentTaskReference.label || (currentTaskReference.isProvisional ? 'Pending' : '');
17
+ const currentTaskReferenceIsProvisional = currentTaskReference.isProvisional;
18
+ const canCopyCurrentTaskReference = Boolean(currentTaskReference.label);
16
19
  const currentWorkstreamReferenceLabel = currentTaskWorkstream
17
20
  ? (getWorkstreamReferenceLabel(currentTaskWorkstream) || currentTaskWorkstream.id)
18
21
  : '';
@@ -32,7 +35,7 @@ export function TaskActionHeader({ editingTaskId, loading, autoSaveState = 'idle
32
35
  return (_jsx("div", { className: styles.stickyActionHeader, children: editingTaskId ? (_jsxs(_Fragment, { children: [_jsxs("div", { className: styles.taskHierarchyHeader, children: [_jsxs("div", { className: styles.taskHierarchyBadgeRow, children: [currentTaskInitiative ? (_jsxs(_Fragment, { children: [_jsx(TaskReferenceBadge, { copied: copiedId === currentInitiativeReferenceLabel, onClick: (e) => handleCopyId(e, currentInitiativeReferenceLabel), disabled: loading, title: "Copy initiative reference", label: currentInitiativeReferenceLabel }), _jsx("span", { className: styles.taskHierarchyDivider, children: "/" })] })) : null, currentTaskWorkstream ? (_jsxs(_Fragment, { children: [_jsx(TaskReferenceBadge, { copied: copiedId === currentWorkstreamReferenceLabel, onClick: (e) => handleCopyId(e, currentWorkstreamReferenceLabel), disabled: loading, title: "Copy workstream reference", label: currentWorkstreamReferenceLabel }), _jsx("span", { className: styles.taskHierarchyDivider, children: "/" })] })) : (_jsxs(_Fragment, { children: [_jsx("button", { type: "button", className: `${styles.taskIdBadge} ${styles.taskHierarchyAddBtn}`, onClick: () => {
33
36
  onWorkstreamInputChange?.('');
34
37
  setShowWorkstreamEditor((value) => !value);
35
- }, disabled: loading || isArchivedReadOnly || isDeletedReadOnly, title: "Attach to workstream", "aria-label": "Attach to workstream", children: _jsx(Plus, { size: 13 }) }), _jsx("span", { className: styles.taskHierarchyDivider, children: "/" })] })), _jsx(TaskReferenceBadge, { copied: copiedId === currentTaskReferenceLabel, onClick: (e) => handleCopyId(e, currentTaskReferenceLabel), disabled: loading, title: t('actionHeader.copyTaskIdTitle'), label: currentTaskReferenceLabel })] }), showWorkstreamEditor && !currentTaskWorkstream ? (_jsxs("div", { className: styles.taskHierarchyEditor, children: [_jsx("input", { type: "text", className: `${styles.input} ${styles.taskHierarchyInput}`, value: workstreamInput, onChange: (event) => onWorkstreamInputChange?.(event.target.value), placeholder: "WS-123", "aria-label": "Attach workstream reference", onKeyDown: (event) => {
38
+ }, disabled: loading || isArchivedReadOnly || isDeletedReadOnly, title: "Attach to workstream", "aria-label": "Attach to workstream", children: _jsx(Plus, { size: 13 }) }), _jsx("span", { className: styles.taskHierarchyDivider, children: "/" })] })), currentTaskReferenceLabel ? (_jsx(TaskReferenceBadge, { copied: canCopyCurrentTaskReference && copiedId === currentTaskReference.label, onClick: (e) => handleCopyId(e, currentTaskReference.label), disabled: loading || !canCopyCurrentTaskReference, title: canCopyCurrentTaskReference ? t('actionHeader.copyTaskIdTitle') : 'Task reference pending sync', label: currentTaskReferenceLabel })) : null, currentTaskReferenceIsProvisional ? (_jsx("span", { className: styles.taskHierarchyMeta, title: "Temporary local reference until cloud sync assigns the final task number.", children: "Pending sync" })) : null] }), showWorkstreamEditor && !currentTaskWorkstream ? (_jsxs("div", { className: styles.taskHierarchyEditor, children: [_jsx("input", { type: "text", className: `${styles.input} ${styles.taskHierarchyInput}`, value: workstreamInput, onChange: (event) => onWorkstreamInputChange?.(event.target.value), placeholder: "WS-123", "aria-label": "Attach workstream reference", onKeyDown: (event) => {
36
39
  if (event.key === 'Enter') {
37
40
  event.preventDefault();
38
41
  handleWorkstreamConfirm();
@@ -13,14 +13,17 @@ import { dispatchOpenMarkdownDocument } from '../../utils/contextFiles';
13
13
  import { dispatchOpenAnnotatedAttachment } from '../../utils/annotatedAttachments';
14
14
  import { getDocumentDownloadName } from '../../utils/documentNames';
15
15
  import { TaskStatusActions } from './TaskStatusActions';
16
- import { getTaskReferenceLabel } from '../../utils/taskReferences';
16
+ import { getTaskReferenceDisplayState } from '../../utils/taskReferences';
17
17
  import { getAssigneeKind, getAssigneeLabel } from '../../utils/assignees';
18
18
  import { getLatestTaskActivity, summarizeTaskActivity } from '../../utils/taskActivity';
19
19
  import { DEFAULT_TYPE_VALUE } from '../../shared/taxonomyDefaults.js';
20
20
  import { getVisibleTaxonomiesForTasks } from '../../utils/taxonomyLifecycle.js';
21
21
  import { getInitiativeReferenceLabel, getWorkstreamReferenceLabel } from '../../utils/planningReferences';
22
22
  const TaskCardInner = ({ task, searchQuery = '', copiedId = null, taxonomies = [], types = [], priorities = [], categories = [], assigneeOptions = [], taskWorkstream = null, taskInitiative = null, isOverlay = false, isArchived = false, readOnlyMode = null, isRecentlyChanged = false, onClick, onCopyId, onToggleInProgress, onToggleReview, onToggleComplete, onToggleCancel, onSetStatus, onArchiveTask, onUnarchive, onDelete, deleteActionTitle, onOpenTaskById, compressed = false, showStatusLabel = true }) => {
23
- const taskReferenceLabel = getTaskReferenceLabel(task) || task.id;
23
+ const taskReference = getTaskReferenceDisplayState(task);
24
+ const taskReferenceLabel = taskReference.label || (taskReference.isProvisional ? 'Pending' : '');
25
+ const isProvisionalReference = taskReference.isProvisional;
26
+ const canCopyTaskReference = Boolean(taskReference.label);
24
27
  const workstreamReferenceLabel = getWorkstreamReferenceLabel(taskWorkstream) || taskWorkstream?.id || '';
25
28
  const initiativeReferenceLabel = getInitiativeReferenceLabel(taskInitiative) || taskInitiative?.id || '';
26
29
  const priorityConfig = priorities.find(p => String(p.value) === String(task.priority));
@@ -160,7 +163,7 @@ const TaskCardInner = ({ task, searchQuery = '', copiedId = null, taxonomies = [
160
163
  event.stopPropagation();
161
164
  onCopyId?.(event, label);
162
165
  };
163
- return (_jsxs("div", { className: cardClasses, onClick: () => onClick?.(task), style: cardStyle, "data-task-card": "true", children: [_jsxs("div", { className: cardStyles.taskHeader, children: [_jsxs("div", { className: cardStyles.taskReferenceCluster, children: [taskInitiative ? (_jsxs(_Fragment, { children: [_jsx("span", { className: cardStyles.taskReferenceText, children: initiativeReferenceLabel }), _jsx("span", { className: cardStyles.taskReferenceDivider, children: "/" })] })) : null, taskWorkstream ? (_jsxs(_Fragment, { children: [_jsx("span", { className: cardStyles.taskReferenceText, children: workstreamReferenceLabel }), _jsx("span", { className: cardStyles.taskReferenceDivider, children: "/" })] })) : null, _jsx(TaskReferenceBadge, { copied: copiedId === taskReferenceLabel, onClick: (e) => handleReferenceCopy(e, taskReferenceLabel), title: "Copy task reference", className: isArchived ? cardStyles.archiveBadge : '', label: renderHighlighted(taskReferenceLabel, searchQuery) })] }), _jsx("div", { className: cardStyles.taskActions, onClick: (e) => e.stopPropagation(), children: !isReadOnly ? (_jsx(TaskStatusActions, { task: task, compressed: compressed, shortLabels: true, showLabel: showStatusLabel, onSetStatus: (nextTask, status) => {
166
+ return (_jsxs("div", { className: cardClasses, onClick: () => onClick?.(task), style: cardStyle, "data-task-card": "true", children: [_jsxs("div", { className: cardStyles.taskHeader, children: [_jsxs("div", { className: cardStyles.taskReferenceCluster, children: [taskInitiative ? (_jsxs(_Fragment, { children: [_jsx("span", { className: cardStyles.taskReferenceText, children: initiativeReferenceLabel }), _jsx("span", { className: cardStyles.taskReferenceDivider, children: "/" })] })) : null, taskWorkstream ? (_jsxs(_Fragment, { children: [_jsx("span", { className: cardStyles.taskReferenceText, children: workstreamReferenceLabel }), _jsx("span", { className: cardStyles.taskReferenceDivider, children: "/" })] })) : null, taskReferenceLabel ? (_jsx(TaskReferenceBadge, { copied: canCopyTaskReference && copiedId === taskReference.label, onClick: (e) => handleReferenceCopy(e, taskReference.label), disabled: !canCopyTaskReference, title: canCopyTaskReference ? 'Copy task reference' : 'Task reference pending sync', className: isArchived ? cardStyles.archiveBadge : '', label: renderHighlighted(taskReferenceLabel, searchQuery) })) : null, isProvisionalReference ? (_jsx("span", { className: styles.taskHierarchyMeta, title: "Temporary local reference until cloud sync assigns the final task number.", children: "Pending sync" })) : null] }), _jsx("div", { className: cardStyles.taskActions, onClick: (e) => e.stopPropagation(), children: !isReadOnly ? (_jsx(TaskStatusActions, { task: task, compressed: compressed, shortLabels: true, showLabel: showStatusLabel, onSetStatus: (nextTask, status) => {
164
167
  if (onSetStatus) {
165
168
  onSetStatus(nextTask, status);
166
169
  return;
@@ -349,7 +349,7 @@ export function TaskForm(props) {
349
349
  : entry));
350
350
  }, onRemove: () => onChecklistItemsChange(checklistItems
351
351
  .filter((_, entryIndex) => entryIndex !== index)
352
- .map((entry, entryIndex) => ({ ...entry, order: entryIndex }))) }, item.id || `checklist-${index}`))) }) })) : (_jsx("div", { className: `${formStyles.settingsHint} tf-text-helper`, children: "No checklist items yet." }))] }))] }))] }), _jsxs("div", { children: [editingTaskId && (_jsxs("div", { className: styles.specialistSection, children: [_jsxs("div", { className: styles.toggleHeading, onClick: () => onShowCommentsChange(!showComments), title: showComments ? t('taskForm.hideComments') : t('taskForm.showComments'), children: [_jsxs("label", { className: styles.label, children: [t('taskForm.commentsSectionTitle'), " ", activityCount > 0 && `(${activityCount})`] }), showComments ? _jsx(ChevronDown, { size: 14 }) : _jsx(ChevronRight, { size: 14 })] }), showComments && (_jsxs("div", { className: formStyles.commentPanel, children: [_jsxs("div", { className: formStyles.commentThread, children: [activityCount === 0 && (_jsx("div", { className: formStyles.emptyComments, children: t('taskForm.noComments') })), taskActivityItems.map((item) => {
352
+ .map((entry, entryIndex) => ({ ...entry, order: entryIndex }))) }, 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 }), editingTaskId && (_jsxs("div", { className: styles.specialistSection, children: [_jsxs("div", { className: styles.toggleHeading, onClick: () => onShowCommentsChange(!showComments), title: showComments ? t('taskForm.hideComments') : t('taskForm.showComments'), children: [_jsxs("label", { className: styles.label, children: [t('taskForm.commentsSectionTitle'), " ", activityCount > 0 && `(${activityCount})`] }), showComments ? _jsx(ChevronDown, { size: 14 }) : _jsx(ChevronRight, { size: 14 })] }), showComments && (_jsxs("div", { className: formStyles.commentPanel, children: [_jsxs("div", { className: formStyles.commentThread, children: [activityCount === 0 && (_jsx("div", { className: formStyles.emptyComments, children: t('taskForm.noComments') })), taskActivityItems.map((item) => {
353
353
  if (item.type === 'comment') {
354
354
  const c = item.comment;
355
355
  const authorRef = String(c.author || '').trim();
@@ -416,5 +416,5 @@ export function TaskForm(props) {
416
416
  e.preventDefault();
417
417
  onAddComment();
418
418
  }
419
- }, rows: 1 }), _jsx("button", { type: "button", className: formStyles.sendCommentBtn, onClick: onAddComment, disabled: !newCommentText.trim(), children: _jsx(Send, { size: 16 }) })] })] }))] })), _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 ? `by ${createdByLabel}` : 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 ? `by ${updatedByLabel}` : t('taskForm.emptyValue') })] })] }), _jsxs("div", { className: formStyles.taskFormDateRow, children: [_jsx("span", { className: formStyles.taskFormDateLabel, children: t('taskForm.completedLabel') }), _jsxs("span", { className: formStyles.taskFormMetaStack, children: [_jsx("span", { className: formStyles.taskFormDateValue, children: completedLabel || t('taskForm.emptyValue') }), _jsx("span", { className: formStyles.taskFormMetaActor, children: completedByLabel ? `by ${completedByLabel}` : t('taskForm.emptyValue') })] })] })] })) }) })] }), _jsxs("div", { className: formStyles.keyboardHint, children: ["Press ", _jsx("kbd", { children: navigator.platform.includes('Mac') ? '⌥' : 'Alt' }), " to toggle"] })] })] }));
419
+ }, rows: 1 }), _jsx("button", { type: "button", className: formStyles.sendCommentBtn, onClick: onAddComment, disabled: !newCommentText.trim(), children: _jsx(Send, { size: 16 }) })] })] }))] })), _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 ? `by ${createdByLabel}` : 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 ? `by ${updatedByLabel}` : t('taskForm.emptyValue') })] })] }), _jsxs("div", { className: formStyles.taskFormDateRow, children: [_jsx("span", { className: formStyles.taskFormDateLabel, children: t('taskForm.completedLabel') }), _jsxs("span", { className: formStyles.taskFormMetaStack, children: [_jsx("span", { className: formStyles.taskFormDateValue, children: completedLabel || t('taskForm.emptyValue') }), _jsx("span", { className: formStyles.taskFormMetaActor, children: completedByLabel ? `by ${completedByLabel}` : t('taskForm.emptyValue') })] })] })] })) }) })] }), _jsxs("div", { className: formStyles.keyboardHint, children: ["Press ", _jsx("kbd", { children: navigator.platform.includes('Mac') ? '⌥' : 'Alt' }), " to toggle"] })] })] }));
420
420
  }
@@ -397,7 +397,7 @@ describe('TaskForm Component', () => {
397
397
  ],
398
398
  },
399
399
  });
400
- expect(screen.getByText('assignee: Unassigned -> Planner Bot')).toBeInTheDocument();
400
+ expect(screen.getByText('ASSIGNED TO: Unassigned -> Planner Bot')).toBeInTheDocument();
401
401
  expect(screen.queryByText(/ai-profile-1/i)).not.toBeInTheDocument();
402
402
  });
403
403
  it('3.13 Update title', async () => {
@@ -601,11 +601,50 @@ describe('TaskForm Component', () => {
601
601
  ]
602
602
  },
603
603
  });
604
- expect(screen.getByText('Description added')).toBeInTheDocument();
604
+ expect(screen.getByText('DESCRIPTION: added')).toBeInTheDocument();
605
605
  expect(screen.queryByText(/Line one/)).not.toBeInTheDocument();
606
606
  expect(screen.queryByText(/Line two/)).not.toBeInTheDocument();
607
607
  expect(screen.queryByText(/Line three/)).not.toBeInTheDocument();
608
608
  });
609
+ it('summarizes title audit events without rendering before and after title text', () => {
610
+ renderForm({
611
+ editingTaskId: '123',
612
+ showComments: true,
613
+ currentTask: {
614
+ id: 'task-activity-3',
615
+ title: 'Renamed task',
616
+ status: 'task',
617
+ createdAt: '2026-03-22T09:00:00.000Z',
618
+ activity: [
619
+ {
620
+ id: 'event:3',
621
+ type: 'event',
622
+ timestamp: '2026-03-22T10:00:00.000Z',
623
+ event: {
624
+ id: 'event-3',
625
+ taskId: 'task-activity-3',
626
+ workspaceId: 'default',
627
+ action: 'task-updated',
628
+ actor: 'user',
629
+ actorType: 'human',
630
+ createdAt: '2026-03-22T10:00:00.000Z',
631
+ details: {
632
+ changes: {
633
+ title: {
634
+ from: 'Old title',
635
+ to: 'New title'
636
+ }
637
+ }
638
+ }
639
+ }
640
+ }
641
+ ]
642
+ },
643
+ });
644
+ expect(screen.getByText('TITLE: updated')).toBeInTheDocument();
645
+ expect(screen.queryByText('Old title')).not.toBeInTheDocument();
646
+ expect(screen.queryByText('New title')).not.toBeInTheDocument();
647
+ });
609
648
  it('falls back to the current task actor label for AI comments when only the id is present', () => {
610
649
  renderForm({
611
650
  editingTaskId: '123',
@@ -2,5 +2,5 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  import { Check, Copy } from 'lucide-react';
3
3
  import styles from '../../Taskforce.module.css';
4
4
  export function ReferenceBadgeButton({ label, title, ariaLabel, copied = false, disabled = false, className = '', onClick, children, }) {
5
- return (_jsxs("button", { type: "button", className: `${styles.taskIdBadge} ${copied ? styles.copiedId : ''} ${className}`.trim(), onClick: onClick, disabled: disabled, title: title, "aria-label": ariaLabel, children: [copied ? _jsx(Check, { size: 13 }) : _jsx(Copy, { size: 13 }), _jsx("span", { children: children ?? label })] }));
5
+ return (_jsxs("button", { type: "button", className: `${styles.taskIdBadge} ${copied ? styles.copiedId : ''} ${className}`.trim(), onClick: onClick, disabled: disabled, title: title, "aria-label": ariaLabel, children: [copied ? _jsx(Check, { size: 13 }) : _jsx(Copy, { size: 13 }), _jsx("span", { className: styles.taskIdBadgeLabel, children: children ?? label })] }));
6
6
  }
@@ -6,6 +6,7 @@ import styles from './PlansPage.module.css';
6
6
  import { PlanComparisonPage } from './PlanComparisonPage';
7
7
  import { AppShellHeader } from './AppShellHeader';
8
8
  import { resolveTaskforceCloudEnvironmentFromBaseUrl } from '../../config/cloudEnvironment';
9
+ import { invalidateBillingStatusCache, loadBillingStatusCached } from '../../utils/billingStatusCache';
9
10
  const TRANSIENT_CHECKOUT_QUERY_CLEAR_DELAY_MS = 1600;
10
11
  const ACTIVE_ENTITLEMENT_STATES = new Set(['active', 'trialing', 'grace']);
11
12
  const GATE_MESSAGES = {
@@ -116,19 +117,15 @@ export function PlansPage({ isAuthenticated, runtimeMode, authSessionResolved, c
116
117
  }, TRANSIENT_CHECKOUT_QUERY_CLEAR_DELAY_MS);
117
118
  }, [clearTransientQueryState]);
118
119
  const loadBillingStatus = useCallback(async () => {
120
+ const billingStatusUrl = buildApiUrl('/api/taskforce/billing/status');
119
121
  if (!isAuthenticated) {
120
122
  setBillingStatus(null);
123
+ invalidateBillingStatusCache(billingStatusUrl);
121
124
  return;
122
125
  }
123
126
  try {
124
- const res = await fetch(buildApiUrl('/api/taskforce/billing/status'), {
125
- method: 'GET',
126
- credentials: 'include'
127
- });
128
- if (!res.ok)
129
- return;
130
- const payload = await res.json().catch(() => ({}));
131
- setBillingStatus(payload || null);
127
+ const payload = await loadBillingStatusCached(billingStatusUrl);
128
+ setBillingStatus(payload);
132
129
  }
133
130
  catch {
134
131
  // best effort only
@@ -156,16 +153,19 @@ export function PlansPage({ isAuthenticated, runtimeMode, authSessionResolved, c
156
153
  const checkoutState = String(payload?.checkoutState || '').trim().toLowerCase();
157
154
  if (checkoutState === 'free') {
158
155
  setFeedback({ type: 'success', message: 'Plan selected. Your access is ready.' });
156
+ invalidateBillingStatusCache(buildApiUrl('/api/taskforce/billing/status'));
159
157
  await loadBillingStatus();
160
158
  return;
161
159
  }
162
160
  if (checkoutState === 'updated') {
163
161
  setFeedback({ type: 'success', message: String(payload?.message || 'Your subscription was updated successfully.') });
162
+ invalidateBillingStatusCache(buildApiUrl('/api/taskforce/billing/status'));
164
163
  await loadBillingStatus();
165
164
  return;
166
165
  }
167
166
  if (checkoutState === 'already_current_plan') {
168
167
  setFeedback({ type: 'success', message: String(payload?.message || 'You are already on this plan.') });
168
+ invalidateBillingStatusCache(buildApiUrl('/api/taskforce/billing/status'));
169
169
  await loadBillingStatus();
170
170
  return;
171
171
  }
@@ -216,6 +216,7 @@ export function PlansPage({ isAuthenticated, runtimeMode, authSessionResolved, c
216
216
  setFeedback({ type: 'error', message: String(payload?.error || 'Unable to confirm checkout completion.') });
217
217
  return false;
218
218
  }
219
+ invalidateBillingStatusCache(buildApiUrl('/api/taskforce/billing/status'));
219
220
  await loadBillingStatus();
220
221
  return true;
221
222
  }