@plannotator/ui 0.22.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +60 -0
- package/assets/diff-options.png +0 -0
- package/assets/icon-codex.png +0 -0
- package/assets/look-flat.png +0 -0
- package/assets/look-grid.png +0 -0
- package/assets/review-sections.png +0 -0
- package/assets/review-tree.png +0 -0
- package/assets/workspaces.webp +0 -0
- package/components/AISettingsTab.tsx +145 -0
- package/components/ActionMenu.tsx +101 -0
- package/components/AgentControls.tsx +216 -0
- package/components/AgentsTab.tsx +1294 -0
- package/components/AnnotationPanel.tsx +731 -0
- package/components/AnnotationSidebar.tsx +86 -0
- package/components/AnnotationToolbar.tsx +323 -0
- package/components/AnnotationToolstrip.tsx +355 -0
- package/components/ApproveDropdown.tsx +170 -0
- package/components/AttachmentsButton.tsx +410 -0
- package/components/BlockRenderer.tsx +163 -0
- package/components/BorderTrail.tsx +38 -0
- package/components/CodeFilePicker.tsx +64 -0
- package/components/CodeFilePopout.tsx +602 -0
- package/components/CodePathValidationContext.tsx +14 -0
- package/components/CommentPopover.tsx +512 -0
- package/components/CompletionOverlay.tsx +99 -0
- package/components/ConfirmDialog.tsx +128 -0
- package/components/DocBadges.tsx +231 -0
- package/components/EditorAnnotationCard.tsx +65 -0
- package/components/ExportModal.tsx +565 -0
- package/components/FloatingQuickLabelPicker.tsx +144 -0
- package/components/GitHubIcon.tsx +16 -0
- package/components/GitLabIcon.tsx +30 -0
- package/components/GraphvizBlock.tsx +511 -0
- package/components/ImageAnnotator/Canvas.tsx +118 -0
- package/components/ImageAnnotator/Toolbar.tsx +218 -0
- package/components/ImageAnnotator/index.tsx +272 -0
- package/components/ImageAnnotator/types.ts +39 -0
- package/components/ImageAnnotator/utils.ts +147 -0
- package/components/ImageThumbnail.tsx +126 -0
- package/components/ImportModal.tsx +146 -0
- package/components/InlineMarkdown.tsx +1094 -0
- package/components/KeyboardShortcuts.tsx +208 -0
- package/components/Landing.tsx +496 -0
- package/components/ListItemBody.tsx +62 -0
- package/components/ListMarker.tsx +73 -0
- package/components/LookAndFeelAnnouncementDialog.tsx +230 -0
- package/components/MarkdownEditor.tsx +44 -0
- package/components/MenuVersionSection.tsx +91 -0
- package/components/MermaidBlock.tsx +579 -0
- package/components/ModeToggle.tsx +68 -0
- package/components/OpenInAppButton.tsx +299 -0
- package/components/OverlayScrollArea.tsx +82 -0
- package/components/PermissionModeSetup.tsx +92 -0
- package/components/PinpointOverlay.tsx +99 -0
- package/components/PlanAIAnnouncementDialog.tsx +187 -0
- package/components/PlanHeaderMenu.tsx +299 -0
- package/components/PopoutDialog.tsx +91 -0
- package/components/Popover.tsx +27 -0
- package/components/ProviderIcons.tsx +51 -0
- package/components/PullRequestIcon.tsx +12 -0
- package/components/QuickLabelDropdown.tsx +63 -0
- package/components/RenderedMarkdown.tsx +54 -0
- package/components/RepoIcon.tsx +12 -0
- package/components/ResizeHandle.tsx +86 -0
- package/components/ReviewAgentsIcon.tsx +18 -0
- package/components/SearchableSelect.tsx +157 -0
- package/components/Settings.tsx +2220 -0
- package/components/SparklesIcon.tsx +45 -0
- package/components/StickyHeaderLane.tsx +260 -0
- package/components/TableOfContents.tsx +157 -0
- package/components/TaterSpritePullup.tsx +34 -0
- package/components/TaterSpriteRunning.tsx +54 -0
- package/components/TaterSpriteSitting.tsx +37 -0
- package/components/TextShimmer.tsx +57 -0
- package/components/ThemeProvider.tsx +162 -0
- package/components/ThemeTab.tsx +139 -0
- package/components/ToolbarButtons.tsx +116 -0
- package/components/Tooltip.tsx +45 -0
- package/components/Viewer.tsx +977 -0
- package/components/ai/AIProviderBar.tsx +95 -0
- package/components/ai/DocumentAIChatPanel.tsx +313 -0
- package/components/blocks/AlertBlock.tsx +58 -0
- package/components/blocks/Callout.tsx +60 -0
- package/components/blocks/CodeBlock.tsx +75 -0
- package/components/blocks/HtmlBlock.tsx +122 -0
- package/components/blocks/MathBlock.tsx +36 -0
- package/components/blocks/TableBlock.tsx +146 -0
- package/components/blocks/TablePopout.tsx +279 -0
- package/components/blocks/TableToolbar.tsx +153 -0
- package/components/blocks/proseBody.tsx +102 -0
- package/components/core/button.tsx +44 -0
- package/components/core/textarea.tsx +26 -0
- package/components/diagramLanguages.ts +14 -0
- package/components/goal-setup/GoalSetupSurface.tsx +1354 -0
- package/components/html-viewer/HtmlViewer.tsx +386 -0
- package/components/html-viewer/bridge-script.ts +505 -0
- package/components/html-viewer/index.ts +1 -0
- package/components/html-viewer/useHtmlAnnotation.ts +400 -0
- package/components/icons/AgentIcons.tsx +66 -0
- package/components/icons/AppIcon.tsx +56 -0
- package/components/icons/MessagesIcon.tsx +11 -0
- package/components/icons/ObsidianIcons.tsx +208 -0
- package/components/icons/app/android-studio.svg +369 -0
- package/components/icons/app/antigravity.svg +97 -0
- package/components/icons/app/cursor.svg +16 -0
- package/components/icons/app/file-explorer.svg +20 -0
- package/components/icons/app/finder.png +0 -0
- package/components/icons/app/ghostty.svg +13 -0
- package/components/icons/app/iterm2.svg +13 -0
- package/components/icons/app/powershell.svg +14 -0
- package/components/icons/app/sublime-text.svg +17 -0
- package/components/icons/app/terminal.png +0 -0
- package/components/icons/app/textmate.png +0 -0
- package/components/icons/app/vscode.svg +39 -0
- package/components/icons/app/warp.png +0 -0
- package/components/icons/app/xcode.png +0 -0
- package/components/icons/app/zed-dark.svg +15 -0
- package/components/icons/app/zed.svg +15 -0
- package/components/icons/themeIcons.tsx +47 -0
- package/components/mermaidSvg.ts +33 -0
- package/components/plan-diff/PlanCleanDiffView.tsx +884 -0
- package/components/plan-diff/PlanDiffBadge.tsx +48 -0
- package/components/plan-diff/PlanDiffModeSwitcher.tsx +103 -0
- package/components/plan-diff/PlanDiffViewer.tsx +214 -0
- package/components/plan-diff/PlanRawDiffView.tsx +102 -0
- package/components/plan-diff/VSCodeIcon.tsx +132 -0
- package/components/settings/HooksTab.tsx +208 -0
- package/components/sidebar/ArchiveBrowser.tsx +98 -0
- package/components/sidebar/CountBadge.tsx +12 -0
- package/components/sidebar/FileBrowser.tsx +498 -0
- package/components/sidebar/MessagesBrowser.tsx +109 -0
- package/components/sidebar/SidebarContainer.tsx +353 -0
- package/components/sidebar/SidebarTabs.tsx +155 -0
- package/components/sidebar/VersionBrowser.tsx +141 -0
- package/components/types.d.ts +7 -0
- package/components/ui/badge.tsx +44 -0
- package/components/ui/button.tsx +83 -0
- package/components/ui/card.tsx +58 -0
- package/components/ui/dialog.tsx +106 -0
- package/components/ui/dropdown-menu.tsx +240 -0
- package/components/ui/state-pill.tsx +45 -0
- package/components/ui/tabs.tsx +46 -0
- package/components/ui/textarea.tsx +25 -0
- package/config/configStore.ts +214 -0
- package/config/index.ts +4 -0
- package/config/reviewView.ts +42 -0
- package/config/settings.ts +310 -0
- package/config/useConfig.ts +20 -0
- package/configure.ts +69 -0
- package/globals.d.ts +14 -0
- package/hooks/pfm/useCodeFilePopout.ts +111 -0
- package/hooks/useAIChat.ts +532 -0
- package/hooks/useAIProviderConfig.ts +115 -0
- package/hooks/useActiveSection.ts +78 -0
- package/hooks/useAgentJobs.ts +306 -0
- package/hooks/useAgentSettings.ts +579 -0
- package/hooks/useAgents.ts +90 -0
- package/hooks/useAnnotationDraft.ts +523 -0
- package/hooks/useAnnotationHighlighter.ts +1170 -0
- package/hooks/useArchive.ts +171 -0
- package/hooks/useAutoClose.ts +104 -0
- package/hooks/useCodeAnnotationDraft.ts +194 -0
- package/hooks/useCodeFilePopout.ts +1 -0
- package/hooks/useDismissOnOutsideAndEscape.ts +50 -0
- package/hooks/useDraggable.ts +108 -0
- package/hooks/useEditorAnnotations.ts +64 -0
- package/hooks/useExternalAnnotationHighlights.ts +105 -0
- package/hooks/useExternalAnnotations.ts +268 -0
- package/hooks/useFileBrowser.ts +402 -0
- package/hooks/useInputMethodSwitch.ts +89 -0
- package/hooks/useIsMobile.ts +17 -0
- package/hooks/useLinkedDoc.ts +494 -0
- package/hooks/useOverlayViewport.ts +36 -0
- package/hooks/usePinpoint.ts +182 -0
- package/hooks/usePlanDiff.ts +176 -0
- package/hooks/usePrintMode.ts +27 -0
- package/hooks/useResizablePanel.ts +173 -0
- package/hooks/useScrollViewport.ts +38 -0
- package/hooks/useSharing.ts +441 -0
- package/hooks/useSidebar.ts +55 -0
- package/hooks/useUpdateCheck.ts +132 -0
- package/hooks/useValidatedCodePaths.ts +94 -0
- package/icons/GitUser.tsx +17 -0
- package/lib/utils.ts +11 -0
- package/package.json +117 -0
- package/plannotator.webp +0 -0
- package/print.css +427 -0
- package/shortcuts/code-review/ai.shortcuts.ts +25 -0
- package/shortcuts/code-review/allFilesDiff.shortcuts.ts +46 -0
- package/shortcuts/code-review/annotationToolbar.shortcuts.ts +30 -0
- package/shortcuts/code-review/fileTree.shortcuts.ts +35 -0
- package/shortcuts/code-review/prComments.shortcuts.ts +18 -0
- package/shortcuts/code-review/suggestionModal.shortcuts.ts +25 -0
- package/shortcuts/code-review/tourDialog.shortcuts.ts +18 -0
- package/shortcuts/core.ts +399 -0
- package/shortcuts/index.ts +21 -0
- package/shortcuts/plan-review/annotationPanel.shortcuts.ts +25 -0
- package/shortcuts/plan-review/annotationToolbar.shortcuts.ts +39 -0
- package/shortcuts/plan-review/commentPopover.shortcuts.ts +21 -0
- package/shortcuts/plan-review/goalSetup.shortcuts.ts +10 -0
- package/shortcuts/plan-review/imageAnnotator.shortcuts.ts +42 -0
- package/shortcuts/plan-review/inputMethod.shortcuts.ts +30 -0
- package/shortcuts/plan-review/sidebar.shortcuts.ts +34 -0
- package/shortcuts/plan-review/viewer.shortcuts.ts +24 -0
- package/shortcuts/runtime.ts +259 -0
- package/sprite_package_additional/index.html +34 -0
- package/sprite_package_additional/sprite.png +0 -0
- package/sprite_package_new/index.html +34 -0
- package/sprite_package_new/sprite.png +0 -0
- package/sprite_package_pulluphang/index.html +34 -0
- package/sprite_package_pulluphang/sprite.png +0 -0
- package/styles.css +1 -0
- package/theme.css +893 -0
- package/themes/adwaita.css +62 -0
- package/themes/andromeeda.css +66 -0
- package/themes/aurora-x.css +66 -0
- package/themes/ayu-dark.css +66 -0
- package/themes/caffeine.css +113 -0
- package/themes/catppuccin.css +62 -0
- package/themes/claude-plus.css +60 -0
- package/themes/cursor-hc.css +34 -0
- package/themes/cursor-midnight.css +34 -0
- package/themes/cursor.css +62 -0
- package/themes/dark-plus.css +66 -0
- package/themes/doom-64.css +109 -0
- package/themes/dracula.css +33 -0
- package/themes/everforest-hard.css +62 -0
- package/themes/everforest-soft.css +62 -0
- package/themes/everforest.css +62 -0
- package/themes/github.css +66 -0
- package/themes/gruvbox.css +62 -0
- package/themes/houston.css +66 -0
- package/themes/kanagawa-dragon.css +34 -0
- package/themes/kanagawa-lotus.css +34 -0
- package/themes/kanagawa-wave.css +34 -0
- package/themes/laserwave.css +66 -0
- package/themes/material.css +62 -0
- package/themes/min.css +66 -0
- package/themes/monokai-pro.css +34 -0
- package/themes/neutral.css +59 -0
- package/themes/night-owl.css +66 -0
- package/themes/nord.css +66 -0
- package/themes/one-dark-pro.css +66 -0
- package/themes/one-light.css +66 -0
- package/themes/paulmillr.css +34 -0
- package/themes/plannotator.css +60 -0
- package/themes/plastic.css +66 -0
- package/themes/poimandres.css +66 -0
- package/themes/quantum-rose.css +109 -0
- package/themes/red.css +66 -0
- package/themes/rose-pine.css +62 -0
- package/themes/simple.css +124 -0
- package/themes/slack.css +66 -0
- package/themes/snazzy-light.css +66 -0
- package/themes/soft-pop.css +60 -0
- package/themes/solar-dusk.css +109 -0
- package/themes/solarized.css +66 -0
- package/themes/synthwave-84.css +34 -0
- package/themes/terminal.css +62 -0
- package/themes/tinacious.css +57 -0
- package/themes/tokyo-night.css +62 -0
- package/themes/vesper.css +62 -0
- package/themes/vitesse-black.css +66 -0
- package/themes/vitesse.css +62 -0
- package/types.ts +264 -0
- package/utils/agentSwitch.ts +71 -0
- package/utils/aiChatFormat.ts +33 -0
- package/utils/aiPrompt.ts +29 -0
- package/utils/aiProvider.ts +217 -0
- package/utils/anchors.ts +11 -0
- package/utils/annotateAgentTerminal.ts +29 -0
- package/utils/annotationHelpers.ts +101 -0
- package/utils/bear.ts +59 -0
- package/utils/blockTargeting.ts +240 -0
- package/utils/callback.ts +99 -0
- package/utils/commentContent.ts +8 -0
- package/utils/defaultNotesApp.ts +20 -0
- package/utils/diffFonts.ts +33 -0
- package/utils/editorMode.ts +31 -0
- package/utils/fileBrowser.ts +40 -0
- package/utils/generateId.ts +7 -0
- package/utils/generateIdentity.ts +29 -0
- package/utils/identity.ts +112 -0
- package/utils/inlineTransforms.ts +41 -0
- package/utils/inputMethod.ts +17 -0
- package/utils/lookAndFeelAnnouncement.ts +18 -0
- package/utils/obsidian.ts +201 -0
- package/utils/octarine.ts +53 -0
- package/utils/parser.ts +991 -0
- package/utils/permissionMode.ts +77 -0
- package/utils/planAIAnnouncement.ts +17 -0
- package/utils/planAgentInstructions.ts +137 -0
- package/utils/planDiffEngine.ts +590 -0
- package/utils/planSave.ts +49 -0
- package/utils/platform.ts +11 -0
- package/utils/quickLabels.ts +77 -0
- package/utils/reviewAgentInstructions.ts +182 -0
- package/utils/sanitizeHtml.ts +45 -0
- package/utils/sharing.ts +350 -0
- package/utils/slugify.ts +37 -0
- package/utils/storage.ts +141 -0
- package/utils/themeRegistry.ts +567 -0
- package/utils/uiPreferences.ts +34 -0
- package/utils/upload.ts +56 -0
- package/utils/wideMode.ts +48 -0
|
@@ -0,0 +1,498 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* FileBrowser — markdown/text file tree for the sidebar
|
|
3
|
+
*
|
|
4
|
+
* Displays collapsible trees of markdown/text files from user-configured directories.
|
|
5
|
+
* Clicking a file opens it in the main viewer for annotation.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import React from "react";
|
|
9
|
+
import type { VaultNode } from "../../types";
|
|
10
|
+
import type { DirState } from "../../hooks/useFileBrowser";
|
|
11
|
+
import { CountBadge } from "./CountBadge";
|
|
12
|
+
import { ObsidianIconRaw } from "../icons/ObsidianIcons";
|
|
13
|
+
import type { WorkspaceFileChange, WorkspaceStatusPayload } from "@plannotator/core/workspace-status-types";
|
|
14
|
+
import { normalizeBrowserPath } from "@plannotator/core/browser-paths";
|
|
15
|
+
|
|
16
|
+
interface FileBrowserProps {
|
|
17
|
+
dirs: DirState[];
|
|
18
|
+
expandedFolders: Set<string>;
|
|
19
|
+
onToggleFolder: (key: string) => void;
|
|
20
|
+
collapsedDirs: Set<string>;
|
|
21
|
+
onToggleCollapse: (dirPath: string) => void;
|
|
22
|
+
onSelectFile: (absolutePath: string, dirPath: string) => void;
|
|
23
|
+
activeFile: string | null;
|
|
24
|
+
onFetchAll: () => void;
|
|
25
|
+
onRetryVaultDir?: (vaultPath: string) => void;
|
|
26
|
+
annotationCounts?: Map<string, number>;
|
|
27
|
+
highlightedFiles?: Set<string>;
|
|
28
|
+
editStatuses?: Map<string, FileEditStatus>;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export interface FileEditStatus {
|
|
32
|
+
key?: string;
|
|
33
|
+
path?: string;
|
|
34
|
+
status: "clean" | "dirty" | "saving" | "saved" | "conflict" | "error" | "missing";
|
|
35
|
+
dirty: boolean;
|
|
36
|
+
conflict?: boolean;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
interface AggregateWorkspaceChange {
|
|
40
|
+
additions: number;
|
|
41
|
+
deletions: number;
|
|
42
|
+
files: number;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export function normalizePathForLookup(path: string): string {
|
|
46
|
+
return normalizeBrowserPath(path);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function joinLookupPath(rootPath: string, relativePath: string): string {
|
|
50
|
+
const root = normalizePathForLookup(rootPath);
|
|
51
|
+
const relative = normalizePathForLookup(relativePath).replace(/^\/+/, "");
|
|
52
|
+
if (!relative || relative === ".") return root;
|
|
53
|
+
if (root === "/" || /^[A-Za-z]:\/$/.test(root)) return normalizePathForLookup(`${root}${relative}`);
|
|
54
|
+
return normalizePathForLookup(`${root}/${relative}`);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export function getPathLookupCandidates(
|
|
58
|
+
absolutePath: string,
|
|
59
|
+
relativePath?: string,
|
|
60
|
+
workspaceStatus?: WorkspaceStatusPayload,
|
|
61
|
+
): string[] {
|
|
62
|
+
const candidates = [absolutePath, normalizePathForLookup(absolutePath)];
|
|
63
|
+
if (relativePath && workspaceStatus?.rootPath) {
|
|
64
|
+
candidates.push(joinLookupPath(workspaceStatus.rootPath, relativePath));
|
|
65
|
+
}
|
|
66
|
+
const seen = new Set<string>();
|
|
67
|
+
return candidates.filter((path) => {
|
|
68
|
+
const normalized = normalizePathForLookup(path);
|
|
69
|
+
if (seen.has(normalized)) return false;
|
|
70
|
+
seen.add(normalized);
|
|
71
|
+
return true;
|
|
72
|
+
});
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function getPathMapValue<T>(map: Map<string, T> | undefined, paths: string | string[]): T | undefined {
|
|
76
|
+
if (!map) return undefined;
|
|
77
|
+
const candidates = Array.isArray(paths) ? paths : [paths];
|
|
78
|
+
const normalizedCandidates = candidates.map(normalizePathForLookup);
|
|
79
|
+
for (let index = 0; index < candidates.length; index += 1) {
|
|
80
|
+
const path = candidates[index];
|
|
81
|
+
const normalized = normalizedCandidates[index];
|
|
82
|
+
if (map.has(path)) return map.get(path);
|
|
83
|
+
if (map.has(normalized)) return map.get(normalized);
|
|
84
|
+
}
|
|
85
|
+
for (const [path, value] of map.entries()) {
|
|
86
|
+
if (normalizedCandidates.includes(normalizePathForLookup(path))) return value;
|
|
87
|
+
}
|
|
88
|
+
return undefined;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function pathSetHas(paths: Set<string> | undefined, candidates: string | string[]): boolean {
|
|
92
|
+
if (!paths) return false;
|
|
93
|
+
const candidatePaths = Array.isArray(candidates) ? candidates : [candidates];
|
|
94
|
+
const normalizedCandidates = candidatePaths.map(normalizePathForLookup);
|
|
95
|
+
for (let index = 0; index < candidatePaths.length; index += 1) {
|
|
96
|
+
if (paths.has(candidatePaths[index]) || paths.has(normalizedCandidates[index])) return true;
|
|
97
|
+
}
|
|
98
|
+
for (const path of paths) {
|
|
99
|
+
if (normalizedCandidates.includes(normalizePathForLookup(path))) return true;
|
|
100
|
+
}
|
|
101
|
+
return false;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
export function getFileEditStatus(
|
|
105
|
+
absolutePath: string,
|
|
106
|
+
editStatuses?: Map<string, FileEditStatus>,
|
|
107
|
+
relativePath?: string,
|
|
108
|
+
workspaceStatus?: WorkspaceStatusPayload,
|
|
109
|
+
): FileEditStatus | undefined {
|
|
110
|
+
return getPathMapValue(editStatuses, getPathLookupCandidates(absolutePath, relativePath, workspaceStatus));
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
function normalizeWorkspaceStatus(
|
|
114
|
+
workspaceStatus?: WorkspaceStatusPayload
|
|
115
|
+
): WorkspaceStatusPayload | undefined {
|
|
116
|
+
if (!workspaceStatus) return workspaceStatus;
|
|
117
|
+
const files: WorkspaceStatusPayload["files"] = {};
|
|
118
|
+
for (const [path, change] of Object.entries(workspaceStatus.files ?? {})) {
|
|
119
|
+
const normalizedPath = normalizePathForLookup(path);
|
|
120
|
+
const normalizedOldPath = change.oldPath ? normalizePathForLookup(change.oldPath) : undefined;
|
|
121
|
+
files[normalizedPath] = {
|
|
122
|
+
...change,
|
|
123
|
+
path: normalizedPath,
|
|
124
|
+
oldPath: normalizedOldPath,
|
|
125
|
+
};
|
|
126
|
+
}
|
|
127
|
+
return {
|
|
128
|
+
...workspaceStatus,
|
|
129
|
+
rootPath: normalizePathForLookup(workspaceStatus.rootPath),
|
|
130
|
+
files,
|
|
131
|
+
};
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/** Recursively sum annotation counts for all descendant files of a folder node */
|
|
135
|
+
function getAggregateCount(
|
|
136
|
+
node: VaultNode,
|
|
137
|
+
dirPath: string,
|
|
138
|
+
counts: Map<string, number>,
|
|
139
|
+
workspaceStatus?: WorkspaceStatusPayload,
|
|
140
|
+
): number {
|
|
141
|
+
if (node.type === "file") {
|
|
142
|
+
return getPathMapValue(counts, getPathLookupCandidates(`${dirPath}/${node.path}`, node.path, workspaceStatus)) ?? 0;
|
|
143
|
+
}
|
|
144
|
+
let total = 0;
|
|
145
|
+
for (const child of node.children ?? []) {
|
|
146
|
+
total += getAggregateCount(child, dirPath, counts, workspaceStatus);
|
|
147
|
+
}
|
|
148
|
+
return total;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
export function getWorkspaceChange(
|
|
152
|
+
absolutePath: string,
|
|
153
|
+
workspaceStatus?: WorkspaceStatusPayload,
|
|
154
|
+
relativePath?: string,
|
|
155
|
+
): WorkspaceFileChange | undefined {
|
|
156
|
+
const files = workspaceStatus?.files;
|
|
157
|
+
if (!files) return undefined;
|
|
158
|
+
const candidates = getPathLookupCandidates(absolutePath, relativePath, workspaceStatus);
|
|
159
|
+
const normalizedCandidates = candidates.map(normalizePathForLookup);
|
|
160
|
+
for (let index = 0; index < candidates.length; index += 1) {
|
|
161
|
+
const direct = files[candidates[index]] ?? files[normalizedCandidates[index]];
|
|
162
|
+
if (direct) return direct;
|
|
163
|
+
}
|
|
164
|
+
for (const [path, change] of Object.entries(files)) {
|
|
165
|
+
if (normalizedCandidates.includes(normalizePathForLookup(path))) return change;
|
|
166
|
+
}
|
|
167
|
+
return undefined;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
export function isFileTreeSelectionDisabled(
|
|
171
|
+
workspaceChange: WorkspaceFileChange | undefined,
|
|
172
|
+
editStatus: FileEditStatus | undefined,
|
|
173
|
+
): boolean {
|
|
174
|
+
return workspaceChange?.status === "deleted" && editStatus?.status !== "missing";
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
export function getAggregateWorkspaceChange(
|
|
178
|
+
node: VaultNode,
|
|
179
|
+
dirPath: string,
|
|
180
|
+
workspaceStatus?: WorkspaceStatusPayload
|
|
181
|
+
): AggregateWorkspaceChange {
|
|
182
|
+
if (node.type === "file") {
|
|
183
|
+
const change = getWorkspaceChange(`${dirPath}/${node.path}`, workspaceStatus, node.path);
|
|
184
|
+
return change
|
|
185
|
+
? { additions: change.additions, deletions: change.deletions, files: 1 }
|
|
186
|
+
: { additions: 0, deletions: 0, files: 0 };
|
|
187
|
+
}
|
|
188
|
+
return (node.children ?? []).reduce<AggregateWorkspaceChange>((total, child) => {
|
|
189
|
+
const childTotal = getAggregateWorkspaceChange(child, dirPath, workspaceStatus);
|
|
190
|
+
return {
|
|
191
|
+
additions: total.additions + childTotal.additions,
|
|
192
|
+
deletions: total.deletions + childTotal.deletions,
|
|
193
|
+
files: total.files + childTotal.files,
|
|
194
|
+
};
|
|
195
|
+
}, { additions: 0, deletions: 0, files: 0 });
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
const TreeNode: React.FC<{
|
|
199
|
+
node: VaultNode;
|
|
200
|
+
depth: number;
|
|
201
|
+
dirPath: string;
|
|
202
|
+
expandedFolders: Set<string>;
|
|
203
|
+
onToggleFolder: (key: string) => void;
|
|
204
|
+
onSelectFile: (absolutePath: string, dirPath: string) => void;
|
|
205
|
+
activeFile: string | null;
|
|
206
|
+
annotationCounts?: Map<string, number>;
|
|
207
|
+
highlightedFiles?: Set<string>;
|
|
208
|
+
editStatuses?: Map<string, FileEditStatus>;
|
|
209
|
+
workspaceStatus?: WorkspaceStatusPayload;
|
|
210
|
+
}> = ({ node, depth, dirPath, expandedFolders, onToggleFolder, onSelectFile, activeFile, annotationCounts, highlightedFiles, editStatuses, workspaceStatus }) => {
|
|
211
|
+
const folderKey = `${dirPath}:${node.path}`;
|
|
212
|
+
const absolutePath = `${dirPath}/${node.path}`;
|
|
213
|
+
const isExpanded = expandedFolders.has(folderKey);
|
|
214
|
+
const isActive = node.type === "file" && absolutePath === activeFile;
|
|
215
|
+
const paddingLeft = 8 + depth * 14;
|
|
216
|
+
|
|
217
|
+
if (node.type === "folder") {
|
|
218
|
+
const aggregateCount = annotationCounts ? getAggregateCount(node, dirPath, annotationCounts, workspaceStatus) : 0;
|
|
219
|
+
const aggregateChange = getAggregateWorkspaceChange(node, dirPath, workspaceStatus);
|
|
220
|
+
return (
|
|
221
|
+
<>
|
|
222
|
+
<button
|
|
223
|
+
onClick={() => onToggleFolder(folderKey)}
|
|
224
|
+
className="file-tree-folder w-full flex items-center gap-1.5 py-1 px-2 text-[11px] text-muted-foreground hover:text-foreground hover:bg-muted/50 transition-colors rounded-sm"
|
|
225
|
+
style={{ paddingLeft }}
|
|
226
|
+
>
|
|
227
|
+
<svg
|
|
228
|
+
className={`w-3 h-3 flex-shrink-0 transition-transform ${isExpanded ? "rotate-90" : ""}`}
|
|
229
|
+
fill="none"
|
|
230
|
+
viewBox="0 0 24 24"
|
|
231
|
+
stroke="currentColor"
|
|
232
|
+
strokeWidth={2}
|
|
233
|
+
>
|
|
234
|
+
<path strokeLinecap="round" strokeLinejoin="round" d="M9 5l7 7-7 7" />
|
|
235
|
+
</svg>
|
|
236
|
+
<svg className="w-3 h-3 flex-shrink-0 text-muted-foreground/60" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}>
|
|
237
|
+
<path strokeLinecap="round" strokeLinejoin="round" d="M3 7v10a2 2 0 002 2h14a2 2 0 002-2V9a2 2 0 00-2-2h-6l-2-2H5a2 2 0 00-2 2z" />
|
|
238
|
+
</svg>
|
|
239
|
+
<span className="truncate">{node.name}</span>
|
|
240
|
+
<div className="ml-auto flex flex-shrink-0 items-center gap-1.5 text-[10px]">
|
|
241
|
+
{(aggregateChange.additions > 0 || aggregateChange.deletions > 0) && (
|
|
242
|
+
<>
|
|
243
|
+
{aggregateChange.additions > 0 && <span className="additions">+{aggregateChange.additions}</span>}
|
|
244
|
+
{aggregateChange.deletions > 0 && <span className="deletions">-{aggregateChange.deletions}</span>}
|
|
245
|
+
</>
|
|
246
|
+
)}
|
|
247
|
+
{aggregateCount > 0 && <CountBadge count={aggregateCount} />}
|
|
248
|
+
</div>
|
|
249
|
+
</button>
|
|
250
|
+
{isExpanded && node.children?.map((child) => (
|
|
251
|
+
<TreeNode
|
|
252
|
+
key={child.path}
|
|
253
|
+
node={child}
|
|
254
|
+
depth={depth + 1}
|
|
255
|
+
dirPath={dirPath}
|
|
256
|
+
expandedFolders={expandedFolders}
|
|
257
|
+
onToggleFolder={onToggleFolder}
|
|
258
|
+
onSelectFile={onSelectFile}
|
|
259
|
+
activeFile={activeFile}
|
|
260
|
+
annotationCounts={annotationCounts}
|
|
261
|
+
highlightedFiles={highlightedFiles}
|
|
262
|
+
editStatuses={editStatuses}
|
|
263
|
+
workspaceStatus={workspaceStatus}
|
|
264
|
+
/>
|
|
265
|
+
))}
|
|
266
|
+
</>
|
|
267
|
+
);
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
const displayName = node.name.replace(/\.(mdx?|txt|html?)$/i, "");
|
|
271
|
+
const lookupCandidates = getPathLookupCandidates(absolutePath, node.path, workspaceStatus);
|
|
272
|
+
const fileCount = getPathMapValue(annotationCounts, lookupCandidates) ?? 0;
|
|
273
|
+
const isHighlighted = pathSetHas(highlightedFiles, lookupCandidates);
|
|
274
|
+
const editStatus = getFileEditStatus(absolutePath, editStatuses, node.path, workspaceStatus);
|
|
275
|
+
const workspaceChange = getWorkspaceChange(absolutePath, workspaceStatus, node.path);
|
|
276
|
+
const isDeleted = workspaceChange?.status === "deleted";
|
|
277
|
+
const isSelectionDisabled = isFileTreeSelectionDisabled(workspaceChange, editStatus);
|
|
278
|
+
const editMarker =
|
|
279
|
+
editStatus?.status === "conflict" || editStatus?.status === "error"
|
|
280
|
+
? { label: "!", className: "bg-destructive/15 text-destructive", title: editStatus.status === "conflict" ? "Save conflict" : "Save failed" }
|
|
281
|
+
: editStatus?.status === "missing"
|
|
282
|
+
? { label: "!", className: "bg-warning/15 text-warning-foreground", title: "File missing on disk" }
|
|
283
|
+
: editStatus?.status === "saving"
|
|
284
|
+
? { label: "...", className: "bg-primary/10 text-primary", title: "Saving" }
|
|
285
|
+
: editStatus?.dirty
|
|
286
|
+
? { label: "•", className: "bg-primary/10 text-primary", title: "Unsaved edits" }
|
|
287
|
+
: editStatus?.status === "saved"
|
|
288
|
+
? { label: "✓", className: "bg-success/15 text-success", title: "Saved" }
|
|
289
|
+
: null;
|
|
290
|
+
const statusMarker = workspaceChange?.status === "added"
|
|
291
|
+
? { label: "A", className: "text-success", title: "Added file" }
|
|
292
|
+
: workspaceChange?.status === "untracked"
|
|
293
|
+
? { label: "U", className: "text-primary", title: "Untracked file" }
|
|
294
|
+
: workspaceChange?.status === "deleted"
|
|
295
|
+
? { label: "D", className: "text-destructive", title: "Deleted file" }
|
|
296
|
+
: workspaceChange?.status === "renamed"
|
|
297
|
+
? { label: "R", className: "text-[#007aff]", title: workspaceChange.oldPath ? `Renamed from ${workspaceChange.oldPath}` : "Renamed file" }
|
|
298
|
+
: workspaceChange?.status === "conflicted"
|
|
299
|
+
? { label: "!", className: "text-destructive", title: "Git conflict" }
|
|
300
|
+
: null;
|
|
301
|
+
return (
|
|
302
|
+
<button
|
|
303
|
+
onClick={() => {
|
|
304
|
+
if (!isSelectionDisabled) onSelectFile(absolutePath, dirPath);
|
|
305
|
+
}}
|
|
306
|
+
disabled={isSelectionDisabled}
|
|
307
|
+
className={`file-tree-item w-full text-left group ${isActive ? "active" : ""} ${fileCount > 0 ? "has-annotations" : ""} ${isHighlighted ? 'file-annotation-flash' : ''} ${isSelectionDisabled ? 'opacity-70 cursor-default' : ''}`}
|
|
308
|
+
style={{ paddingLeft: paddingLeft + 15 }}
|
|
309
|
+
title={isDeleted ? `${node.path} (${editStatus?.status === "missing" ? "missing on disk" : "deleted on disk"})` : node.path}
|
|
310
|
+
>
|
|
311
|
+
<svg className="w-3 h-3 flex-shrink-0 opacity-40" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}>
|
|
312
|
+
<path strokeLinecap="round" strokeLinejoin="round" d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z" />
|
|
313
|
+
</svg>
|
|
314
|
+
<span className={`truncate flex-1 min-w-0 ${isDeleted ? "line-through" : ""}`}>{displayName}</span>
|
|
315
|
+
<div className="ml-auto flex flex-shrink-0 items-center gap-1.5 text-[10px]">
|
|
316
|
+
{editMarker && (
|
|
317
|
+
<span
|
|
318
|
+
className={`inline-flex h-4 min-w-4 items-center justify-center rounded-full px-1 text-[9px] font-semibold leading-none ${editMarker.className}`}
|
|
319
|
+
title={editMarker.title}
|
|
320
|
+
>
|
|
321
|
+
{editMarker.label}
|
|
322
|
+
</span>
|
|
323
|
+
)}
|
|
324
|
+
{fileCount > 0 && <CountBadge count={fileCount} active={isActive} />}
|
|
325
|
+
{workspaceChange && (
|
|
326
|
+
<>
|
|
327
|
+
{workspaceChange.additions > 0 && <span className="additions">+{workspaceChange.additions}</span>}
|
|
328
|
+
{workspaceChange.deletions > 0 && <span className="deletions">-{workspaceChange.deletions}</span>}
|
|
329
|
+
{statusMarker && (
|
|
330
|
+
<span className={`font-semibold ${statusMarker.className}`} title={statusMarker.title}>
|
|
331
|
+
{statusMarker.label}
|
|
332
|
+
</span>
|
|
333
|
+
)}
|
|
334
|
+
</>
|
|
335
|
+
)}
|
|
336
|
+
</div>
|
|
337
|
+
</button>
|
|
338
|
+
);
|
|
339
|
+
};
|
|
340
|
+
|
|
341
|
+
const DirSection: React.FC<{
|
|
342
|
+
dir: DirState;
|
|
343
|
+
expandedFolders: Set<string>;
|
|
344
|
+
onToggleFolder: (key: string) => void;
|
|
345
|
+
onSelectFile: (absolutePath: string, dirPath: string) => void;
|
|
346
|
+
activeFile: string | null;
|
|
347
|
+
onRetry: () => void;
|
|
348
|
+
annotationCounts?: Map<string, number>;
|
|
349
|
+
highlightedFiles?: Set<string>;
|
|
350
|
+
editStatuses?: Map<string, FileEditStatus>;
|
|
351
|
+
}> = ({ dir, expandedFolders, onToggleFolder, onSelectFile, activeFile, onRetry, annotationCounts, highlightedFiles, editStatuses }) => {
|
|
352
|
+
const workspaceStatus = React.useMemo(() => normalizeWorkspaceStatus(dir.workspaceStatus), [dir.workspaceStatus]);
|
|
353
|
+
|
|
354
|
+
if (dir.isLoading) {
|
|
355
|
+
return (
|
|
356
|
+
<div className="p-3 text-[11px] text-muted-foreground">
|
|
357
|
+
Loading...
|
|
358
|
+
</div>
|
|
359
|
+
);
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
if (dir.error) {
|
|
363
|
+
return (
|
|
364
|
+
<div className="p-3 space-y-2">
|
|
365
|
+
<div className="text-[11px] text-destructive">{dir.error}</div>
|
|
366
|
+
<button
|
|
367
|
+
onClick={onRetry}
|
|
368
|
+
className="text-[10px] text-primary hover:underline"
|
|
369
|
+
>
|
|
370
|
+
Retry
|
|
371
|
+
</button>
|
|
372
|
+
</div>
|
|
373
|
+
);
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
if (dir.tree.length === 0) {
|
|
377
|
+
return (
|
|
378
|
+
<div className="px-3 py-2 text-[11px] text-muted-foreground">
|
|
379
|
+
No markdown or text files found
|
|
380
|
+
</div>
|
|
381
|
+
);
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
return (
|
|
385
|
+
<div className="py-1 px-1">
|
|
386
|
+
{dir.tree.map((node) => (
|
|
387
|
+
<TreeNode
|
|
388
|
+
key={node.path}
|
|
389
|
+
node={node}
|
|
390
|
+
depth={0}
|
|
391
|
+
dirPath={dir.path}
|
|
392
|
+
expandedFolders={expandedFolders}
|
|
393
|
+
onToggleFolder={onToggleFolder}
|
|
394
|
+
onSelectFile={onSelectFile}
|
|
395
|
+
activeFile={activeFile}
|
|
396
|
+
annotationCounts={annotationCounts}
|
|
397
|
+
highlightedFiles={highlightedFiles}
|
|
398
|
+
editStatuses={editStatuses}
|
|
399
|
+
workspaceStatus={workspaceStatus}
|
|
400
|
+
/>
|
|
401
|
+
))}
|
|
402
|
+
</div>
|
|
403
|
+
);
|
|
404
|
+
};
|
|
405
|
+
|
|
406
|
+
export const FileBrowser: React.FC<FileBrowserProps> = ({
|
|
407
|
+
dirs,
|
|
408
|
+
expandedFolders,
|
|
409
|
+
onToggleFolder,
|
|
410
|
+
collapsedDirs,
|
|
411
|
+
onToggleCollapse,
|
|
412
|
+
onSelectFile,
|
|
413
|
+
activeFile,
|
|
414
|
+
onFetchAll,
|
|
415
|
+
onRetryVaultDir,
|
|
416
|
+
annotationCounts,
|
|
417
|
+
highlightedFiles,
|
|
418
|
+
editStatuses,
|
|
419
|
+
}) => {
|
|
420
|
+
if (dirs.length === 0) {
|
|
421
|
+
return (
|
|
422
|
+
<div className="p-3 text-[11px] text-muted-foreground">
|
|
423
|
+
No directories configured. Add directories in Settings → Files.
|
|
424
|
+
</div>
|
|
425
|
+
);
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
// Summary header
|
|
429
|
+
const totalCount = annotationCounts ? Array.from(annotationCounts.values()).reduce((s, c) => s + c, 0) : 0;
|
|
430
|
+
const fileCount = annotationCounts?.size ?? 0;
|
|
431
|
+
const workspaceTotals = dirs.reduce(
|
|
432
|
+
(total, dir) => {
|
|
433
|
+
if (!dir.workspaceStatus?.available) return total;
|
|
434
|
+
return {
|
|
435
|
+
files: total.files + dir.workspaceStatus.totals.files,
|
|
436
|
+
additions: total.additions + dir.workspaceStatus.totals.additions,
|
|
437
|
+
deletions: total.deletions + dir.workspaceStatus.totals.deletions,
|
|
438
|
+
};
|
|
439
|
+
},
|
|
440
|
+
{ files: 0, additions: 0, deletions: 0 }
|
|
441
|
+
);
|
|
442
|
+
|
|
443
|
+
return (
|
|
444
|
+
<div className="flex flex-col">
|
|
445
|
+
{totalCount > 0 && (
|
|
446
|
+
<div className="px-3 py-1.5 text-[10px] text-muted-foreground border-b border-border/30">
|
|
447
|
+
{totalCount} annotation{totalCount === 1 ? '' : 's'} in {fileCount} file{fileCount === 1 ? '' : 's'}
|
|
448
|
+
</div>
|
|
449
|
+
)}
|
|
450
|
+
{workspaceTotals.files > 0 && (
|
|
451
|
+
<div className="file-tree-status-summary flex items-center gap-1.5 px-3 py-1.5 text-[10px] text-muted-foreground border-b border-border/30">
|
|
452
|
+
<span>{workspaceTotals.files} changed</span>
|
|
453
|
+
{workspaceTotals.additions > 0 && <span className="additions ml-auto">+{workspaceTotals.additions}</span>}
|
|
454
|
+
{workspaceTotals.deletions > 0 && <span className={`deletions ${workspaceTotals.additions > 0 ? "" : "ml-auto"}`}>-{workspaceTotals.deletions}</span>}
|
|
455
|
+
</div>
|
|
456
|
+
)}
|
|
457
|
+
{dirs.map((dir) => {
|
|
458
|
+
const isCollapsed = collapsedDirs.has(dir.path);
|
|
459
|
+
return (
|
|
460
|
+
<div key={dir.path}>
|
|
461
|
+
<button
|
|
462
|
+
onClick={() => onToggleCollapse(dir.path)}
|
|
463
|
+
className="w-full flex items-center gap-1.5 px-3 py-2 border-b border-border/30 hover:bg-muted/50 transition-colors"
|
|
464
|
+
title={dir.path}
|
|
465
|
+
>
|
|
466
|
+
<svg
|
|
467
|
+
className={`w-3 h-3 flex-shrink-0 text-muted-foreground/60 transition-transform ${isCollapsed ? "" : "rotate-90"}`}
|
|
468
|
+
fill="none"
|
|
469
|
+
viewBox="0 0 24 24"
|
|
470
|
+
stroke="currentColor"
|
|
471
|
+
strokeWidth={2}
|
|
472
|
+
>
|
|
473
|
+
<path strokeLinecap="round" strokeLinejoin="round" d="M9 5l7 7-7 7" />
|
|
474
|
+
</svg>
|
|
475
|
+
{dir.isVault && <ObsidianIconRaw className="w-[11px] h-[13px] flex-shrink-0 opacity-70" />}
|
|
476
|
+
<div className="text-[10px] font-medium text-muted-foreground uppercase tracking-wider truncate">
|
|
477
|
+
{dir.name}
|
|
478
|
+
</div>
|
|
479
|
+
</button>
|
|
480
|
+
{!isCollapsed && (
|
|
481
|
+
<DirSection
|
|
482
|
+
dir={dir}
|
|
483
|
+
expandedFolders={expandedFolders}
|
|
484
|
+
onToggleFolder={onToggleFolder}
|
|
485
|
+
onSelectFile={onSelectFile}
|
|
486
|
+
activeFile={activeFile}
|
|
487
|
+
onRetry={dir.isVault && onRetryVaultDir ? () => onRetryVaultDir(dir.path) : onFetchAll}
|
|
488
|
+
annotationCounts={annotationCounts}
|
|
489
|
+
highlightedFiles={highlightedFiles}
|
|
490
|
+
editStatuses={editStatuses}
|
|
491
|
+
/>
|
|
492
|
+
)}
|
|
493
|
+
</div>
|
|
494
|
+
);
|
|
495
|
+
})}
|
|
496
|
+
</div>
|
|
497
|
+
);
|
|
498
|
+
};
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* MessagesBrowser — Sidebar list of recent assistant messages.
|
|
3
|
+
*
|
|
4
|
+
* Used by annotate-last. Lets the user pick which assistant message to
|
|
5
|
+
* annotate when the newest transcript entry isn't the one they intended
|
|
6
|
+
* (e.g., after `/rewind`).
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import React from "react";
|
|
10
|
+
|
|
11
|
+
export interface PickerMessage {
|
|
12
|
+
messageId: string;
|
|
13
|
+
text: string;
|
|
14
|
+
timestamp?: string;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
interface MessagesBrowserProps {
|
|
18
|
+
messages: PickerMessage[];
|
|
19
|
+
selectedMessageId: string | null;
|
|
20
|
+
onSelect: (messageId: string) => void;
|
|
21
|
+
annotationCounts?: Map<string, number>;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
// Hard cap for browsers where line-clamp is unavailable, and to avoid huge sidebar text nodes.
|
|
25
|
+
const PREVIEW_MAX_CHARS = 140;
|
|
26
|
+
|
|
27
|
+
function previewText(text: string): string {
|
|
28
|
+
const normalized = text.replace(/\s+/g, " ").trim();
|
|
29
|
+
return normalized.length > PREVIEW_MAX_CHARS
|
|
30
|
+
? normalized.slice(0, PREVIEW_MAX_CHARS).trimEnd() + "…"
|
|
31
|
+
: normalized;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function formatTimestamp(ts?: string): string | null {
|
|
35
|
+
if (!ts) return null;
|
|
36
|
+
const d = new Date(ts);
|
|
37
|
+
if (Number.isNaN(d.getTime())) return null;
|
|
38
|
+
return d.toLocaleString([], {
|
|
39
|
+
hour: "2-digit",
|
|
40
|
+
minute: "2-digit",
|
|
41
|
+
month: "short",
|
|
42
|
+
day: "numeric",
|
|
43
|
+
});
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export const MessagesBrowser: React.FC<MessagesBrowserProps> = ({
|
|
47
|
+
messages,
|
|
48
|
+
selectedMessageId,
|
|
49
|
+
onSelect,
|
|
50
|
+
annotationCounts,
|
|
51
|
+
}) => {
|
|
52
|
+
if (messages.length === 0) {
|
|
53
|
+
return (
|
|
54
|
+
<div className="p-4 text-xs text-muted-foreground text-center">
|
|
55
|
+
No recent assistant messages found.
|
|
56
|
+
</div>
|
|
57
|
+
);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
return (
|
|
61
|
+
<div className="p-2">
|
|
62
|
+
<div className="px-2 pt-1 pb-2 text-[10px] font-medium text-muted-foreground uppercase tracking-wider">
|
|
63
|
+
Recent messages — newest first
|
|
64
|
+
</div>
|
|
65
|
+
<div className="space-y-0.5">
|
|
66
|
+
{messages.map((msg, idx) => {
|
|
67
|
+
const isSelected = msg.messageId === selectedMessageId;
|
|
68
|
+
const isDefault = idx === 0;
|
|
69
|
+
const ts = formatTimestamp(msg.timestamp);
|
|
70
|
+
const annotationCount = annotationCounts?.get(msg.messageId) ?? 0;
|
|
71
|
+
return (
|
|
72
|
+
<button
|
|
73
|
+
key={msg.messageId}
|
|
74
|
+
onClick={() => onSelect(msg.messageId)}
|
|
75
|
+
className={`w-full text-left px-2 py-1.5 rounded text-xs transition-colors flex items-start gap-2 ${
|
|
76
|
+
isSelected
|
|
77
|
+
? "bg-primary/10 text-primary border border-primary/30"
|
|
78
|
+
: "text-foreground hover:bg-muted/50 border border-transparent"
|
|
79
|
+
}`}
|
|
80
|
+
>
|
|
81
|
+
<span className="font-mono text-[10px] text-muted-foreground pt-0.5 w-8 shrink-0 text-right">
|
|
82
|
+
#{idx + 1}
|
|
83
|
+
{isDefault ? " ★" : ""}
|
|
84
|
+
</span>
|
|
85
|
+
<span className="flex-1 min-w-0">
|
|
86
|
+
<span className="line-clamp-2 leading-snug">
|
|
87
|
+
{previewText(msg.text)}
|
|
88
|
+
</span>
|
|
89
|
+
{ts && (
|
|
90
|
+
<span className="block text-[10px] text-muted-foreground mt-0.5">
|
|
91
|
+
{ts}
|
|
92
|
+
</span>
|
|
93
|
+
)}
|
|
94
|
+
</span>
|
|
95
|
+
{annotationCount > 0 && (
|
|
96
|
+
<span
|
|
97
|
+
className="shrink-0 min-w-5 h-5 px-1 rounded-full bg-primary/10 text-primary border border-primary/30 text-[10px] font-semibold inline-flex items-center justify-center"
|
|
98
|
+
title={`${annotationCount} annotation${annotationCount === 1 ? "" : "s"}`}
|
|
99
|
+
>
|
|
100
|
+
{annotationCount}
|
|
101
|
+
</span>
|
|
102
|
+
)}
|
|
103
|
+
</button>
|
|
104
|
+
);
|
|
105
|
+
})}
|
|
106
|
+
</div>
|
|
107
|
+
</div>
|
|
108
|
+
);
|
|
109
|
+
};
|