@taskforcehq/taskforce 0.3.324 → 0.3.326

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 (64) hide show
  1. package/dist/Taskforce.module.css +39 -0
  2. package/dist/components/features/TaskSettings.js +26 -9
  3. package/dist/components/task/TaskKanban.d.ts +2 -2
  4. package/dist/components/views/StandaloneLayout.js +22 -22
  5. package/dist/components/views/panels/PlanningDrawer.d.ts +2 -2
  6. package/dist/components/views/panels/PlanningDrawer.js +26 -12
  7. package/dist/components/views/planningScope.d.ts +4 -0
  8. package/dist/components/views/planningScope.js +5 -0
  9. package/dist/core/AiProfileService.js +6 -1
  10. package/dist/hooks/useTaskforce.js +74 -17
  11. package/dist/hooks/workspace/workspaceLocalState.d.ts +25 -0
  12. package/dist/hooks/workspace/workspaceLocalState.js +38 -0
  13. package/dist/mcp/adminWorkspaceRegistrar.js +0 -10
  14. package/dist/mcp/clientIntegrations.d.ts +1 -1
  15. package/dist/mcp/clientIntegrations.js +50 -0
  16. package/dist/mcp/clientProfileDefaults.js +4 -0
  17. package/dist/mcp/collaborationRegistrar.js +3 -3
  18. package/dist/mcp/documentAssetRegistrar.js +24 -72
  19. package/dist/mcp/documentHelpers.d.ts +11 -4
  20. package/dist/mcp/documentHelpers.js +21 -17
  21. package/dist/mcp/runtime.d.ts +4 -0
  22. package/dist/mcp/runtime.js +466 -534
  23. package/dist/mcp/taskPlanningRegistrar.js +45 -81
  24. package/dist/mcp/toolProfiles.d.ts +7 -0
  25. package/dist/mcp/toolProfiles.js +36 -0
  26. package/dist/mcp/toolRegistry.d.ts +87 -609
  27. package/dist/mcp/toolRegistry.js +93 -88
  28. package/dist/resources/templates/workflow-sources/workflowDocs.mjs +10 -10
  29. package/dist/resources/templates/workflows/collaborate.yaml +2 -3
  30. package/dist/resources/templates/workflows/do.yaml +2 -3
  31. package/dist/resources/templates/workflows/evaluate.yaml +2 -3
  32. package/dist/resources/templates/workflows/execute.yaml +12 -15
  33. package/dist/resources/templates/workflows/plan.yaml +3 -4
  34. package/dist/resources/templates/workflows/review.yaml +2 -3
  35. package/dist/shared/taxonomyLibrary.js +1 -1
  36. package/dist/sync/cloudSyncApi.js +22 -3
  37. package/dist/types.js +1 -1
  38. package/dist/ui/assets/{AgentsModule-DvkbUHi0.js → AgentsModule-C4LcqhcZ.js} +1 -1
  39. package/dist/ui/assets/{AnnotatedAttachmentWorkspace-jhOhxqHT.js → AnnotatedAttachmentWorkspace-B-Dq22d2.js} +1 -1
  40. package/dist/ui/assets/{ContextAttachmentManager-BAmEyLHL.js → ContextAttachmentManager-DsXPTvi-.js} +1 -1
  41. package/dist/ui/assets/{DocumentWorkspace-X-CpD5OO.js → DocumentWorkspace-CxA1wO3U.js} +1 -1
  42. package/dist/ui/assets/{EntityActivityTimeline-DD33onAC.js → EntityActivityTimeline-B8nSe8l3.js} +1 -1
  43. package/dist/ui/assets/{InitiativesModule-5nDXtIjf.js → InitiativesModule-VY95v38E.js} +1 -1
  44. package/dist/ui/assets/{PlansPage-B8nDR1hQ.js → PlansPage-CQbKDv1a.js} +1 -1
  45. package/dist/ui/assets/{TaskContextUpload-DrGrXJHm.js → TaskContextUpload-DvDlNpHC.js} +1 -1
  46. package/dist/ui/assets/TaskSettings-CUWbYYtP.js +12 -0
  47. package/dist/ui/assets/{WorkflowsModule-DWCGhEWT.js → WorkflowsModule-CgM3Jako.js} +1 -1
  48. package/dist/ui/assets/documentReferences-Do9YjrS3.js +1 -0
  49. package/dist/ui/assets/index--mh3pktj.js +5 -0
  50. package/dist/ui/assets/index-BawaaEXS.css +1 -0
  51. package/dist/ui/index.html +2 -2
  52. package/dist/utils/theme.js +1 -1
  53. package/package.json +1 -1
  54. package/src/resources/templates/workflow-sources/workflowDocs.mjs +10 -10
  55. package/src/resources/templates/workflows/collaborate.yaml +2 -3
  56. package/src/resources/templates/workflows/do.yaml +2 -3
  57. package/src/resources/templates/workflows/evaluate.yaml +2 -3
  58. package/src/resources/templates/workflows/execute.yaml +12 -15
  59. package/src/resources/templates/workflows/plan.yaml +3 -4
  60. package/src/resources/templates/workflows/review.yaml +2 -3
  61. package/dist/ui/assets/TaskSettings-BXVvZZwH.js +0 -9
  62. package/dist/ui/assets/documentReferences-Crup321p.js +0 -1
  63. package/dist/ui/assets/index-B9k24Aym.js +0 -5
  64. package/dist/ui/assets/index-DNeR5dYW.css +0 -1
@@ -1342,6 +1342,45 @@
1342
1342
  outline: none;
1343
1343
  }
1344
1344
 
1345
+ .mcpCodexTrustNote {
1346
+ display: flex;
1347
+ flex-direction: column;
1348
+ gap: 10px;
1349
+ margin-top: 12px;
1350
+ padding: 12px;
1351
+ border: 1px solid var(--border-primary);
1352
+ border-radius: var(--radius-sm);
1353
+ background: color-mix(in srgb, var(--surface-raised) 82%, transparent);
1354
+ }
1355
+
1356
+ .mcpCodexTrustHeader {
1357
+ display: flex;
1358
+ align-items: flex-start;
1359
+ justify-content: space-between;
1360
+ gap: 12px;
1361
+ }
1362
+
1363
+ .mcpCodexTrustTitle {
1364
+ font-size: 12px;
1365
+ font-weight: 700;
1366
+ color: var(--text-primary);
1367
+ }
1368
+
1369
+ .mcpCodexTrustCode {
1370
+ margin: 0;
1371
+ padding: 10px 12px;
1372
+ overflow-x: auto;
1373
+ border-radius: var(--radius-sm);
1374
+ background: #1e1e2e;
1375
+ color: #f8fafc;
1376
+ }
1377
+
1378
+ .mcpCodexTrustCode code {
1379
+ font-family: "JetBrains Mono", "Fira Code", monospace;
1380
+ font-size: 12px;
1381
+ line-height: 1.5;
1382
+ }
1383
+
1345
1384
  .mcpAuthRequiredCard {
1346
1385
  display: flex;
1347
1386
  align-items: flex-start;
@@ -13,6 +13,12 @@ const THEME_ICON_MAP = {
13
13
  sparkles: Sparkles,
14
14
  };
15
15
  const DEFAULT_MCP_TOKEN_SCOPES = ['tasks:read', 'tasks:write', 'workspace:read'];
16
+ const MCP_CONFIG_CODE_COLORS = {
17
+ key: '#93c5fd',
18
+ token: '#c084fc',
19
+ punctuation: '#cbd5e1',
20
+ text: '#f8fafc'
21
+ };
16
22
  export function TaskSettings(props) {
17
23
  const { settingsModel, onSectionChange, renderMode = 'full' } = props;
18
24
  const isMcpOnly = renderMode === 'mcp-only';
@@ -249,6 +255,12 @@ export function TaskSettings(props) {
249
255
  : null;
250
256
  const selectedMcpClientUsesOAuth = selectedMcpClient === 'chatgpt-desktop' || selectedMcpClient === 'claude-desktop';
251
257
  const agentRegistrationPrompt = 'Resolve your profile with Taskforce.';
258
+ const codexTrustSnippet = useMemo(() => {
259
+ const root = String(projectRoot || '').trim();
260
+ if (!root)
261
+ return '';
262
+ return `[projects.${JSON.stringify(root)}]\ntrust_level = "trusted"`;
263
+ }, [projectRoot]);
252
264
  const activeChatGptConnector = useMemo(() => mcpConnectorGrants.find((grant) => grant.provider === 'chatgpt' && grant.status === 'active') || null, [mcpConnectorGrants]);
253
265
  const activeClaudeConnector = useMemo(() => mcpConnectorGrants.find((grant) => grant.provider === 'claude' && grant.status === 'active') || null, [mcpConnectorGrants]);
254
266
  const selectedConnectorGrant = selectedMcpClient === 'chatgpt-desktop'
@@ -269,10 +281,10 @@ export function TaskSettings(props) {
269
281
  return JSON.stringify(payload, null, 2);
270
282
  }, [cloudClientIntegration, cloudMcpServerId]);
271
283
  const renderMcpConfigBlock = (serverId, serverConfig, rootKey = 'mcpServers', wrapperKeys = []) => {
272
- const dimmed = { color: 'var(--text-helper)' };
273
- const highlighted = { color: 'var(--brand-primary-strong)', fontWeight: 'bold' };
274
- const bracketColor = { color: 'var(--text-secondary)' };
275
- const normalText = { color: 'var(--text-primary, #fff)' };
284
+ const dimmed = { color: MCP_CONFIG_CODE_COLORS.key };
285
+ const highlighted = { color: MCP_CONFIG_CODE_COLORS.token, fontWeight: 'bold' };
286
+ const bracketColor = { color: MCP_CONFIG_CODE_COLORS.punctuation };
287
+ const normalText = { color: MCP_CONFIG_CODE_COLORS.text };
276
288
  const tokenText = cloudMcpTokenValue;
277
289
  const containerKeys = [...wrapperKeys, rootKey];
278
290
  const serverIndent = ' '.repeat(containerKeys.length + 1);
@@ -284,8 +296,8 @@ export function TaskSettings(props) {
284
296
  return (_jsxs(_Fragment, { children: [_jsx("span", { style: bracketColor, children: "{" }), "\n", containerKeys.map((key, index) => (_jsxs(React.Fragment, { children: [`${' '.repeat(index + 1)}`, _jsx("span", { style: dimmed, children: `"${key}"` }), _jsx("span", { style: dimmed, children: ":" }), " ", _jsx("span", { style: bracketColor, children: "{" }), "\n"] }, `mcp-container-open-${key}-${index}`))), serverIndent, _jsxs("span", { style: normalText, children: [`"${serverId}"`, ": ", configSegments.map((segment, index) => (_jsxs(React.Fragment, { children: [segment, index < configSegments.length - 1 && _jsx("span", { style: highlighted, children: tokenText })] }, `config-segment-${index}`)))] }), "\n", [...containerKeys].reverse().map((key, index) => (_jsxs(React.Fragment, { children: [`${' '.repeat(containerKeys.length - index)}`, _jsx("span", { style: bracketColor, children: "}" }), "\n"] }, `mcp-container-close-${key}-${index}`))), _jsx("span", { style: bracketColor, children: "}" })] }));
285
297
  };
286
298
  const renderTextConfigBlock = (text) => {
287
- const normalText = { color: 'var(--text-primary, #fff)' };
288
- const highlighted = { color: 'var(--brand-primary-strong)', fontWeight: 'bold' };
299
+ const normalText = { color: MCP_CONFIG_CODE_COLORS.text };
300
+ const highlighted = { color: MCP_CONFIG_CODE_COLORS.token, fontWeight: 'bold' };
289
301
  const tokenText = cloudMcpTokenValue;
290
302
  const segments = String(text || '').split(tokenText);
291
303
  return segments.map((segment, index) => (_jsxs(React.Fragment, { children: [_jsx("span", { style: normalText, children: segment }), index < segments.length - 1 && _jsx("span", { style: highlighted, children: tokenText })] }, `text-config-${index}`)));
@@ -296,6 +308,11 @@ export function TaskSettings(props) {
296
308
  setTimeout(() => setCopiedConfig(false), 2000);
297
309
  };
298
310
  const renderMcpProfilePrompt = () => (_jsxs("div", { className: styles.mcpRegistrationHint, children: [_jsx("span", { children: "Once the MCP is configured, prompt your agent:" }), _jsx("button", { type: "button", className: styles.mcpRegistrationPrompt, onClick: () => copyToClipboard(agentRegistrationPrompt, 'registration'), title: copiedConfig === 'registration' ? 'Copied' : 'Copy prompt', children: agentRegistrationPrompt })] }));
311
+ const renderCodexTrustGuidance = () => {
312
+ if (selectedMcpClient !== 'codex' || !codexTrustSnippet)
313
+ return null;
314
+ return (_jsxs("div", { className: styles.mcpCodexTrustNote, children: [_jsxs("div", { className: styles.mcpCodexTrustHeader, children: [_jsxs("div", { children: [_jsx("div", { className: styles.mcpCodexTrustTitle, children: "Codex Project Trust" }), _jsx("div", { className: styles.settingDescription, children: "Project-local .codex/config.toml is only loaded after Codex trusts this repository. Add this to ~/.codex/config.toml if you use a repo-local Codex config." })] }), _jsx("button", { type: "button", className: `${styles.copyBtn} ${copiedConfig === 'codex-trust' ? styles.copyBtnActive : ''}`, onClick: () => copyToClipboard(codexTrustSnippet, 'codex-trust'), title: copiedConfig === 'codex-trust' ? 'Copied' : 'Copy Codex trust snippet', "aria-label": "Copy Codex trust snippet", children: copiedConfig === 'codex-trust' ? _jsx(Check, { size: 14 }) : _jsx(Copy, { size: 14 }) })] }), _jsx("pre", { className: styles.mcpCodexTrustCode, children: _jsx("code", { children: codexTrustSnippet }) })] }));
315
+ };
299
316
  const renderCloudMcpAuthRequired = () => (_jsxs("div", { className: `${styles.mcpAuthRequiredCard} ${styles.marginTop16}`, children: [_jsx("div", { className: styles.mcpAuthRequiredIcon, children: _jsx(Plug, { size: 20 }) }), _jsxs("div", { className: styles.mcpAuthRequiredBody, children: [_jsx("h3", { className: styles.settingTitle, children: "Sign in to use Cloud MCP" }), _jsx("p", { className: styles.settingDescription, children: "Cloud MCP connects remote agents through your Taskforce cloud account. Register or sign in to generate cloud MCP tokens, copy client configuration, and connect hosted MCP agents to this workspace." }), onOpenCloudAuth && (_jsxs("div", { className: styles.mcpAuthRequiredActions, children: [_jsx("button", { type: "button", className: styles.copyBtn, onClick: () => onOpenCloudAuth('login'), children: "Sign In" }), _jsx("button", { type: "button", className: styles.copyBtn, onClick: () => onOpenCloudAuth('register'), children: "Register" })] }))] })] }));
300
317
  const handleCopyCloudMcpConfig = () => {
301
318
  if (!selectedMcpClientUsesOAuth && !currentMcpTokenValue) {
@@ -919,12 +936,12 @@ export function TaskSettings(props) {
919
936
  ? new Date(selectedConnectorGrant.lastUsedAt).toLocaleString()
920
937
  : '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, 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
921
938
  ? renderMcpConfigBlock(cloudMcpServerId, cloudMcpServerConfig, cloudClientIntegration.rootKey || 'mcpServers', cloudClientIntegration.wrapperKeys || [])
922
- : renderTextConfigBlock(cloudClientIntegration.renderedText || '') }) })] }), mcpConfigNotice && (_jsxs("div", { className: `${mcpConfigNotice.type === 'success' ? styles.successMessage : styles.errorMessage} ${styles.marginTop12}`, children: [mcpConfigNotice.type === 'success' ? _jsx(Check, { size: 14 }) : _jsx(AlertTriangle, { size: 14 }), mcpConfigNotice.message] })), _jsxs("div", { className: `${styles.codeBlockWrapper} ${styles.marginTop12}`, children: [_jsx("div", { className: styles.codeHeader, children: _jsx("span", { className: styles.codeLabel, children: cloudClientIntegration.instructionLabel }) }), _jsxs("div", { style: { padding: '16px 20px' }, children: [_jsx("div", { className: styles.settingDescription, children: cloudClientIntegration.instructionText }), renderMcpProfilePrompt()] })] })] })] })) : (_jsxs(_Fragment, { children: [_jsx("div", { className: `${styles.settingGroup} ${styles.marginTop16}`, children: _jsxs("div", { className: styles.settingLabelGroup, children: [_jsx("h3", { className: styles.settingTitle, children: "Cloud MCP" }), _jsx("span", { className: styles.settingDescription, children: "Cloud MCP connects agents to Taskforce through the hosted MCP endpoint. Use this for remote MCP clients or tools that cannot access your local Taskforce install." })] }) }), _jsxs("div", { className: `${styles.codeBlockWrapper} ${styles.codeBlockWrapperCustom} ${styles.marginTop16}`, children: [_jsx("div", { className: styles.codeHeader, children: _jsx("div", { className: styles.settingLabelGroup, style: { gap: '2px' }, children: _jsx("span", { className: styles.codeLabel, children: "Generate Token" }) }) }), _jsx("div", { className: styles.mcpTokenCardBody, children: _jsxs("div", { className: styles.mcpTokenFormGrid, children: [_jsxs("div", { className: styles.settingsItem, style: { gap: '6px' }, 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, style: { gap: '6px' }, 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.mcpTokenOutputWrap, children: [_jsx("div", { className: styles.settingLabelGroup, children: _jsx("span", { className: styles.settingDescription, children: "Full Token" }) }), _jsx("input", { type: "text", className: `${styles.settingInput} ${styles.mcpTokenValueInput}`, placeholder: "Generate a token to populate the full secret", value: generatedTokenValue || '', onChange: (e) => {
939
+ : renderTextConfigBlock(cloudClientIntegration.renderedText || '') }) })] }), mcpConfigNotice && (_jsxs("div", { className: `${mcpConfigNotice.type === 'success' ? styles.successMessage : styles.errorMessage} ${styles.marginTop12}`, children: [mcpConfigNotice.type === 'success' ? _jsx(Check, { size: 14 }) : _jsx(AlertTriangle, { size: 14 }), mcpConfigNotice.message] })), _jsxs("div", { className: `${styles.codeBlockWrapper} ${styles.marginTop12}`, children: [_jsx("div", { className: styles.codeHeader, children: _jsx("span", { className: styles.codeLabel, children: cloudClientIntegration.instructionLabel }) }), _jsxs("div", { style: { padding: '16px 20px' }, children: [_jsx("div", { className: styles.settingDescription, children: cloudClientIntegration.instructionText }), renderCodexTrustGuidance(), renderMcpProfilePrompt()] })] })] })] })) : (_jsxs(_Fragment, { children: [_jsx("div", { className: `${styles.settingGroup} ${styles.marginTop16}`, children: _jsxs("div", { className: styles.settingLabelGroup, children: [_jsx("h3", { className: styles.settingTitle, children: "Cloud MCP" }), _jsx("span", { className: styles.settingDescription, children: "Cloud MCP connects agents to Taskforce through the hosted MCP endpoint. Use this for remote MCP clients or tools that cannot access your local Taskforce install." })] }) }), _jsxs("div", { className: `${styles.codeBlockWrapper} ${styles.codeBlockWrapperCustom} ${styles.marginTop16}`, children: [_jsx("div", { className: styles.codeHeader, children: _jsx("div", { className: styles.settingLabelGroup, style: { gap: '2px' }, children: _jsx("span", { className: styles.codeLabel, children: "Generate Token" }) }) }), _jsx("div", { className: styles.mcpTokenCardBody, children: _jsxs("div", { className: styles.mcpTokenFormGrid, children: [_jsxs("div", { className: styles.settingsItem, style: { gap: '6px' }, 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, style: { gap: '6px' }, 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.mcpTokenOutputWrap, children: [_jsx("div", { className: styles.settingLabelGroup, children: _jsx("span", { className: styles.settingDescription, children: "Full Token" }) }), _jsx("input", { type: "text", className: `${styles.settingInput} ${styles.mcpTokenValueInput}`, placeholder: "Generate a token to populate the full secret", value: generatedTokenValue || '', onChange: (e) => {
923
940
  setGeneratedTokenValue(e.target.value);
924
941
  setMcpConnectionResult(null);
925
942
  } })] }), _jsxs("div", { className: styles.mcpTokenActionRow, children: [_jsxs("button", { className: styles.copyBtn, onClick: () => void handleTestMcpConnection(), disabled: !currentMcpTokenValue || mcpConnectionBusy, title: "Test Token", "aria-label": "Test Token", children: [mcpConnectionBusy ? _jsx(Loader2, { size: 14, className: styles.spinIcon }) : _jsx(Zap, { size: 14 }), _jsx("span", { children: "TEST TOKEN" })] }), _jsxs("button", { className: `${styles.copyBtn} ${copiedConfig === 'token' ? styles.copyBtnActive : ''}`, onClick: () => currentMcpTokenValue && copyToClipboard(currentMcpTokenValue, 'token'), disabled: !currentMcpTokenValue, title: "Copy Token", "aria-label": "Copy Token", children: [copiedConfig === 'token' ? _jsx(Check, { size: 14 }) : _jsx(Copy, { size: 14 }), _jsx("span", { children: "COPY TOKEN" })] }), _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" })] })] }), mcpTokenIssueNotice && (_jsxs("div", { className: `${mcpTokenIssueNotice.type === 'success' ? styles.successMessage : styles.errorMessage} ${styles.marginTop12}`, children: [mcpTokenIssueNotice.type === 'success' ? _jsx(Check, { size: 14 }) : _jsx(AlertTriangle, { size: 14 }), mcpTokenIssueNotice.message] })), mcpConnectionResult && (_jsxs("div", { className: `${mcpConnectionResult.type === 'success' ? styles.successMessage : styles.errorMessage} ${styles.marginTop12}`, children: [mcpConnectionResult.type === 'success' ? _jsx(Check, { size: 14 }) : _jsx(AlertTriangle, { size: 14 }), _jsxs("div", { children: [_jsx("div", { children: mcpConnectionResult.message }), mcpConnectionResult.type === 'success' && (mcpConnectionResult.workspaceName || mcpConnectionResult.workspaceId) && (_jsxs("div", { className: styles.settingHelper, children: ["Workspace: ", mcpConnectionResult.workspaceName || mcpConnectionResult.workspaceId, mcpConnectionResult.workspaceId ? ` (${mcpConnectionResult.workspaceId})` : ''] })), mcpConnectionResult.type === 'success' && Array.isArray(mcpConnectionResult.scopes) && mcpConnectionResult.scopes.length > 0 && (_jsxs("div", { className: styles.settingHelper, children: ["Granted scopes: ", mcpConnectionResult.scopes.join(', ')] })), mcpConnectionResult.endpoint && (_jsxs("div", { className: styles.settingHelper, children: ["Endpoint: ", mcpConnectionResult.endpoint] })), mcpConnectionResult.type === 'error' && mcpConnectionResult.errorCode && (_jsxs("div", { className: styles.settingHelper, children: ["Error code: ", mcpConnectionResult.errorCode] })), mcpConnectionResult.type === 'error' && mcpConnectionResult.followup && (_jsx("div", { className: styles.settingHelper, children: mcpConnectionResult.followup }))] })] }))] }), _jsxs("div", { className: styles.settingGroup, 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
926
943
  ? renderMcpConfigBlock(cloudMcpServerId, cloudMcpServerConfig, cloudClientIntegration.rootKey || 'mcpServers', cloudClientIntegration.wrapperKeys || [])
927
- : renderTextConfigBlock(cloudClientIntegration.renderedText || '') }) })] }), mcpConfigNotice && (_jsxs("div", { className: `${mcpConfigNotice.type === 'success' ? styles.successMessage : styles.errorMessage} ${styles.marginTop12}`, children: [mcpConfigNotice.type === 'success' ? _jsx(Check, { size: 14 }) : _jsx(AlertTriangle, { size: 14 }), mcpConfigNotice.message] })), _jsxs("div", { className: `${styles.codeBlockWrapper} ${styles.marginTop12}`, children: [_jsx("div", { className: styles.codeHeader, children: _jsx("span", { className: styles.codeLabel, children: cloudClientIntegration.instructionLabel }) }), _jsxs("div", { style: { padding: '16px 20px' }, children: [_jsx("div", { className: styles.settingDescription, children: cloudClientIntegration.instructionText }), renderMcpProfilePrompt()] })] })] }), _jsxs("div", { className: `${styles.settingGroup} ${styles.marginTop16}`, children: [_jsxs("div", { className: styles.toggleHeading, onClick: () => setShowMcpTokensList((prev) => !prev), onKeyDown: (e) => {
944
+ : renderTextConfigBlock(cloudClientIntegration.renderedText || '') }) })] }), mcpConfigNotice && (_jsxs("div", { className: `${mcpConfigNotice.type === 'success' ? styles.successMessage : styles.errorMessage} ${styles.marginTop12}`, children: [mcpConfigNotice.type === 'success' ? _jsx(Check, { size: 14 }) : _jsx(AlertTriangle, { size: 14 }), mcpConfigNotice.message] })), _jsxs("div", { className: `${styles.codeBlockWrapper} ${styles.marginTop12}`, children: [_jsx("div", { className: styles.codeHeader, children: _jsx("span", { className: styles.codeLabel, children: cloudClientIntegration.instructionLabel }) }), _jsxs("div", { style: { padding: '16px 20px' }, children: [_jsx("div", { className: styles.settingDescription, children: cloudClientIntegration.instructionText }), renderCodexTrustGuidance(), renderMcpProfilePrompt()] })] })] }), _jsxs("div", { className: `${styles.settingGroup} ${styles.marginTop16}`, children: [_jsxs("div", { className: styles.toggleHeading, onClick: () => setShowMcpTokensList((prev) => !prev), onKeyDown: (e) => {
928
945
  if (e.key === 'Enter' || e.key === ' ') {
929
946
  e.preventDefault();
930
947
  setShowMcpTokensList((prev) => !prev);
@@ -936,7 +953,7 @@ export function TaskSettings(props) {
936
953
  background: 'rgba(255, 255, 255, 0.03)'
937
954
  }, children: [_jsxs("div", { className: styles.settingLabelGroup, children: [_jsxs("div", { className: styles.labelGroupFlex, children: [_jsx("label", { className: styles.settingLabel, children: token.name }), _jsx("span", { className: `${styles.configBadge} ${token.status === 'revoked' ? styles.configBadgeRevoked : token.status === 'expired' ? styles.configBadgeExpired : styles.configBadgeActive}`, children: token.status })] }), _jsx("span", { className: styles.settingDescription, style: { fontFamily: '"JetBrains Mono", "Fira Code", monospace' }, children: token.tokenPrefix }), _jsxs("span", { className: styles.settingDescription, children: ["Scopes: ", token.scopes.join(', ')] }), _jsxs("span", { className: styles.settingDescription, children: ["Created ", new Date(token.createdAt).toLocaleDateString(), token.lastUsedAt ? ` · Last used ${new Date(token.lastUsedAt).toLocaleString()}` : ' · Never used'] })] }), _jsxs("div", { style: { display: 'flex', gap: '8px', flexWrap: 'wrap', justifyContent: 'flex-end' }, children: [token.status === 'active' && (_jsx("button", { className: styles.copyBtn, onClick: () => void handleMcpTokenAction(token.id, 'regenerate'), disabled: mcpActionBusyId === `regenerate:${token.id}`, children: "Regenerate" })), token.status === 'active' && (_jsx("button", { className: styles.copyBtn, onClick: () => void handleMcpTokenAction(token.id, 'revoke'), disabled: mcpActionBusyId === `revoke:${token.id}`, children: "Revoke" })), token.status !== 'active' && (_jsx("button", { className: styles.copyBtn, onClick: () => void handleMcpTokenAction(token.id, 'delete'), disabled: mcpActionBusyId === `delete:${token.id}`, children: "Delete" }))] })] }, token.id))) }))] }))] })] })) })), showLocalMcp && (_jsxs(_Fragment, { children: [_jsx("div", { className: `${styles.settingGroup} ${styles.marginTop16}`, children: _jsxs("div", { className: styles.settingLabelGroup, children: [_jsx("h3", { className: styles.settingTitle, children: "Local MCP" }), _jsx("span", { className: styles.settingDescription, children: "Local MCP connects through your local Taskforce install and does not require a cloud access token. Use this for Codex, Claude Code CLI, Gemini CLI, and similar local MCP clients." })] }) }), _jsxs("div", { className: `${styles.codeBlockWrapper} ${styles.codeBlockWrapperCustom}`, children: [_jsxs("div", { className: styles.codeHeader, children: [_jsx("span", { className: styles.codeLabel, children: "Local 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: () => handleCopyConfig('full'), 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: localMcpServerConfig
938
955
  ? renderMcpConfigBlock(localMcpServer.serverId, localMcpServerConfig, localClientIntegration.rootKey || 'mcpServers', localClientIntegration.wrapperKeys || [])
939
- : renderTextConfigBlock(localClientIntegration.renderedText || '') }) })] }), _jsxs("div", { className: `${styles.codeBlockWrapper} ${styles.marginTop12}`, children: [_jsx("div", { className: styles.codeHeader, children: _jsx("span", { className: styles.codeLabel, children: localClientIntegration.instructionLabel }) }), _jsxs("div", { style: { padding: '16px 20px' }, children: [_jsx("div", { className: styles.settingDescription, children: localClientIntegration.instructionText }), renderMcpProfilePrompt()] })] }), _jsx("div", { className: `${styles.settingGroup} ${styles.marginTop12}`, children: _jsxs("div", { className: styles.toggleHeading, onClick: () => setShowAdvancedMcp((prev) => !prev), onKeyDown: (e) => {
956
+ : renderTextConfigBlock(localClientIntegration.renderedText || '') }) })] }), _jsxs("div", { className: `${styles.codeBlockWrapper} ${styles.marginTop12}`, children: [_jsx("div", { className: styles.codeHeader, children: _jsx("span", { className: styles.codeLabel, children: localClientIntegration.instructionLabel }) }), _jsxs("div", { style: { padding: '16px 20px' }, children: [_jsx("div", { className: styles.settingDescription, children: localClientIntegration.instructionText }), renderCodexTrustGuidance(), renderMcpProfilePrompt()] })] }), _jsx("div", { className: `${styles.settingGroup} ${styles.marginTop12}`, children: _jsxs("div", { className: styles.toggleHeading, onClick: () => setShowAdvancedMcp((prev) => !prev), onKeyDown: (e) => {
940
957
  if (e.key === 'Enter' || e.key === ' ') {
941
958
  e.preventDefault();
942
959
  setShowAdvancedMcp((prev) => !prev);
@@ -66,8 +66,8 @@ interface TaskKanbanProps {
66
66
  sortBy?: TaskSortKey;
67
67
  sortOrder?: 'asc' | 'desc';
68
68
  planningDropTargets?: React.ReactNode;
69
- onAssignTaskToWorkstream?: (taskId: string, workstreamId: string | null) => void | Promise<void>;
70
- onAssignWorkstreamToInitiative?: (workstreamId: string, initiativeId: string | null) => void | Promise<void>;
69
+ onAssignTaskToWorkstream?: (taskId: string, workstreamId: string | null) => boolean | void | Promise<boolean | void>;
70
+ onAssignWorkstreamToInitiative?: (workstreamId: string, initiativeId: string | null) => boolean | void | Promise<boolean | void>;
71
71
  showTaskCardStatusLabel?: boolean;
72
72
  workstreams?: WorkstreamItem[];
73
73
  initiatives?: InitiativeItem[];
@@ -33,7 +33,7 @@ import { normalizeTaskSearchQuery, taskMatchesSearch } from '../../utils/taskSea
33
33
  import { formatInitiativeReference, formatWorkstreamReference, parseWorkstreamReference, resolveInitiativeReference } from '../../utils/planningReferences';
34
34
  import { DEFAULT_TYPE_VALUE } from '../../shared/taxonomyDefaults.js';
35
35
  import { getImageReferenceLabel } from '../../utils/imageReferences';
36
- import { isPlanningEntityVisibleForScope, scopePlanningSummaries } from './planningScope';
36
+ import { isPlanningEntityVisibleForScope, scopePlanningSummaries, scopeTasksToPlanningSelection } from './planningScope';
37
37
  import { buildReferenceMismatchSummaries, buildSyncDiagnosticsSummary, buildSyncEventRows, getActiveReferenceMismatchCount, resolveSyncStageLabel, resolveSyncStatusMeta, } from '../../utils/syncStatusPresentation';
38
38
  import { buildCustomTaxonomySortOptions } from '../../utils/taxonomySorting.js';
39
39
  import { getLocale, t } from '../../localization';
@@ -1135,14 +1135,10 @@ export function StandaloneLayout(props) {
1135
1135
  });
1136
1136
  }, [groupBy, scheduleOnlyExpired, hierarchyVisibleTasks, todayDateOnly]);
1137
1137
  const scopedBoardVisibleTasks = useMemo(() => {
1138
- if (!selectedPlanningTaskIds || selectedPlanningTaskIds.size === 0)
1139
- return boardVisibleTasks;
1140
- return boardVisibleTasks.filter((task) => selectedPlanningTaskIds.has(task.id));
1138
+ return scopeTasksToPlanningSelection(boardVisibleTasks, selectedPlanningTaskIds);
1141
1139
  }, [boardVisibleTasks, selectedPlanningTaskIds]);
1142
1140
  const scopedKanbanAllTasks = useMemo(() => {
1143
- if (!selectedPlanningTaskIds || selectedPlanningTaskIds.size === 0)
1144
- return kanbanAllTasks;
1145
- return kanbanAllTasks.filter((task) => selectedPlanningTaskIds.has(task.id));
1141
+ return scopeTasksToPlanningSelection(kanbanAllTasks, selectedPlanningTaskIds);
1146
1142
  }, [kanbanAllTasks, selectedPlanningTaskIds]);
1147
1143
  const expiredRecoveryCandidates = useMemo(() => {
1148
1144
  if (groupBy !== 'schedule')
@@ -2699,7 +2695,7 @@ export function StandaloneLayout(props) {
2699
2695
  const workstream = props.workstreams.find((item) => item.id === workstreamId);
2700
2696
  if (!workstream) {
2701
2697
  pushNotice('Workstream not found.', 'error');
2702
- return;
2698
+ return false;
2703
2699
  }
2704
2700
  const nextInitiativeRaw = typeof initiativeReferenceOverride === 'string'
2705
2701
  ? initiativeReferenceOverride.trim()
@@ -2711,40 +2707,42 @@ export function StandaloneLayout(props) {
2711
2707
  : resolveInitiativeReference(planningStructure.initiatives, nextInitiativeRaw);
2712
2708
  if (nextInitiativeRaw.length > 0 && !resolvedInitiative) {
2713
2709
  pushNotice('Initiative not found by that reference.', 'error');
2714
- return;
2710
+ return false;
2715
2711
  }
2716
2712
  const normalizedParentId = resolvedInitiative?.id || null;
2717
2713
  if ((workstream.initiativeId || null) === normalizedParentId) {
2718
2714
  pushNotice(normalizedParentId ? 'Workstream already belongs to that initiative.' : 'Workstream is already standalone.', 'info');
2719
- return;
2715
+ return false;
2720
2716
  }
2721
2717
  try {
2722
2718
  await props.updateWorkstream(workstreamId, { initiativeId: normalizedParentId });
2723
2719
  setPlanningDraftInitiativeId(resolvedInitiative ? formatInitiativeReference(resolvedInitiative) : '');
2724
2720
  pushNotice(normalizedParentId ? 'Initiative set' : 'Initiative removed', 'success');
2721
+ return true;
2725
2722
  }
2726
2723
  catch {
2727
2724
  pushNotice('Failed to set initiative.', 'error');
2725
+ return false;
2728
2726
  }
2729
2727
  }, [planningDraftInitiativeId, planningStructure.initiatives, props, pushNotice]);
2730
2728
  const handleAssignTaskToWorkstream = useCallback(async (taskId, workstreamId) => {
2731
2729
  const task = hierarchyTaskById.get(taskId);
2732
2730
  if (!task) {
2733
2731
  pushNotice('Task not found.', 'error');
2734
- return;
2732
+ return false;
2735
2733
  }
2736
2734
  const normalizedWorkstreamId = workstreamId || null;
2737
2735
  if (planningStructure.initiativeById.has(taskId) || planningStructure.workstreamById.has(taskId)) {
2738
2736
  pushNotice('Only execution tasks can be moved into workstreams.', 'error');
2739
- return;
2737
+ return false;
2740
2738
  }
2741
2739
  if (normalizedWorkstreamId && !planningStructure.workstreamById.has(normalizedWorkstreamId)) {
2742
2740
  pushNotice('Workstream not found.', 'error');
2743
- return;
2741
+ return false;
2744
2742
  }
2745
2743
  if ((task.workstreamId || null) === normalizedWorkstreamId) {
2746
2744
  pushNotice(normalizedWorkstreamId ? 'Task already belongs to that workstream.' : 'Task is already standalone.', 'info');
2747
- return;
2745
+ return false;
2748
2746
  }
2749
2747
  try {
2750
2748
  const res = await fetch(`/api/taskforce/task/${taskId}`, {
@@ -2755,21 +2753,23 @@ export function StandaloneLayout(props) {
2755
2753
  if (!res.ok) {
2756
2754
  const data = await res.json().catch(() => ({}));
2757
2755
  pushNotice(data?.error || 'Failed to set workstream.', 'error');
2758
- return;
2756
+ return false;
2759
2757
  }
2760
2758
  await res.json().catch(() => null);
2761
2759
  await fetchTasks(true);
2762
2760
  pushNotice(normalizedWorkstreamId ? 'Workstream set' : 'Workstream removed', 'success');
2761
+ return true;
2763
2762
  }
2764
2763
  catch {
2765
2764
  pushNotice('Failed to set workstream.', 'error');
2765
+ return false;
2766
2766
  }
2767
2767
  }, [fetchTasks, hierarchyTaskById, planningStructure.initiativeById, planningStructure.workstreamById, pushNotice]);
2768
2768
  const handleAttachTaskToWorkstreamByReference = useCallback(async (workstreamId, taskReference) => {
2769
2769
  const needle = taskReference.trim();
2770
2770
  if (!needle) {
2771
2771
  pushNotice('Enter a task reference.', 'error');
2772
- return;
2772
+ return false;
2773
2773
  }
2774
2774
  const parsedReference = parseTaskReference(needle);
2775
2775
  const matchedTask = hierarchyTasks.find((task) => (task.id === needle
@@ -2777,20 +2777,20 @@ export function StandaloneLayout(props) {
2777
2777
  || (parsedReference !== null && task.referenceNumber === parsedReference)));
2778
2778
  if (!matchedTask) {
2779
2779
  pushNotice('Task not found by that reference.', 'error');
2780
- return;
2780
+ return false;
2781
2781
  }
2782
- await handleAssignTaskToWorkstream(matchedTask.id, workstreamId);
2782
+ return handleAssignTaskToWorkstream(matchedTask.id, workstreamId);
2783
2783
  }, [handleAssignTaskToWorkstream, hierarchyTasks, pushNotice]);
2784
2784
  const handleAttachWorkstreamToInitiativeByReference = useCallback(async (initiativeId, workstreamReference) => {
2785
2785
  const needle = workstreamReference.trim();
2786
2786
  if (!needle) {
2787
2787
  pushNotice('Enter a workstream reference.', 'error');
2788
- return;
2788
+ return false;
2789
2789
  }
2790
2790
  const initiative = planningStructure.initiativeById.get(initiativeId);
2791
2791
  if (!initiative) {
2792
2792
  pushNotice('Initiative not found.', 'error');
2793
- return;
2793
+ return false;
2794
2794
  }
2795
2795
  const parsedReference = parseWorkstreamReference(needle);
2796
2796
  const matchedWorkstream = props.workstreams.find((workstream) => (workstream.id === needle
@@ -2798,9 +2798,9 @@ export function StandaloneLayout(props) {
2798
2798
  || (parsedReference !== null && workstream.referenceNumber === parsedReference)));
2799
2799
  if (!matchedWorkstream) {
2800
2800
  pushNotice('Workstream not found by that reference.', 'error');
2801
- return;
2801
+ return false;
2802
2802
  }
2803
- await handleAssignInitiativeToWorkstream(matchedWorkstream.id, formatInitiativeReference(initiative));
2803
+ return handleAssignInitiativeToWorkstream(matchedWorkstream.id, formatInitiativeReference(initiative));
2804
2804
  }, [handleAssignInitiativeToWorkstream, planningStructure.initiativeById, props.workstreams, pushNotice]);
2805
2805
  const getPlanningContextAttachmentDraft = useCallback((entityType, entityId) => {
2806
2806
  const key = `${entityType}:${entityId}`;
@@ -125,8 +125,8 @@ type PlanningDrawerProps = {
125
125
  onAssignInitiativeToWorkstream: (workstreamId: string, initiativeReferenceOverride?: string | null) => void;
126
126
  onOpenTaskById?: (taskId: string) => void;
127
127
  onRemoveTaskFromWorkstream?: (taskId: string, workstreamId: string) => void;
128
- onAttachTaskToWorkstreamByReference?: (workstreamId: string, taskReference: string) => void;
129
- onAttachWorkstreamToInitiativeByReference?: (initiativeId: string, workstreamReference: string) => void;
128
+ onAttachTaskToWorkstreamByReference?: (workstreamId: string, taskReference: string) => boolean | void | Promise<boolean | void>;
129
+ onAttachWorkstreamToInitiativeByReference?: (initiativeId: string, workstreamReference: string) => boolean | void | Promise<boolean | void>;
130
130
  onAddPlanningContextFile?: (entityType: 'initiative' | 'workstream', entityId: string, file: AttachmentItem) => void;
131
131
  onRemovePlanningContextFile?: (entityType: 'initiative' | 'workstream', entityId: string, index: number) => void;
132
132
  onUpdatePlanningContextCaption?: (entityType: 'initiative' | 'workstream', entityId: string, index: number, caption: string) => void;
@@ -165,6 +165,7 @@ function PlanningDrawerList({ initiatives, standaloneWorkstreams, allStandaloneW
165
165
  }
166
166
  function PlanningDrawerDetailView({ detail, currentWorkspaceId, onOpenNestedWorkstreamDetails, onEdit, onArchive, onUnarchive, onCreateTaskInWorkstream, onCreateWorkstreamInInitiative, onOpenTaskById, onRemoveTaskFromWorkstream, onAssignInitiativeToWorkstream, onAttachTaskToWorkstreamByReference, onAttachWorkstreamToInitiativeByReference, onAddPlanningContextFile, onRemovePlanningContextFile, onUpdatePlanningContextCaption, parentInitiative = null, assigneeOptions, }) {
167
167
  const [attachReference, setAttachReference] = React.useState('');
168
+ const [isAttachingReference, setIsAttachingReference] = React.useState(false);
168
169
  const [showArchivedChildren, setShowArchivedChildren] = React.useState(false);
169
170
  const [showRecentActivity, setShowRecentActivity] = React.useState(true);
170
171
  const { active } = useDndContext();
@@ -207,24 +208,37 @@ function PlanningDrawerDetailView({ detail, currentWorkspaceId, onOpenNestedWork
207
208
  : formatWorkstreamReference(detail.item);
208
209
  React.useEffect(() => {
209
210
  setAttachReference('');
211
+ setIsAttachingReference(false);
210
212
  setShowArchivedChildren(false);
211
213
  setShowRecentActivity(true);
212
214
  }, [detail.item.id, detail.type]);
213
- const handleAttachReference = React.useCallback(() => {
215
+ const hasAttachReferenceHandler = detail.type === 'initiative'
216
+ ? Boolean(onAttachWorkstreamToInitiativeByReference)
217
+ : Boolean(onAttachTaskToWorkstreamByReference);
218
+ const canAttachReference = Boolean(attachReference.trim()) && hasAttachReferenceHandler && !isAttachingReference;
219
+ const handleAttachReference = React.useCallback(async () => {
214
220
  const value = attachReference.trim();
215
- if (!value)
221
+ if (!value || isAttachingReference)
216
222
  return;
217
- if (detail.type === 'initiative') {
218
- onAttachWorkstreamToInitiativeByReference?.(detail.item.id, value);
223
+ const attach = detail.type === 'initiative'
224
+ ? onAttachWorkstreamToInitiativeByReference
225
+ : onAttachTaskToWorkstreamByReference;
226
+ if (!attach)
227
+ return;
228
+ setIsAttachingReference(true);
229
+ try {
230
+ const result = await attach(detail.item.id, value);
231
+ if (result !== false)
232
+ setAttachReference('');
219
233
  }
220
- else {
221
- onAttachTaskToWorkstreamByReference?.(detail.item.id, value);
234
+ finally {
235
+ setIsAttachingReference(false);
222
236
  }
223
- setAttachReference('');
224
237
  }, [
225
238
  attachReference,
226
239
  detail.item.id,
227
240
  detail.type,
241
+ isAttachingReference,
228
242
  onAttachTaskToWorkstreamByReference,
229
243
  onAttachWorkstreamToInitiativeByReference,
230
244
  ]);
@@ -318,15 +332,15 @@ function PlanningDrawerDetailView({ detail, currentWorkspaceId, onOpenNestedWork
318
332
  ? 'A top-level planning container that groups related workstreams.'
319
333
  : 'A coordination lane that groups related tasks and keeps execution organized.') }), _jsx("div", { className: panelStyles.progressTrack, "aria-label": `${detail.item.progressPercent}% complete`, children: _jsx("div", { className: panelStyles.progressFill, style: { width: `${detail.item.progressPercent}%` } }) }), _jsxs("div", { className: panelStyles.detailActionRow, children: [_jsx("button", { type: "button", className: styles.secondaryHeaderBtn, onClick: onEdit, children: _jsxs("span", { children: ["Edit ", detailLabel] }) }), detail.item.isArchived ? (_jsx("button", { type: "button", className: styles.secondaryHeaderBtn, onClick: onUnarchive, children: _jsx("span", { children: "Unarchive" }) })) : (_jsx("button", { type: "button", className: styles.secondaryHeaderBtn, onClick: onArchive, children: _jsx("span", { children: "Archive" }) }))] })] }), _jsxs("div", { className: panelStyles.metricGrid, children: [_jsxs("div", { className: panelStyles.metricCard, children: [_jsx("span", { className: panelStyles.metricValue, children: detail.item.taskCount }), _jsx("span", { className: panelStyles.metricLabel, children: "Tasks in scope" })] }), _jsxs("div", { className: panelStyles.metricCard, children: [_jsx("span", { className: panelStyles.metricValue, children: detail.item.completedTaskCount }), _jsx("span", { className: panelStyles.metricLabel, children: "Done / terminal" })] }), _jsxs("div", { className: panelStyles.metricCard, children: [_jsx("span", { className: panelStyles.metricValue, children: detail.item.ownerLabel || 'Unassigned' }), _jsx("span", { className: panelStyles.metricLabel, children: "Owner" })] }), _jsxs("div", { className: panelStyles.metricCard, children: [_jsx("span", { className: panelStyles.metricValue, children: (detail.item.commentCount || 0) + (detail.item.attachmentCount || 0) }), _jsx("span", { className: panelStyles.metricLabel, children: "Context items" })] })] }), _jsxs("div", { ref: setChildDropRef, className: `${panelStyles.listCard} ${canDropTask || canDropWorkstream ? panelStyles.sectionDropReady : ''} ${isChildDropOver ? panelStyles.sectionDropActive : ''} ${canDropTask || canDropWorkstream ? panelStyles.sectionDropMode : ''}`.trim(), children: [_jsxs("div", { className: panelStyles.sectionHeaderRow, children: [_jsx("div", { className: panelStyles.sectionTitle, children: childLabel }), _jsxs("div", { className: panelStyles.headerActionGroup, children: [archivedChildCount > 0 ? (_jsx("button", { type: "button", className: `tf-control-icon ${showArchivedChildren ? panelStyles.toggleActive : ''}`.trim(), title: showArchivedChildren ? `Hide ${archivedToggleLabel}` : `Show ${archivedToggleLabel}`, "aria-label": showArchivedChildren ? `Hide ${archivedToggleLabel}` : `Show ${archivedToggleLabel}`, "aria-pressed": showArchivedChildren, onClick: () => setShowArchivedChildren((current) => !current), children: showArchivedChildren ? _jsx(EyeOff, { size: 15 }) : _jsx(Eye, { size: 15 }) })) : null, detail.type === 'initiative' ? (_jsx("button", { type: "button", className: "tf-control-icon", title: "Create workstream in this initiative", "aria-label": "Create workstream in this initiative", onClick: () => onCreateWorkstreamInInitiative?.(detail.item.id), children: _jsx(Plus, { size: 15 }) })) : (_jsx("button", { type: "button", className: "tf-control-icon", title: "Create task in this workstream", "aria-label": "Create task in this workstream", onClick: () => onCreateTaskInWorkstream?.(detail.item.id), children: _jsx(Plus, { size: 15 }) }))] })] }), _jsxs("div", { className: panelStyles.attachReferenceRow, children: [_jsx(HierarchyActionBadge, { onClick: (event) => {
320
334
  event.stopPropagation();
321
- handleAttachReference();
322
- }, disabled: !attachReference.trim(), title: attachActionLabel, ariaLabel: attachActionLabel, children: _jsx(Link2, { size: 13 }) }), _jsx("span", { className: styles.taskHierarchyDivider, children: "/" }), _jsx("input", { type: "text", className: `${styles.input} ${panelStyles.attachReferenceInput}`.trim(), placeholder: detail.type === 'initiative'
335
+ void handleAttachReference();
336
+ }, disabled: !canAttachReference, title: attachActionLabel, ariaLabel: attachActionLabel, children: _jsx(Link2, { size: 13 }) }), _jsx("span", { className: styles.taskHierarchyDivider, children: "/" }), _jsx("input", { type: "text", className: `${styles.input} ${panelStyles.attachReferenceInput}`.trim(), placeholder: detail.type === 'initiative'
323
337
  ? 'Paste workstream reference (e.g. WS-123)'
324
- : 'Paste task reference (e.g. T-123)', value: attachReference, onChange: (event) => setAttachReference(event.target.value), onKeyDown: (event) => {
338
+ : 'Paste task reference (e.g. T-123)', disabled: isAttachingReference, value: attachReference, onChange: (event) => setAttachReference(event.target.value), onKeyDown: (event) => {
325
339
  if (event.key === 'Enter') {
326
340
  event.preventDefault();
327
- handleAttachReference();
341
+ void handleAttachReference();
328
342
  }
329
- } })] }), detail.type === 'initiative' ? (visibleChildItems.length > 0 ? visibleChildItems.map((workstream) => {
343
+ } }), _jsx("button", { type: "button", className: panelStyles.attachReferenceSubmit, disabled: !canAttachReference, onClick: () => void handleAttachReference(), children: isAttachingReference ? 'Attaching' : 'Attach' })] }), detail.type === 'initiative' ? (visibleChildItems.length > 0 ? visibleChildItems.map((workstream) => {
330
344
  const workstreamReferenceLabel = formatWorkstreamReference(workstream);
331
345
  const handleOpenWorkstream = () => onOpenNestedWorkstreamDetails?.(workstream.id);
332
346
  return (_jsx("div", { className: `${panelStyles.listItemButton} ${panelStyles.detailCardButton}`.trim(), children: _jsxs("div", { className: `${panelStyles.listItem} ${panelStyles.detailChildCard} ${workstream.isArchived ? panelStyles.detailChildCardArchived : ''}`.trim(), children: [_jsx("div", { className: panelStyles.detailChildCardTopRow, children: _jsxs("div", { className: panelStyles.detailChildReferenceRow, children: [_jsx(HierarchyActionBadge, { onClick: (event) => {
@@ -9,7 +9,11 @@ type PlanningInitiativeLike<TWorkstream extends PlanningWorkstreamLike> = {
9
9
  isArchived?: boolean;
10
10
  workstreams: TWorkstream[];
11
11
  };
12
+ type PlanningTaskLike = {
13
+ id: string;
14
+ };
12
15
  export declare function isPlanningEntityVisibleForScope(taskScope: PlanningTaskScope, isArchived: boolean): boolean;
16
+ export declare function scopeTasksToPlanningSelection<TTask extends PlanningTaskLike>(tasks: TTask[], selectedTaskIds: Set<string> | null): TTask[];
13
17
  export declare function scopePlanningSummaries<TWorkstream extends PlanningWorkstreamLike, TInitiative extends PlanningInitiativeLike<TWorkstream>>(initiatives: TInitiative[], workstreams: TWorkstream[], taskScope: PlanningTaskScope): {
14
18
  initiatives: TInitiative[];
15
19
  standaloneWorkstreams: TWorkstream[];
@@ -3,6 +3,11 @@ export function isPlanningEntityVisibleForScope(taskScope, isArchived) {
3
3
  return false;
4
4
  return taskScope === 'archived' ? isArchived : !isArchived;
5
5
  }
6
+ export function scopeTasksToPlanningSelection(tasks, selectedTaskIds) {
7
+ if (selectedTaskIds === null)
8
+ return tasks;
9
+ return tasks.filter((task) => selectedTaskIds.has(task.id));
10
+ }
6
11
  export function scopePlanningSummaries(initiatives, workstreams, taskScope) {
7
12
  const scopedWorkstreams = workstreams.filter((workstream) => (isPlanningEntityVisibleForScope(taskScope, Boolean(workstream.isArchived))));
8
13
  const visibleInitiatives = taskScope === 'deleted'
@@ -305,8 +305,13 @@ function inferAiProfileProviderModelDefaults(name) {
305
305
  return { provider: 'OpenAI', model: 'Codex' };
306
306
  if (normalized.includes('gemini'))
307
307
  return { provider: 'Google', model: 'Gemini' };
308
+ if (normalized.includes('grok'))
309
+ return { provider: 'xAI', model: 'Grok CLI' };
308
310
  return {};
309
311
  }
312
+ function formatCliAiProfileDescriptionName(name) {
313
+ return String(name || '').trim().replace(/\s+cli$/i, '') || 'AI';
314
+ }
310
315
  function inferAiProfileMetadataDefaults(name, surfaceType) {
311
316
  const inferredProviderModel = inferAiProfileProviderModelDefaults(name);
312
317
  switch (surfaceType) {
@@ -324,7 +329,7 @@ function inferAiProfileMetadataDefaults(name, surfaceType) {
324
329
  };
325
330
  case 'cli':
326
331
  return {
327
- description: `${name} CLI coding agent connected through MCP.`,
332
+ description: `${formatCliAiProfileDescriptionName(name)} CLI coding agent connected through MCP.`,
328
333
  role: 'CLI Coding Agent',
329
334
  ...inferredProviderModel
330
335
  };