@taskforcehq/taskforce 0.3.319 → 0.3.321

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 (23) hide show
  1. package/dist/TaskforceCore.js +65 -19
  2. package/dist/hooks/workspace/useTaskforceWorkspaceBootstrap.js +29 -5
  3. package/dist/mcp/runtime.js +72 -10
  4. package/dist/mcp/taskPlanningRegistrar.js +1 -0
  5. package/dist/server/routes/admin.js +40 -29
  6. package/dist/server/routes/auth.js +24 -0
  7. package/dist/server/routes.js +18 -27
  8. package/dist/ui/assets/{AgentsModule-DxsN8GQI.js → AgentsModule-7jpDeuVR.js} +1 -1
  9. package/dist/ui/assets/{AnnotatedAttachmentWorkspace-DUkRcfo7.js → AnnotatedAttachmentWorkspace-BJLe8DFj.js} +1 -1
  10. package/dist/ui/assets/{ContextAttachmentManager-D_uqJ94r.js → ContextAttachmentManager-AmEny3eI.js} +1 -1
  11. package/dist/ui/assets/{DocumentWorkspace-DAsuvSWR.js → DocumentWorkspace-Cqdq31Ha.js} +1 -1
  12. package/dist/ui/assets/{EntityActivityTimeline-QLUNczJS.js → EntityActivityTimeline-BlS7DhA4.js} +1 -1
  13. package/dist/ui/assets/{InitiativesModule-D23morXe.js → InitiativesModule-C1qfO1uZ.js} +1 -1
  14. package/dist/ui/assets/{PlansPage-X6c0yJ1a.js → PlansPage-DFt5w-0Y.js} +1 -1
  15. package/dist/ui/assets/{TaskContextUpload-BERMUT_S.js → TaskContextUpload-zuro5gDT.js} +1 -1
  16. package/dist/ui/assets/{TaskSettings-DlX_5CSg.js → TaskSettings-CJPC2Bft.js} +1 -1
  17. package/dist/ui/assets/{WorkflowsModule-CVu4lhC1.js → WorkflowsModule-CEEGuxsf.js} +1 -1
  18. package/dist/ui/assets/documentReferences-Ca2P--i5.js +1 -0
  19. package/dist/ui/assets/index-CGDQY0nN.js +5 -0
  20. package/dist/ui/index.html +1 -1
  21. package/package.json +1 -1
  22. package/dist/ui/assets/documentReferences-BCXQQ60P.js +0 -1
  23. package/dist/ui/assets/index-CdlMnBxP.js +0 -5
@@ -54,6 +54,7 @@ function TaskforceCoreWithRouter({ config = {}, initialTaskId, onTaskCountChange
54
54
  const isPricingRoute = location.pathname === '/pricing';
55
55
  const isPlansRoute = location.pathname === '/plans';
56
56
  const isPlansScreen = location.pathname === '/' && String(routeQuery.get('screen') || '').trim().toLowerCase() === 'plans';
57
+ const plansPath = buildPlansPath();
57
58
  const planSelectionPath = buildPlansPath({ gate: 'plan_selection_required' });
58
59
  const navigate = useNavigate();
59
60
  const [loginEmail, setLoginEmail] = useState('');
@@ -579,14 +580,44 @@ function TaskforceCoreWithRouter({ config = {}, initialTaskId, onTaskCountChange
579
580
  persistWorkspaceSyncStatePatch,
580
581
  refreshSetupContext
581
582
  ]);
582
- const nextPath = useMemo(() => {
583
+ const nextTarget = useMemo(() => {
583
584
  const raw = new URLSearchParams(location.search).get('next') || '/';
584
- if (!raw.startsWith('/'))
585
- return '/';
586
- if (raw === '/login')
585
+ if (raw.startsWith('/')) {
586
+ if (raw === '/login')
587
+ return '/';
588
+ return raw;
589
+ }
590
+ try {
591
+ const parsed = new URL(raw);
592
+ const host = parsed.hostname.toLowerCase();
593
+ const isLocal = host === 'localhost' || host === '127.0.0.1' || host === '::1';
594
+ const isTaskforceHost = host === 'taskforcehq.ai' || host.endsWith('.taskforcehq.ai');
595
+ if ((parsed.protocol === 'http:' || parsed.protocol === 'https:') && (isLocal || isTaskforceHost)) {
596
+ return parsed.toString();
597
+ }
598
+ }
599
+ catch {
587
600
  return '/';
588
- return raw;
601
+ }
602
+ return '/';
589
603
  }, [location.search]);
604
+ const nextPath = useMemo(() => nextTarget.startsWith('/') ? nextTarget : '/', [nextTarget]);
605
+ const navigateToPostAuthTarget = useCallback((target, options) => {
606
+ if (!target)
607
+ return;
608
+ if (target.startsWith('/')) {
609
+ navigate(target, { replace: options?.replace === true });
610
+ return;
611
+ }
612
+ if (typeof window !== 'undefined') {
613
+ if (options?.replace === true) {
614
+ window.location.replace(target);
615
+ }
616
+ else {
617
+ window.location.assign(target);
618
+ }
619
+ }
620
+ }, [navigate]);
590
621
  const shouldReturnToCloudWorkspaceSelection = useMemo(() => {
591
622
  try {
592
623
  const parsed = new URL(nextPath, 'https://taskforce.local');
@@ -622,9 +653,9 @@ function TaskforceCoreWithRouter({ config = {}, initialTaskId, onTaskCountChange
622
653
  interval: selectedSignupPlanVersionId ? resolvedSignupInterval : null
623
654
  });
624
655
  }
625
- return nextPath;
656
+ return nextTarget;
626
657
  }, [
627
- nextPath,
658
+ nextTarget,
628
659
  planSelectionPath,
629
660
  resolvedSignupInterval,
630
661
  selectedSignupPlanId,
@@ -688,9 +719,13 @@ function TaskforceCoreWithRouter({ config = {}, initialTaskId, onTaskCountChange
688
719
  ? resolveWorkspaceSetupPath()
689
720
  : pendingPostAuthRedirect.path;
690
721
  const currentRoute = `${location.pathname}${location.search}${location.hash || ''}`;
691
- if (currentRoute !== destination) {
692
- navigate(destination, { replace: true });
722
+ if (destination.startsWith('/')) {
723
+ if (currentRoute !== destination) {
724
+ navigate(destination, { replace: true });
725
+ }
726
+ return;
693
727
  }
728
+ navigateToPostAuthTarget(destination, { replace: true });
694
729
  }, [
695
730
  authSessionResolved,
696
731
  cloudAccountAccessPending,
@@ -699,6 +734,7 @@ function TaskforceCoreWithRouter({ config = {}, initialTaskId, onTaskCountChange
699
734
  location.pathname,
700
735
  location.search,
701
736
  navigate,
737
+ navigateToPostAuthTarget,
702
738
  pendingPostAuthRedirect,
703
739
  resolveWorkspaceSetupPath
704
740
  ]);
@@ -726,9 +762,13 @@ function TaskforceCoreWithRouter({ config = {}, initialTaskId, onTaskCountChange
726
762
  ? pendingPostAuthRedirect.path
727
763
  : planSelectionPath;
728
764
  const currentRoute = `${location.pathname}${location.search}${location.hash || ''}`;
729
- if (currentRoute !== destination) {
730
- navigate(destination, { replace: true });
765
+ if (destination.startsWith('/')) {
766
+ if (currentRoute !== destination) {
767
+ navigate(destination, { replace: true });
768
+ }
769
+ return;
731
770
  }
771
+ navigateToPostAuthTarget(destination, { replace: true });
732
772
  }, [
733
773
  accountAccessGate?.canAccessApp,
734
774
  accountAccessGateResolved,
@@ -739,6 +779,7 @@ function TaskforceCoreWithRouter({ config = {}, initialTaskId, onTaskCountChange
739
779
  location.pathname,
740
780
  location.search,
741
781
  navigate,
782
+ navigateToPostAuthTarget,
742
783
  pendingPostAuthRedirect,
743
784
  planSelectionPath
744
785
  ]);
@@ -901,8 +942,9 @@ function TaskforceCoreWithRouter({ config = {}, initialTaskId, onTaskCountChange
901
942
  if (shouldDelayAuthenticatedLoginRedirect) {
902
943
  return;
903
944
  }
904
- if (isLoginRoute && isAuthenticated && !shouldStayOnInviteRoute)
905
- navigate(nextPath, { replace: true });
945
+ if (isLoginRoute && isAuthenticated && !shouldStayOnInviteRoute) {
946
+ navigateToPostAuthTarget(nextTarget, { replace: true });
947
+ }
906
948
  }
907
949
  else {
908
950
  if (!isLoginRoute) {
@@ -1243,7 +1285,7 @@ function TaskforceCoreWithRouter({ config = {}, initialTaskId, onTaskCountChange
1243
1285
  || pendingVerificationCommercialState === 'checkout_pending';
1244
1286
  setCommercialOnboardingRedirectActive(commercialOnboardingGate);
1245
1287
  setPendingPostAuthRedirect({
1246
- path: pendingVerificationRedirectPath || nextPath,
1288
+ path: pendingVerificationRedirectPath || nextTarget,
1247
1289
  workspaceSetupRequired: pendingVerificationWorkspaceSetupRequired,
1248
1290
  commercialOnboardingGate
1249
1291
  });
@@ -1299,7 +1341,7 @@ function TaskforceCoreWithRouter({ config = {}, initialTaskId, onTaskCountChange
1299
1341
  setLoginBusy(false);
1300
1342
  return;
1301
1343
  }
1302
- navigate(nextPath, { replace: true });
1344
+ navigateToPostAuthTarget(nextTarget, { replace: true });
1303
1345
  }
1304
1346
  else if (authMode === 'login') {
1305
1347
  if (inviteToken.trim()) {
@@ -1319,14 +1361,14 @@ function TaskforceCoreWithRouter({ config = {}, initialTaskId, onTaskCountChange
1319
1361
  setLoginDisplayName('');
1320
1362
  setLoginPassword('');
1321
1363
  setLoginPasswordConfirm('');
1322
- navigate(nextPath, { replace: true });
1364
+ navigateToPostAuthTarget(nextTarget, { replace: true });
1323
1365
  }
1324
1366
  else {
1325
1367
  setLoginEmail('');
1326
1368
  setLoginDisplayName('');
1327
1369
  setLoginPassword('');
1328
1370
  setLoginPasswordConfirm('');
1329
- navigate(nextPath, { replace: true });
1371
+ navigateToPostAuthTarget(nextTarget, { replace: true });
1330
1372
  }
1331
1373
  setLoginBusy(false);
1332
1374
  }, children: loginBusy ? 'Working...' : (authMode === 'login' ? 'Sign In'
@@ -1567,6 +1609,11 @@ function TaskforceCoreWithRouter({ config = {}, initialTaskId, onTaskCountChange
1567
1609
  });
1568
1610
  const result = createResult || saveResult;
1569
1611
  if (!result?.success) {
1612
+ if (result?.code === 'WORKSPACE_LIMIT_REACHED') {
1613
+ setSetupBusy(false);
1614
+ navigate(plansPath, { replace: true });
1615
+ return;
1616
+ }
1570
1617
  setSetupError(result?.error || t('setup.saveWorkspaceFailed'));
1571
1618
  }
1572
1619
  else {
@@ -1579,8 +1626,7 @@ function TaskforceCoreWithRouter({ config = {}, initialTaskId, onTaskCountChange
1579
1626
  setSetupBusy(false);
1580
1627
  return;
1581
1628
  }
1582
- const shouldApplyStarterPack = workspaceSetupSource === 'local'
1583
- && Boolean(selectedSetupPack)
1629
+ const shouldApplyStarterPack = Boolean(selectedSetupPack)
1584
1630
  && (setupTaxonomySections.categories || setupTaxonomySections.types || setupTaxonomySections.priorities);
1585
1631
  if (shouldApplyStarterPack && selectedSetupPack) {
1586
1632
  const applyResult = await settingsModel.onApplySystemTaxonomyPack({
@@ -622,10 +622,34 @@ export function useTaskforceWorkspaceBootstrap(args) {
622
622
  setWorkspaceBootstrapPending(true);
623
623
  try {
624
624
  await fetchBootstrapConfig({ ignoreAuthGuard });
625
- await Promise.all([
626
- fetchTasks(tasksSilent, { ignoreAuthGuard }),
627
- fetchReferenceData({ ignoreAuthGuard })
628
- ]);
625
+ const workspaceId = String(currentWorkspaceIdRef.current || 'default').trim() || 'default';
626
+ const deferredStartedAt = getBootstrapTimestamp();
627
+ void fetchTasks(tasksSilent, { ignoreAuthGuard }).then(() => {
628
+ logBootstrapDebug('cloud_bootstrap_tasks_loaded_deferred', {
629
+ reason,
630
+ workspaceId,
631
+ durationMs: Math.max(0, Math.round(getBootstrapTimestamp() - deferredStartedAt))
632
+ });
633
+ }).catch((error) => {
634
+ logBootstrapDebug('cloud_bootstrap_tasks_deferred_failed', {
635
+ reason,
636
+ workspaceId,
637
+ error: error instanceof Error ? error.message : String(error)
638
+ });
639
+ });
640
+ void fetchReferenceData({ ignoreAuthGuard }).then(() => {
641
+ logBootstrapDebug('cloud_bootstrap_reference_data_loaded_deferred', {
642
+ reason,
643
+ workspaceId,
644
+ durationMs: Math.max(0, Math.round(getBootstrapTimestamp() - deferredStartedAt))
645
+ });
646
+ }).catch((error) => {
647
+ logBootstrapDebug('cloud_bootstrap_reference_data_deferred_failed', {
648
+ reason,
649
+ workspaceId,
650
+ error: error instanceof Error ? error.message : String(error)
651
+ });
652
+ });
629
653
  void fetchPlanningEntities({ ignoreAuthGuard });
630
654
  if (includeDeferredArchive) {
631
655
  scheduleDeferredArchiveBootstrap();
@@ -636,7 +660,7 @@ export function useTaskforceWorkspaceBootstrap(args) {
636
660
  authenticated: true,
637
661
  workspaceSetupRequired: false,
638
662
  success: true,
639
- workspaceId: String(currentWorkspaceIdRef.current || 'default').trim() || 'default'
663
+ workspaceId
640
664
  });
641
665
  return { success: true, authenticated: true, workspaceSetupRequired: false };
642
666
  }
@@ -740,6 +740,21 @@ export function createTaskforceMcpServer(options = {}) {
740
740
  }))
741
741
  .filter((actor) => Boolean(actor.id));
742
742
  }
743
+ function buildTaskWriteSummary(task) {
744
+ return {
745
+ id: task?.id || null,
746
+ referenceLabel: getTaskReferenceLabel(task) || null,
747
+ title: task?.title || null,
748
+ status: task?.status || null,
749
+ assignee: task?.assignee || null,
750
+ workstreamId: task?.workstreamId || null,
751
+ scheduledDate: task?.scheduledDate || null,
752
+ dueDate: task?.dueDate || null,
753
+ scheduledWeekKey: task?.scheduledWeekKey || null,
754
+ orderInDay: task?.orderInDay ?? null,
755
+ updatedAt: task?.updatedAt || null,
756
+ };
757
+ }
743
758
  function buildAiProfileDescriptor(profile) {
744
759
  return {
745
760
  id: profile.id,
@@ -2178,7 +2193,7 @@ export function createTaskforceMcpServer(options = {}) {
2178
2193
  const assigneeHint = describeEligibleAssignees();
2179
2194
  const mutatingProfileTokenProperty = {
2180
2195
  type: "string",
2181
- description: "Optional AI profile token returned by resolve_profile. Pass this when your MCP client does not preserve session identity between calls."
2196
+ description: "Optional AI profile token returned by resolve_profile. Pass this when your MCP client does not preserve session identity between calls. In stateless/cloud MCP clients without ambient AI binding, this is usually required on mutating calls after bootstrap."
2182
2197
  };
2183
2198
  const prefix = `[Project: ${PROJECT_NAME}] `;
2184
2199
  const agentNote = runtimeMode === 'local'
@@ -2190,11 +2205,32 @@ export function createTaskforceMcpServer(options = {}) {
2190
2205
  .filter(Boolean)
2191
2206
  .map((part) => `${part.charAt(0).toUpperCase()}${part.slice(1)}`)
2192
2207
  .join(' ');
2208
+ const nonDestructiveWriteTools = new Set([
2209
+ 'add_annotated_attachment_marker',
2210
+ 'add_comment',
2211
+ 'add_document_review_comment',
2212
+ 'bulk_add_annotated_attachment_markers',
2213
+ 'create_annotated_attachment_session',
2214
+ 'create_document_review_session',
2215
+ 'create_initiative',
2216
+ 'create_task',
2217
+ 'create_workstream',
2218
+ 'link_task_context',
2219
+ 'resolve_profile',
2220
+ 'review_document',
2221
+ 'review_image',
2222
+ ]);
2193
2223
  const annotationsForTool = (name) => {
2194
2224
  const title = titleForTool(name);
2195
- return isMcpWriteToolName(name)
2196
- ? { title, destructiveHint: true }
2197
- : { title, readOnlyHint: true };
2225
+ if (!isMcpWriteToolName(name)) {
2226
+ return { title, readOnlyHint: true, destructiveHint: false, openWorldHint: false };
2227
+ }
2228
+ return {
2229
+ title,
2230
+ readOnlyHint: false,
2231
+ destructiveHint: !nonDestructiveWriteTools.has(name),
2232
+ openWorldHint: false,
2233
+ };
2198
2234
  };
2199
2235
  const tools = registeredTools
2200
2236
  .map((tool) => tool.buildDescriptor({
@@ -2915,7 +2951,13 @@ export function createTaskforceMcpServer(options = {}) {
2915
2951
  workstreamIds: [resolvedWorkstreamId],
2916
2952
  });
2917
2953
  return {
2918
- content: [{ type: "text", text: JSON.stringify(updated, null, 2) }],
2954
+ content: [{
2955
+ type: "text",
2956
+ text: JSON.stringify({
2957
+ task: buildTaskWriteSummary(updated),
2958
+ workstreamId: resolvedWorkstreamId,
2959
+ }, null, 2)
2960
+ }],
2919
2961
  };
2920
2962
  }
2921
2963
  case "clear_task_workstream": {
@@ -2935,7 +2977,14 @@ export function createTaskforceMcpServer(options = {}) {
2935
2977
  ...(priorWorkstreamId ? { includeWorkstreams: true, workstreamIds: [priorWorkstreamId] } : {}),
2936
2978
  });
2937
2979
  return {
2938
- content: [{ type: "text", text: JSON.stringify(updated, null, 2) }],
2980
+ content: [{
2981
+ type: "text",
2982
+ text: JSON.stringify({
2983
+ task: buildTaskWriteSummary(updated),
2984
+ workstreamId: null,
2985
+ previousWorkstreamId: priorWorkstreamId,
2986
+ }, null, 2)
2987
+ }],
2939
2988
  };
2940
2989
  }
2941
2990
  case "set_workstream_initiative": {
@@ -4001,7 +4050,7 @@ export function createTaskforceMcpServer(options = {}) {
4001
4050
  persistedOrderInDay: result.task.orderInDay ?? null
4002
4051
  },
4003
4052
  warnings: result.warnings,
4004
- task: result.task
4053
+ task: buildTaskWriteSummary(result.task)
4005
4054
  }, null, 2)
4006
4055
  }],
4007
4056
  };
@@ -4154,6 +4203,7 @@ export function createTaskforceMcpServer(options = {}) {
4154
4203
  priority: resolveNumberOption('priority', createArgs.priority, getActivePriorities().map(p => ({ value: p.value, label: p.label }))),
4155
4204
  approach: resolveStringOption('approach', createArgs.approach, getActiveApproaches().map(a => ({ value: a.value, label: a.label }))),
4156
4205
  assignee: resolveRuntimeTaskAssigneeInput(createAssigneeInput),
4206
+ workstreamId: createArgs.workstreamId ? resolveWorkstreamByIdentifierOrThrow(createArgs.workstreamId).id : undefined,
4157
4207
  status: resolveStringOption('status', createArgs.status, STATUS_OPTIONS),
4158
4208
  complexity: resolveNumberOption('complexity', createArgs.complexity, COMPLEXITY_OPTIONS)
4159
4209
  };
@@ -4219,7 +4269,7 @@ export function createTaskforceMcpServer(options = {}) {
4219
4269
  content: [{
4220
4270
  type: "text",
4221
4271
  text: JSON.stringify({
4222
- task: updated,
4272
+ task: buildTaskWriteSummary(updated),
4223
4273
  started: true,
4224
4274
  claimed: resolved.claimed && updated.assignee === activeAiProfileId,
4225
4275
  statusChanged: resolved.previousStatus !== String(updated.status || '').trim(),
@@ -4294,7 +4344,13 @@ export function createTaskforceMcpServer(options = {}) {
4294
4344
  includeStats: true,
4295
4345
  });
4296
4346
  return {
4297
- content: [{ type: "text", text: JSON.stringify(updated, null, 2) }],
4347
+ content: [{
4348
+ type: "text",
4349
+ text: JSON.stringify({
4350
+ task: buildTaskWriteSummary(updated),
4351
+ changedFields: Object.keys(finalUpdates),
4352
+ }, null, 2)
4353
+ }],
4298
4354
  };
4299
4355
  }
4300
4356
  catch (error) {
@@ -4385,7 +4441,13 @@ export function createTaskforceMcpServer(options = {}) {
4385
4441
  includeStats: true,
4386
4442
  });
4387
4443
  return {
4388
- content: [{ type: "text", text: JSON.stringify(updated, null, 2) }],
4444
+ content: [{
4445
+ type: "text",
4446
+ text: JSON.stringify({
4447
+ task: buildTaskWriteSummary(updated),
4448
+ assignee: resolvedAssignee,
4449
+ }, null, 2)
4450
+ }],
4389
4451
  };
4390
4452
  }
4391
4453
  case "bulk_assign_tasks": {
@@ -353,6 +353,7 @@ export function registerTaskPlanningTools(registerTool, executeTool) {
353
353
  title: stringProperty('Task title.'),
354
354
  description: stringProperty('Optional task description.'),
355
355
  assignee: stringProperty(`${context.assigneeHint} Call list_assignees to inspect the current eligible IDs.`),
356
+ workstreamId: stringProperty('Optional workstream ID or workstream reference such as WS-123. Use this to create the task directly inside a workstream without a separate set_task_workstream call.'),
356
357
  category: stringProperty('Task category value.'),
357
358
  type: stringProperty('Task type value.', context.typeEnum),
358
359
  priority: numberProperty(context.priorityDesc),
@@ -262,7 +262,7 @@ export function registerAdminRoutes(deps) {
262
262
  const scopeLabels = {
263
263
  'tasks:read': 'Read tasks',
264
264
  'tasks:write': 'Update tasks',
265
- 'workspace:read': 'Read workspace details'
265
+ 'workspace:read': 'Read workspace'
266
266
  };
267
267
  const options = input.workspaces.map((workspace) => {
268
268
  const selected = workspace.id === input.preselectedWorkspaceId ? ' selected' : '';
@@ -285,25 +285,19 @@ export function registerAdminRoutes(deps) {
285
285
  :root { color-scheme: dark; }
286
286
  body { margin: 0; font-family: "Outfit", -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif; background: radial-gradient(circle at top, rgba(255,138,0,0.08), transparent 30%), #111126; color: #f8f7ff; }
287
287
  .wrap { min-height: 100vh; display: flex; align-items: center; justify-content: center; padding: 32px 24px; }
288
- .card { width: min(680px, 100%); background: rgba(22,22,44,0.95); border: 1px solid rgba(255,255,255,0.08); border-radius: 36px; box-shadow: 0 24px 80px rgba(0,0,0,0.35); padding: 44px 56px; }
289
- .eyebrow { display: inline-flex; align-items: center; gap: 10px; margin-bottom: 18px; color: rgba(255,255,255,0.55); font-size: 12px; font-weight: 700; letter-spacing: 0.12em; text-transform: uppercase; }
290
- .eyebrow-mark { width: 10px; height: 10px; border-radius: 999px; background: linear-gradient(135deg, #ff8a00 0%, #8d6bff 100%); box-shadow: 0 0 18px rgba(255,138,0,0.35); }
291
- h1 { margin: 0; font-size: clamp(2.25rem, 5vw, 3rem); line-height: 0.96; letter-spacing: -0.04em; color: #f8f7ff; }
292
- .brand-word { font-family: "Russo One", sans-serif; font-size: 0.94em; letter-spacing: -0.03em; }
293
- .lede { margin: 18px 0 8px; font-size: 1.15rem; line-height: 1.5; color: rgba(255,255,255,0.78); max-width: 34ch; }
288
+ .card { width: min(720px, 100%); background: rgba(22,22,44,0.95); border: 1px solid rgba(255,255,255,0.08); border-radius: 32px; box-shadow: 0 24px 80px rgba(0,0,0,0.35); padding: 40px 48px; }
289
+ h1 { margin: 0; font-size: clamp(1.95rem, 4.2vw, 2.6rem); line-height: 1.02; letter-spacing: -0.035em; color: #f8f7ff; }
290
+ .lede { margin: 16px 0 8px; font-size: 1.1rem; line-height: 1.45; color: rgba(255,255,255,0.78); max-width: 48ch; text-wrap: pretty; }
294
291
  .lede strong { color: #fff; }
295
- .user-meta { margin: 0 0 28px; color: rgba(255,255,255,0.5); font-size: 0.9rem; }
292
+ .session-row { display: flex; align-items: center; gap: 14px; flex-wrap: wrap; margin: 0 0 28px; }
293
+ .user-meta { margin: 0; color: rgba(255,255,255,0.5); font-size: 0.9rem; }
294
+ .account-switch { padding: 0; border: 0; min-width: 0; background: transparent; color: #cbd8ff; font-size: 0.9rem; font-weight: 600; box-shadow: none; }
295
+ .account-switch:hover { transform: none; color: #ffffff; text-decoration: underline; }
296
296
  label { display: block; font-size: 13px; font-weight: 700; margin: 24px 0 10px; color: rgba(255,255,255,0.92); letter-spacing: 0.01em; }
297
297
  select { width: 100%; border-radius: 18px; border: 1px solid rgba(255,255,255,0.1); background: #1f2039; color: #fff; padding: 16px 18px; font-size: 15px; font-weight: 600; box-shadow: inset 0 1px 0 rgba(255,255,255,0.02); }
298
298
  .scopes { display:flex; flex-wrap:wrap; gap:10px; margin-top:8px; }
299
299
  .scope-chip { display:inline-flex; padding:8px 14px; border-radius:999px; background:#2a2947; border:1px solid rgba(255,255,255,0.09); font-size:13px; font-weight:600; color:rgba(255,255,255,0.92); }
300
- .details { margin-top: 24px; padding: 0; border: 1px solid rgba(255,255,255,0.08); border-radius: 18px; background: rgba(255,255,255,0.04); overflow: hidden; }
301
- .details summary { list-style: none; cursor: pointer; padding: 16px 18px; font-size: 0.92rem; font-weight: 700; color: rgba(255,255,255,0.86); }
302
- .details summary::-webkit-details-marker { display: none; }
303
- .details-body { padding: 0 18px 18px; color: rgba(255,255,255,0.68); font-size: 0.9rem; line-height: 1.5; }
304
- .details-row { margin: 0 0 8px; }
305
- .details-row strong { color: rgba(255,255,255,0.92); }
306
- .legal { margin: 24px 2px 0; color: rgba(255,255,255,0.58); font-size: 0.9rem; line-height: 1.55; max-width: 56ch; }
300
+ .legal { margin: 28px 2px 0; color: rgba(255,255,255,0.58); font-size: 0.9rem; line-height: 1.55; max-width: 58ch; }
307
301
  .error { margin-bottom: 18px; padding: 12px 14px; border-radius: 14px; background: rgba(255,99,132,0.12); color: #ffb6c6; border: 1px solid rgba(255,99,132,0.25); }
308
302
  .actions { display:flex; justify-content:flex-end; gap:14px; margin-top: 36px; }
309
303
  button { border-radius: 18px; padding: 15px 22px; font-size: 15px; font-weight: 700; cursor: pointer; border: 1px solid rgba(255,255,255,0.08); min-width: 156px; transition: transform 160ms ease, box-shadow 160ms ease, border-color 160ms ease, background-color 160ms ease; }
@@ -313,7 +307,8 @@ export function registerAdminRoutes(deps) {
313
307
  a { color: #cbd8ff; }
314
308
  @media (max-width: 720px) {
315
309
  .card { padding: 30px 22px; border-radius: 28px; }
316
- h1 { font-size: 2rem; }
310
+ h1 { font-size: 1.85rem; }
311
+ .lede { max-width: none; }
317
312
  .actions { flex-direction: column-reverse; }
318
313
  button { width: 100%; }
319
314
  }
@@ -322,22 +317,17 @@ export function registerAdminRoutes(deps) {
322
317
  <body>
323
318
  <div class="wrap">
324
319
  <form class="card" method="post" action="/oauth/mcp/authorize">
325
- <div class="eyebrow"><span class="eyebrow-mark"></span>Taskforce MCP</div>
326
- <h1>Authorize <span class="brand-word">Taskforce</span> Connector</h1>
320
+ <h1>Authorize Taskforce Connector</h1>
327
321
  <p class="lede"><strong>${escapeHtml(input.clientName)}</strong> wants to connect to Taskforce for <strong>${escapeHtml(input.userName)}</strong>.</p>
328
- ${input.userMeta ? `<p class="user-meta">Signed in as ${escapeHtml(input.userMeta)}</p>` : ''}
322
+ <div class="session-row">
323
+ ${input.userMeta ? `<p class="user-meta">Signed in as ${escapeHtml(input.userMeta)}</p>` : ''}
324
+ <button class="account-switch" type="submit" formaction="/api/taskforce/auth/logout" formmethod="post" formnovalidate>Use different account</button>
325
+ </div>
329
326
  ${input.error ? `<div class="error">${escapeHtml(input.error)}</div>` : ''}
330
- <label for="workspaceId">Workspace</label>
327
+ <label for="workspaceId">Select Workspace</label>
331
328
  <select id="workspaceId" name="workspaceId" required>${options}</select>
332
329
  <label>Requested Permissions</label>
333
330
  <div class="scopes">${scopeBadges}</div>
334
- <details class="details">
335
- <summary>Technical details</summary>
336
- <div class="details-body">
337
- <div class="details-row"><strong>Client:</strong> ${escapeHtml(input.clientName)}</div>
338
- <div class="details-row"><strong>Redirect URI:</strong> ${escapeHtml(input.redirectUri)}</div>
339
- </div>
340
- </details>
341
331
  <p class="legal">By authorizing this connector, you agree to Taskforce&apos;s <a href="${termsUrl}" target="_blank" rel="noopener noreferrer">Terms of Service</a> and acknowledge the <a href="${privacyUrl}" target="_blank" rel="noopener noreferrer">Privacy Policy</a>.</p>
342
332
  <input type="hidden" name="client_id" value="${escapeHtml(input.clientId)}" />
343
333
  <input type="hidden" name="redirect_uri" value="${escapeHtml(input.redirectUri)}" />
@@ -345,6 +335,7 @@ export function registerAdminRoutes(deps) {
345
335
  <input type="hidden" name="state" value="${escapeHtml(input.state)}" />
346
336
  <input type="hidden" name="code_challenge" value="${escapeHtml(input.codeChallenge)}" />
347
337
  <input type="hidden" name="code_challenge_method" value="${escapeHtml(input.codeChallengeMethod)}" />
338
+ <input type="hidden" name="next" value="${escapeHtml(input.authorizeUrl)}" />
348
339
  <div class="actions">
349
340
  <button type="submit" name="decision" value="deny">Cancel</button>
350
341
  <button type="submit" name="decision" value="approve">Authorize</button>
@@ -2667,8 +2658,18 @@ export function registerAdminRoutes(deps) {
2667
2658
  const userIdentity = core.resolveUserIdentity(access.userId);
2668
2659
  const userName = String(userIdentity?.displayName || userIdentity?.email || access.userId).trim() || String(access.userId);
2669
2660
  const userMeta = userIdentity?.email && userIdentity.email !== userName ? String(userIdentity.email).trim() : null;
2661
+ const authorizeUrl = new URL('/oauth/mcp/authorize', resolveMcpOauthIssuer(req));
2662
+ authorizeUrl.searchParams.set('client_id', clientId);
2663
+ authorizeUrl.searchParams.set('redirect_uri', redirectUri);
2664
+ authorizeUrl.searchParams.set('response_type', responseType);
2665
+ authorizeUrl.searchParams.set('code_challenge', codeChallenge);
2666
+ authorizeUrl.searchParams.set('code_challenge_method', codeChallengeMethod);
2667
+ if (state)
2668
+ authorizeUrl.searchParams.set('state', state);
2669
+ if (scopes.length > 0)
2670
+ authorizeUrl.searchParams.set('scope', scopes.join(' '));
2670
2671
  res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8' });
2671
- res.end(renderMcpOauthAuthorizePage({ userName, userMeta, workspaces, preselectedWorkspaceId: preferredWorkspaceId, clientName: clientRecord.clientName, clientId: clientRecord.id, redirectUri, scopes, state, codeChallenge, codeChallengeMethod }));
2672
+ res.end(renderMcpOauthAuthorizePage({ userName, userMeta, authorizeUrl: authorizeUrl.toString(), workspaces, preselectedWorkspaceId: preferredWorkspaceId, clientName: clientRecord.clientName, clientId: clientRecord.id, redirectUri, scopes, state, codeChallenge, codeChallengeMethod }));
2672
2673
  }
2673
2674
  catch (error) {
2674
2675
  const statusCode = resolveErrorStatusCode(error);
@@ -2713,8 +2714,18 @@ export function registerAdminRoutes(deps) {
2713
2714
  const userIdentity = core.resolveUserIdentity(access.userId);
2714
2715
  const userName = String(userIdentity?.displayName || userIdentity?.email || access.userId).trim() || String(access.userId);
2715
2716
  const userMeta = userIdentity?.email && userIdentity.email !== userName ? String(userIdentity.email).trim() : null;
2717
+ const authorizeUrl = new URL('/oauth/mcp/authorize', resolveMcpOauthIssuer(req));
2718
+ authorizeUrl.searchParams.set('client_id', clientId);
2719
+ authorizeUrl.searchParams.set('redirect_uri', redirectUri);
2720
+ authorizeUrl.searchParams.set('response_type', responseType);
2721
+ authorizeUrl.searchParams.set('code_challenge', codeChallenge);
2722
+ authorizeUrl.searchParams.set('code_challenge_method', codeChallengeMethod);
2723
+ if (state)
2724
+ authorizeUrl.searchParams.set('state', state);
2725
+ if (scopes.length > 0)
2726
+ authorizeUrl.searchParams.set('scope', scopes.join(' '));
2716
2727
  res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8' });
2717
- res.end(renderMcpOauthAuthorizePage({ userName, userMeta, workspaces, preselectedWorkspaceId: preferredWorkspaceId, clientName: clientRecord.clientName, clientId: clientRecord.id, redirectUri, scopes, state, codeChallenge, codeChallengeMethod }));
2728
+ res.end(renderMcpOauthAuthorizePage({ userName, userMeta, authorizeUrl: authorizeUrl.toString(), workspaces, preselectedWorkspaceId: preferredWorkspaceId, clientName: clientRecord.clientName, clientId: clientRecord.id, redirectUri, scopes, state, codeChallenge, codeChallengeMethod }));
2718
2729
  }
2719
2730
  catch (error) {
2720
2731
  const statusCode = resolveErrorStatusCode(error);
@@ -2068,7 +2068,31 @@ export function registerAuthRoutes(deps) {
2068
2068
  for (const domain of domains) {
2069
2069
  clearCookies.push(`${clearBase}; Domain=${domain}`);
2070
2070
  }
2071
+ const body = await parseFormBody(req);
2072
+ const requestedNext = String(body.next || '').trim();
2073
+ const next = (() => {
2074
+ if (!requestedNext)
2075
+ return '';
2076
+ try {
2077
+ const parsed = new URL(requestedNext);
2078
+ const host = parsed.hostname.toLowerCase();
2079
+ const isLocal = host === 'localhost' || host === '127.0.0.1' || host === '::1';
2080
+ const isTaskforceHost = host === 'taskforcehq.ai' || host.endsWith('.taskforcehq.ai');
2081
+ if ((parsed.protocol === 'http:' || parsed.protocol === 'https:') && (isLocal || isTaskforceHost)) {
2082
+ return parsed.toString();
2083
+ }
2084
+ }
2085
+ catch {
2086
+ return '';
2087
+ }
2088
+ return '';
2089
+ })();
2071
2090
  res.setHeader('Set-Cookie', [...new Set(clearCookies)]);
2091
+ if (next) {
2092
+ res.writeHead(303, { Location: next });
2093
+ res.end();
2094
+ return;
2095
+ }
2072
2096
  res.writeHead(200, { 'Content-Type': 'application/json' });
2073
2097
  res.end(JSON.stringify({ success: true }));
2074
2098
  });