@taskforcehq/taskforce 0.3.308 → 0.3.311

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 (54) hide show
  1. package/README.md +59 -135
  2. package/dist/components/context/ContextAttachmentManager.js +4 -3
  3. package/dist/components/ui/DisclosureSectionHeader.d.ts +12 -0
  4. package/dist/components/ui/DisclosureSectionHeader.js +8 -0
  5. package/dist/components/views/PlanComparisonPage.js +1 -1
  6. package/dist/components/views/PlanComparisonPage.test.js +1 -0
  7. package/dist/components/views/StandaloneLayout.js +21 -4
  8. package/dist/components/views/panels/PlanningDrawer.js +2 -1
  9. package/dist/components/views/panels/PlanningDrawer.test.js +2 -2
  10. package/dist/components/views/standalone/modals/AccountHubModal.d.ts +2 -1
  11. package/dist/components/views/standalone/modals/AccountHubModal.js +3 -2
  12. package/dist/components/views/standalone/modals/AccountHubModal.test.js +18 -12
  13. package/dist/components/views/standalone/modals/HelpModal.d.ts +3 -1
  14. package/dist/components/views/standalone/modals/HelpModal.js +4 -2
  15. package/dist/components/views/standalone/modals/HelpModal.test.d.ts +1 -0
  16. package/dist/components/views/standalone/modals/HelpModal.test.js +23 -0
  17. package/dist/core/PlanEntitlementService.d.ts +2 -0
  18. package/dist/core/PlanEntitlementService.js +57 -4
  19. package/dist/core/PlanVersionPolicy.test.js +32 -0
  20. package/dist/localization/locales/en-US.js +1 -1
  21. package/dist/localization/locales/es-419.js +1 -1
  22. package/dist/localization/locales/pt-BR.js +1 -1
  23. package/dist/mcp/runtime.js +49 -14
  24. package/dist/mcp/runtime.test.js +91 -0
  25. package/dist/server/routes/billing.js +0 -1
  26. package/dist/server/routes/billing.test.js +60 -0
  27. package/dist/ui/assets/AgentsModule-D8u9P5dZ.js +1 -0
  28. package/dist/ui/assets/{AnnotatedAttachmentWorkspace-CCckzWYZ.js → AnnotatedAttachmentWorkspace-CsUauFA2.js} +1 -1
  29. package/dist/ui/assets/ContextAttachmentManager-DbQvDHFz.js +3 -0
  30. package/dist/ui/assets/{DocumentWorkspace-BVaWPA25.js → DocumentWorkspace-BmTkN1A8.js} +1 -1
  31. package/dist/ui/assets/{EntityActivityTimeline-DSj0ThaK.js → EntityActivityTimeline-BXHTM-Nh.js} +1 -1
  32. package/dist/ui/assets/{InitiativesModule-AxMkWtxH.js → InitiativesModule-D33RyXx0.js} +1 -1
  33. package/dist/ui/assets/PlansPage-D0RXlT9_.js +1 -0
  34. package/dist/ui/assets/PlansPage-DeAzWYTb.css +1 -0
  35. package/dist/ui/assets/{TaskContextUpload-Dw4rTF4i.js → TaskContextUpload-Vsg67lpR.js} +1 -1
  36. package/dist/ui/assets/{TaskSettings-f2ga42_V.js → TaskSettings-DshLw7_d.js} +1 -1
  37. package/dist/ui/assets/{WorkflowsModule-E4UL3mov.js → WorkflowsModule-CLM-1BnF.js} +1 -1
  38. package/dist/ui/assets/documentReferences-BPWp6ce0.js +1 -0
  39. package/dist/ui/assets/{index-BvAbqx5Q.css → index-DOR9PRqt.css} +1 -1
  40. package/dist/ui/assets/index-zvbo2Jse.js +5 -0
  41. package/dist/ui/assets/{vendor-icons-B32hGuF2.js → vendor-icons-CLnehDTw.js} +1 -1
  42. package/dist/ui/index.html +3 -3
  43. package/dist/utils/supportContact.d.ts +9 -0
  44. package/dist/utils/supportContact.js +32 -0
  45. package/dist/utils/supportContact.test.d.ts +1 -0
  46. package/dist/utils/supportContact.test.js +19 -0
  47. package/package.json +1 -1
  48. package/dist/ui/assets/AgentsModule-BS4Pi_nC.js +0 -1
  49. package/dist/ui/assets/ContextAttachmentManager-CfG_ER7C.js +0 -3
  50. package/dist/ui/assets/PlansPage-B_z-D6Nh.css +0 -1
  51. package/dist/ui/assets/PlansPage-Ndt7mYfo.js +0 -1
  52. package/dist/ui/assets/documentReferences-BOUcJm6-.js +0 -1
  53. package/dist/ui/assets/index-MAHKvFqp.js +0 -4
  54. /package/dist/ui/agent-logos/{ChatGPT.png → chatgpt.png} +0 -0
package/README.md CHANGED
@@ -1,182 +1,106 @@
1
1
  # Taskforce
2
2
 
3
- Taskforce is a task management system for humans working with AI.
3
+ Taskforce is a shared task and planning workspace for humans collaborating with AI.
4
4
 
5
- It combines structured task tracking, planning layers, document and image review workflows, and built-in MCP access so AI collaborators can participate in the same operational system instead of living in a separate chat transcript.
5
+ It brings tasks, planning, document review, image review, and AI access into one place so people and AI collaborators can work from the same source of truth.
6
6
 
7
7
  Taskforce is currently in beta and under active development.
8
8
 
9
- ## What Taskforce includes today
9
+ ## Install Taskforce
10
10
 
11
- ### Core task management
12
- - Tasks with title, description, status, priority, type, category, assignee, due date, scheduled date, and checklist support
13
- - Custom workspace taxonomy for categories, types, priorities, approaches, and complexity
14
- - Task comments and task attachments
15
- - Schedule board for planning work by day
16
- - Archive and restore flows for completed or cancelled work
17
-
18
- ### Planning model
19
- - Initiatives, workstreams, tasks, and checklists
20
- - Workstream and initiative assignment for tasks
21
- - Initiative templates for reusable planning structures such as launch, migration, or incident work
22
-
23
- ### AI collaboration
24
- - Built-in MCP support for local and cloud usage
25
- - AI profile resolution and task ownership for agent collaborators
26
- - MCP tools for reading and updating tasks, planning entities, documents, image reviews, schedules, and attachments
27
- - Local MCP configuration for local clients such as Codex, Claude Code CLI, and Gemini CLI
28
- - Cloud MCP token flow for remote clients
11
+ Install Taskforce once, then run it from any workspace:
29
12
 
30
- ### Document and image review
31
- - Canonical document assets with document review sessions and threaded review comments
32
- - Image Notes for visual review with saved annotation sessions and markers
33
- - MCP helper flows for document review and image review
34
-
35
- ### Runtime and deployment model
36
- - Local runtime and cloud runtime support
37
- - Separate app, site, and admin surfaces in the repo
38
- - Auth, billing, and account flows for the hosted product
39
- - Workspace sync and cloud/local runtime routing infrastructure
40
-
41
- ## What is intentionally not part of the product right now
42
-
43
- These exist in the codebase in some form, but should not be treated as current product surface area.
44
-
45
- - **Workflow editor**: currently disabled and should not be presented as an active feature
46
- - **Embedded widget/in-app overlay version**: not part of the product right now, not tested recently, and likely to be deprecated
47
-
48
- ## Product surfaces
49
-
50
- - `apps/app` - the main Taskforce application
51
- - `apps/site` - the public website / marketing surface
52
- - `apps/admin` - the admin surface
53
-
54
- ## Repository structure
55
-
56
- ```text
57
- apps/
58
- app/ Main product app
59
- site/ Public website
60
- admin/ Admin surface
61
- src/
62
- components/ UI components and product modules
63
- core/ Core application logic
64
- server/ Server routes and runtime logic
65
- mcp/ MCP runtime and tool registry
66
- sync/ Sync model and transfer logic
67
- storage/ Persistence and asset storage
68
- tests/
69
- e2e/ Playwright realtime and interactive browser scenarios
70
- soak/ Playwright soak and endurance scenarios
13
+ ```bash
14
+ npm install --global @taskforcehq/taskforce
71
15
  ```
72
16
 
73
- Most fast regression coverage is colocated beside the code under `src/` and `apps/` as `*.test.ts` and `*.test.tsx` files.
74
-
75
- ## Getting started
76
-
77
- ### Prerequisites
78
- - Node.js 20+
79
- - npm
80
-
81
- ### Install dependencies
17
+ Then open Taskforce from the workspace you want to manage:
82
18
 
83
19
  ```bash
84
- npm ci
20
+ taskforce open
85
21
  ```
86
22
 
87
- ### Run the main app locally
23
+ You can also install with Homebrew:
88
24
 
89
25
  ```bash
90
- npm run dev
26
+ brew tap taskforcehq/tap
27
+ brew install taskforce
91
28
  ```
92
29
 
93
- If you need the runtime-oriented local flow used by the product rather than simple frontend dev mode, use the repo scripts documented in `package.json` and the active docs under `docs/reference/` and `docs/runbooks/`.
30
+ The npm package page may show `npm i @taskforcehq/taskforce`, but most users should include `--global` so the `taskforce` command is available from the terminal.
94
31
 
95
- ### Build
32
+ ## Get Started
33
+
34
+ Go to the folder you want Taskforce to manage, then run:
96
35
 
97
36
  ```bash
98
- npm run build
37
+ taskforce open
99
38
  ```
100
39
 
101
- ## MCP
102
-
103
- Taskforce includes built-in MCP support so AI clients can operate directly against the workspace.
40
+ Taskforce opens in your browser and stores workspace data in that folder, under `.taskforce/`.
104
41
 
105
- ### Local MCP
106
- Use Local MCP for local stdio-based clients. This is the intended path for tools such as:
107
- - Codex
108
- - Claude Code CLI
109
- - Gemini CLI
42
+ To confirm which workspace Taskforce is using:
110
43
 
111
- Local MCP does not require a cloud token.
44
+ ```bash
45
+ taskforce status
46
+ ```
112
47
 
113
- ### Cloud MCP
114
- Use Cloud MCP for hosted remote clients. Cloud MCP uses token-based access and the hosted MCP endpoint.
48
+ To connect a local AI client through MCP, start the MCP server from the same workspace:
115
49
 
116
- ## Major product capabilities
50
+ ```bash
51
+ taskforce mcp
52
+ ```
117
53
 
118
- ### 1. Shared task system for humans and AI
119
- Taskforce is designed so people and AI agents can work from the same task graph, rather than forcing humans to maintain one system and agents to operate from another.
54
+ ## What Taskforce Includes
120
55
 
121
- ### 2. Planning beyond flat tasks
122
- Taskforce supports a planning hierarchy that can represent real work:
123
- - Initiative
124
- - Workstream
125
- - Task
126
- - Checklist
56
+ ### Task Management
127
57
 
128
- This is meant to support launch work, migrations, larger feature tracks, and other multi-layer efforts.
58
+ - Tasks with title, description, status, priority, type, category, assignee, due date, scheduled date, and checklist support
59
+ - Custom workspace taxonomy for categories, types, priorities, approaches, and complexity
60
+ - Task comments and task attachments
61
+ - Schedule board for planning work by day
62
+ - Archive and restore flows for completed or cancelled work
129
63
 
130
- ### 3. Review-native workflows
131
- Taskforce includes first-class review surfaces for:
132
- - documents, via document review sessions and comments
133
- - images, via Image Notes sessions and markers
64
+ ### Planning
134
65
 
135
- These are not decorative attachments. They are structured review workflows.
66
+ - Initiatives, workstreams, tasks, and checklists
67
+ - Workstream and initiative assignment for tasks
68
+ - Initiative templates for reusable planning structures such as launches, migrations, and incident response
136
69
 
137
- ### 4. AI-operable product state
138
- Taskforce exposes product state through MCP so agents can:
139
- - read tasks and planning structures
140
- - update status and ownership
141
- - create and comment on review sessions
142
- - work with schedules and attachments
70
+ ### AI Collaboration
143
71
 
144
- ## What to avoid saying about Taskforce right now
72
+ - Built-in MCP access for AI clients
73
+ - AI profiles and task ownership for agent collaborators
74
+ - Tools for agents to read and update tasks, planning entities, documents, image reviews, schedules, and attachments
75
+ - Local MCP support for clients such as Codex, Claude Code CLI, and Gemini CLI
76
+ - Cloud MCP access for hosted remote clients
145
77
 
146
- Do not describe Taskforce as:
147
- - a production-ready embedded widget product
148
- - a workflow editor product
149
- - a generic developer overlay tool
150
- - something centered only on AI-developer pairs
78
+ ### Document And Image Review
151
79
 
152
- That undersells or misstates where the product is now.
80
+ - Document review sessions with threaded comments
81
+ - Image Notes for visual review with saved annotation sessions and markers
82
+ - AI-assisted document and image review workflows
153
83
 
154
- A better framing is:
84
+ ### Sync And Hosted Workspaces
155
85
 
156
- > Taskforce is a task and planning system for humans collaborating with AI, with built-in review workflows and MCP access.
86
+ - Local workspace data for private, machine-local use
87
+ - Hosted workspaces for cloud-backed collaboration
88
+ - Workspace sync between local and cloud environments
89
+ - Account, auth, and billing flows for hosted usage
157
90
 
158
- ## Development notes
91
+ ## MCP
159
92
 
160
- ### Validation
161
- Useful local checks include:
93
+ Taskforce includes built-in MCP support so AI clients can work directly with your workspace.
162
94
 
163
- ```bash
164
- npm run build
165
- npm run test
166
- ```
95
+ Local MCP is for AI clients running on the same machine as your workspace. It does not require a cloud token.
167
96
 
168
- Testing guidance lives in:
97
+ Cloud MCP is for hosted remote clients and uses token-based access.
169
98
 
170
- - `docs/reference/engineering/test-infrastructure.md`
171
- - `docs/runbooks/sync-soak-runbook.md`
172
- - `docs/reference/operations/external-qa-workflow.md`
99
+ ## Requirements
173
100
 
174
- ### Important docs
175
- - `docs/reference/engineering/test-infrastructure.md`
176
- - `docs/reference/platform/runtime-modes-contract.md`
177
- - `docs/reference/operations/external-qa-workflow.md`
178
- - `docs/runbooks/sync-soak-runbook.md`
101
+ - Node.js 20+
102
+ - npm, or Homebrew on macOS
179
103
 
180
104
  ## Status
181
105
 
182
- Taskforce is in active beta development. The product surface is moving, so this README is intentionally conservative and only describes capabilities that are complete enough to stand behind today.
106
+ Taskforce is in active beta development. This README describes the user-facing capabilities that are ready to try today.
@@ -1,7 +1,8 @@
1
- import { jsxs as _jsxs, jsx as _jsx, Fragment as _Fragment } from "react/jsx-runtime";
1
+ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
2
2
  import { useEffect, useMemo, useRef, useState } from 'react';
3
- import { AlertTriangle, Copy, Download, File, FileUp, Loader2, PencilLine, Trash2, ChevronDown, ChevronRight } from 'lucide-react';
3
+ import { AlertTriangle, Copy, Download, File, FileUp, Loader2, PencilLine, Trash2 } from 'lucide-react';
4
4
  import styles from '../../Taskforce.module.css';
5
+ import { DisclosureSectionHeader } from '../ui/DisclosureSectionHeader';
5
6
  import { dispatchOpenMarkdownDocument } from '../../utils/contextFiles';
6
7
  import { canOpenAnnotatedAttachment, dispatchOpenAnnotatedAttachment } from '../../utils/annotatedAttachments';
7
8
  import { dispatchTaskforceContextAssetsMutated } from '../../utils/contextAssetEvents';
@@ -460,7 +461,7 @@ export function ContextAttachmentManager(props) {
460
461
  const path = typeof item === 'string' ? item : item.path;
461
462
  return !isImagePath(path);
462
463
  });
463
- return (_jsxs("div", { className: styles.specialistSection, children: [_jsxs("div", { className: styles.toggleHeading, onClick: () => setShowContextUpload(!showContextUpload), title: showContextUpload ? 'Hide context documents' : 'Show context documents', children: [_jsxs("label", { className: styles.label, children: ["Context Documents ", attachments.length > 0 && `(${attachments.length})`] }), showContextUpload ? _jsx(ChevronDown, { size: 14 }) : _jsx(ChevronRight, { size: 14 })] }), showContextUpload && (_jsxs(_Fragment, { children: [_jsxs("p", { className: styles.settingsHint, children: ["Upload files into ", ownerTypeLabel, " context storage, or add an external http(s) URL."] }), _jsxs("div", { className: styles.contextUploadPanel, children: [_jsxs("div", { className: styles.contextUploadActions, children: [_jsxs("button", { type: "button", className: styles.secondaryHeaderBtn, onClick: () => inputRef.current?.click(), disabled: uploading, children: [uploading ? _jsx(Loader2, { size: 12, className: styles.spinner }) : _jsx(FileUp, { size: 12 }), uploading ? 'Uploading...' : 'Upload File'] }), _jsxs("button", { type: "button", className: styles.secondaryHeaderBtn, onClick: () => {
464
+ return (_jsxs("div", { className: styles.specialistSection, children: [_jsx(DisclosureSectionHeader, { label: "Context Documents", count: attachments.length, expanded: showContextUpload, expandedTitle: "Hide context documents", collapsedTitle: "Show context documents", onToggle: () => setShowContextUpload(!showContextUpload) }), showContextUpload && (_jsxs(_Fragment, { children: [_jsxs("p", { className: styles.settingsHint, children: ["Upload files into ", ownerTypeLabel, " context storage, or add an external http(s) URL."] }), _jsxs("div", { className: styles.contextUploadPanel, children: [_jsxs("div", { className: styles.contextUploadActions, children: [_jsxs("button", { type: "button", className: styles.secondaryHeaderBtn, onClick: () => inputRef.current?.click(), disabled: uploading, children: [uploading ? _jsx(Loader2, { size: 12, className: styles.spinner }) : _jsx(FileUp, { size: 12 }), uploading ? 'Uploading...' : 'Upload File'] }), _jsxs("button", { type: "button", className: styles.secondaryHeaderBtn, onClick: () => {
464
465
  void handlePasteImage();
465
466
  }, disabled: uploading, children: [_jsx(Copy, { size: 12 }), "Paste Image from Clipboard"] })] }), _jsx("input", { ref: inputRef, type: "file", accept: ACCEPTED_CONTEXT_FILE_TYPES, multiple: true, className: styles.hiddenInput, onChange: (e) => {
466
467
  void handleFilesSelected(e.target.files);
@@ -0,0 +1,12 @@
1
+ interface DisclosureSectionHeaderProps {
2
+ label: string;
3
+ count?: number;
4
+ showZeroCount?: boolean;
5
+ expanded: boolean;
6
+ expandedTitle: string;
7
+ collapsedTitle: string;
8
+ onToggle: () => void;
9
+ className?: string;
10
+ }
11
+ export declare function DisclosureSectionHeader({ label, count, showZeroCount, expanded, expandedTitle, collapsedTitle, onToggle, className, }: DisclosureSectionHeaderProps): import("react/jsx-runtime").JSX.Element;
12
+ export {};
@@ -0,0 +1,8 @@
1
+ import { jsxs as _jsxs, jsx as _jsx } from "react/jsx-runtime";
2
+ import { ChevronDown, ChevronRight } from 'lucide-react';
3
+ import styles from './DisclosureSectionHeader.module.css';
4
+ export function DisclosureSectionHeader({ label, count, showZeroCount = false, expanded, expandedTitle, collapsedTitle, onToggle, className = '', }) {
5
+ const shouldShowCount = typeof count === 'number' && (showZeroCount || count > 0);
6
+ const Icon = expanded ? ChevronDown : ChevronRight;
7
+ return (_jsxs("button", { type: "button", className: `${styles.header} ${className}`.trim(), onClick: onToggle, "aria-expanded": expanded, title: expanded ? expandedTitle : collapsedTitle, children: [_jsxs("span", { className: styles.title, children: [label, shouldShowCount ? ` (${count})` : ''] }), _jsx(Icon, { className: styles.icon, size: 14, "aria-hidden": "true" })] }));
8
+ }
@@ -386,5 +386,5 @@ export function PlanComparisonPage({ heading, subtitle, variant = 'app', theme =
386
386
  isCurrentPlan: false
387
387
  });
388
388
  }, disabled: !planVersionId || !planSelectionEnabled, children: hasMultipleBillingOptions ? 'Start Yearly' : 'Get Started' })) : null] }))] }, plan.planId));
389
- }) }), _jsxs("div", { className: styles.pricingNotes, children: [_jsx("p", { children: "Prices are listed in USD. Taxes may apply." }), _jsx("p", { children: "Local install is free on every plan. Cloud limits apply only to hosted workspaces." })] }), _jsxs("section", { className: `${styles.foundingBeta} ${styles.cloudCapacityNote}`, "aria-labelledby": "cloud-capacity-heading", children: [_jsx("h2", { id: "cloud-capacity-heading", children: "Need more cloud capacity?" }), _jsx("p", { children: "Pro users can request expanded limits for cloud workspaces, AI agents, and storage." })] }), _jsxs("div", { className: `${styles.planCard} ${styles.planNoticeCard} ${styles.teamComingSoonCard}`, children: [_jsx("h2", { className: styles.planName, children: "Team workspaces are coming soon." }), _jsx("p", { className: styles.planDescription, children: "Shared workspaces, collaborators, team agent rosters, admin controls, and larger cloud limits are in development." })] })] }))] }) }) }));
389
+ }) }), _jsxs("div", { className: styles.pricingNotes, children: [_jsx("p", { children: "Prices are listed in USD. Taxes may apply." }), _jsx("p", { children: "Local install is free on every plan. Cloud limits apply only to hosted workspaces." })] }), _jsxs("section", { className: `${styles.foundingBeta} ${styles.cloudCapacityNote}`, "aria-labelledby": "cloud-capacity-heading", children: [_jsx("h2", { id: "cloud-capacity-heading", children: "Need more cloud capacity?" }), _jsx("p", { children: "Pro users can request expanded limits for cloud workspaces, AI agents, and storage." }), _jsxs("p", { children: ["Contact ", _jsx("a", { href: "mailto:support@taskforcehq.ai", children: "support@taskforcehq.ai" }), " to discuss capacity needs."] })] }), _jsxs("div", { className: `${styles.planCard} ${styles.planNoticeCard} ${styles.teamComingSoonCard}`, children: [_jsx("h2", { className: styles.planName, children: "Team workspaces are coming soon." }), _jsx("p", { className: styles.planDescription, children: "Shared workspaces, collaborators, team agent rosters, admin controls, and larger cloud limits are in development." })] })] }))] }) }) }));
390
390
  }
@@ -72,6 +72,7 @@ describe('PlanComparisonPage', () => {
72
72
  expect(screen.getByText(/Early members get access while the product is still forming/)).toBeInTheDocument();
73
73
  expect(screen.getByText('Prices are listed in USD. Taxes may apply.')).toBeInTheDocument();
74
74
  expect(screen.getByText('Local install is free on every plan. Cloud limits apply only to hosted workspaces.')).toBeInTheDocument();
75
+ expect(screen.getByRole('link', { name: 'support@taskforcehq.ai' })).toHaveAttribute('href', 'mailto:support@taskforcehq.ai');
75
76
  const foundingBeta = screen.getByRole('heading', { name: 'Founding beta' }).closest('section');
76
77
  const firstPlanCard = container.querySelector('[data-testid^="plan-card-"]');
77
78
  const planCards = container.querySelectorAll('[data-testid^="plan-card-"]');
@@ -2,7 +2,7 @@ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-run
2
2
  import React, { useMemo, useCallback, useState, useEffect, useRef, Suspense } from 'react';
3
3
  import { createPortal } from 'react-dom';
4
4
  import { useLocation, useNavigate } from 'react-router-dom';
5
- import { Settings, Plus, Filter, Layers, User, Maximize2, Minimize2, Check, ChevronLeft, ArrowUp, ArrowDown, ArrowUpDown, AlertTriangle, Save, Loader2, Folder, FileText, LogOut, NotebookTabs, ClipboardCheck, Cloud, CloudOff, Users, ClockAlert, ChevronsUp, ChevronsDown, EyeOff, Zap, HelpCircle, Bot, Image, Workflow, Plug } from 'lucide-react';
5
+ import { Settings, Plus, Filter, Layers, User, Maximize2, Minimize2, Check, ChevronLeft, ArrowUp, ArrowDown, ArrowUpDown, AlertTriangle, Save, Loader2, Folder, FileText, LogOut, NotebookTabs, ClipboardCheck, Cloud, CloudOff, Users, ClockAlert, Mail, ChevronsUp, ChevronsDown, EyeOff, Zap, HelpCircle, Bot, Image, Workflow, Plug } from 'lucide-react';
6
6
  import { STATUS_OPTIONS, getStatusDefinition } from '../../utils/constants';
7
7
  import styles from '../../Taskforce.module.css';
8
8
  import shellStyles from './WorkspaceShell.module.css';
@@ -41,6 +41,7 @@ import { resolveAnnotatedAttachmentDetail } from '../../utils/annotatedAttachmen
41
41
  import { ACCOUNT_PROFILE_SUMMARY_PATH, invalidateAccountProfileSummaryCache, loadAccountProfileSummaryCached } from '../../utils/accountProfileSummaryCache';
42
42
  import { prepareAvatarUploadFile } from '../../utils/avatarUpload';
43
43
  import { resolveCommercialLifecyclePresentation } from '../../utils/commercialLifecyclePresentation';
44
+ import { buildSupportMailtoHref } from '../../utils/supportContact';
44
45
  import { AppShellHeader } from './AppShellHeader';
45
46
  import { getTaskforceCloudEnvironmentConfig, parseTaskforceCloudEnvironment, resolveTaskforceCloudEnvironmentFromBaseUrl } from '../../config/cloudEnvironment';
46
47
  import { logBootstrapDebug, persistRemoteUiState } from '../../hooks/workspace/workspaceLocalState';
@@ -1382,6 +1383,15 @@ export function StandaloneLayout(props) {
1382
1383
  const authIdentityLabel = authDisplayName || authEmailLabel;
1383
1384
  const hasAuthIdentity = authIdentityLabel.length > 0;
1384
1385
  const hasAuthEmail = authEmailLabel.length > 0;
1386
+ const supportMailtoHref = useMemo(() => buildSupportMailtoHref({
1387
+ workspaceName: currentWorkspaceLabel,
1388
+ workspaceId: currentWorkspaceId,
1389
+ accountLabel: authIdentityLabel || authEmailLabel,
1390
+ runtimeMode
1391
+ }), [authEmailLabel, authIdentityLabel, currentWorkspaceId, currentWorkspaceLabel, runtimeMode]);
1392
+ const openSupportEmail = useCallback(() => {
1393
+ window.location.href = supportMailtoHref;
1394
+ }, [supportMailtoHref]);
1385
1395
  const accountBadgeLabel = (authIdentityLabel || authEmailLabel || '').trim();
1386
1396
  const shouldShowResolvedAccountInitial = isAuthenticated && authSessionResolved && accountBadgeLabel.length > 0;
1387
1397
  const accountBadgeInitial = shouldShowResolvedAccountInitial ? accountBadgeLabel.charAt(0).toUpperCase() : '';
@@ -2428,7 +2438,10 @@ export function StandaloneLayout(props) {
2428
2438
  }, role: "menuitem", children: [_jsx(User, { size: 15 }), "Account Hub"] }), _jsxs("button", { className: accountStyles.accountMenuItem, onClick: () => {
2429
2439
  setShowAccountMenu(false);
2430
2440
  setShowHelpModal(true);
2431
- }, role: "menuitem", children: [_jsx(HelpCircle, { size: 15 }), "Help & Tutorial"] }), runtimeMode === 'local' && !isAuthenticated && !isAuthBootstrapPending && (_jsxs("button", { className: accountStyles.accountMenuItem, onClick: () => {
2441
+ }, role: "menuitem", children: [_jsx(HelpCircle, { size: 15 }), "Help & Support"] }), _jsxs("button", { className: accountStyles.accountMenuItem, onClick: () => {
2442
+ setShowAccountMenu(false);
2443
+ openSupportEmail();
2444
+ }, role: "menuitem", children: [_jsx(Mail, { size: 15 }), "Contact Support"] }), runtimeMode === 'local' && !isAuthenticated && !isAuthBootstrapPending && (_jsxs("button", { className: accountStyles.accountMenuItem, onClick: () => {
2432
2445
  openSharedLoginRoute('login');
2433
2446
  }, role: "menuitem", children: [_jsx(User, { size: 15 }), "Sign In / Register"] })), isAuthenticated && (_jsxs("button", { className: accountStyles.accountMenuItem, onClick: async () => {
2434
2447
  setShowAccountMenu(false);
@@ -3315,15 +3328,19 @@ export function StandaloneLayout(props) {
3315
3328
  }, currentTask: currentTask })] }), _jsx(Modal, { isOpen: activeTab === 'settings', onClose: handleCloseModal, title: "Settings", size: "xl", theme: currentTheme, draggable: true, isSettings: true, closeOnOverlayClick: false, children: _jsx(Suspense, { fallback: _jsx("div", { style: { display: 'flex', alignItems: 'center', justifyContent: 'center', padding: '2rem' }, children: _jsx(Loader2, { size: 20, className: styles.spinner }) }), children: _jsx(TaskSettings, { settingsModel: {
3316
3329
  ...settingsModel,
3317
3330
  onOpenCloudAuth: openSharedLoginRoute
3318
- }, onSectionChange: setSettingsSection }) }) }), _jsx(ScheduleWarningModal, { prompt: scheduleWarningPrompt, theme: currentTheme, onClose: cancelScheduleWarning, onConfirm: confirmScheduleWarning }), _jsx(SyncEnableWarningModal, { isOpen: showSyncEnableWarning, theme: currentTheme, onClose: cancelSyncEnableWarning, onConfirm: confirmSyncEnableWarning }), _jsx(HelpModal, { isOpen: showHelpModal, theme: currentTheme, onClose: () => setShowHelpModal(false), onOpenSettings: () => {
3331
+ }, onSectionChange: setSettingsSection }) }) }), _jsx(ScheduleWarningModal, { prompt: scheduleWarningPrompt, theme: currentTheme, onClose: cancelScheduleWarning, onConfirm: confirmScheduleWarning }), _jsx(SyncEnableWarningModal, { isOpen: showSyncEnableWarning, theme: currentTheme, onClose: cancelSyncEnableWarning, onConfirm: confirmSyncEnableWarning }), _jsx(HelpModal, { isOpen: showHelpModal, theme: currentTheme, onClose: () => setShowHelpModal(false), supportMailtoHref: supportMailtoHref, onOpenSettings: () => {
3319
3332
  setShowHelpModal(false);
3320
3333
  setActiveTab('settings');
3321
3334
  setSettingsSection('general');
3335
+ }, onOpenAccountHub: () => {
3336
+ setShowHelpModal(false);
3337
+ startAccountSurfaceTiming('account_hub');
3338
+ setShowAccountHub(true);
3322
3339
  } }), _jsx(EditProfileModal, { isOpen: showEditProfileModal, theme: currentTheme, onClose: closeEditProfileModal, onSave: () => { void handleSaveProfile(); }, displayName: profileDisplayNameDraft, email: authEmailLabel, avatarDisplayUrl: profileAvatarDisplayUrl, accountBadgeInitial: accountBadgeInitial, saveBusy: profileSaveBusy, avatarBusy: profileAvatarBusy, saveError: profileSaveError, saveNotice: profileSaveNotice, onDisplayNameChange: setProfileDisplayNameDraft, onOpenAvatarManager: () => {
3323
3340
  setProfileSaveError(null);
3324
3341
  setProfileSaveNotice(null);
3325
3342
  setShowAvatarPhotoManager(true);
3326
- } }), _jsx(AvatarImageManagerModal, { isOpen: showAvatarPhotoManager, theme: currentTheme, title: "Edit Profile Photo", currentImageUrl: profileAvatarDisplayUrl, fallbackInitial: accountBadgeInitial, accept: PROFILE_AVATAR_ACCEPT, busy: profileAvatarBusy, hasPendingImage: Boolean(profileAvatarDraftId), canRemove: Boolean(profileAvatarDisplayUrl), error: profileSaveError, notice: profileSaveNotice, onClose: () => setShowAvatarPhotoManager(false), onApplyImage: handleProfileAvatarSelected, onRemoveImage: handleRemoveProfileAvatar, onDiscardPendingImage: handleDiscardPendingProfileAvatar }), _jsx(AccountHubModal, { isOpen: showAccountHub, theme: currentTheme, runtimeMode: runtimeMode, authRequiredForApi: authRequiredForApi, isAuthenticated: isAuthenticated, hasAuthIdentity: hasAuthIdentity, authIdentityLabel: authIdentityLabel, billingLoading: billingLoading, billingError: billingError, billingActionError: billingActionError, billingNotice: billingNotice, billingActionBusy: billingActionBusy, billingIntervalChoice: billingIntervalChoice, accountProfileSummary: accountProfileSummary, currentWorkspaceId: currentWorkspaceId, canOpenTeamManagement: canOpenTeamManagement, canManageWorkspaceSync: canManageWorkspaceSync, workspaceCloudSyncEnabled: workspaceCloudSyncEnabled, syncStatusLabel: syncStatusMeta.label, workspaceSyncError: workspaceSyncError, syncControlBusy: syncControlBusy, cloudAuthEnabled: cloudAuthConfigured, availableAuthProviders: availableAuthProviders, onFetchLoginMethods: fetchLoginMethods, onUnlinkLoginMethod: unlinkLoginMethod, onAddPassword: addPasswordToAccount, onLinkProvider: (provider) => beginOAuthLink(provider), onClose: () => setShowAccountHub(false), onOpenWorkspaceAudit: () => {
3343
+ } }), _jsx(AvatarImageManagerModal, { isOpen: showAvatarPhotoManager, theme: currentTheme, title: "Edit Profile Photo", currentImageUrl: profileAvatarDisplayUrl, fallbackInitial: accountBadgeInitial, accept: PROFILE_AVATAR_ACCEPT, busy: profileAvatarBusy, hasPendingImage: Boolean(profileAvatarDraftId), canRemove: Boolean(profileAvatarDisplayUrl), error: profileSaveError, notice: profileSaveNotice, onClose: () => setShowAvatarPhotoManager(false), onApplyImage: handleProfileAvatarSelected, onRemoveImage: handleRemoveProfileAvatar, onDiscardPendingImage: handleDiscardPendingProfileAvatar }), _jsx(AccountHubModal, { isOpen: showAccountHub, theme: currentTheme, runtimeMode: runtimeMode, authRequiredForApi: authRequiredForApi, isAuthenticated: isAuthenticated, hasAuthIdentity: hasAuthIdentity, authIdentityLabel: authIdentityLabel, billingLoading: billingLoading, billingError: billingError, billingActionError: billingActionError, billingNotice: billingNotice, billingActionBusy: billingActionBusy, billingIntervalChoice: billingIntervalChoice, accountProfileSummary: accountProfileSummary, currentWorkspaceId: currentWorkspaceId, canOpenTeamManagement: canOpenTeamManagement, canManageWorkspaceSync: canManageWorkspaceSync, workspaceCloudSyncEnabled: workspaceCloudSyncEnabled, syncStatusLabel: syncStatusMeta.label, workspaceSyncError: workspaceSyncError, syncControlBusy: syncControlBusy, supportMailtoHref: supportMailtoHref, cloudAuthEnabled: cloudAuthConfigured, availableAuthProviders: availableAuthProviders, onFetchLoginMethods: fetchLoginMethods, onUnlinkLoginMethod: unlinkLoginMethod, onAddPassword: addPasswordToAccount, onLinkProvider: (provider) => beginOAuthLink(provider), onClose: () => setShowAccountHub(false), onOpenWorkspaceAudit: () => {
3327
3344
  setShowAccountHub(false);
3328
3345
  void openTeamManagementModal();
3329
3346
  setTeamManagementTab('audit');
@@ -7,6 +7,7 @@ import styles from '../../../Taskforce.module.css';
7
7
  import panelStyles from './PlanningDrawer.module.css';
8
8
  import { ReferenceBadgeButton } from '../../ui/ReferenceBadgeButton';
9
9
  import { HierarchyActionBadge } from '../../ui/HierarchyActionBadge';
10
+ import { DisclosureSectionHeader } from '../../ui/DisclosureSectionHeader';
10
11
  import { IconMap, getStatusDefinition, resolveColor } from '../../../utils/constants';
11
12
  import { formatInitiativeReference, formatWorkstreamReference, } from '../../../utils/planningReferences.js';
12
13
  import { getEntityActivityItems } from '../../../utils/taskActivity';
@@ -363,7 +364,7 @@ function PlanningDrawerDetailView({ detail, currentWorkspaceId, onOpenNestedWork
363
364
  event.preventDefault();
364
365
  handleOpenTask();
365
366
  }, children: [_jsx("span", { className: panelStyles.listItemTitle, children: task.title }), _jsx("span", { className: panelStyles.listItemMeta, children: taskMeta })] })] }) }, task.id));
366
- }) : (_jsxs("div", { className: panelStyles.sectionNote, children: ["No tasks are linked to this workstream yet. Use the ", _jsx("code", { children: "+" }), " action above to add the first one."] })))] }), _jsx("div", { className: panelStyles.listCard, children: _jsx(React.Suspense, { fallback: _jsx(DeferredPlanningPanelFallback, {}), children: _jsx(DeferredContextAttachmentManager, { ownerType: detail.type, ownerId: detail.item.id, ownerReferenceLabel: detailReferenceLabel, workspaceId: currentWorkspaceId, attachments: detail.item.attachments || [], onAddAttachment: (file) => onAddPlanningContextFile?.(detail.type, detail.item.id, file), onRemoveAttachment: (index) => onRemovePlanningContextFile?.(detail.type, detail.item.id, index), onUpdateAttachmentCaption: (index, caption) => (onUpdatePlanningContextCaption?.(detail.type, detail.item.id, index, caption)) }) }) }), _jsxs("div", { className: panelStyles.listCard, children: [_jsxs("button", { type: "button", className: panelStyles.sectionToggleButton, onClick: () => setShowRecentActivity((current) => !current), "aria-expanded": showRecentActivity, title: showRecentActivity ? 'Hide recent activity' : 'Show recent activity', children: [_jsxs("span", { className: panelStyles.sectionTitle, children: ["Recent activity (", activityCount, ")"] }), showRecentActivity ? _jsx(ChevronDown, { size: 14 }) : _jsx(ChevronRight, { size: 14 })] }), showRecentActivity ? (_jsx(React.Suspense, { fallback: _jsx(DeferredPlanningPanelFallback, {}), children: _jsx(DeferredEntityActivityTimeline, { activity: detail.item.activity, comments: detail.item.comments, formatFieldValue: formatActivityFieldValue, resolveActorPresentation: resolveActivityActorPresentation, onOpenTaskById: onOpenTaskById, emptyMessage: `No ${detail.type} activity yet.`, listClassName: panelStyles.activityTimelineList, emptyClassName: panelStyles.activityTimelineEmpty }) })) : null] })] }));
367
+ }) : (_jsxs("div", { className: panelStyles.sectionNote, children: ["No tasks are linked to this workstream yet. Use the ", _jsx("code", { children: "+" }), " action above to add the first one."] })))] }), _jsx("div", { className: panelStyles.listCard, children: _jsx(React.Suspense, { fallback: _jsx(DeferredPlanningPanelFallback, {}), children: _jsx(DeferredContextAttachmentManager, { ownerType: detail.type, ownerId: detail.item.id, ownerReferenceLabel: detailReferenceLabel, workspaceId: currentWorkspaceId, attachments: detail.item.attachments || [], onAddAttachment: (file) => onAddPlanningContextFile?.(detail.type, detail.item.id, file), onRemoveAttachment: (index) => onRemovePlanningContextFile?.(detail.type, detail.item.id, index), onUpdateAttachmentCaption: (index, caption) => (onUpdatePlanningContextCaption?.(detail.type, detail.item.id, index, caption)) }) }) }), _jsxs("div", { className: panelStyles.listCard, children: [_jsx(DisclosureSectionHeader, { label: "Recent Activity", count: activityCount, showZeroCount: true, expanded: showRecentActivity, expandedTitle: "Hide recent activity", collapsedTitle: "Show recent activity", onToggle: () => setShowRecentActivity((current) => !current) }), showRecentActivity ? (_jsx(React.Suspense, { fallback: _jsx(DeferredPlanningPanelFallback, {}), children: _jsx(DeferredEntityActivityTimeline, { activity: detail.item.activity, comments: detail.item.comments, formatFieldValue: formatActivityFieldValue, resolveActorPresentation: resolveActivityActorPresentation, onOpenTaskById: onOpenTaskById, emptyMessage: `No ${detail.type} activity yet.`, listClassName: panelStyles.activityTimelineList, emptyClassName: panelStyles.activityTimelineEmpty }) })) : null] })] }));
367
368
  }
368
369
  function PlanningDrawerEditor({ editor, draftTitle, draftDescription, draftOwner, draftInitiativeId, assigneeOptions, draftInitiativeSummary, onChangeDraftTitle, onChangeDraftDescription, onChangeDraftOwner, onChangeDraftInitiativeId, onCancel, onSubmit, isSubmitting, onAssignInitiativeToWorkstream, }) {
369
370
  const entityLabel = editor.entityType === 'initiative' ? 'Initiative' : 'Workstream';
@@ -490,10 +490,10 @@ describe('PlanningDrawer context attachments', () => {
490
490
  detail: { type: 'workstream', item: workstreamSummary },
491
491
  });
492
492
  const contextHeading = await screen.findByText('Context Documents (1)');
493
- const activityHeading = screen.getByText('Recent activity (1)');
493
+ const activityHeading = screen.getByText('Recent Activity (1)');
494
494
  expect(Boolean(contextHeading.compareDocumentPosition(activityHeading) & Node.DOCUMENT_POSITION_FOLLOWING)).toBe(true);
495
495
  expect(await screen.findByText('Task added: Investigate bootstrap timeouts')).toBeInTheDocument();
496
- await user.click(screen.getByRole('button', { name: 'Recent activity (1)' }));
496
+ await user.click(screen.getByRole('button', { name: 'Recent Activity (1)' }));
497
497
  expect(screen.queryByText('Task added: Investigate bootstrap timeouts')).not.toBeInTheDocument();
498
498
  });
499
499
  it('wires workstream context caption and remove actions through planning callbacks', async () => {
@@ -56,6 +56,7 @@ interface AccountHubModalProps {
56
56
  onStartCheckout: () => void;
57
57
  onToggleWorkspaceSync: (enabled: boolean) => void;
58
58
  onOpenHelp: () => void;
59
+ supportMailtoHref: string;
59
60
  }
60
- export declare function AccountHubModal({ isOpen, theme, runtimeMode, authRequiredForApi, isAuthenticated, hasAuthIdentity, authIdentityLabel, billingLoading, billingError, billingActionError, billingNotice, billingActionBusy, billingIntervalChoice, accountProfileSummary, currentWorkspaceId, canOpenTeamManagement, canManageWorkspaceSync, workspaceCloudSyncEnabled, syncStatusLabel, workspaceSyncError, syncControlBusy, cloudAuthEnabled, availableAuthProviders, onFetchLoginMethods, onUnlinkLoginMethod, onAddPassword, onLinkProvider, onClose, onOpenWorkspaceAudit, onBillingIntervalChange, onRefreshBilling, onUpdateInterval, onManageBilling, onStartCheckout, onToggleWorkspaceSync, onOpenHelp, }: AccountHubModalProps): import("react/jsx-runtime").JSX.Element;
61
+ export declare function AccountHubModal({ isOpen, theme, runtimeMode, authRequiredForApi, isAuthenticated, hasAuthIdentity, authIdentityLabel, billingLoading, billingError, billingActionError, billingNotice, billingActionBusy, billingIntervalChoice, accountProfileSummary, currentWorkspaceId, canOpenTeamManagement, canManageWorkspaceSync, workspaceCloudSyncEnabled, syncStatusLabel, workspaceSyncError, syncControlBusy, cloudAuthEnabled, availableAuthProviders, onFetchLoginMethods, onUnlinkLoginMethod, onAddPassword, onLinkProvider, onClose, onOpenWorkspaceAudit, onBillingIntervalChange, onRefreshBilling, onUpdateInterval, onManageBilling, onStartCheckout, onToggleWorkspaceSync, onOpenHelp, supportMailtoHref, }: AccountHubModalProps): import("react/jsx-runtime").JSX.Element;
61
62
  export {};
@@ -1,5 +1,6 @@
1
1
  import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  import { useState, useEffect } from 'react';
3
+ import { Mail } from 'lucide-react';
3
4
  import { Modal } from '../../../ui/Modal';
4
5
  import modalStyles from '../../../ui/Modal.module.css';
5
6
  import accountStyles from '../../StandaloneAccount.module.css';
@@ -8,7 +9,7 @@ import managementStyles from '../../StandaloneManagement.module.css';
8
9
  import styles from '../../../../Taskforce.module.css';
9
10
  import { resolveCommercialLifecyclePresentation } from '../../../../utils/commercialLifecyclePresentation';
10
11
  import { getPasswordPolicyGuidance, validatePasswordPolicy } from '../../../../shared/passwordPolicy';
11
- export function AccountHubModal({ isOpen, theme, runtimeMode, authRequiredForApi, isAuthenticated, hasAuthIdentity, authIdentityLabel, billingLoading, billingError, billingActionError, billingNotice, billingActionBusy, billingIntervalChoice, accountProfileSummary, currentWorkspaceId, canOpenTeamManagement, canManageWorkspaceSync, workspaceCloudSyncEnabled, syncStatusLabel, workspaceSyncError, syncControlBusy, cloudAuthEnabled, availableAuthProviders, onFetchLoginMethods, onUnlinkLoginMethod, onAddPassword, onLinkProvider, onClose, onOpenWorkspaceAudit, onBillingIntervalChange, onRefreshBilling, onUpdateInterval, onManageBilling, onStartCheckout, onToggleWorkspaceSync, onOpenHelp, }) {
12
+ export function AccountHubModal({ isOpen, theme, runtimeMode, authRequiredForApi, isAuthenticated, hasAuthIdentity, authIdentityLabel, billingLoading, billingError, billingActionError, billingNotice, billingActionBusy, billingIntervalChoice, accountProfileSummary, currentWorkspaceId, canOpenTeamManagement, canManageWorkspaceSync, workspaceCloudSyncEnabled, syncStatusLabel, workspaceSyncError, syncControlBusy, cloudAuthEnabled, availableAuthProviders, onFetchLoginMethods, onUnlinkLoginMethod, onAddPassword, onLinkProvider, onClose, onOpenWorkspaceAudit, onBillingIntervalChange, onRefreshBilling, onUpdateInterval, onManageBilling, onStartCheckout, onToggleWorkspaceSync, onOpenHelp, supportMailtoHref, }) {
12
13
  const lifecyclePresentation = resolveCommercialLifecyclePresentation(accountProfileSummary);
13
14
  const addPasswordGuidance = getPasswordPolicyGuidance();
14
15
  const billingRecoveryMessage = lifecyclePresentation.message;
@@ -104,5 +105,5 @@ export function AccountHubModal({ isOpen, theme, runtimeMode, authRequiredForApi
104
105
  ? (workspaceCloudSyncEnabled
105
106
  ? `Enabled · ${syncStatusLabel}`
106
107
  : 'Disabled')
107
- : 'Sign in to cloud account to enable workspace sync.' }), workspaceSyncError && (_jsx("p", { className: `${authStyles.loginError} ${managementStyles.errorText}`, children: workspaceSyncError })), _jsx("div", { className: managementStyles.actionRowEnd, children: _jsx("button", { className: styles.cancelBtn, disabled: !canManageWorkspaceSync || syncControlBusy, onClick: () => onToggleWorkspaceSync(!workspaceCloudSyncEnabled), children: workspaceCloudSyncEnabled ? 'Disable Sync' : 'Enable Sync' }) })] }), _jsxs("div", { className: `${modalStyles.formActions} ${managementStyles.modalActionsEnd}`, children: [_jsx("button", { className: styles.cancelBtn, onClick: onOpenHelp, children: "Help & Tutorial" }), _jsx("button", { className: styles.submitBtn, onClick: onClose, children: "Close" })] })] }) }));
108
+ : 'Sign in to cloud account to enable workspace sync.' }), workspaceSyncError && (_jsx("p", { className: `${authStyles.loginError} ${managementStyles.errorText}`, children: workspaceSyncError })), _jsx("div", { className: managementStyles.actionRowEnd, children: _jsx("button", { className: styles.cancelBtn, disabled: !canManageWorkspaceSync || syncControlBusy, onClick: () => onToggleWorkspaceSync(!workspaceCloudSyncEnabled), children: workspaceCloudSyncEnabled ? 'Disable Sync' : 'Enable Sync' }) })] }), _jsxs("div", { className: `${modalStyles.formActions} ${managementStyles.modalActionsEnd}`, children: [_jsx("button", { className: styles.cancelBtn, onClick: onOpenHelp, children: "Help & Support" }), _jsxs("a", { className: `${styles.cancelBtn} ${managementStyles.supportEmailBtn}`, href: supportMailtoHref, children: [_jsx(Mail, { size: 15 }), "Contact Support"] }), _jsx("button", { className: styles.submitBtn, onClick: onClose, children: "Close" })] })] }) }));
108
109
  }
@@ -16,7 +16,7 @@ describe('AccountHubModal', () => {
16
16
  message: 'Your latest checkout expired. Choose a plan to continue.',
17
17
  billingConflictCode: 'CHECKOUT_SESSION_EXPIRED',
18
18
  planSelectionRequired: true
19
- }, currentWorkspaceId: "workspace-1", canOpenTeamManagement: false, canManageWorkspaceSync: false, workspaceCloudSyncEnabled: false, syncStatusLabel: "Disabled", workspaceSyncError: null, syncControlBusy: false, cloudAuthEnabled: true, availableAuthProviders: [], onFetchLoginMethods: async () => ({ success: true, methods: [] }), onUnlinkLoginMethod: async () => ({ success: true }), onAddPassword: async () => ({ success: true }), onLinkProvider: () => { }, onClose: () => { }, onOpenWorkspaceAudit: () => { }, onBillingIntervalChange: () => { }, onRefreshBilling: () => { }, onUpdateInterval: () => { }, onManageBilling: () => { }, onStartCheckout: onStartCheckout, onToggleWorkspaceSync: () => { }, onOpenHelp: () => { } }));
19
+ }, currentWorkspaceId: "workspace-1", canOpenTeamManagement: false, canManageWorkspaceSync: false, workspaceCloudSyncEnabled: false, syncStatusLabel: "Disabled", workspaceSyncError: null, syncControlBusy: false, supportMailtoHref: "mailto:support@taskforcehq.ai", cloudAuthEnabled: true, availableAuthProviders: [], onFetchLoginMethods: async () => ({ success: true, methods: [] }), onUnlinkLoginMethod: async () => ({ success: true }), onAddPassword: async () => ({ success: true }), onLinkProvider: () => { }, onClose: () => { }, onOpenWorkspaceAudit: () => { }, onBillingIntervalChange: () => { }, onRefreshBilling: () => { }, onUpdateInterval: () => { }, onManageBilling: () => { }, onStartCheckout: onStartCheckout, onToggleWorkspaceSync: () => { }, onOpenHelp: () => { } }));
20
20
  expect(screen.getByText('Your previous checkout expired. Choose a plan to continue.')).toBeInTheDocument();
21
21
  expect(screen.getByText('Pro Plan')).toBeInTheDocument();
22
22
  const cta = screen.getByRole('button', { name: 'Choose Plan' });
@@ -30,21 +30,27 @@ describe('AccountHubModal', () => {
30
30
  planName: 'Pro Plan',
31
31
  entitlementState: 'suspended',
32
32
  stripeCustomerId: 'cus_123'
33
- }, currentWorkspaceId: "workspace-1", canOpenTeamManagement: false, canManageWorkspaceSync: false, workspaceCloudSyncEnabled: false, syncStatusLabel: "Disabled", workspaceSyncError: null, syncControlBusy: false, cloudAuthEnabled: true, availableAuthProviders: [], onFetchLoginMethods: async () => ({ success: true, methods: [] }), onUnlinkLoginMethod: async () => ({ success: true }), onAddPassword: async () => ({ success: true }), onLinkProvider: () => { }, onClose: () => { }, onOpenWorkspaceAudit: () => { }, onBillingIntervalChange: () => { }, onRefreshBilling: () => { }, onUpdateInterval: () => { }, onManageBilling: onManageBilling, onStartCheckout: () => { }, onToggleWorkspaceSync: () => { }, onOpenHelp: () => { } }));
33
+ }, currentWorkspaceId: "workspace-1", canOpenTeamManagement: false, canManageWorkspaceSync: false, workspaceCloudSyncEnabled: false, syncStatusLabel: "Disabled", workspaceSyncError: null, syncControlBusy: false, supportMailtoHref: "mailto:support@taskforcehq.ai", cloudAuthEnabled: true, availableAuthProviders: [], onFetchLoginMethods: async () => ({ success: true, methods: [] }), onUnlinkLoginMethod: async () => ({ success: true }), onAddPassword: async () => ({ success: true }), onLinkProvider: () => { }, onClose: () => { }, onOpenWorkspaceAudit: () => { }, onBillingIntervalChange: () => { }, onRefreshBilling: () => { }, onUpdateInterval: () => { }, onManageBilling: onManageBilling, onStartCheckout: () => { }, onToggleWorkspaceSync: () => { }, onOpenHelp: () => { } }));
34
34
  expect(screen.getByText('Your access is suspended because billing needs attention. Update billing to restore access.')).toBeInTheDocument();
35
35
  fireEvent.click(screen.getAllByRole('button', { name: 'Manage Billing' })[1]);
36
36
  expect(onManageBilling).toHaveBeenCalledTimes(1);
37
37
  });
38
38
  it('shows trialing accounts as active without blocked recovery copy', () => {
39
- render(_jsx(AccountHubModal, { isOpen: true, theme: "dark", runtimeMode: "cloud", authRequiredForApi: true, isAuthenticated: true, hasAuthIdentity: true, authIdentityLabel: "owner@example.com", billingLoading: false, billingError: null, billingActionError: null, billingNotice: null, billingActionBusy: false, billingIntervalChoice: "month", accountProfileSummary: {
40
- planId: 'starter',
41
- planName: 'Starter',
42
- entitlementState: 'trialing',
43
- stripeCustomerId: 'cus_123',
44
- trialEnd: '2026-05-01T12:00:00.000Z'
45
- }, currentWorkspaceId: "workspace-1", canOpenTeamManagement: false, canManageWorkspaceSync: false, workspaceCloudSyncEnabled: false, syncStatusLabel: "Disabled", workspaceSyncError: null, syncControlBusy: false, cloudAuthEnabled: true, availableAuthProviders: [], onFetchLoginMethods: async () => ({ success: true, methods: [] }), onUnlinkLoginMethod: async () => ({ success: true }), onAddPassword: async () => ({ success: true }), onLinkProvider: () => { }, onClose: () => { }, onOpenWorkspaceAudit: () => { }, onBillingIntervalChange: () => { }, onRefreshBilling: () => { }, onUpdateInterval: () => { }, onManageBilling: () => { }, onStartCheckout: () => { }, onToggleWorkspaceSync: () => { }, onOpenHelp: () => { } }));
46
- expect(screen.getByText(/Your trial is active through/)).toBeInTheDocument();
47
- expect(screen.queryByText('Your access is suspended because billing needs attention. Update billing to restore access.')).not.toBeInTheDocument();
39
+ const nowSpy = vi.spyOn(Date, 'now').mockReturnValue(new Date('2026-04-20T12:00:00.000Z').getTime());
40
+ try {
41
+ render(_jsx(AccountHubModal, { isOpen: true, theme: "dark", runtimeMode: "cloud", authRequiredForApi: true, isAuthenticated: true, hasAuthIdentity: true, authIdentityLabel: "owner@example.com", billingLoading: false, billingError: null, billingActionError: null, billingNotice: null, billingActionBusy: false, billingIntervalChoice: "month", accountProfileSummary: {
42
+ planId: 'starter',
43
+ planName: 'Starter',
44
+ entitlementState: 'trialing',
45
+ stripeCustomerId: 'cus_123',
46
+ trialEnd: '2026-05-01T12:00:00.000Z'
47
+ }, currentWorkspaceId: "workspace-1", canOpenTeamManagement: false, canManageWorkspaceSync: false, workspaceCloudSyncEnabled: false, syncStatusLabel: "Disabled", workspaceSyncError: null, syncControlBusy: false, supportMailtoHref: "mailto:support@taskforcehq.ai", cloudAuthEnabled: true, availableAuthProviders: [], onFetchLoginMethods: async () => ({ success: true, methods: [] }), onUnlinkLoginMethod: async () => ({ success: true }), onAddPassword: async () => ({ success: true }), onLinkProvider: () => { }, onClose: () => { }, onOpenWorkspaceAudit: () => { }, onBillingIntervalChange: () => { }, onRefreshBilling: () => { }, onUpdateInterval: () => { }, onManageBilling: () => { }, onStartCheckout: () => { }, onToggleWorkspaceSync: () => { }, onOpenHelp: () => { } }));
48
+ expect(screen.getByText(/Your trial is active through/)).toBeInTheDocument();
49
+ expect(screen.queryByText('Your access is suspended because billing needs attention. Update billing to restore access.')).not.toBeInTheDocument();
50
+ }
51
+ finally {
52
+ nowSpy.mockRestore();
53
+ }
48
54
  });
49
55
  it('shows an updating message instead of an expired active-trial date', () => {
50
56
  const nowSpy = vi.spyOn(Date, 'now').mockReturnValue(new Date('2026-04-20T12:00:00.000Z').getTime());
@@ -55,7 +61,7 @@ describe('AccountHubModal', () => {
55
61
  entitlementState: 'trialing',
56
62
  stripeCustomerId: 'cus_123',
57
63
  trialEnd: '2026-04-12T16:09:09.000Z'
58
- }, currentWorkspaceId: "workspace-1", canOpenTeamManagement: false, canManageWorkspaceSync: false, workspaceCloudSyncEnabled: false, syncStatusLabel: "Disabled", workspaceSyncError: null, syncControlBusy: false, cloudAuthEnabled: true, availableAuthProviders: [], onFetchLoginMethods: async () => ({ success: true, methods: [] }), onUnlinkLoginMethod: async () => ({ success: true }), onAddPassword: async () => ({ success: true }), onLinkProvider: () => { }, onClose: () => { }, onOpenWorkspaceAudit: () => { }, onBillingIntervalChange: () => { }, onRefreshBilling: () => { }, onUpdateInterval: () => { }, onManageBilling: () => { }, onStartCheckout: () => { }, onToggleWorkspaceSync: () => { }, onOpenHelp: () => { } }));
64
+ }, currentWorkspaceId: "workspace-1", canOpenTeamManagement: false, canManageWorkspaceSync: false, workspaceCloudSyncEnabled: false, syncStatusLabel: "Disabled", workspaceSyncError: null, syncControlBusy: false, supportMailtoHref: "mailto:support@taskforcehq.ai", cloudAuthEnabled: true, availableAuthProviders: [], onFetchLoginMethods: async () => ({ success: true, methods: [] }), onUnlinkLoginMethod: async () => ({ success: true }), onAddPassword: async () => ({ success: true }), onLinkProvider: () => { }, onClose: () => { }, onOpenWorkspaceAudit: () => { }, onBillingIntervalChange: () => { }, onRefreshBilling: () => { }, onUpdateInterval: () => { }, onManageBilling: () => { }, onStartCheckout: () => { }, onToggleWorkspaceSync: () => { }, onOpenHelp: () => { } }));
59
65
  expect(screen.getByText('Your trial end date has passed. Billing status is updating.')).toBeInTheDocument();
60
66
  expect(screen.queryByText(/Your trial is active through/)).not.toBeInTheDocument();
61
67
  }
@@ -4,6 +4,8 @@ interface HelpModalProps {
4
4
  theme: ModalThemeName;
5
5
  onClose: () => void;
6
6
  onOpenSettings: () => void;
7
+ onOpenAccountHub?: () => void;
8
+ supportMailtoHref: string;
7
9
  }
8
- export declare function HelpModal({ isOpen, theme, onClose, onOpenSettings, }: HelpModalProps): import("react/jsx-runtime").JSX.Element;
10
+ export declare function HelpModal({ isOpen, theme, onClose, onOpenSettings, onOpenAccountHub, supportMailtoHref, }: HelpModalProps): import("react/jsx-runtime").JSX.Element;
9
11
  export {};
@@ -1,8 +1,10 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { Mail, Settings, UserCircle, Wrench } from 'lucide-react';
2
3
  import { Modal } from '../../../ui/Modal';
3
4
  import modalStyles from '../../../ui/Modal.module.css';
4
5
  import managementStyles from '../../StandaloneManagement.module.css';
5
6
  import styles from '../../../../Taskforce.module.css';
6
- export function HelpModal({ isOpen, theme, onClose, onOpenSettings, }) {
7
- return (_jsx(Modal, { isOpen: isOpen, onClose: onClose, title: "Quick Start", size: "md", theme: theme, draggable: true, children: _jsxs("div", { className: `${modalStyles.form} ${managementStyles.modalBody}`, children: [_jsx("p", { className: managementStyles.mutedText, children: "Use this quick guide to get productive in Taskforce dashboard mode." }), _jsxs("div", { children: [_jsx("strong", { children: "1. Capture work" }), _jsxs("p", { className: managementStyles.sectionText, children: ["Click ", _jsx("code", { children: "Add Task" }), ", write a short title, and save."] })] }), _jsxs("div", { children: [_jsx("strong", { children: "2. Plan the board" }), _jsxs("p", { className: managementStyles.sectionText, children: ["Use ", _jsx("code", { children: "Sort" }), ", ", _jsx("code", { children: "Group" }), ", and ", _jsx("code", { children: "Filters" }), " in the header."] })] }), _jsxs("div", { children: [_jsx("strong", { children: "3. Execute and stage" }), _jsxs("p", { className: managementStyles.sectionText, children: ["Move tasks through ", _jsx("code", { children: "Task" }), ", ", _jsx("code", { children: "In Progress" }), ", ", _jsx("code", { children: "Review" }), ", and ", _jsx("code", { children: "Done" }), "."] })] }), _jsxs("div", { children: [_jsx("strong", { children: "4. Archive finished work" }), _jsx("p", { className: managementStyles.sectionText, children: "Archive completed/cancelled tasks to keep active board signal high." })] }), _jsxs("div", { className: `${modalStyles.formActions} ${managementStyles.modalActionsEnd}`, children: [_jsx("button", { className: styles.cancelBtn, onClick: onOpenSettings, children: "Open Settings" }), _jsx("button", { className: styles.submitBtn, onClick: onClose, children: "Close Tutorial" })] })] }) }));
7
+ import { TASKFORCE_SUPPORT_EMAIL } from '../../../../utils/supportContact';
8
+ export function HelpModal({ isOpen, theme, onClose, onOpenSettings, onOpenAccountHub, supportMailtoHref, }) {
9
+ return (_jsx(Modal, { isOpen: isOpen, onClose: onClose, title: "Help & Support", size: "md", theme: theme, draggable: true, children: _jsxs("div", { className: `${modalStyles.form} ${managementStyles.modalBody}`, children: [_jsxs("div", { className: managementStyles.helpIntro, children: [_jsx("p", { className: "tf-text-body", children: "Need a hand with Taskforce? Email support with what you were trying to do, what happened, and any workspace context that would help us reproduce it." }), _jsxs("a", { className: `${styles.submitBtn} ${managementStyles.supportEmailBtn}`, href: supportMailtoHref, children: [_jsx(Mail, { size: 16 }), "Email Support"] }), _jsxs("p", { className: "tf-text-helper", children: ["Opens your email client to ", TASKFORCE_SUPPORT_EMAIL, "."] })] }), _jsxs("div", { className: `${managementStyles.supportPanel} tf-surface-panel`, children: [_jsxs("div", { className: managementStyles.supportPanelHeader, children: [_jsx(Wrench, { size: 16 }), _jsxs("div", { children: [_jsx("h3", { className: "tf-heading-card", children: "Before you email" }), _jsx("p", { className: "tf-text-secondary", children: "These are the fastest places to check for common workspace, account, and setup issues." })] })] }), _jsxs("div", { className: managementStyles.supportRouteList, children: [_jsxs("button", { className: `${styles.cancelBtn} ${managementStyles.supportRouteBtn}`, onClick: onOpenSettings, children: [_jsx(Settings, { size: 15 }), "Open Settings"] }), onOpenAccountHub && (_jsxs("button", { className: `${styles.cancelBtn} ${managementStyles.supportRouteBtn}`, onClick: onOpenAccountHub, children: [_jsx(UserCircle, { size: 15 }), "Open Account Hub"] }))] })] }), _jsx("div", { className: `${modalStyles.formActions} ${managementStyles.modalActionsEnd}`, children: _jsx("button", { className: styles.cancelBtn, onClick: onClose, children: "Close" }) })] }) }));
8
10
  }
@@ -0,0 +1,23 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { fireEvent, render, screen } from '@testing-library/react';
3
+ import { describe, expect, it, vi } from 'vitest';
4
+ import { TASKFORCE_SUPPORT_EMAIL } from '../../../../utils/supportContact';
5
+ import { HelpModal } from './HelpModal';
6
+ vi.mock('../../../ui/Modal', () => ({
7
+ Modal: ({ isOpen, children, title }) => (isOpen ? _jsxs("div", { children: [_jsx("h1", { children: title }), children] }) : null)
8
+ }));
9
+ describe('HelpModal', () => {
10
+ it('presents support actions instead of tutorial steps', () => {
11
+ const onOpenSettings = vi.fn();
12
+ const onOpenAccountHub = vi.fn();
13
+ render(_jsx(HelpModal, { isOpen: true, theme: "dark", onClose: () => { }, onOpenSettings: onOpenSettings, onOpenAccountHub: onOpenAccountHub, supportMailtoHref: `mailto:${TASKFORCE_SUPPORT_EMAIL}` }));
14
+ expect(screen.getByRole('heading', { name: 'Help & Support' })).toBeInTheDocument();
15
+ expect(screen.getByRole('link', { name: 'Email Support' })).toHaveAttribute('href', `mailto:${TASKFORCE_SUPPORT_EMAIL}`);
16
+ expect(screen.queryByText('Close Tutorial')).not.toBeInTheDocument();
17
+ expect(screen.queryByText('Capture work')).not.toBeInTheDocument();
18
+ fireEvent.click(screen.getByRole('button', { name: 'Open Settings' }));
19
+ expect(onOpenSettings).toHaveBeenCalledTimes(1);
20
+ fireEvent.click(screen.getByRole('button', { name: 'Open Account Hub' }));
21
+ expect(onOpenAccountHub).toHaveBeenCalledTimes(1);
22
+ });
23
+ });