From 6de996489d9710cc04d0de27ce1f15af4a7fafe1 Mon Sep 17 00:00:00 2001 From: Dotta Date: Sun, 10 May 2026 07:45:44 -0500 Subject: [PATCH] Split LLM Wiki entrypoint implementations --- .../plugins/plugin-llm-wiki/src/ui/app.tsx | 7120 ++++++++++++++++ .../plugins/plugin-llm-wiki/src/ui/index.tsx | 7121 +---------------- packages/plugins/plugin-llm-wiki/src/wiki.ts | 4977 +----------- .../plugins/plugin-llm-wiki/src/wiki/core.ts | 4976 ++++++++++++ 4 files changed, 12098 insertions(+), 12096 deletions(-) create mode 100644 packages/plugins/plugin-llm-wiki/src/ui/app.tsx create mode 100644 packages/plugins/plugin-llm-wiki/src/wiki/core.ts diff --git a/packages/plugins/plugin-llm-wiki/src/ui/app.tsx b/packages/plugins/plugin-llm-wiki/src/ui/app.tsx new file mode 100644 index 00000000000..8110e827bc6 --- /dev/null +++ b/packages/plugins/plugin-llm-wiki/src/ui/app.tsx @@ -0,0 +1,7120 @@ +import { + AssigneePicker, + FileTree, + IssuesList as PluginIssuesList, + ManagedRoutinesList as PluginManagedRoutinesList, + MarkdownBlock, + MarkdownEditor, + ProjectPicker, + usePluginAction, + usePluginData, + usePluginStream, + usePluginToast, + useHostLocation, + useHostNavigation, + type FileTreeNode, + type ManagedRoutinesListItem, + type PluginPageProps, + type PluginRouteSidebarProps, + type PluginSettingsPageProps, + type PluginSidebarProps, +} from "@paperclipai/plugin-sdk/ui"; +import { useCallback, useEffect, useMemo, useRef, useState, type AnchorHTMLAttributes, type CSSProperties, type ReactElement, type ReactNode } from "react"; +import { readIngestOperationIssueId, uploadIssueAttachmentFile } from "./issue-attachments.js"; + +// --------------------------------------------------------------------------- +// Shared design tokens — copied from the UX wireframe shared.css so the plugin +// looks identical inside the host whether or not host theme tokens are +// available at runtime. +// --------------------------------------------------------------------------- + +const tokens = { + border: "var(--border, oklch(0.269 0 0))", + card: "var(--card, oklch(0.205 0 0))", + bg: "var(--background, oklch(0.145 0 0))", + fg: "var(--foreground, oklch(0.985 0 0))", + muted: "var(--muted-foreground, oklch(0.708 0 0))", + accent: "var(--accent, oklch(0.269 0 0))", + primary: "var(--primary, oklch(0.985 0 0))", + primaryFg: "var(--primary-foreground, oklch(0.205 0 0))", + destructive: "var(--destructive, oklch(0.637 0.237 25.331))", + pluginBg: "oklch(0.3 0.06 70)", + pluginFg: "oklch(0.92 0.08 80)", + pluginBorder: "oklch(0.55 0.15 70)", + hiddenOpBg: "oklch(0.27 0.04 280)", + hiddenOpFg: "oklch(0.85 0.08 280)", + hiddenOpBorder: "oklch(0.45 0.1 280)", + callout: { bg: "oklch(0.2 0.04 250)", fg: "oklch(0.85 0.08 250)", border: "oklch(0.4 0.1 250)" }, + statusDone: "oklch(0.65 0.16 145)", + statusRunning: "oklch(0.7 0.13 200)", + statusBlocked: "oklch(0.6 0.21 25)", + statusInProgress: "oklch(0.58 0.18 280)", + statusTodo: "oklch(0.6 0.17 250)", + statusPaused: "oklch(0.72 0.15 70)", +}; + +type Tone = "todo" | "in_progress" | "in_review" | "done" | "blocked" | "running" | "paused" | "failed" | "queued" | "default"; + +const toneStyles: Record = { + default: { background: "var(--secondary, oklch(0.269 0 0))", color: tokens.fg, border: `1px solid ${tokens.border}` }, + todo: { background: "oklch(0.27 0.06 250)", color: "oklch(0.85 0.1 250)" }, + in_progress: { background: "oklch(0.27 0.06 280)", color: "oklch(0.85 0.1 280)" }, + in_review: { background: "oklch(0.27 0.07 305)", color: "oklch(0.85 0.1 305)" }, + done: { background: "oklch(0.27 0.06 145)", color: "oklch(0.85 0.1 145)" }, + blocked: { background: "oklch(0.27 0.08 25)", color: "oklch(0.82 0.13 25)" }, + running: { background: "oklch(0.27 0.06 200)", color: "oklch(0.83 0.11 200)" }, + paused: { background: "oklch(0.27 0.07 70)", color: "oklch(0.85 0.1 70)" }, + failed: { background: "oklch(0.27 0.08 25)", color: "oklch(0.82 0.13 25)" }, + queued: { background: "oklch(0.27 0.06 250)", color: "oklch(0.85 0.1 250)" }, +}; + +const fontStack = `ui-sans-serif, system-ui, -apple-system, "Segoe UI", Roboto, sans-serif`; +const mobileMediaQuery = "(max-width: 767px)"; +const PLUGIN_ID = "paperclipai.plugin-llm-wiki"; +const WIKI_SIDEBAR_NAV_STATE_KEY = "paperclipWikiSidebarTreePath"; +const ROUTE_SIDEBAR_EXPANDED_STORAGE_PREFIX = `${PLUGIN_ID}:route-sidebar-expanded:v2`; +const WIKI_TOC_STICKY_TOP = 88; +const WIKI_SPACE_PREFETCH_LIMIT = 8; +const DEFAULT_ROUTE_SIDEBAR_EXPANDED_PATHS = [ + "wiki", + "wiki/sources", + "wiki/projects", + "wiki/entities", + "wiki/concepts", + "wiki/synthesis", +] as const; + +// --------------------------------------------------------------------------- +// Shared types coming back from the worker. +// --------------------------------------------------------------------------- + +type FolderStatus = { + configured: boolean; + path: string | null; + realPath: string | null; + access: "read" | "readWrite"; + readable: boolean; + writable: boolean; + requiredDirectories: string[]; + requiredFiles: string[]; + missingDirectories: string[]; + missingFiles: string[]; + healthy: boolean; + problems: { code: string; message: string; path?: string }[]; + checkedAt: string; +}; + +type ManagedAgent = { + status: string; + source?: "managed" | "selected"; + agentId?: string | null; + resourceKey?: string | null; + details?: { name?: string; status?: string; adapterType?: string | null; icon?: string | null; urlKey?: string | null } | null; + defaultDrift?: { entryFile: string; changedFiles: string[] } | null; +}; + +type ManagedProject = { + status: string; + source?: "managed" | "selected"; + projectId?: string | null; + resourceKey?: string | null; + details?: { name?: string; status?: string; color?: string | null } | null; +}; + +type ManagedRoutine = { + status: string; + routineId?: string | null; + resourceKey?: string | null; + missingRefs?: Array<{ pluginKey?: string; resourceKind: string; resourceKey: string }>; + defaultDrift?: { + changedFields: string[]; + defaultTitle?: string | null; + defaultDescription?: string | null; + } | null; + routine?: { + id?: string; + title?: string; + status?: string; + assigneeAgentId?: string | null; + projectId?: string | null; + lastTriggeredAt?: string | null; + lastEnqueuedAt?: string | null; + managedByPlugin?: { + pluginDisplayName?: string; + resourceKey?: string; + } | null; + } | null; + details?: { + title?: string; + status?: string; + cronExpression?: string | null; + enabled?: boolean; + nextRunAt?: string | null; + lastRunAt?: string | null; + assigneeAgentId?: string | null; + } | null; +}; + +type ManagedSkill = { + status: string; + skillId?: string | null; + resourceKey?: string | null; + defaultDrift?: { changedFiles: string[] } | null; + skill?: { + id?: string; + name?: string; + key?: string; + description?: string | null; + } | null; + details?: { + name?: string; + key?: string; + description?: string | null; + } | null; +}; + +type OverviewData = { + status: "ok"; + checkedAt: string; + wikiId: string; + folder: FolderStatus; + managedAgent: ManagedAgent; + managedProject: ManagedProject; + managedSkills: ManagedSkill[]; + operationCount: number; + eventIngestion: EventIngestionSettings; + capabilities: string[]; + prompts: { query: string; lint: string }; +}; + +type EventIngestionSettings = { + enabled: boolean; + sources: { + issues: boolean; + comments: boolean; + documents: boolean; + }; + wikiId: string; + maxCharacters: number; +}; + +type WikiEventIngestionSource = "issues" | "comments" | "documents"; + +type PaperclipIngestionSourceScope = + | { kind: "active_projects"; limit: number; statuses?: Array<"in_progress" | "todo" | "done"> } + | { kind: "selected_projects"; projectIds: string[] } + | { kind: "root_issues"; issueIds: string[] } + | { kind: "company_all"; requiresBoardConfirmation: true }; + +type PaperclipIngestionProfile = { + version: 1; + enabled: boolean; + sourceScopes: PaperclipIngestionSourceScope[]; + sourceKinds: { + issues: boolean; + comments: boolean; + documents: boolean; + attachments: "off" | "metadata_only"; + workProducts: "off" | "metadata_only"; + }; + cursor: { + maxWindowCharacters: number; + maxCharactersPerSource: number; + minSourceAgeMinutes: number; + maxWindowsPerRun: number; + staleAfterHours: number; + }; + backfill: { + defaultStartAt?: string | null; + defaultEndAt?: string | null; + requireManualQueue: boolean; + }; +}; + +type PaperclipIngestionProfileData = { + wikiId: string; + space: Pick; + profile: PaperclipIngestionProfile; + effectiveState: "enabled" | "disabled" | "policy_blocked" | "pending_approval" | "enabled_no_scopes"; + policyBlocks: string[]; + historicalPageCount: number; + overlapCount: number; +}; + +type SettingsData = { + folder: FolderStatus; + managedAgent: ManagedAgent; + managedProject: ManagedProject; + managedRoutine?: ManagedRoutine; + managedRoutines?: ManagedRoutine[]; + managedSkills?: ManagedSkill[]; + distillationPolicy?: { + autoApplyAllowed: boolean; + autoApplyRestriction: string | null; + deploymentMode: "local_trusted" | "authenticated" | null; + deploymentExposure: "private" | "public" | null; + }; + eventIngestion: EventIngestionSettings; + agentOptions: Array<{ id: string; name: string; status?: string | null; adapterType?: string | null; icon?: string | null; urlKey?: string | null }>; + projectOptions: Array<{ id: string; name: string; status?: string | null; color?: string | null }>; + capabilities: string[]; +}; + +type WikiSpace = { + id: string; + companyId: string; + wikiId: string; + slug: string; + displayName: string; + spaceType: string; + folderMode: string; + rootFolderKey: string; + pathPrefix: string | null; + configuredRootPath: string | null; + accessScope: string; + ownerUserId: string | null; + ownerAgentId: string | null; + teamKey: string | null; + settings: Record; + status: string; + createdAt: string | null; + updatedAt: string | null; +}; + +type WikiSpacesData = { + spaces: WikiSpace[]; +}; + +type WikiSpaceWithFolderStatus = WikiSpace & { + relativeRoot: string; + folder: FolderStatus; +}; + +const DEFAULT_SPACE_SLUG = "default"; + +type WikiPageRow = { + path: string; + title: string | null; + pageType: string | null; + backlinkCount: number; + sourceCount: number; + contentHash: string | null; + updatedAt: string; +}; + +type WikiSourceRow = { + rawPath: string; + title: string | null; + sourceType: string; + url: string | null; + status: string; + createdAt: string; +}; + +type PagesData = { + pages: WikiPageRow[]; + sources: WikiSourceRow[]; +}; + +type PageContentData = { + wikiId: string; + path: string; + contents: string; + title: string | null; + pageType: string | null; + backlinks: string[]; + sourceRefs: Array | string>; + updatedAt: string | null; + hash: string; +}; + +type WikiOperationRow = { + id: string; + operationType: string; + status: string; + hiddenIssueId: string | null; + hiddenIssueIdentifier: string | null; + hiddenIssueTitle: string | null; + hiddenIssueStatus: string | null; + projectId: string | null; + runIds: unknown[]; + costCents: number; + warnings: unknown[]; + affectedPages: unknown[]; + metadata?: Record; + createdAt: string; + updatedAt: string; +}; + +type OperationsData = { + operations: WikiOperationRow[]; +}; + +type TemplateData = { + path: string; + contents: string; + hash: string | null; + exists: boolean; +}; + +type WikiFrontmatterValue = string | string[]; + +type WikiFrontmatterProperty = { + key: string; + value: WikiFrontmatterValue; +}; + +type ParsedWikiMarkdown = { + body: string; + frontmatter: WikiFrontmatterProperty[]; +}; + +type WikiTocHeading = { + id: string; + text: string; + level: number; +}; + +// --------------------------------------------------------------------------- +// Small presentational primitives. +// --------------------------------------------------------------------------- + +function Badge({ children, tone = "default", style }: { children: ReactNode; tone?: Tone; style?: CSSProperties }) { + return ( + {children} + ); +} + +function HiddenOpBadge() { + return ( + 📖 wiki task + ); +} + +function StatusIcon({ status }: { status: string }) { + const map: Record = { + done: { color: tokens.statusDone, filled: true }, + in_progress: { color: tokens.statusInProgress }, + running: { color: tokens.statusRunning, pulse: true }, + queued: { color: tokens.statusTodo }, + todo: { color: tokens.statusTodo }, + blocked: { color: tokens.statusBlocked }, + failed: { color: tokens.statusBlocked }, + paused: { color: tokens.statusPaused }, + }; + const tone = map[status] ?? { color: tokens.muted }; + return ( + + ); +} + +function Card({ children, style }: { children: ReactNode; style?: CSSProperties }) { + return ( +
{children}
+ ); +} + +function CardHeader({ title, right, badges }: { title: ReactNode; right?: ReactNode; badges?: ReactNode }) { + return ( +
+

{title}

+ {badges} + {right ?
{right}
: null} +
+ ); +} + +function CardBody({ children, padding = 16 }: { children: ReactNode; padding?: number | string }) { + return
{children}
; +} + +const unfilledSurfaceStyle: CSSProperties = { + background: "transparent", +}; + +function PropRow({ label, value }: { label: ReactNode; value: ReactNode }) { + return ( +
+ {label} + {value} +
+ ); +} + +function Tiny({ children, style }: { children: ReactNode; style?: CSSProperties }) { + return
{children}
; +} + +function Mono({ children, style }: { children: ReactNode; style?: CSSProperties }) { + return {children}; +} + +type ButtonVariant = "primary" | "default" | "ghost" | "destructive"; +type ButtonSize = "sm" | "md"; + +function Button({ + variant = "default", + size = "md", + disabled, + loading, + onClick, + children, + type = "button", + style, + title, +}: { + variant?: ButtonVariant; + size?: ButtonSize; + disabled?: boolean; + loading?: boolean; + onClick?: () => void; + children: ReactNode; + type?: "button" | "submit"; + style?: CSSProperties; + title?: string; +}) { + const palette: Record = { + primary: { + background: tokens.primary, + color: tokens.primaryFg, + border: `1px solid transparent`, + }, + default: { + background: tokens.card, + color: tokens.fg, + border: `1px solid ${tokens.border}`, + }, + ghost: { + background: "transparent", + color: tokens.fg, + border: `1px solid transparent`, + }, + destructive: { + background: "transparent", + color: "oklch(0.7 0.2 25)", + border: `1px solid oklch(0.5 0.18 25)`, + }, + }; + return ( + + ); +} + +function TextInput(props: React.InputHTMLAttributes) { + return ( + + ); +} + +function TextArea(props: React.TextareaHTMLAttributes) { + return ( +