@taskforcehq/taskforce 0.3.304 → 0.3.305

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 (138) hide show
  1. package/README.md +140 -247
  2. package/dist/TaskforceCore.js +165 -70
  3. package/dist/TaskforceCore.test.js +799 -90
  4. package/dist/compat/workspaceSyncCompat.js +6 -0
  5. package/dist/components/features/DocumentIndex.d.ts +1 -1
  6. package/dist/components/features/DocumentIndex.js +1 -1
  7. package/dist/components/features/DocumentViewer.d.ts +1 -1
  8. package/dist/components/features/DocumentViewer.js +32 -9
  9. package/dist/components/features/DocumentWorkspace.d.ts +5 -22
  10. package/dist/components/features/DocumentWorkspace.js +12 -155
  11. package/dist/components/features/DocumentWorkspace.test.js +193 -6
  12. package/dist/components/features/TaskSettings.js +29 -3
  13. package/dist/components/features/TaskSettings.test.js +42 -1
  14. package/dist/components/features/documentWorkspaceModel.d.ts +24 -0
  15. package/dist/components/features/documentWorkspaceModel.js +57 -0
  16. package/dist/components/features/useDocumentWorkspaceController.d.ts +34 -0
  17. package/dist/components/features/useDocumentWorkspaceController.js +113 -0
  18. package/dist/components/task/TaskForm.d.ts +8 -8
  19. package/dist/components/task/TaskForm.js +358 -221
  20. package/dist/components/task/TaskForm.test.js +456 -381
  21. package/dist/components/task/TaskKanban.js +2 -1
  22. package/dist/components/views/PlanComparisonPage.js +5 -8
  23. package/dist/components/views/PlanComparisonPage.test.js +51 -41
  24. package/dist/components/views/PlansPage.js +126 -84
  25. package/dist/components/views/PlansPage.test.js +308 -39
  26. package/dist/components/views/StandaloneLayout.js +28 -22
  27. package/dist/components/views/panels/FilterToolbar.test.js +6 -4
  28. package/dist/components/views/standalone/modals/AccountHubModal.d.ts +2 -14
  29. package/dist/components/views/standalone/modals/AccountHubModal.js +12 -14
  30. package/dist/components/views/standalone/modals/AccountHubModal.test.js +24 -1
  31. package/dist/config/envSchema.js +1 -1
  32. package/dist/core/AiProfileService.d.ts +6 -1
  33. package/dist/core/AiProfileService.js +161 -16
  34. package/dist/core/AiProfileService.test.js +3 -1
  35. package/dist/core/AiProfiles.test.js +200 -0
  36. package/dist/core/AuthTokenService.test.d.ts +1 -0
  37. package/dist/core/AuthTokenService.test.js +78 -0
  38. package/dist/core/EntitlementsPolicy.test.js +75 -13
  39. package/dist/core/PlanEntitlementService.d.ts +7 -1
  40. package/dist/core/PlanEntitlementService.js +117 -42
  41. package/dist/core/PlanVersionPolicy.test.js +19 -26
  42. package/dist/core/SignupMonetizationSettings.test.js +46 -21
  43. package/dist/core/SystemAdmin.test.js +212 -56
  44. package/dist/core/TaskTaxonomyValidation.test.js +53 -0
  45. package/dist/core/Taskforce.d.ts +3 -0
  46. package/dist/core/Taskforce.js +50 -10
  47. package/dist/core/WorkspacePlanMode.test.js +12 -12
  48. package/dist/core/shared.d.ts +2 -0
  49. package/dist/core/shared.js +11 -0
  50. package/dist/hooks/sync/auth.js +3 -1
  51. package/dist/hooks/sync/transfers.d.ts +2 -1
  52. package/dist/hooks/sync/transfers.js +8 -4
  53. package/dist/hooks/useSyncOrchestrator.d.ts +7 -4
  54. package/dist/hooks/useSyncOrchestrator.js +75 -7
  55. package/dist/hooks/useSyncOrchestrator.retry-closure.test.js +80 -10
  56. package/dist/hooks/useTaskMutations.d.ts +2 -1
  57. package/dist/hooks/useTaskMutations.js +12 -1
  58. package/dist/hooks/useTaskMutations.test.js +30 -1
  59. package/dist/hooks/useTaskforce.d.ts +5 -2
  60. package/dist/hooks/useTaskforce.js +17 -5
  61. package/dist/hooks/useTaskforce.sync-behavior.test.js +78 -0
  62. package/dist/hooks/useWorkspaceSyncController.d.ts +3 -1
  63. package/dist/hooks/useWorkspaceSyncController.js +11 -1
  64. package/dist/hooks/useWorkspaceSyncController.test.js +1 -0
  65. package/dist/mcp/adminWorkspaceRegistrar.d.ts +2 -0
  66. package/dist/mcp/adminWorkspaceRegistrar.js +154 -0
  67. package/dist/mcp/collaborationRegistrar.d.ts +2 -0
  68. package/dist/mcp/collaborationRegistrar.js +71 -0
  69. package/dist/mcp/documentAssetRegistrar.d.ts +2 -0
  70. package/dist/mcp/documentAssetRegistrar.js +346 -0
  71. package/dist/mcp/runtime.js +2362 -3530
  72. package/dist/mcp/runtime.test.js +159 -0
  73. package/dist/mcp/taskPlanningRegistrar.d.ts +2 -0
  74. package/dist/mcp/taskPlanningRegistrar.js +418 -0
  75. package/dist/mcp/toolCatalog.d.ts +35 -0
  76. package/dist/mcp/toolCatalog.js +3 -0
  77. package/dist/server/routes/admin.d.ts +1 -5
  78. package/dist/server/routes/admin.js +138 -61
  79. package/dist/server/routes/annotatedAttachments.d.ts +1 -1
  80. package/dist/server/routes/annotatedAttachments.js +1 -1
  81. package/dist/server/routes/auth.d.ts +1 -4
  82. package/dist/server/routes/auth.js +23 -63
  83. package/dist/server/routes/authSupport.d.ts +30 -0
  84. package/dist/server/routes/authSupport.js +81 -0
  85. package/dist/server/routes/billing.d.ts +1 -1
  86. package/dist/server/routes/billing.js +104 -188
  87. package/dist/server/routes/billing.test.js +214 -86
  88. package/dist/server/routes/documentReviews.d.ts +1 -1
  89. package/dist/server/routes/documentReviews.js +1 -1
  90. package/dist/server/routes/documents.d.ts +4 -2
  91. package/dist/server/routes/documents.js +73 -7
  92. package/dist/server/routes/primitives.d.ts +11 -0
  93. package/dist/server/routes/primitives.js +73 -0
  94. package/dist/server/routes/resources.d.ts +1 -1
  95. package/dist/server/routes/resources.js +1 -1
  96. package/dist/server/routes/shared.d.ts +1 -1
  97. package/dist/server/routes/sync.d.ts +1 -1
  98. package/dist/server/routes/sync.js +1 -1
  99. package/dist/server/routes/tasks.d.ts +1 -1
  100. package/dist/server/routes/tasks.js +1 -1
  101. package/dist/server/routes/workspaces.d.ts +1 -1
  102. package/dist/server/routes/workspaces.js +1 -1
  103. package/dist/server/routes.d.ts +2 -9
  104. package/dist/server/routes.js +76 -162
  105. package/dist/server/routes.test.js +672 -30
  106. package/dist/sync/contentSyncState.js +16 -7
  107. package/dist/sync/syncApplyHandlers.test.js +2 -0
  108. package/dist/sync/syncService.d.ts +1 -0
  109. package/dist/sync/syncService.js +8 -0
  110. package/dist/sync/syncService.test.js +14 -0
  111. package/dist/sync/workspaceSyncModel.d.ts +3 -0
  112. package/dist/sync/workspaceSyncModel.js +12 -3
  113. package/dist/sync/workspaceSyncModel.test.js +34 -0
  114. package/dist/sync/workspaceSyncState.d.ts +2 -0
  115. package/dist/sync/workspaceSyncState.js +1 -0
  116. package/dist/ui/assets/{AgentsModule-BLWVbuKP.js → AgentsModule-N2MnQBZk.js} +1 -1
  117. package/dist/ui/assets/{AnnotatedAttachmentWorkspace-BlS-cqnl.js → AnnotatedAttachmentWorkspace-BG6P_GVW.js} +1 -1
  118. package/dist/ui/assets/DocumentWorkspace-B3nV_Gc5.css +1 -0
  119. package/dist/ui/assets/DocumentWorkspace-DRbDMwh8.js +252 -0
  120. package/dist/ui/assets/{InitiativesModule-BjR6iBf9.js → InitiativesModule-Dhm7M8e7.js} +1 -1
  121. package/dist/ui/assets/PlansPage-Cq0zXj3I.js +1 -0
  122. package/dist/ui/assets/TaskSettings-ln0aF7xc.js +9 -0
  123. package/dist/ui/assets/{WorkflowsModule-DnFqdUME.js → WorkflowsModule-BcS4afRn.js} +1 -1
  124. package/dist/ui/assets/{index-Ii0B0rTO.css → index-C2-OXZV7.css} +1 -1
  125. package/dist/ui/assets/index-DcSI5F86.js +6 -0
  126. package/dist/ui/assets/{vendor-icons-I7ZwG1z_.js → vendor-icons-BSUaRwF8.js} +1 -1
  127. package/dist/ui/index.html +3 -3
  128. package/dist/utils/commercialLifecyclePresentation.d.ts +19 -0
  129. package/dist/utils/commercialLifecyclePresentation.js +196 -0
  130. package/package.json +8 -1
  131. package/scripts/check-cloud-mcp.mjs +83 -0
  132. package/scripts/playwright-auth.sh +5 -1
  133. package/scripts/run-billing-performance-smoke.sh +24 -0
  134. package/dist/ui/assets/DocumentWorkspace-BH_6DF6x.js +0 -250
  135. package/dist/ui/assets/DocumentWorkspace-C_8T8oz-.css +0 -1
  136. package/dist/ui/assets/PlansPage-CSDQ4sLa.js +0 -1
  137. package/dist/ui/assets/TaskSettings-CEDy34Jo.js +0 -9
  138. package/dist/ui/assets/index-DXUdtH1B.js +0 -6
@@ -4,6 +4,10 @@ function normalizeTimestamp(value) {
4
4
  const normalized = String(value || '').trim();
5
5
  return normalized.length > 0 ? normalized : null;
6
6
  }
7
+ function normalizeSetupIntent(value) {
8
+ const normalized = String(value || '').trim().toLowerCase();
9
+ return normalized === 'attach-cloud-import' ? 'attach-cloud-import' : null;
10
+ }
7
11
  function resolveLegacyPhase(input, base) {
8
12
  const enabled = Boolean(input.enabled ?? input.cloudSyncEnabled);
9
13
  if (!enabled)
@@ -46,6 +50,7 @@ export function normalizeWorkspaceSyncStateCompat(rawState) {
46
50
  version: 2,
47
51
  enabled: Boolean(state.enabled ?? state.cloudSyncEnabled),
48
52
  phase: 'idle',
53
+ setupIntent: normalizeSetupIntent(state.setupIntent),
49
54
  pullCursor: normalizeTimestamp(state.pullCursor),
50
55
  lastPullAt: normalizeTimestamp(state.lastPullAt),
51
56
  lastPushAt: normalizeTimestamp(state.lastPushAt),
@@ -67,6 +72,7 @@ export function normalizeWorkspaceSyncStateCompat(rawState) {
67
72
  const changed = currentVersion !== 2
68
73
  || Boolean(state.enabled ?? state.cloudSyncEnabled) !== normalized.enabled
69
74
  || String(state.phase || '').trim() !== normalized.phase
75
+ || normalizeSetupIntent(state.setupIntent) !== normalized.setupIntent
70
76
  || normalizeTimestamp(state.pullCursor) !== normalized.pullCursor
71
77
  || normalizeTimestamp(state.lastPullAt) !== normalized.lastPullAt
72
78
  || normalizeTimestamp(state.lastPushAt) !== normalized.lastPushAt
@@ -1,4 +1,4 @@
1
- import { type DocMeta } from './DocumentWorkspace';
1
+ import { type DocMeta } from './documentWorkspaceModel';
2
2
  interface Props {
3
3
  documents: DocMeta[];
4
4
  selectedDocId: string | null;
@@ -2,7 +2,7 @@ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-run
2
2
  import { Search, RefreshCw, FileText, ClipboardList, Eye, BookOpen, File } from 'lucide-react';
3
3
  import styles from '../../Taskforce.module.css';
4
4
  import indexStyles from './DocumentIndex.module.css';
5
- import { getEffectiveAttachmentState } from './DocumentWorkspace';
5
+ import { getEffectiveAttachmentState } from './documentWorkspaceModel';
6
6
  import { getReadableDocumentName } from '../../utils/documentNames';
7
7
  function DocTypeIcon({ docType }) {
8
8
  switch (docType) {
@@ -1,4 +1,4 @@
1
- import { type DocMeta } from './DocumentWorkspace';
1
+ import { type DocMeta } from './documentWorkspaceModel';
2
2
  export declare function buildPrintDocumentHtml(markdown: string): string;
3
3
  export declare function schedulePrintCleanup(cleanup: () => void): number;
4
4
  interface Props {
@@ -8,7 +8,7 @@ import { Printer, Link, Download, Clock, HardDrive, Tag, Pencil, X, Save, Eye, S
8
8
  import styles from '../../Taskforce.module.css';
9
9
  import viewerStyles from './DocumentViewer.module.css';
10
10
  import taskFormStyles from '../task/TaskForm.module.css';
11
- import { getEffectiveAttachmentState } from './DocumentWorkspace';
11
+ import { getEffectiveAttachmentState } from './documentWorkspaceModel';
12
12
  import { DocumentGenerateModal } from './DocumentGenerateModal';
13
13
  import { fetchTaskforceApi, resolveTaskforceApiUrl } from '../../utils/taskforceApiClient';
14
14
  import { getDocumentDownloadName, getReadableDocumentName } from '../../utils/documentNames';
@@ -522,6 +522,7 @@ export function DocumentViewer({ doc, taskTitle, apiBaseUrl = '', onSaved, onDel
522
522
  const [reviewTitleDraft, setReviewTitleDraft] = useState('');
523
523
  const [editingCommentId, setEditingCommentId] = useState(null);
524
524
  const [editingCommentDraft, setEditingCommentDraft] = useState('');
525
+ const [showReviewSidebar, setShowReviewSidebar] = useState(true);
525
526
  // Edit mode state
526
527
  const [editMode, setEditMode] = useState(false);
527
528
  const [draft, setDraft] = useState('');
@@ -534,7 +535,7 @@ export function DocumentViewer({ doc, taskTitle, apiBaseUrl = '', onSaved, onDel
534
535
  const [renameValue, setRenameValue] = useState(readableName);
535
536
  const isEditable = doc.editable !== false;
536
537
  const attachmentState = getEffectiveAttachmentState(doc);
537
- const canDelete = attachmentState === 'unattached' && !!doc.assetId;
538
+ const canDelete = !!doc.assetId;
538
539
  const isDirty = editMode && draft !== (content ?? '');
539
540
  const textareaRef = useRef(null);
540
541
  const renameInputRef = useRef(null);
@@ -913,13 +914,20 @@ export function DocumentViewer({ doc, taskTitle, apiBaseUrl = '', onSaved, onDel
913
914
  const handleDelete = useCallback(async () => {
914
915
  if (!doc.assetId || deleting)
915
916
  return;
916
- if (!window.confirm(`Permanently delete "${readableName}" from storage?`))
917
+ const isAttached = attachmentState === 'attached';
918
+ const linkedTaskCount = Math.max(0, Number(doc.attachmentCount || 0));
919
+ const confirmationMessage = isAttached
920
+ ? `Delete "${readableName}" everywhere?\n\nThis will remove it from ${linkedTaskCount} linked task${linkedTaskCount === 1 ? '' : 's'} and soft-delete the document across sync.`
921
+ : `Permanently delete "${readableName}" from storage?`;
922
+ if (!window.confirm(confirmationMessage))
917
923
  return;
918
924
  setDeleting(true);
919
925
  try {
920
926
  const res = await fetchTaskforceApi(`/api/taskforce/documents/${encodeURIComponent(doc.assetId)}`, {
921
927
  method: 'DELETE',
922
928
  credentials: 'include',
929
+ headers: { 'Content-Type': 'application/json' },
930
+ body: JSON.stringify(isAttached ? { forceDetach: true } : {}),
923
931
  }, apiBaseUrl);
924
932
  const data = await res.json().catch(() => ({}));
925
933
  if (!res.ok)
@@ -932,7 +940,7 @@ export function DocumentViewer({ doc, taskTitle, apiBaseUrl = '', onSaved, onDel
932
940
  finally {
933
941
  setDeleting(false);
934
942
  }
935
- }, [apiBaseUrl, deleting, doc.assetId, onDeleted, readableName]);
943
+ }, [apiBaseUrl, attachmentState, deleting, doc.assetId, doc.attachmentCount, onDeleted, readableName]);
936
944
  const handleToggleReviewStatus = useCallback(async () => {
937
945
  if (!activeReviewSession || reviewBusy)
938
946
  return;
@@ -992,7 +1000,7 @@ export function DocumentViewer({ doc, taskTitle, apiBaseUrl = '', onSaved, onDel
992
1000
  }, [activeReviewSession, apiBaseUrl, reviewBusy, reviewTitleDraft]);
993
1001
  const previewContent = editMode ? draft : (content ?? '');
994
1002
  const handleCaptureReviewAnchor = useCallback(() => {
995
- if (editMode)
1003
+ if (editMode || !showReviewSidebar)
996
1004
  return;
997
1005
  const selection = typeof window.getSelection === 'function' ? window.getSelection() : null;
998
1006
  const selectedText = selection?.toString().replace(/\s+/g, ' ').trim() || '';
@@ -1011,7 +1019,16 @@ export function DocumentViewer({ doc, taskTitle, apiBaseUrl = '', onSaved, onDel
1011
1019
  if (!anchor)
1012
1020
  return;
1013
1021
  setPendingReviewAnchor(anchor);
1014
- }, [editMode]);
1022
+ }, [editMode, showReviewSidebar]);
1023
+ const handleToggleReviewSidebar = useCallback(() => {
1024
+ setShowReviewSidebar((current) => {
1025
+ const nextValue = !current;
1026
+ if (!nextValue) {
1027
+ setPendingReviewAnchor(null);
1028
+ }
1029
+ return nextValue;
1030
+ });
1031
+ }, []);
1015
1032
  const handleAddReviewComment = useCallback(async () => {
1016
1033
  const body = reviewDraft.trim();
1017
1034
  if (!body || reviewBusy)
@@ -1210,10 +1227,16 @@ export function DocumentViewer({ doc, taskTitle, apiBaseUrl = '', onSaved, onDel
1210
1227
  return;
1211
1228
  setRenameValue(readableName);
1212
1229
  setRenaming(true);
1213
- }, children: readableName })), taskTitle && (_jsx("span", { className: viewerStyles.metaTaskName, title: `From task: ${taskTitle}`, children: taskTitle })), attachmentState === 'unattached' && (_jsx("span", { className: viewerStyles.statusBadge, children: "Unattached" })), doc.updatedAt && (_jsxs("span", { className: viewerStyles.metaDate, title: formatDate(doc.updatedAt), children: [_jsx(Clock, { size: 11 }), formatDate(doc.updatedAt)] })), _jsxs("span", { className: viewerStyles.metaSize, children: [_jsx(HardDrive, { size: 11 }), formatBytes(doc.sizeBytes)] }), documentReferenceLabel && (_jsx(ReferenceBadgeButton, { label: documentReferenceLabel, copied: copiedReference, onClick: () => { void handleCopyReference(); }, title: "Copy document reference" })), saveStatus.type === 'saved' && (_jsxs("span", { className: `${viewerStyles.saveStatus} ${viewerStyles.saveStatusSaved}`, children: [_jsx(CheckCircle, { size: 11 }), "Saved"] })), saveStatus.type === 'error' && (_jsxs("span", { className: `${viewerStyles.saveStatus} ${viewerStyles.saveStatusError}`, title: saveStatus.message, children: [_jsx(AlertCircle, { size: 11 }), saveStatus.message || 'Save failed'] })), isDirty && saveStatus.type !== 'saving' && (_jsx("span", { className: viewerStyles.unsaved, children: "Unsaved changes" }))] }), _jsx("div", { className: viewerStyles.actions, children: editMode ? (_jsxs(_Fragment, { children: [_jsxs("div", { className: viewerStyles.layoutToggle, children: [_jsx("button", { className: `${viewerStyles.layoutBtn} ${layout === 'editor' ? viewerStyles.layoutBtnActive : ''}`, onClick: () => setLayout('editor'), title: "Editor only", children: _jsx(Pencil, { size: 12 }) }), _jsx("button", { className: `${viewerStyles.layoutBtn} ${layout === 'split' ? viewerStyles.layoutBtnActive : ''}`, onClick: () => setLayout('split'), title: "Split view", children: _jsx(SplitSquareHorizontal, { size: 12 }) }), _jsx("button", { className: `${viewerStyles.layoutBtn} ${layout === 'preview' ? viewerStyles.layoutBtnActive : ''}`, onClick: () => setLayout('preview'), title: "Preview only", children: _jsx(Eye, { size: 12 }) })] }), _jsxs("button", { className: viewerStyles.actionBtn, onClick: handleCancelEdit, title: "Discard changes", children: [_jsx(X, { size: 13 }), "Cancel"] }), _jsxs("button", { className: `${viewerStyles.actionBtn} ${viewerStyles.saveBtn}`, onClick: handleSave, disabled: saveStatus.type === 'saving' || !isDirty, title: "Save (Ctrl+S)", children: [_jsx(Save, { size: 13 }), saveStatus.type === 'saving' ? 'Saving…' : 'Save'] })] })) : (_jsxs(_Fragment, { children: [doc.assetId && (_jsxs("button", { className: viewerStyles.actionBtn, onClick: () => setRenaming((prev) => !prev), title: "Rename document", children: [_jsx(Pencil, { size: 13 }), renaming ? 'Close Rename' : 'Rename'] })), isEditable && (_jsxs("button", { className: `${viewerStyles.actionBtn} ${viewerStyles.editBtn}`, onClick: handleEnterEdit, title: "Edit document", children: [_jsx(Pencil, { size: 13 }), "Edit"] })), _jsxs("button", { className: viewerStyles.actionBtn, onClick: handleCopyLink, title: "Copy link", children: [_jsx(Link, { size: 13 }), copied ? 'Copied!' : 'Copy link'] }), _jsxs("button", { className: viewerStyles.actionBtn, onClick: handleDownload, title: "Download .md", children: [_jsx(Download, { size: 13 }), "Download"] }), _jsxs("button", { className: viewerStyles.actionBtn, onClick: handlePrint, title: "Print", children: [_jsx(Printer, { size: 13 }), "Print"] }), _jsxs("button", { className: viewerStyles.actionBtn, onClick: () => setMonochromeMode((prev) => !prev), title: "Toggle black and white style", children: [_jsx(Eye, { size: 13 }), monochromeMode ? 'Color' : 'B/W'] }), enableTaskGeneration && doc.docType === 'implementation-plan' && !editMode && (_jsxs("button", { className: `${viewerStyles.actionBtn} ${viewerStyles.generateBtn}`, onClick: () => setShowGenModal(true), title: "Generate tasks from this plan", children: [_jsx(Zap, { size: 13 }), "Generate Tasks"] })), canDelete && (_jsxs("button", { className: `${viewerStyles.actionBtn} ${viewerStyles.dangerBtn}`, onClick: () => { void handleDelete(); }, disabled: deleting, title: "Permanently delete this unattached document", children: [_jsx(Trash2, { size: 13 }), deleting ? 'Deleting…' : 'Delete Permanently'] }))] })) })] }), loadingContent && (_jsx("div", { className: viewerStyles.viewerState, children: "Loading\u2026" })), !loadingContent && loadError && (_jsx("div", { className: viewerStyles.viewerStateError, children: loadError })), !loadingContent && !loadError && (editMode ? (_jsxs("div", { className: `${viewerStyles.editorArea} ${EDITOR_LAYOUT_CLASS[layout]}`, children: [(layout === 'editor' || layout === 'split') && (_jsxs("div", { className: viewerStyles.editorPane, children: [_jsx("div", { className: viewerStyles.editorPaneLabel, children: "Markdown" }), _jsx("textarea", { ref: textareaRef, className: `${viewerStyles.editorTextarea} tf-scrollbar`, value: draft, onScroll: () => syncPaneScroll('editor'), onChange: e => {
1230
+ }, children: readableName })), taskTitle && (_jsx("span", { className: viewerStyles.metaTaskName, title: `From task: ${taskTitle}`, children: taskTitle })), attachmentState === 'unattached' && (_jsx("span", { className: viewerStyles.statusBadge, children: "Unattached" })), doc.updatedAt && (_jsxs("span", { className: viewerStyles.metaDate, title: formatDate(doc.updatedAt), children: [_jsx(Clock, { size: 11 }), formatDate(doc.updatedAt)] })), _jsxs("span", { className: viewerStyles.metaSize, children: [_jsx(HardDrive, { size: 11 }), formatBytes(doc.sizeBytes)] }), documentReferenceLabel && (_jsx(ReferenceBadgeButton, { label: documentReferenceLabel, copied: copiedReference, onClick: () => { void handleCopyReference(); }, title: "Copy document reference" })), saveStatus.type === 'saved' && (_jsxs("span", { className: `${viewerStyles.saveStatus} ${viewerStyles.saveStatusSaved}`, children: [_jsx(CheckCircle, { size: 11 }), "Saved"] })), saveStatus.type === 'error' && (_jsxs("span", { className: `${viewerStyles.saveStatus} ${viewerStyles.saveStatusError}`, title: saveStatus.message, children: [_jsx(AlertCircle, { size: 11 }), saveStatus.message || 'Save failed'] })), isDirty && saveStatus.type !== 'saving' && (_jsx("span", { className: viewerStyles.unsaved, children: "Unsaved changes" }))] }), _jsx("div", { className: viewerStyles.actions, children: editMode ? (_jsxs(_Fragment, { children: [_jsxs("div", { className: viewerStyles.layoutToggle, children: [_jsx("button", { className: `${viewerStyles.layoutBtn} ${layout === 'editor' ? viewerStyles.layoutBtnActive : ''}`, onClick: () => setLayout('editor'), title: "Editor only", children: _jsx(Pencil, { size: 12 }) }), _jsx("button", { className: `${viewerStyles.layoutBtn} ${layout === 'split' ? viewerStyles.layoutBtnActive : ''}`, onClick: () => setLayout('split'), title: "Split view", children: _jsx(SplitSquareHorizontal, { size: 12 }) }), _jsx("button", { className: `${viewerStyles.layoutBtn} ${layout === 'preview' ? viewerStyles.layoutBtnActive : ''}`, onClick: () => setLayout('preview'), title: "Preview only", children: _jsx(Eye, { size: 12 }) })] }), _jsxs("button", { className: viewerStyles.actionBtn, onClick: handleCancelEdit, title: "Discard changes", children: [_jsx(X, { size: 13 }), "Cancel"] }), _jsxs("button", { className: `${viewerStyles.actionBtn} ${viewerStyles.saveBtn}`, onClick: handleSave, disabled: saveStatus.type === 'saving' || !isDirty, title: "Save (Ctrl+S)", children: [_jsx(Save, { size: 13 }), saveStatus.type === 'saving' ? 'Saving…' : 'Save'] })] })) : (_jsxs(_Fragment, { children: [doc.assetId && (_jsxs("button", { className: viewerStyles.actionBtn, onClick: () => setRenaming((prev) => !prev), title: "Rename document", children: [_jsx(Pencil, { size: 13 }), renaming ? 'Close Rename' : 'Rename'] })), isEditable && (_jsxs("button", { className: `${viewerStyles.actionBtn} ${viewerStyles.editBtn}`, onClick: handleEnterEdit, title: "Edit document", children: [_jsx(Pencil, { size: 13 }), "Edit"] })), _jsxs("button", { className: viewerStyles.actionBtn, onClick: handleToggleReviewSidebar, title: showReviewSidebar ? 'Hide review and comments' : 'Show review and comments', children: [_jsx(MessageSquareText, { size: 13 }), showReviewSidebar ? 'Hide Review' : 'Show Review'] }), _jsxs("button", { className: viewerStyles.actionBtn, onClick: handleCopyLink, title: "Copy link", children: [_jsx(Link, { size: 13 }), copied ? 'Copied!' : 'Copy link'] }), _jsxs("button", { className: viewerStyles.actionBtn, onClick: handleDownload, title: "Download .md", children: [_jsx(Download, { size: 13 }), "Download"] }), _jsxs("button", { className: viewerStyles.actionBtn, onClick: handlePrint, title: "Print", children: [_jsx(Printer, { size: 13 }), "Print"] }), _jsxs("button", { className: viewerStyles.actionBtn, onClick: () => setMonochromeMode((prev) => !prev), title: "Toggle black and white style", children: [_jsx(Eye, { size: 13 }), monochromeMode ? 'Color' : 'B/W'] }), enableTaskGeneration && doc.docType === 'implementation-plan' && !editMode && (_jsxs("button", { className: `${viewerStyles.actionBtn} ${viewerStyles.generateBtn}`, onClick: () => setShowGenModal(true), title: "Generate tasks from this plan", children: [_jsx(Zap, { size: 13 }), "Generate Tasks"] })), canDelete && (_jsxs("button", { className: `${viewerStyles.actionBtn} ${viewerStyles.dangerBtn}`, onClick: () => { void handleDelete(); }, disabled: deleting, title: attachmentState === 'attached'
1231
+ ? 'Remove this document from linked tasks and delete it'
1232
+ : 'Permanently delete this unattached document', children: [_jsx(Trash2, { size: 13 }), deleting
1233
+ ? 'Deleting…'
1234
+ : attachmentState === 'attached'
1235
+ ? 'Delete & Unlink'
1236
+ : 'Delete Permanently'] }))] })) })] }), loadingContent && (_jsx("div", { className: viewerStyles.viewerState, children: "Loading\u2026" })), !loadingContent && loadError && (_jsx("div", { className: viewerStyles.viewerStateError, children: loadError })), !loadingContent && !loadError && (editMode ? (_jsxs("div", { className: `${viewerStyles.editorArea} ${EDITOR_LAYOUT_CLASS[layout]}`, children: [(layout === 'editor' || layout === 'split') && (_jsxs("div", { className: viewerStyles.editorPane, children: [_jsx("div", { className: viewerStyles.editorPaneLabel, children: "Markdown" }), _jsx("textarea", { ref: textareaRef, className: `${viewerStyles.editorTextarea} tf-scrollbar`, value: draft, onScroll: () => syncPaneScroll('editor'), onChange: e => {
1214
1237
  setDraft(e.target.value);
1215
1238
  setSaveStatus(prev => prev.type === 'saving' ? prev : { type: 'idle' });
1216
- }, spellCheck: true, placeholder: "Write markdown here\u2026" })] })), (layout === 'preview' || layout === 'split') && (_jsxs("div", { className: viewerStyles.previewPane, children: [_jsx("div", { className: viewerStyles.editorPaneLabel, children: "Preview" }), _jsx("div", { ref: previewScrollRef, className: `${styles.docMarkdown} ${viewerStyles.markdownSurface}`, "data-testid": "doc-preview-scroll", onScroll: () => syncPaneScroll('preview'), children: _jsx(Markdown, { remarkPlugins: [remarkGfm], rehypePlugins: [rehypeSanitize], components: markdownComponents, children: previewContent }) })] }))] })) : (_jsxs("div", { className: viewerStyles.viewerBody, children: [_jsx("div", { className: `${viewerStyles.viewerContent} ${viewerStyles.viewerMain} tf-scrollbar`, children: _jsx("div", { ref: markdownContainerRef, className: `${styles.docMarkdown} ${viewerStyles.markdownSurface}`, onMouseUp: handleCaptureReviewAnchor, children: _jsx(Markdown, { remarkPlugins: [remarkGfm], rehypePlugins: [rehypeSanitize], components: markdownComponents, children: previewContent }) }) }), _jsx("aside", { className: viewerStyles.reviewRail, children: _jsxs("section", { className: viewerStyles.reviewSection, "aria-label": "Review and comments", children: [_jsxs("div", { className: viewerStyles.reviewHeader, children: [_jsxs("div", { children: [_jsxs("div", { className: viewerStyles.reviewTitle, children: [_jsx(MessageSquareText, { size: 14 }), "Review & Comments"] }), _jsx("p", { className: viewerStyles.reviewSubtitle, children: "Comments stay attached to this document review session." })] }), _jsxs("div", { className: viewerStyles.reviewControls, children: [reviewSessions.length > 1 && (_jsx("select", { className: `${styles.select} ${viewerStyles.reviewSelect}`, value: activeReviewSession?.id || '', onChange: (event) => setSelectedReviewSessionId(event.target.value || null), "aria-label": "Select review session", children: reviewSessions.map((session) => (_jsx("option", { value: session.id, children: session.title?.trim() || 'Untitled review' }, session.id))) })), _jsxs("button", { type: "button", className: viewerStyles.actionBtn, onClick: () => { void createReviewSession(); }, disabled: reviewBusy || !doc.assetId, children: [_jsx(MessageSquareText, { size: 13 }), "Start Review"] }), activeReviewSession && (_jsxs("button", { type: "button", className: viewerStyles.actionBtn, onClick: () => { void handleToggleReviewStatus(); }, disabled: reviewBusy, children: [activeReviewSession.status === 'resolved' ? _jsx(RotateCcw, { size: 13 }) : _jsx(CheckCheck, { size: 13 }), activeReviewSession.status === 'resolved' ? 'Reopen' : 'Resolve'] }))] })] }), activeReviewSession && (_jsxs("div", { className: viewerStyles.reviewSessionMeta, children: [_jsx("span", { className: `${viewerStyles.statusBadge} ${activeReviewSession.status === 'resolved' ? viewerStyles.reviewStatusResolved : ''}`, children: activeReviewSession.status === 'resolved' ? 'Resolved' : 'Open' }), editingReviewTitle ? (_jsxs("div", { className: viewerStyles.reviewTitleEditRow, children: [_jsx("input", { type: "text", className: styles.input, value: reviewTitleDraft, onChange: (event) => setReviewTitleDraft(event.target.value), placeholder: "Untitled review" }), _jsx("button", { type: "button", className: styles.secondaryHeaderBtn, onClick: () => { void handleSaveReviewTitle(); }, disabled: reviewBusy, children: "Save Title" })] })) : (_jsx("span", { className: viewerStyles.reviewSessionLabel, children: activeReviewSession.title?.trim() || 'Untitled review' })), _jsxs("span", { className: viewerStyles.reviewSessionTimestamp, children: ["Updated ", formatReviewTimestamp(activeReviewSession.updatedAt)] }), !editingReviewTitle && (_jsxs("button", { type: "button", className: viewerStyles.actionBtn, onClick: () => setEditingReviewTitle(true), disabled: reviewBusy, children: [_jsx(Pencil, { size: 13 }), "Rename Review"] }))] })), reviewError && (_jsx("div", { className: viewerStyles.viewerStateError, children: reviewError })), _jsxs("div", { className: taskFormStyles.commentPanel, children: [_jsxs("div", { className: taskFormStyles.commentThread, children: [loadingReviews && (_jsx("div", { className: taskFormStyles.emptyComments, children: "Loading review comments\u2026" })), !loadingReviews && !activeReviewSession && (_jsx("div", { className: taskFormStyles.emptyComments, children: "Start a review to collect comments for this document." })), !loadingReviews && activeReviewSession && activeReviewSession.comments.length === 0 && (_jsx("div", { className: taskFormStyles.emptyComments, children: "No comments yet. Add the first review note for this document." })), !loadingReviews && activeReviewSession && activeReviewSession.comments.map((comment) => {
1239
+ }, spellCheck: true, placeholder: "Write markdown here\u2026" })] })), (layout === 'preview' || layout === 'split') && (_jsxs("div", { className: viewerStyles.previewPane, children: [_jsx("div", { className: viewerStyles.editorPaneLabel, children: "Preview" }), _jsx("div", { ref: previewScrollRef, className: `${styles.docMarkdown} ${viewerStyles.markdownSurface}`, "data-testid": "doc-preview-scroll", onScroll: () => syncPaneScroll('preview'), children: _jsx(Markdown, { remarkPlugins: [remarkGfm], rehypePlugins: [rehypeSanitize], components: markdownComponents, children: previewContent }) })] }))] })) : (_jsxs("div", { className: `${viewerStyles.viewerBody} ${!showReviewSidebar ? viewerStyles.viewerBodyFullWidth : ''}`, children: [_jsx("div", { className: `${viewerStyles.viewerContent} ${viewerStyles.viewerMain} tf-scrollbar`, children: _jsx("div", { ref: markdownContainerRef, className: `${styles.docMarkdown} ${viewerStyles.markdownSurface}`, onMouseUp: showReviewSidebar ? handleCaptureReviewAnchor : undefined, children: _jsx(Markdown, { remarkPlugins: [remarkGfm], rehypePlugins: [rehypeSanitize], components: markdownComponents, children: previewContent }) }) }), showReviewSidebar && (_jsx("aside", { className: viewerStyles.reviewRail, children: _jsxs("section", { className: viewerStyles.reviewSection, "aria-label": "Review and comments", children: [_jsxs("div", { className: viewerStyles.reviewHeader, children: [_jsxs("div", { children: [_jsxs("div", { className: viewerStyles.reviewTitle, children: [_jsx(MessageSquareText, { size: 14 }), "Review & Comments"] }), _jsx("p", { className: viewerStyles.reviewSubtitle, children: "Comments stay attached to this document review session." })] }), _jsxs("div", { className: viewerStyles.reviewControls, children: [reviewSessions.length > 1 && (_jsx("select", { className: `${styles.select} ${viewerStyles.reviewSelect}`, value: activeReviewSession?.id || '', onChange: (event) => setSelectedReviewSessionId(event.target.value || null), "aria-label": "Select review session", children: reviewSessions.map((session) => (_jsx("option", { value: session.id, children: session.title?.trim() || 'Untitled review' }, session.id))) })), _jsxs("button", { type: "button", className: viewerStyles.actionBtn, onClick: () => { void createReviewSession(); }, disabled: reviewBusy || !doc.assetId, children: [_jsx(MessageSquareText, { size: 13 }), "Start Review"] }), activeReviewSession && (_jsxs("button", { type: "button", className: viewerStyles.actionBtn, onClick: () => { void handleToggleReviewStatus(); }, disabled: reviewBusy, children: [activeReviewSession.status === 'resolved' ? _jsx(RotateCcw, { size: 13 }) : _jsx(CheckCheck, { size: 13 }), activeReviewSession.status === 'resolved' ? 'Reopen' : 'Resolve'] }))] })] }), activeReviewSession && (_jsxs("div", { className: viewerStyles.reviewSessionMeta, children: [_jsx("span", { className: `${viewerStyles.statusBadge} ${activeReviewSession.status === 'resolved' ? viewerStyles.reviewStatusResolved : ''}`, children: activeReviewSession.status === 'resolved' ? 'Resolved' : 'Open' }), editingReviewTitle ? (_jsxs("div", { className: viewerStyles.reviewTitleEditRow, children: [_jsx("input", { type: "text", className: styles.input, value: reviewTitleDraft, onChange: (event) => setReviewTitleDraft(event.target.value), placeholder: "Untitled review" }), _jsx("button", { type: "button", className: styles.secondaryHeaderBtn, onClick: () => { void handleSaveReviewTitle(); }, disabled: reviewBusy, children: "Save Title" })] })) : (_jsx("span", { className: viewerStyles.reviewSessionLabel, children: activeReviewSession.title?.trim() || 'Untitled review' })), _jsxs("span", { className: viewerStyles.reviewSessionTimestamp, children: ["Updated ", formatReviewTimestamp(activeReviewSession.updatedAt)] }), !editingReviewTitle && (_jsxs("button", { type: "button", className: viewerStyles.actionBtn, onClick: () => setEditingReviewTitle(true), disabled: reviewBusy, children: [_jsx(Pencil, { size: 13 }), "Rename Review"] }))] })), reviewError && (_jsx("div", { className: viewerStyles.viewerStateError, children: reviewError })), _jsxs("div", { className: taskFormStyles.commentPanel, children: [_jsxs("div", { className: taskFormStyles.commentThread, children: [loadingReviews && (_jsx("div", { className: taskFormStyles.emptyComments, children: "Loading review comments\u2026" })), !loadingReviews && !activeReviewSession && (_jsx("div", { className: taskFormStyles.emptyComments, children: "Start a review to collect comments for this document." })), !loadingReviews && activeReviewSession && activeReviewSession.comments.length === 0 && (_jsx("div", { className: taskFormStyles.emptyComments, children: "No comments yet. Add the first review note for this document." })), !loadingReviews && activeReviewSession && activeReviewSession.comments.map((comment) => {
1217
1240
  const isAi = comment.authorActor?.kind === 'ai';
1218
1241
  const anchorLabel = formatReviewAnchor(comment.anchor);
1219
1242
  const ActorIcon = isAi ? Bot : User;
@@ -1231,5 +1254,5 @@ export function DocumentViewer({ doc, taskTitle, apiBaseUrl = '', onSaved, onDel
1231
1254
  event.preventDefault();
1232
1255
  void handleAddReviewComment();
1233
1256
  }
1234
- } })] }), _jsx("button", { type: "button", className: taskFormStyles.sendCommentBtn, onClick: () => { void handleAddReviewComment(); }, disabled: reviewBusy || !reviewDraft.trim() || activeReviewSession?.status === 'resolved', "aria-label": "Send review comment", title: "Send review comment", children: _jsx(Send, { size: 15 }) })] })] })] }) })] }))), enableTaskGeneration && showGenModal && content !== null && (_jsx(DocumentGenerateModal, { content: content, docTitle: doc.title, apiBaseUrl: apiBaseUrl, onClose: () => setShowGenModal(false), onGenerated: (_count) => setShowGenModal(false) })), !editMode && (_jsx("div", { className: viewerStyles.printHint, children: "For the cleanest output, disable browser Headers and footers in the print dialog." }))] }));
1257
+ } })] }), _jsx("button", { type: "button", className: taskFormStyles.sendCommentBtn, onClick: () => { void handleAddReviewComment(); }, disabled: reviewBusy || !reviewDraft.trim() || activeReviewSession?.status === 'resolved', "aria-label": "Send review comment", title: "Send review comment", children: _jsx(Send, { size: 15 }) })] })] })] }) }))] }))), enableTaskGeneration && showGenModal && content !== null && (_jsx(DocumentGenerateModal, { content: content, docTitle: doc.title, apiBaseUrl: apiBaseUrl, onClose: () => setShowGenModal(false), onGenerated: (_count) => setShowGenModal(false) })), !editMode && (_jsx("div", { className: viewerStyles.printHint, children: "For the cleanest output, disable browser Headers and footers in the print dialog." }))] }));
1235
1258
  }
@@ -1,24 +1,8 @@
1
1
  import { type DocumentAttachmentFilter, type DocumentTypeFilter } from './documentWorkspaceFilters';
2
- export interface DocMeta {
3
- id: string;
4
- assetId?: string;
5
- referenceNumber?: number | null;
6
- referenceLabel?: string | null;
7
- fsPath: string;
8
- apiUrl: string;
9
- taskId: string | null;
10
- displayName: string;
11
- originalFilename?: string | null;
12
- title: string;
13
- docType: 'implementation-plan' | 'review' | 'walkthrough' | 'planning' | 'other';
14
- sizeBytes: number;
15
- updatedAt: string | null;
16
- editable?: boolean;
17
- attachmentCount?: number;
18
- attachmentState?: 'attached' | 'unattached' | 'standalone';
19
- }
2
+ import type { DocMeta } from './documentWorkspaceModel';
20
3
  export type { DocumentAttachmentFilter, DocumentTypeFilter } from './documentWorkspaceFilters';
21
4
  export { DOCUMENT_TYPE_FILTER_OPTIONS, DOCUMENT_ATTACHMENT_FILTER_OPTIONS } from './documentWorkspaceFilters';
5
+ export type { DocMeta } from './documentWorkspaceModel';
22
6
  interface Props {
23
7
  tasks: Array<{
24
8
  id: string;
@@ -30,11 +14,11 @@ interface Props {
30
14
  error: string | null;
31
15
  selectedDoc: DocMeta | null;
32
16
  searchQuery: string;
33
- typeFilters: DocumentTypeFilter[];
34
- attachmentFilters: DocumentAttachmentFilter[];
17
+ hasActiveFilters: boolean;
35
18
  onSearchChange: (value: string) => void;
36
19
  onSelectDoc: (doc: DocMeta | null) => void;
37
20
  onRefresh: () => Promise<void> | void;
21
+ onDeleteSelected: () => Promise<void> | void;
38
22
  enableTaskGeneration?: boolean;
39
23
  }
40
24
  interface ShellProps {
@@ -54,6 +38,5 @@ interface ShellProps {
54
38
  onRequestedDocHandled?: () => void;
55
39
  enableTaskGeneration?: boolean;
56
40
  }
57
- export declare function getEffectiveAttachmentState(doc: Pick<DocMeta, 'attachmentState' | 'attachmentCount' | 'taskId'>): 'attached' | 'unattached';
58
- export declare function DocumentWorkspace({ tasks, apiBaseUrl, documents, loading, error, selectedDoc, searchQuery, typeFilters, attachmentFilters, onSearchChange, onSelectDoc, onRefresh, enableTaskGeneration }: Props): import("react/jsx-runtime").JSX.Element;
41
+ export declare function DocumentWorkspace({ tasks, apiBaseUrl, documents, loading, error, selectedDoc, searchQuery, hasActiveFilters, onSearchChange, onSelectDoc, onRefresh, onDeleteSelected, enableTaskGeneration }: Props): import("react/jsx-runtime").JSX.Element;
59
42
  export declare function DocumentWorkspaceShell({ tasks, runtimeMode, apiBaseUrl, cloudAuthBaseUrl, typeFilters: controlledTypeFilters, attachmentFilters: controlledAttachmentFilters, onTypeFiltersChange, onAttachmentFiltersChange, requestedDocPath, requestedDocAssetId, onRequestedDocHandled, enableTaskGeneration }: ShellProps): import("react/jsx-runtime").JSX.Element;
@@ -1,168 +1,25 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
- import { useState, useEffect, useCallback } from 'react';
3
2
  import styles from '../../Taskforce.module.css';
4
3
  import { DocumentIndex } from './DocumentIndex';
5
4
  import { DocumentViewer } from './DocumentViewer';
6
- import { fetchTaskforceApi } from '../../utils/taskforceApiClient';
7
- import { logDocumentManagerDebug, resolveDocumentManagerApiBase } from '../../utils/documentManagerApi';
8
- import { getReadableDocumentName } from '../../utils/documentNames';
9
- import { DOCUMENT_ATTACHMENT_FILTER_OPTIONS, DOCUMENT_TYPE_FILTER_OPTIONS, } from './documentWorkspaceFilters';
5
+ import { useDocumentWorkspaceController } from './useDocumentWorkspaceController';
10
6
  export { DOCUMENT_TYPE_FILTER_OPTIONS, DOCUMENT_ATTACHMENT_FILTER_OPTIONS } from './documentWorkspaceFilters';
11
- function normalizeDocPath(value) {
12
- let decoded = value;
13
- try {
14
- decoded = decodeURIComponent(value);
15
- }
16
- catch {
17
- decoded = value;
18
- }
19
- const normalized = decoded
20
- .trim()
21
- .replace(/\\/g, '/')
22
- .replace(/^\/+/, '')
23
- .replace(/^\.\//, '')
24
- .replace(/^\.taskforce\//, '');
25
- return normalized;
26
- }
27
- function docPathMatches(candidate, requested) {
28
- if (candidate === requested)
29
- return true;
30
- if (candidate.endsWith(`/${requested}`))
31
- return true;
32
- if (requested.endsWith(`/${candidate}`))
33
- return true;
34
- return false;
35
- }
36
- function extractAssetIdFromPath(value) {
37
- const normalized = normalizeDocPath(String(value || ''));
38
- if (!normalized)
39
- return null;
40
- const match = normalized.match(/(^|\/)(asset-[a-f0-9]{32})(?=[^/]*$)/i);
41
- return match?.[2] ? match[2].toLowerCase() : null;
42
- }
43
- export function getEffectiveAttachmentState(doc) {
44
- if (doc.attachmentState === 'attached' || doc.attachmentState === 'unattached') {
45
- return doc.attachmentState;
46
- }
47
- const attachmentCount = typeof doc.attachmentCount === 'number' && Number.isFinite(doc.attachmentCount)
48
- ? doc.attachmentCount
49
- : 0;
50
- if (attachmentCount > 0 || !!doc.taskId) {
51
- return 'attached';
52
- }
53
- return 'unattached';
54
- }
55
- export function DocumentWorkspace({ tasks, apiBaseUrl = '', documents, loading, error, selectedDoc, searchQuery, typeFilters, attachmentFilters, onSearchChange, onSelectDoc, onRefresh, enableTaskGeneration = false }) {
56
- const filteredDocs = documents.filter(doc => {
57
- const readableName = getReadableDocumentName(doc);
58
- const haystack = `${readableName} ${doc.originalFilename || ''}`.toLowerCase();
59
- const matchesSearch = !searchQuery || haystack.includes(searchQuery.toLowerCase());
60
- const matchesType = typeFilters.length === 0 || typeFilters.includes(doc.docType);
61
- const attachmentState = getEffectiveAttachmentState(doc);
62
- const matchesAttachment = attachmentFilters.length === 0 || attachmentFilters.includes(attachmentState);
63
- return matchesSearch && matchesType && matchesAttachment;
64
- });
65
- const hasActiveFilters = searchQuery.trim().length > 0 || typeFilters.length > 0 || attachmentFilters.length > 0;
7
+ export function DocumentWorkspace({ tasks, apiBaseUrl = '', documents, loading, error, selectedDoc, searchQuery, hasActiveFilters, onSearchChange, onSelectDoc, onRefresh, onDeleteSelected, enableTaskGeneration = false }) {
66
8
  const taskById = new Map(tasks.map(t => [t.id, t]));
67
- return (_jsxs("div", { className: styles.docWorkspace, children: [_jsx("div", { className: styles.docIndexPanel, children: _jsx(DocumentIndex, { documents: filteredDocs, selectedDocId: selectedDoc?.id ?? null, loading: loading, error: error, searchQuery: searchQuery, onSearchChange: onSearchChange, onSelect: (doc) => onSelectDoc(doc), onRefresh: onRefresh, taskById: taskById, hasActiveFilters: hasActiveFilters }) }), _jsx("div", { className: styles.docViewerPanel, children: selectedDoc ? (_jsx(DocumentViewer, { doc: selectedDoc, taskTitle: selectedDoc.taskId ? (taskById.get(selectedDoc.taskId)?.title ?? null) : null, apiBaseUrl: apiBaseUrl, onSaved: (_updatedAt) => onRefresh(), onDeleted: () => {
68
- onSelectDoc(null);
69
- void onRefresh();
70
- }, enableTaskGeneration: enableTaskGeneration })) : !loading && (_jsx("div", { className: styles.docViewerEmpty, children: _jsx("p", { className: "tf-empty-copy", children: filteredDocs.length === 0 ? 'No documents match your current filters.' : 'Select a document to view it.' }) })) })] }));
9
+ return (_jsxs("div", { className: styles.docWorkspace, children: [_jsx("div", { className: styles.docIndexPanel, children: _jsx(DocumentIndex, { documents: documents, selectedDocId: selectedDoc?.id ?? null, loading: loading, error: error, searchQuery: searchQuery, onSearchChange: onSearchChange, onSelect: (doc) => onSelectDoc(doc), onRefresh: onRefresh, taskById: taskById, hasActiveFilters: hasActiveFilters }) }), _jsx("div", { className: styles.docViewerPanel, children: selectedDoc ? (_jsx(DocumentViewer, { doc: selectedDoc, taskTitle: selectedDoc.taskId ? (taskById.get(selectedDoc.taskId)?.title ?? null) : null, apiBaseUrl: apiBaseUrl, onSaved: (_updatedAt) => onRefresh(), onDeleted: () => void onDeleteSelected(), enableTaskGeneration: enableTaskGeneration })) : !loading && (_jsx("div", { className: styles.docViewerEmpty, children: _jsx("p", { className: "tf-empty-copy", children: documents.length === 0 ? 'No documents match your current filters.' : 'Select a document to view it.' }) })) })] }));
71
10
  }
72
11
  export function DocumentWorkspaceShell({ tasks, runtimeMode = 'local', apiBaseUrl = '', cloudAuthBaseUrl = '', typeFilters: controlledTypeFilters, attachmentFilters: controlledAttachmentFilters, onTypeFiltersChange, onAttachmentFiltersChange, requestedDocPath = null, requestedDocAssetId = null, onRequestedDocHandled, enableTaskGeneration = false }) {
73
- const [documents, setDocuments] = useState([]);
74
- const [loading, setLoading] = useState(true);
75
- const [error, setError] = useState(null);
76
- const [selectedDoc, setSelectedDoc] = useState(null);
77
- const [searchQuery, setSearchQuery] = useState('');
78
- const [internalTypeFilters, setInternalTypeFilters] = useState(() => DOCUMENT_TYPE_FILTER_OPTIONS.map((option) => option.value));
79
- const [internalAttachmentFilters, setInternalAttachmentFilters] = useState(() => DOCUMENT_ATTACHMENT_FILTER_OPTIONS.map((option) => option.value));
80
- const documentApiBaseUrl = resolveDocumentManagerApiBase({
12
+ const { documentApiBaseUrl, filteredDocs, loading, error, selectedDoc, searchQuery, hasActiveFilters, setSearchQuery, fetchDocuments, selectDoc, handleDeleteSelected, } = useDocumentWorkspaceController({
81
13
  runtimeMode,
82
14
  apiBaseUrl,
83
15
  cloudAuthBaseUrl,
16
+ typeFilters: controlledTypeFilters,
17
+ attachmentFilters: controlledAttachmentFilters,
18
+ onTypeFiltersChange,
19
+ onAttachmentFiltersChange,
20
+ requestedDocPath,
21
+ requestedDocAssetId,
22
+ onRequestedDocHandled,
84
23
  });
85
- const typeFilters = controlledTypeFilters ?? internalTypeFilters;
86
- const attachmentFilters = controlledAttachmentFilters ?? internalAttachmentFilters;
87
- const handleTypeFiltersChange = onTypeFiltersChange ?? setInternalTypeFilters;
88
- const handleAttachmentFiltersChange = onAttachmentFiltersChange ?? setInternalAttachmentFilters;
89
- const fetchDocuments = useCallback(async () => {
90
- setLoading(true);
91
- setError(null);
92
- try {
93
- const res = await fetchTaskforceApi('/api/taskforce/documents', {
94
- credentials: 'include',
95
- }, documentApiBaseUrl);
96
- if (!res.ok)
97
- throw new Error(`Failed to load documents (${res.status})`);
98
- const data = await res.json();
99
- const nextDocuments = data.documents || [];
100
- logDocumentManagerDebug('list.loaded', {
101
- runtimeMode,
102
- apiBaseUrl: documentApiBaseUrl || '(same-origin)',
103
- count: Array.isArray(nextDocuments) ? nextDocuments.length : 0,
104
- });
105
- setDocuments(nextDocuments);
106
- }
107
- catch (e) {
108
- setError(e?.message || 'Failed to load documents');
109
- logDocumentManagerDebug('list.error', {
110
- runtimeMode,
111
- apiBaseUrl: documentApiBaseUrl || '(same-origin)',
112
- message: e?.message || 'Failed to load documents',
113
- });
114
- }
115
- finally {
116
- setLoading(false);
117
- }
118
- }, [documentApiBaseUrl, runtimeMode]);
119
- useEffect(() => {
120
- fetchDocuments();
121
- }, [fetchDocuments]);
122
- // Auto-select first doc when list loads
123
- useEffect(() => {
124
- if (requestedDocPath || requestedDocAssetId)
125
- return;
126
- if (!selectedDoc && documents.length > 0) {
127
- setSelectedDoc(documents[0]);
128
- }
129
- }, [documents, selectedDoc, requestedDocAssetId, requestedDocPath]);
130
- useEffect(() => {
131
- if ((!requestedDocPath && !requestedDocAssetId) || documents.length === 0)
132
- return;
133
- const requested = requestedDocPath ? normalizeDocPath(requestedDocPath) : '';
134
- const requestedAssetId = String(requestedDocAssetId || '').trim().toLowerCase()
135
- || extractAssetIdFromPath(requestedDocPath);
136
- const matched = documents.find((doc) => {
137
- const candidateAssetId = extractAssetIdFromPath(doc.fsPath) || String(doc.assetId || '').trim().toLowerCase() || null;
138
- if (requestedAssetId && candidateAssetId && candidateAssetId === requestedAssetId)
139
- return true;
140
- if (!requested)
141
- return false;
142
- const candidate = normalizeDocPath(doc.fsPath);
143
- return docPathMatches(candidate, requested);
144
- });
145
- if (matched) {
146
- setSelectedDoc(matched);
147
- onRequestedDocHandled?.();
148
- return;
149
- }
150
- }, [requestedDocAssetId, requestedDocPath, documents, onRequestedDocHandled]);
151
- useEffect(() => {
152
- if ((requestedDocPath || requestedDocAssetId) && !selectedDoc)
153
- return;
154
- const filtered = documents.filter((doc) => {
155
- const readableName = getReadableDocumentName(doc);
156
- const haystack = `${readableName} ${doc.originalFilename || ''}`.toLowerCase();
157
- const matchesSearch = !searchQuery || haystack.includes(searchQuery.toLowerCase());
158
- const matchesType = typeFilters.length === 0 || typeFilters.includes(doc.docType);
159
- const attachmentState = getEffectiveAttachmentState(doc);
160
- const matchesAttachment = attachmentFilters.length === 0 || attachmentFilters.includes(attachmentState);
161
- return matchesSearch && matchesType && matchesAttachment;
162
- });
163
- if (selectedDoc && filtered.some((doc) => doc.id === selectedDoc.id))
164
- return;
165
- setSelectedDoc(filtered[0] ?? null);
166
- }, [attachmentFilters, documents, requestedDocAssetId, requestedDocPath, searchQuery, selectedDoc, typeFilters]);
167
- return (_jsx(DocumentWorkspace, { tasks: tasks, apiBaseUrl: documentApiBaseUrl, documents: documents, loading: loading, error: error, selectedDoc: selectedDoc, searchQuery: searchQuery, typeFilters: typeFilters, attachmentFilters: attachmentFilters, onSearchChange: setSearchQuery, onSelectDoc: setSelectedDoc, onRefresh: fetchDocuments, enableTaskGeneration: enableTaskGeneration }));
24
+ return (_jsx(DocumentWorkspace, { tasks: tasks, apiBaseUrl: documentApiBaseUrl, documents: filteredDocs, loading: loading, error: error, selectedDoc: selectedDoc, searchQuery: searchQuery, hasActiveFilters: hasActiveFilters, onSearchChange: setSearchQuery, onSelectDoc: selectDoc, onRefresh: fetchDocuments, onDeleteSelected: handleDeleteSelected, enableTaskGeneration: enableTaskGeneration }));
168
25
  }