@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.
- package/dist/Taskforce.module.css +39 -0
- package/dist/components/features/TaskSettings.js +26 -9
- package/dist/components/task/TaskKanban.d.ts +2 -2
- package/dist/components/views/StandaloneLayout.js +22 -22
- package/dist/components/views/panels/PlanningDrawer.d.ts +2 -2
- package/dist/components/views/panels/PlanningDrawer.js +26 -12
- package/dist/components/views/planningScope.d.ts +4 -0
- package/dist/components/views/planningScope.js +5 -0
- package/dist/core/AiProfileService.js +6 -1
- package/dist/hooks/useTaskforce.js +74 -17
- package/dist/hooks/workspace/workspaceLocalState.d.ts +25 -0
- package/dist/hooks/workspace/workspaceLocalState.js +38 -0
- package/dist/mcp/adminWorkspaceRegistrar.js +0 -10
- package/dist/mcp/clientIntegrations.d.ts +1 -1
- package/dist/mcp/clientIntegrations.js +50 -0
- package/dist/mcp/clientProfileDefaults.js +4 -0
- package/dist/mcp/collaborationRegistrar.js +3 -3
- package/dist/mcp/documentAssetRegistrar.js +24 -72
- package/dist/mcp/documentHelpers.d.ts +11 -4
- package/dist/mcp/documentHelpers.js +21 -17
- package/dist/mcp/runtime.d.ts +4 -0
- package/dist/mcp/runtime.js +466 -534
- package/dist/mcp/taskPlanningRegistrar.js +45 -81
- package/dist/mcp/toolProfiles.d.ts +7 -0
- package/dist/mcp/toolProfiles.js +36 -0
- package/dist/mcp/toolRegistry.d.ts +87 -609
- package/dist/mcp/toolRegistry.js +93 -88
- package/dist/resources/templates/workflow-sources/workflowDocs.mjs +10 -10
- package/dist/resources/templates/workflows/collaborate.yaml +2 -3
- package/dist/resources/templates/workflows/do.yaml +2 -3
- package/dist/resources/templates/workflows/evaluate.yaml +2 -3
- package/dist/resources/templates/workflows/execute.yaml +12 -15
- package/dist/resources/templates/workflows/plan.yaml +3 -4
- package/dist/resources/templates/workflows/review.yaml +2 -3
- package/dist/shared/taxonomyLibrary.js +1 -1
- package/dist/sync/cloudSyncApi.js +22 -3
- package/dist/types.js +1 -1
- package/dist/ui/assets/{AgentsModule-DvkbUHi0.js → AgentsModule-C4LcqhcZ.js} +1 -1
- package/dist/ui/assets/{AnnotatedAttachmentWorkspace-jhOhxqHT.js → AnnotatedAttachmentWorkspace-B-Dq22d2.js} +1 -1
- package/dist/ui/assets/{ContextAttachmentManager-BAmEyLHL.js → ContextAttachmentManager-DsXPTvi-.js} +1 -1
- package/dist/ui/assets/{DocumentWorkspace-X-CpD5OO.js → DocumentWorkspace-CxA1wO3U.js} +1 -1
- package/dist/ui/assets/{EntityActivityTimeline-DD33onAC.js → EntityActivityTimeline-B8nSe8l3.js} +1 -1
- package/dist/ui/assets/{InitiativesModule-5nDXtIjf.js → InitiativesModule-VY95v38E.js} +1 -1
- package/dist/ui/assets/{PlansPage-B8nDR1hQ.js → PlansPage-CQbKDv1a.js} +1 -1
- package/dist/ui/assets/{TaskContextUpload-DrGrXJHm.js → TaskContextUpload-DvDlNpHC.js} +1 -1
- package/dist/ui/assets/TaskSettings-CUWbYYtP.js +12 -0
- package/dist/ui/assets/{WorkflowsModule-DWCGhEWT.js → WorkflowsModule-CgM3Jako.js} +1 -1
- package/dist/ui/assets/documentReferences-Do9YjrS3.js +1 -0
- package/dist/ui/assets/index--mh3pktj.js +5 -0
- package/dist/ui/assets/index-BawaaEXS.css +1 -0
- package/dist/ui/index.html +2 -2
- package/dist/utils/theme.js +1 -1
- package/package.json +1 -1
- package/src/resources/templates/workflow-sources/workflowDocs.mjs +10 -10
- package/src/resources/templates/workflows/collaborate.yaml +2 -3
- package/src/resources/templates/workflows/do.yaml +2 -3
- package/src/resources/templates/workflows/evaluate.yaml +2 -3
- package/src/resources/templates/workflows/execute.yaml +12 -15
- package/src/resources/templates/workflows/plan.yaml +3 -4
- package/src/resources/templates/workflows/review.yaml +2 -3
- package/dist/ui/assets/TaskSettings-BXVvZZwH.js +0 -9
- package/dist/ui/assets/documentReferences-Crup321p.js +0 -1
- package/dist/ui/assets/index-B9k24Aym.js +0 -5
- 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:
|
|
273
|
-
const highlighted = { color:
|
|
274
|
-
const bracketColor = { color:
|
|
275
|
-
const normalText = { color:
|
|
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:
|
|
288
|
-
const highlighted = { color:
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
218
|
-
onAttachWorkstreamToInitiativeByReference
|
|
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
|
-
|
|
221
|
-
|
|
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: !
|
|
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
|
};
|