Merge pull request #591 from rowboatlabs/dev

Dev
This commit is contained in:
arkml 2026-05-29 22:41:55 +05:30 committed by GitHub
commit 30356e36b1
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
17 changed files with 874 additions and 116 deletions

View file

@ -16,6 +16,8 @@
## Event catalog
All PostHog events include `app_version` automatically. Main-process events add it in `packages/core/src/analytics/posthog.ts`; renderer events get it from the `analytics:bootstrap` IPC payload and an initialization-time `before_send` hook.
### `llm_usage`
Emitted whenever ai-sdk returns token usage (one event per LLM call, not per run).
@ -101,6 +103,7 @@ Persistent across sessions for the same user. Set via `posthog.people.set` or as
| `email` | main on identify | From `/v1/me`; powers PostHog cohort match + integrations |
| `plan`, `status` | main on identify | Subscription state |
| `api_url` | both processes (init + identify) | Distinguishes prod / staging / custom — assign meaning in PostHog dashboard. `https://api.x.rowboatlabs.com` = production |
| `app_version` | both processes (init + identify) | Electron app version; also included automatically on every event |
| `signed_in` | renderer | `true` while rowboat OAuth is connected |
| `{provider}_connected` | renderer | One of `gmail`, `calendar`, `slack`, `rowboat` |
| `total_notes` | renderer (init) | Workspace size signal |

View file

@ -10,11 +10,13 @@
*/
import * as esbuild from 'esbuild';
import { readFile } from 'node:fs/promises';
// In CommonJS, import.meta.url doesn't exist. We need to polyfill it.
// The banner defines __import_meta_url at the top of the bundle,
// and we use define to replace all import.meta.url references with it.
const cjsBanner = `var __import_meta_url = require('url').pathToFileURL(__filename).href;`;
const pkg = JSON.parse(await readFile(new URL('./package.json', import.meta.url), 'utf8'));
await esbuild.build({
entryPoints: ['./dist/main.js'],
@ -36,6 +38,7 @@ await esbuild.build({
// Empty strings disable analytics gracefully.
'process.env.POSTHOG_KEY': JSON.stringify(process.env.VITE_PUBLIC_POSTHOG_KEY ?? ''),
'process.env.POSTHOG_HOST': JSON.stringify(process.env.VITE_PUBLIC_POSTHOG_HOST ?? 'https://us.i.posthog.com'),
'process.env.ROWBOAT_APP_VERSION': JSON.stringify(pkg.version ?? ''),
},
});

View file

@ -1,4 +1,4 @@
import { ipcMain, BrowserWindow, shell, dialog, systemPreferences, desktopCapturer } from 'electron';
import { ipcMain, BrowserWindow, shell, dialog, systemPreferences, desktopCapturer, app } from 'electron';
import { ipc } from '@x/shared';
import path from 'node:path';
import os from 'node:os';
@ -455,6 +455,7 @@ export function setupIpcHandlers() {
return {
installationId: getInstallationId(),
apiUrl: API_URL,
appVersion: app.getVersion(),
};
},
'workspace:getRoot': async () => {

View file

@ -9,6 +9,7 @@
"preview": "vite preview"
},
"dependencies": {
"@eigenpal/docx-editor-react": "^1.0.3",
"@radix-ui/react-avatar": "^1.1.11",
"@radix-ui/react-collapsible": "^1.1.12",
"@radix-ui/react-context-menu": "^2.2.16",
@ -46,6 +47,15 @@
"motion": "^12.23.26",
"nanoid": "^5.1.6",
"posthog-js": "^1.332.0",
"prosemirror-commands": "^1.7.1",
"prosemirror-dropcursor": "^1.8.2",
"prosemirror-history": "^1.5.0",
"prosemirror-keymap": "^1.2.3",
"prosemirror-model": "^1.25.7",
"prosemirror-state": "^1.4.4",
"prosemirror-tables": "^1.8.5",
"prosemirror-transform": "^1.12.0",
"prosemirror-view": "^1.41.8",
"radix-ui": "^1.4.3",
"react": "^19.2.0",
"react-dom": "^19.2.0",

View file

@ -18,6 +18,7 @@ import { BasesView, type BaseConfig, DEFAULT_BASE_CONFIG } from '@/components/ba
import { ImageFileViewer } from '@/components/image-file-viewer';
import { VideoFileViewer } from '@/components/video-file-viewer';
import { AudioFileViewer } from '@/components/audio-file-viewer';
import { DocxFileViewer } from '@/components/docx-file-viewer';
import { PersistentViewerCache } from '@/components/persistent-viewer-cache';
import { UnsupportedFileViewer } from '@/components/unsupported-file-viewer';
import { getViewerType, isCacheableViewerPath } from '@/lib/file-types';
@ -5506,7 +5507,11 @@ function App() {
remove: knowledgeActions.remove,
copyPath: knowledgeActions.copyPath,
revealInFileManager: knowledgeActions.revealInFileManager,
createNote: knowledgeActions.createNote,
createFolder: knowledgeActions.createFolder,
onOpenInNewTab: knowledgeActions.onOpenInNewTab,
}}
onNavigate={(path) => { void navigateToView({ type: 'workspace', path: path === WORKSPACE_ROOT ? undefined : path }) }}
onOpenNote={(path) => navigateToFile(path)}
onCreateWorkspace={async (name) => { await knowledgeActions.createWorkspace(name) }}
/>
@ -5718,6 +5723,10 @@ function App() {
<div className="flex-1 min-h-0 overflow-hidden">
<AudioFileViewer path={selectedPath} />
</div>
) : selectedPath && getViewerType(selectedPath) === 'docx' ? (
<div className="flex-1 min-h-0 overflow-hidden">
<DocxFileViewer path={selectedPath} />
</div>
) : (
<div className="flex-1 min-h-0 overflow-hidden">
<UnsupportedFileViewer path={selectedPath} />

View file

@ -0,0 +1,196 @@
import { Suspense, lazy, useEffect, useRef, useState } from 'react'
import { ExternalLinkIcon, FileTextIcon, Loader2Icon } from 'lucide-react'
import type { DocxEditorRef } from '@eigenpal/docx-editor-react'
// The editor (and its CSS) is heavy and only needed when a .docx is open, so it
// loads in its own chunk the first time a Word document is viewed.
const LazyDocxEditor = lazy(async () => {
const [mod] = await Promise.all([
import('@eigenpal/docx-editor-react'),
import('@eigenpal/docx-editor-react/styles.css'),
])
return { default: mod.DocxEditor }
})
interface DocxFileViewerProps {
path: string
}
type LoadState = 'loading' | 'ready' | 'error'
type SaveState = 'idle' | 'saving' | 'saved' | 'error'
const SAVE_DEBOUNCE_MS = 800
// onChange fires for the editor's own load-time normalization. Ignore changes
// until shortly after the document settles so opening a file never rewrites it.
const ARM_DELAY_MS = 500
function base64ToArrayBuffer(base64: string): ArrayBuffer {
const binary = atob(base64)
const len = binary.length
const bytes = new Uint8Array(len)
for (let i = 0; i < len; i++) bytes[i] = binary.charCodeAt(i)
return bytes.buffer
}
function arrayBufferToBase64(buffer: ArrayBuffer): string {
const bytes = new Uint8Array(buffer)
let binary = ''
const chunk = 0x8000
for (let i = 0; i < bytes.length; i += chunk) {
binary += String.fromCharCode(...bytes.subarray(i, i + chunk))
}
return btoa(binary)
}
function baseName(path: string): string {
const segs = path.split('/')
return segs[segs.length - 1] || path
}
export function DocxFileViewer({ path }: DocxFileViewerProps) {
const [loadState, setLoadState] = useState<LoadState>('loading')
const [buffer, setBuffer] = useState<ArrayBuffer | null>(null)
const [saveState, setSaveState] = useState<SaveState>('idle')
const editorRef = useRef<DocxEditorRef>(null)
const saveTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null)
const armTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null)
const armedRef = useRef(false)
const dirtyRef = useRef(false)
const savingRef = useRef(false)
// Load the .docx bytes whenever the path changes.
useEffect(() => {
let cancelled = false
setLoadState('loading')
setBuffer(null)
setSaveState('idle')
armedRef.current = false
dirtyRef.current = false
savingRef.current = false
;(async () => {
try {
const result = await window.ipc.invoke('workspace:readFile', { path, encoding: 'base64' })
if (cancelled) return
setBuffer(base64ToArrayBuffer(result.data))
setLoadState('ready')
if (armTimerRef.current) clearTimeout(armTimerRef.current)
armTimerRef.current = setTimeout(() => { armedRef.current = true }, ARM_DELAY_MS)
} catch (err) {
console.error('Failed to load docx:', err)
if (!cancelled) setLoadState('error')
}
})()
return () => {
cancelled = true
if (armTimerRef.current) clearTimeout(armTimerRef.current)
}
}, [path])
// Serialize the current document and write it back to disk.
const persist = async () => {
const editor = editorRef.current
if (!editor || savingRef.current) return
savingRef.current = true
dirtyRef.current = false
setSaveState('saving')
try {
const out = await editor.save()
if (out) {
await window.ipc.invoke('workspace:writeFile', {
path,
data: arrayBufferToBase64(out),
opts: { encoding: 'base64' },
})
}
setSaveState('saved')
} catch (err) {
console.error('Failed to save docx:', err)
dirtyRef.current = true
setSaveState('error')
} finally {
savingRef.current = false
// A change landed while we were saving — flush it.
if (dirtyRef.current) scheduleSave()
}
}
const scheduleSave = () => {
if (saveTimerRef.current) clearTimeout(saveTimerRef.current)
saveTimerRef.current = setTimeout(() => { void persist() }, SAVE_DEBOUNCE_MS)
}
const handleChange = () => {
if (!armedRef.current) return
dirtyRef.current = true
scheduleSave()
}
// Flush a pending save when navigating away or unmounting.
useEffect(() => {
return () => {
if (saveTimerRef.current) clearTimeout(saveTimerRef.current)
if (dirtyRef.current) void persist()
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [path])
if (loadState === 'error') {
return (
<div className="flex h-full w-full flex-col items-center justify-center gap-3 px-6 text-center text-muted-foreground">
<FileTextIcon className="size-6" />
<p className="text-sm font-medium text-foreground">Cannot open this document</p>
<p className="max-w-md text-xs">The file may be corrupted or not a valid Word document.</p>
<button
type="button"
onClick={() => { void window.ipc.invoke('shell:openPath', { path }) }}
className="inline-flex items-center gap-1.5 rounded-md border border-border bg-background px-3 py-1.5 text-xs font-medium text-foreground hover:bg-accent"
>
<ExternalLinkIcon className="size-3.5" />
Open in system
</button>
</div>
)
}
if (loadState === 'loading' || !buffer) {
return (
<div className="flex h-full w-full flex-col items-center justify-center gap-3 text-muted-foreground">
<Loader2Icon className="size-6 animate-spin" />
<p className="text-sm">Loading document</p>
</div>
)
}
return (
<div className="relative flex h-full w-full flex-col overflow-hidden">
<Suspense
fallback={
<div className="flex h-full w-full flex-col items-center justify-center gap-3 text-muted-foreground">
<Loader2Icon className="size-6 animate-spin" />
<p className="text-sm">Loading editor</p>
</div>
}
>
<LazyDocxEditor
key={path}
ref={editorRef}
documentBuffer={buffer}
mode="editing"
documentName={baseName(path)}
documentNameEditable={false}
onChange={handleChange}
onError={(err) => { console.error('docx editor error:', err) }}
className="flex-1 min-h-0"
/>
</Suspense>
{saveState !== 'idle' && (
<div className="pointer-events-none absolute bottom-3 right-4 z-10 rounded-md bg-background/80 px-2 py-1 text-xs text-muted-foreground shadow-sm backdrop-blur">
{saveState === 'saving' ? 'Saving…' : saveState === 'saved' ? 'Saved' : 'Save failed'}
</div>
)}
</div>
)
}

View file

@ -69,6 +69,31 @@ function snippet(text?: string): string {
return (text || '').replace(/\s+/g, ' ').trim().slice(0, 180)
}
function isReplyQuoteBoundary(lines: string[], index: number): boolean {
const line = lines[index]?.trim() || ''
if (/^On\b.+\bwrote:\s*$/i.test(line)) return true
if (/^-{2,}\s*(Original Message|Forwarded message)\s*-{2,}$/i.test(line)) return true
if (/^From:\s+\S/i.test(line)) {
const next = lines.slice(index + 1, index + 6).map((value) => value.trim())
return next.some((value) => /^(Sent|Date):\s+\S/i.test(value))
&& next.some((value) => /^To:\s+\S/i.test(value))
&& next.some((value) => /^Subject:\s+\S/i.test(value))
}
return false
}
function stripQuotedReplyText(text: string): string {
const lines = text.replace(/\r\n/g, '\n').replace(/\r/g, '\n').split('\n')
const boundary = lines.findIndex((line, index) => {
if (isReplyQuoteBoundary(lines, index)) return true
return index > 0
&& line.trim().startsWith('>')
&& (lines[index - 1]?.trim() === '' || lines[index - 1]?.trim().startsWith('>'))
})
const visible = boundary >= 0 ? lines.slice(0, boundary) : lines
return visible.join('\n').replace(/[ \t]+\n/g, '\n').replace(/\n{3,}/g, '\n\n').trim()
}
function getInitial(from?: string): string {
return (extractName(from)[0] || '?').toUpperCase()
}
@ -692,7 +717,7 @@ function ComposeBox({
const initialContent = useMemo(() => {
if (mode === 'forward') return buildForwardedContent(thread)
// Gmail-side draft (user's own work) wins over the AI-generated draft.
const source = thread.gmail_draft || thread.draft_response
const source = stripQuotedReplyText(thread.gmail_draft || thread.draft_response || '')
if (!source) return ''
return source
.split(/\n{2,}/)
@ -1048,8 +1073,7 @@ function ThreadDetail({
const MAX_KEPT_OPEN = 5
const PAGE_SIZE = 25
const SECTIONS = ['important', 'other'] as const
type InboxSection = (typeof SECTIONS)[number]
type InboxSection = 'important' | 'other'
interface SectionState {
threads: GmailThread[]

View file

@ -1,7 +1,8 @@
import { useCallback, useEffect, useMemo, useRef, useState } from 'react'
import { useCallback, useMemo, useRef, useState } from 'react'
import {
ChevronRight,
Copy,
ExternalLink,
File as FileIcon,
FilePlus,
Folder as FolderIcon,
@ -53,12 +54,18 @@ type WorkspaceActions = {
remove: (path: string) => Promise<void>
copyPath: (path: string) => void
revealInFileManager: (path: string, isDir: boolean) => void
createNote: (parentPath?: string) => void
createFolder: (parentPath?: string) => Promise<string>
onOpenInNewTab?: (path: string) => void
}
type WorkspaceViewProps = {
tree: TreeNode[]
initialPath?: string | null
actions: WorkspaceActions
// Folder currently being browsed. Controlled by the app so drill-down
// participates in the global back/forward history.
onNavigate: (path: string) => void
onOpenNote: (path: string) => void
onCreateWorkspace: (name: string) => Promise<void>
}
@ -71,6 +78,12 @@ function getFileManagerName(): string {
return 'File Manager'
}
function fileExtensionLabel(name: string): string {
const dot = name.lastIndexOf('.')
if (dot <= 0 || dot === name.length - 1) return 'File'
return `${name.slice(dot + 1).toUpperCase()} file`
}
function findNode(nodes: TreeNode[] | undefined, path: string): TreeNode | null {
if (!nodes) return null
for (const node of nodes) {
@ -113,8 +126,8 @@ function readFileAsBase64(file: File): Promise<string> {
})
}
export function WorkspaceView({ tree, initialPath, actions, onOpenNote, onCreateWorkspace }: WorkspaceViewProps) {
const [currentPath, setCurrentPath] = useState<string>(initialPath || WORKSPACE_ROOT)
export function WorkspaceView({ tree, initialPath, actions, onNavigate, onOpenNote, onCreateWorkspace }: WorkspaceViewProps) {
const currentPath = initialPath || WORKSPACE_ROOT
const [addOpen, setAddOpen] = useState(false)
const [newName, setNewName] = useState('')
const [creating, setCreating] = useState(false)
@ -127,10 +140,6 @@ export function WorkspaceView({ tree, initialPath, actions, onOpenNote, onCreate
const filesInputRef = useRef<HTMLInputElement | null>(null)
const folderInputRef = useRef<HTMLInputElement | null>(null)
useEffect(() => {
if (initialPath) setCurrentPath(initialPath)
}, [initialPath])
const isRoot = currentPath === WORKSPACE_ROOT
const fileManagerName = getFileManagerName()
@ -160,12 +169,12 @@ export function WorkspaceView({ tree, initialPath, actions, onOpenNote, onCreate
(item: TreeNode) => {
if (renameTarget) return
if (item.kind === 'dir') {
setCurrentPath(item.path)
onNavigate(item.path)
} else {
onOpenNote(item.path)
}
},
[onOpenNote, renameTarget],
[onNavigate, onOpenNote, renameTarget],
)
const beginRename = useCallback((item: TreeNode) => {
@ -295,7 +304,7 @@ export function WorkspaceView({ tree, initialPath, actions, onOpenNote, onCreate
<div className="flex min-w-0 items-center gap-1 text-sm">
<button
type="button"
onClick={() => setCurrentPath(WORKSPACE_ROOT)}
onClick={() => onNavigate(WORKSPACE_ROOT)}
className={cn(
'inline-flex items-center gap-1.5 rounded-md px-2 py-1 transition-colors',
isRoot ? 'text-foreground' : 'text-muted-foreground hover:text-foreground hover:bg-accent',
@ -316,7 +325,7 @@ export function WorkspaceView({ tree, initialPath, actions, onOpenNote, onCreate
) : (
<button
type="button"
onClick={() => setCurrentPath(crumb.path)}
onClick={() => onNavigate(crumb.path)}
className="rounded-md px-2 py-1 text-muted-foreground transition-colors hover:bg-accent hover:text-foreground truncate"
>
{crumb.name}
@ -326,31 +335,42 @@ export function WorkspaceView({ tree, initialPath, actions, onOpenNote, onCreate
)
})}
</div>
{isRoot ? (
<Button size="sm" onClick={() => setAddOpen(true)}>
<Plus className="size-4" />
Add workspace
<div className="grid shrink-0 grid-cols-2 items-center gap-2">
<Button
size="sm"
variant="outline"
className="w-full"
onClick={() => actions.revealInFileManager(currentPath, true)}
>
<FolderOpen className="size-4" />
Open in {fileManagerName}
</Button>
) : (
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button size="sm">
<Plus className="size-4" />
Add
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end">
<DropdownMenuItem onClick={() => filesInputRef.current?.click()}>
<FilePlus className="mr-2 size-4" />
Add files
</DropdownMenuItem>
<DropdownMenuItem onClick={() => folderInputRef.current?.click()}>
<FolderPlus className="mr-2 size-4" />
Add folder
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
)}
{isRoot ? (
<Button size="sm" className="w-full" onClick={() => setAddOpen(true)}>
<Plus className="size-4" />
Add workspace
</Button>
) : (
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button size="sm" className="w-full">
<Plus className="size-4" />
Add
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end">
<DropdownMenuItem onClick={() => filesInputRef.current?.click()}>
<FilePlus className="mr-2 size-4" />
Add files
</DropdownMenuItem>
<DropdownMenuItem onClick={() => folderInputRef.current?.click()}>
<FolderPlus className="mr-2 size-4" />
Add folder
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
)}
</div>
</div>
<input
ref={filesInputRef}
@ -429,31 +449,56 @@ export function WorkspaceView({ tree, initialPath, actions, onOpenNote, onCreate
) : (
<div className="truncate text-sm font-medium">{item.name}</div>
)}
{item.kind === 'dir' && !isRenaming && (
<div className="text-xs text-muted-foreground">
{childCount} {childCount === 1 ? 'item' : 'items'}
{!isRenaming && (
<div className="truncate text-xs text-muted-foreground">
{item.kind === 'dir'
? `${childCount} ${childCount === 1 ? 'item' : 'items'}`
: fileExtensionLabel(item.name)}
</div>
)}
</div>
</button>
)
const isDir = item.kind === 'dir'
return (
<ContextMenu key={item.path}>
<ContextMenuTrigger asChild>{card}</ContextMenuTrigger>
<ContextMenuContent className="w-48">
<ContextMenuItem onClick={() => beginRename(item)}>
<Pencil className="mr-2 size-4" />
Rename
</ContextMenuItem>
<ContextMenuContent className="w-48" onCloseAutoFocus={(e) => e.preventDefault()}>
{isDir && (
<>
<ContextMenuItem onClick={() => actions.createNote(item.path)}>
<FilePlus className="mr-2 size-4" />
New Note
</ContextMenuItem>
<ContextMenuItem onClick={() => void actions.createFolder(item.path)}>
<FolderPlus className="mr-2 size-4" />
New Folder
</ContextMenuItem>
<ContextMenuSeparator />
</>
)}
{!isDir && actions.onOpenInNewTab && (
<>
<ContextMenuItem onClick={() => actions.onOpenInNewTab!(item.path)}>
<ExternalLink className="mr-2 size-4" />
Open in new tab
</ContextMenuItem>
<ContextMenuSeparator />
</>
)}
<ContextMenuItem onClick={() => { actions.copyPath(item.path); toast('Path copied', 'success') }}>
<Copy className="mr-2 size-4" />
Copy Path
</ContextMenuItem>
<ContextMenuItem onClick={() => actions.revealInFileManager(item.path, item.kind === 'dir')}>
<ContextMenuItem onClick={() => actions.revealInFileManager(item.path, isDir)}>
<FolderOpen className="mr-2 size-4" />
Show in {fileManagerName}
Open in {fileManagerName}
</ContextMenuItem>
<ContextMenuSeparator />
<ContextMenuItem onClick={() => beginRename(item)}>
<Pencil className="mr-2 size-4" />
Rename
</ContextMenuItem>
<ContextMenuItem variant="destructive" onClick={() => void handleDelete(item)}>
<Trash2 className="mr-2 size-4" />
Delete

View file

@ -1,5 +1,6 @@
import { useEffect } from 'react'
import posthog from 'posthog-js'
import { identifyUser, resetAnalyticsIdentity } from '@/lib/analytics'
/**
* Identifies the user in PostHog when signed into Rowboat,
@ -17,7 +18,7 @@ export function useAnalyticsIdentity() {
// Identify if Rowboat account is connected
const rowboat = config.rowboat
if (rowboat?.connected && rowboat?.userId) {
posthog.identify(rowboat.userId)
identifyUser(rowboat.userId)
}
// Set provider connection flags
@ -69,7 +70,7 @@ export function useAnalyticsIdentity() {
// Rowboat sign-in
if (event.success) {
if (event.userId) {
posthog.identify(event.userId)
identifyUser(event.userId)
}
posthog.people.set({ signed_in: true, rowboat_connected: true })
posthog.capture('user_signed_in')
@ -80,7 +81,7 @@ export function useAnalyticsIdentity() {
// future events on this device don't get attributed to the prior user.
posthog.people.set({ signed_in: false, rowboat_connected: false })
posthog.capture('user_signed_out')
posthog.reset()
resetAnalyticsIdentity()
})
return cleanup

View file

@ -1,5 +1,42 @@
import posthog from 'posthog-js'
let appVersion: string | undefined
let apiUrl: string | undefined
function appVersionProperties(): Record<string, string> {
return appVersion ? { app_version: appVersion } : {}
}
export function configureAnalyticsContext(props: { appVersion?: string; apiUrl?: string }) {
appVersion = props.appVersion?.trim() || undefined
apiUrl = props.apiUrl?.trim() || undefined
const eventProperties = appVersionProperties()
if (Object.keys(eventProperties).length > 0) {
posthog.register(eventProperties)
}
const personProperties = {
...(apiUrl ? { api_url: apiUrl } : {}),
...eventProperties,
}
if (Object.keys(personProperties).length > 0) {
posthog.people.set(personProperties)
}
}
export function identifyUser(userId: string, properties?: Record<string, unknown>) {
posthog.identify(userId, {
...properties,
...appVersionProperties(),
})
}
export function resetAnalyticsIdentity() {
posthog.reset()
configureAnalyticsContext({ appVersion, apiUrl })
}
export function chatSessionCreated(runId: string) {
posthog.capture('chat_session_created', { run_id: runId })
}

View file

@ -6,7 +6,7 @@
* also uses it to decide what to keep mounted.
*/
export type ViewerType = 'html' | 'image' | 'video' | 'audio' | 'pdf'
export type ViewerType = 'html' | 'image' | 'video' | 'audio' | 'pdf' | 'docx'
const VIEWER_BY_EXT: Record<string, ViewerType> = {
html: 'html',
@ -31,6 +31,7 @@ const VIEWER_BY_EXT: Record<string, ViewerType> = {
flac: 'audio',
aac: 'audio',
pdf: 'pdf',
docx: 'docx',
}
function extensionOf(path: string): string {

View file

@ -2,9 +2,10 @@ import { StrictMode } from 'react'
import { createRoot } from 'react-dom/client'
import './index.css'
import App from './App.tsx'
import posthog from 'posthog-js'
import { PostHogProvider } from 'posthog-js/react'
import type { CaptureResult } from 'posthog-js'
import { ThemeProvider } from '@/contexts/theme-context'
import { configureAnalyticsContext } from './lib/analytics'
// Fetch the stable installation ID from main so renderer + main share one
// PostHog distinct_id. Falls back to PostHog's auto-generated anonymous ID
@ -12,19 +13,36 @@ import { ThemeProvider } from '@/contexts/theme-context'
async function bootstrap() {
let installationId: string | undefined
let apiUrl: string | undefined
let appVersion: string | undefined
try {
const result = await window.ipc.invoke('analytics:bootstrap', null)
installationId = result.installationId
apiUrl = result.apiUrl
appVersion = result.appVersion
} catch (err) {
console.error('[Analytics] Failed to bootstrap from main:', err)
}
configureAnalyticsContext({ apiUrl, appVersion })
const options = {
api_host: import.meta.env.VITE_PUBLIC_POSTHOG_HOST,
defaults: '2025-11-30',
defaults: '2025-11-30' as const,
...(installationId ? { bootstrap: { distinctID: installationId } } : {}),
} as const
before_send: (event: CaptureResult | null) => {
if (!event) return event
if (appVersion) {
event.properties = {
...event.properties,
app_version: appVersion,
}
}
return event
},
loaded: () => {
configureAnalyticsContext({ apiUrl, appVersion })
},
}
createRoot(document.getElementById('root')!).render(
<StrictMode>
@ -36,11 +54,7 @@ async function bootstrap() {
</StrictMode>,
)
// Tag the active person record with api_url so anonymous users are also
// segmentable by environment.
if (apiUrl) {
posthog.people.set({ api_url: apiUrl })
}
// The loaded callback applies api_url/app_version once PostHog has initialized.
}
bootstrap()

View file

@ -6,6 +6,7 @@ import { API_URL } from '../config/env.js';
// In dev/tsc, fall back to process.env so local runs work too.
const POSTHOG_KEY = process.env.POSTHOG_KEY ?? process.env.VITE_PUBLIC_POSTHOG_KEY ?? '';
const POSTHOG_HOST = process.env.POSTHOG_HOST ?? process.env.VITE_PUBLIC_POSTHOG_HOST ?? 'https://us.i.posthog.com';
const APP_VERSION = (process.env.ROWBOAT_APP_VERSION ?? process.env.npm_package_version ?? '').trim();
let client: PostHog | null = null;
let initAttempted = false;
@ -29,7 +30,7 @@ function getClient(): PostHog | null {
// distinguishes prod / staging / custom — meaning is assigned in PostHog).
client.identify({
distinctId: getInstallationId(),
properties: { api_url: API_URL },
properties: { api_url: API_URL, ...appVersionProperties() },
});
} catch (err) {
console.error('[Analytics] Failed to init PostHog:', err);
@ -42,6 +43,10 @@ function activeDistinctId(): string {
return identifiedUserId ?? getInstallationId();
}
function appVersionProperties(): Record<string, string> {
return APP_VERSION ? { app_version: APP_VERSION } : {};
}
export function capture(event: string, properties?: Record<string, unknown>): void {
const ph = getClient();
if (!ph) return;
@ -49,7 +54,10 @@ export function capture(event: string, properties?: Record<string, unknown>): vo
ph.capture({
distinctId: activeDistinctId(),
event,
properties,
properties: {
...properties,
...appVersionProperties(),
},
});
} catch (err) {
console.error('[Analytics] capture failed:', err);
@ -68,6 +76,7 @@ export function identify(userId: string, properties?: Record<string, unknown>):
properties: {
...properties,
api_url: API_URL,
...appVersionProperties(),
},
});
identifiedUserId = userId;

View file

@ -0,0 +1,42 @@
import { describe, expect, it } from 'vitest';
import {
sanitizeReplyBodyForGmailReply,
stripGmailQuotedReplyHtml,
stripGmailQuotedReplyText,
} from './sync_gmail.js';
describe('Gmail reply body sanitization', () => {
it('strips Gmail quote attribution and older quoted text from plain text replies', () => {
const body = [
'Sounds good, thanks. I will send it over today.',
'',
'On Thu, 28 May 2026 at 23:45, PRAKHAR <prakhar9999pandey@gmail.com> wrote:',
'> Can you share the final file?',
'> Thanks',
].join('\n');
expect(stripGmailQuotedReplyText(body)).toBe('Sounds good, thanks. I will send it over today.');
});
it('strips Gmail quote blocks from html replies', () => {
const html = [
'<p>Sounds good, thanks.</p>',
'<div class="gmail_quote">',
'<div dir="ltr" class="gmail_attr">On Thu, 28 May 2026 at 23:45, PRAKHAR wrote:<br></div>',
'<blockquote>Older thread text</blockquote>',
'</div>',
].join('');
expect(stripGmailQuotedReplyHtml(html)).toBe('<p>Sounds good, thanks.</p>');
});
it('regenerates html from clean text if only the text boundary is detected', () => {
const result = sanitizeReplyBodyForGmailReply(
'<p>Sounds good, thanks.</p><p>Older thread text</p>',
'Sounds good, thanks.\n\nOn Thu, 28 May 2026 at 23:45, PRAKHAR <prakhar9999pandey@gmail.com> wrote:\nOlder thread text',
);
expect(result.bodyText).toBe('Sounds good, thanks.');
expect(result.bodyHtml).toBe('<p>Sounds good, thanks.</p>');
});
});

View file

@ -35,7 +35,7 @@ const nhm = new NodeHtmlMarkdown();
// previously cached snapshots (e.g. attachment / recipient parsing fixes). The
// short-circuit in buildAndCacheSnapshot only reuses a cache whose version matches,
// so stale entries are transparently rebuilt on the next sync.
const SNAPSHOT_PARSER_VERSION = 2;
const SNAPSHOT_PARSER_VERSION = 3;
interface SnapshotCacheEntry {
historyId: string;
@ -405,6 +405,112 @@ function normalizeBody(body: string): string {
return body.replace(/\r\n/g, '\n').replace(/\n{3,}/g, '\n\n').trim();
}
function isGmailQuoteAttribution(line: string): boolean {
const trimmed = line.trim();
return /^On\b.+\bwrote:\s*$/i.test(trimmed);
}
function isOriginalMessageBoundary(line: string): boolean {
return /^-{2,}\s*Original Message\s*-{2,}$/i.test(line.trim());
}
function isForwardedMessageBoundary(line: string): boolean {
return /^-{2,}\s*Forwarded message\s*-{2,}$/i.test(line.trim());
}
function isOutlookHeaderBoundary(lines: string[], index: number): boolean {
if (!/^From:\s+\S/i.test(lines[index]?.trim() || '')) return false;
const next = lines.slice(index + 1, index + 6).map((line) => line.trim());
return next.some((line) => /^(Sent|Date):\s+\S/i.test(line))
&& next.some((line) => /^To:\s+\S/i.test(line))
&& next.some((line) => /^Subject:\s+\S/i.test(line));
}
function findQuotedReplyBoundary(lines: string[]): number {
for (let i = 0; i < lines.length; i += 1) {
const line = lines[i] || '';
if (
isGmailQuoteAttribution(line)
|| isOriginalMessageBoundary(line)
|| isForwardedMessageBoundary(line)
|| isOutlookHeaderBoundary(lines, i)
) {
return i;
}
// Gmail plain text drafts often carry older messages as a quoted block.
// Treat a trailing blockquote as history, but avoid stripping an inline
// quote the user is actively writing at the top of the reply.
if (i > 0 && line.trim().startsWith('>') && (lines[i - 1]?.trim() === '' || lines[i - 1]?.trim().startsWith('>'))) {
return i;
}
}
return -1;
}
export function stripGmailQuotedReplyText(text: string): string {
const normalized = text.replace(/\r\n/g, '\n').replace(/\r/g, '\n');
const lines = normalized.split('\n');
const boundary = findQuotedReplyBoundary(lines);
const visible = boundary >= 0 ? lines.slice(0, boundary) : lines;
return visible
.join('\n')
.replace(/[ \t]+\n/g, '\n')
.replace(/\n{3,}/g, '\n\n')
.trim();
}
function htmlQuoteBoundaryIndex(html: string): number {
const candidates: number[] = [];
const patterns = [
/<[^>]+\bclass\s*=\s*["'][^"']*\bgmail_(?:quote|attr)\b[^"']*["'][^>]*>/i,
/<blockquote\b[^>]*(?:type\s*=\s*["']cite["']|class\s*=\s*["'][^"']*\bgmail_quote\b[^"']*["'])[^>]*>/i,
/<(p|div|li)\b[^>]*>\s*(?:<(?:span|b|strong|i|em)\b[^>]*>\s*)*On\b[\s\S]{0,800}?\bwrote:\s*(?:<br\s*\/?>\s*)?(?:<\/(?:span|b|strong|i|em)>\s*)*<\/\1>/i,
/<(p|div|li)\b[^>]*>\s*-{2,}\s*(?:Original Message|Forwarded message)\s*-{2,}\s*<\/\1>/i,
];
for (const pattern of patterns) {
const match = pattern.exec(html);
if (match?.index !== undefined) candidates.push(match.index);
}
return candidates.length > 0 ? Math.min(...candidates) : -1;
}
export function stripGmailQuotedReplyHtml(html: string): string {
const boundary = htmlQuoteBoundaryIndex(html);
const visible = boundary >= 0 ? html.slice(0, boundary) : html;
return visible.trim();
}
function textToHtml(text: string): string {
return text
.split(/\n{2,}/)
.map((para) => `<p>${escapeHtml(para).replace(/\n/g, '<br />')}</p>`)
.join('');
}
function escapeHtml(value: string): string {
return value
.replace(/&/g, '&amp;')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;')
.replace(/"/g, '&quot;')
.replace(/'/g, '&#39;');
}
export function sanitizeReplyBodyForGmailReply(bodyHtml: string, bodyText: string): { bodyHtml: string; bodyText: string } {
const cleanText = stripGmailQuotedReplyText(bodyText);
const cleanHtml = stripGmailQuotedReplyHtml(bodyHtml);
const textWasStripped = cleanText !== bodyText.replace(/\r\n/g, '\n').replace(/\r/g, '\n').trim();
const htmlWasStripped = cleanHtml !== bodyHtml.trim();
return {
bodyText: cleanText,
bodyHtml: textWasStripped && !htmlWasStripped ? textToHtml(cleanText) : cleanHtml,
};
}
function headerValue(headers: gmail.Schema$MessagePartHeader[] | undefined, name: string): string | undefined {
return headers?.find(h => h.name?.toLowerCase() === name.toLowerCase())?.value || undefined;
}
@ -636,9 +742,13 @@ async function buildAndCacheSnapshot(
const sentMessages = parsed.filter((m) => !m.isDraft);
const draftMessages = parsed.filter((m) => m.isDraft);
const visibleMessages = sentMessages.map(({ isDraft: _isDraft, ...rest }) => rest);
const visibleMessages = sentMessages.map((msg) => {
const rest: Partial<typeof msg> = { ...msg };
delete rest.isDraft;
return rest as Omit<typeof msg, 'isDraft'>;
});
const latestDraftBody = draftMessages.length > 0
? draftMessages[draftMessages.length - 1]!.body.trim()
? stripGmailQuotedReplyText(draftMessages[draftMessages.length - 1]!.body)
: '';
if (visibleMessages.length === 0) return null;
@ -674,7 +784,10 @@ async function buildAndCacheSnapshot(
const classification = await classifyThread(snapshot, userEmail, { skipDraft });
snapshot.importance = classification.importance;
if (classification.summary) snapshot.summary = classification.summary;
if (classification.draftResponse) snapshot.draft_response = classification.draftResponse;
if (classification.draftResponse) {
const draftResponse = stripGmailQuotedReplyText(classification.draftResponse);
if (draftResponse) snapshot.draft_response = draftResponse;
}
} catch (err) {
console.warn(`[Gmail] classify failed for ${threadId}:`, err);
}
@ -947,16 +1060,20 @@ async function fullSync(auth: OAuth2Client, syncDir: string, attachmentsDir: str
// If the state file holds a last_sync timestamp (e.g. left over from a
// prior Composio sync, or from a previous successful native sync that
// we're falling back to after a history.list 404), use that as the
// floor instead of the default lookback. Carries forward Composio's
// last_sync on first migration so we don't refetch the last 7 days.
// floor — but never reach back further than lookbackDays. This caps the
// window at "1 week at most": if last_sync is within the lookback window
// we resume from it (a smaller window), otherwise we clamp to lookbackDays
// ago. Mail older than the cap that arrived during a long offline gap is
// intentionally skipped rather than backfilled.
const state = loadState(stateFile);
const lookbackFloor = new Date();
lookbackFloor.setDate(lookbackFloor.getDate() - lookbackDays);
let pastDate: Date;
if (state.last_sync) {
if (state.last_sync && new Date(state.last_sync) > lookbackFloor) {
pastDate = new Date(state.last_sync);
console.log(`Performing full sync from last_sync=${state.last_sync}...`);
} else {
pastDate = new Date();
pastDate.setDate(pastDate.getDate() - lookbackDays);
pastDate = lookbackFloor;
console.log(`Performing full sync of last ${lookbackDays} days...`);
}
@ -1222,12 +1339,22 @@ async function performSync() {
// this runs once, the cache directory is populated and we fall back to
// partial-sync on subsequent calls.
const cacheMissing = !fs.existsSync(CACHE_DIR) || fs.readdirSync(CACHE_DIR).length === 0;
// partialSync replays *every* messageAdded since the stored historyId,
// regardless of date — so after a long offline gap a still-valid
// historyId would pull the entire gap (e.g. 3 weeks). To honor the
// "1 week at most" cap, bypass it when last_sync is older than the
// lookback window and run a (date-clamped) fullSync instead.
const gapMs = state.last_sync ? Date.now() - new Date(state.last_sync).getTime() : 0;
const gapTooLarge = gapMs > LOOKBACK_DAYS * 24 * 60 * 60 * 1000;
if (!state.historyId) {
console.log("No history ID found, starting full sync...");
await fullSync(auth, SYNC_DIR, ATTACHMENTS_DIR, STATE_FILE, LOOKBACK_DAYS);
} else if (cacheMissing) {
console.log("History ID present but inbox cache empty — running full sync to backfill snapshots...");
await fullSync(auth, SYNC_DIR, ATTACHMENTS_DIR, STATE_FILE, LOOKBACK_DAYS);
} else if (gapTooLarge) {
console.log(`Last sync older than ${LOOKBACK_DAYS} days — running full sync clamped to the lookback window instead of partial sync...`);
await fullSync(auth, SYNC_DIR, ATTACHMENTS_DIR, STATE_FILE, LOOKBACK_DAYS);
} else {
console.log("History ID found, starting partial sync...");
await partialSync(auth, state.historyId, SYNC_DIR, ATTACHMENTS_DIR, STATE_FILE, LOOKBACK_DAYS);
@ -1330,6 +1457,10 @@ export async function sendThreadReply(opts: SendReplyOptions): Promise<SendReply
const safeBcc = opts.bcc?.trim() ? requireSafeHeaderValue('Bcc', opts.bcc) : undefined;
const safeInReplyTo = opts.inReplyTo ? requireSafeHeaderValue('In-Reply-To', opts.inReplyTo) : undefined;
const safeReferences = opts.references ? requireSafeHeaderValue('References', opts.references) : undefined;
const replyBody = opts.threadId
? sanitizeReplyBodyForGmailReply(opts.bodyHtml, opts.bodyText)
: { bodyHtml: opts.bodyHtml.trim(), bodyText: opts.bodyText.trim() };
if (!replyBody.bodyText.trim()) return { error: 'Draft is empty.' };
const boundary = `b_${Date.now()}_${Math.random().toString(36).slice(2, 10)}`;
const headers: string[] = [];
@ -1348,13 +1479,13 @@ export async function sendThreadReply(opts: SendReplyOptions): Promise<SendReply
parts.push('Content-Type: text/plain; charset="UTF-8"');
parts.push('Content-Transfer-Encoding: base64');
parts.push('');
parts.push(encodeMimeBase64(opts.bodyText));
parts.push(encodeMimeBase64(replyBody.bodyText));
parts.push('');
parts.push(`--${boundary}`);
parts.push('Content-Type: text/html; charset="UTF-8"');
parts.push('Content-Transfer-Encoding: base64');
parts.push('');
parts.push(encodeMimeBase64(opts.bodyHtml));
parts.push(encodeMimeBase64(replyBody.bodyHtml));
parts.push('');
parts.push(`--${boundary}--`);

View file

@ -38,6 +38,7 @@ const ipcSchemas = {
res: z.object({
installationId: z.string(),
apiUrl: z.string(),
appVersion: z.string(),
}),
},
'workspace:getRoot': {

315
apps/x/pnpm-lock.yaml generated
View file

@ -4,6 +4,12 @@ settings:
autoInstallPeers: true
excludeLinksFromLockfile: false
catalogs:
default:
vitest:
specifier: 4.1.7
version: 4.1.7
importers:
.:
@ -127,6 +133,9 @@ importers:
apps/renderer:
dependencies:
'@eigenpal/docx-editor-react':
specifier: ^1.0.3
version: 1.0.3(@types/react-dom@19.2.3(@types/react@19.2.7))(@types/react@19.2.7)(ai@5.0.117(zod@4.2.1))(prosemirror-commands@1.7.1)(prosemirror-dropcursor@1.8.2)(prosemirror-history@1.5.0)(prosemirror-keymap@1.2.3)(prosemirror-model@1.25.7)(prosemirror-state@1.4.4)(prosemirror-tables@1.8.5)(prosemirror-transform@1.12.0)(prosemirror-view@1.41.8)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)
'@radix-ui/react-avatar':
specifier: ^1.1.11
version: 1.1.11(@types/react-dom@19.2.3(@types/react@19.2.7))(@types/react@19.2.7)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)
@ -186,16 +195,16 @@ importers:
version: 3.22.4(@tiptap/core@3.22.4(@tiptap/pm@3.22.4))(@tiptap/pm@3.22.4)
'@tiptap/extension-placeholder':
specifier: 3.22.4
version: 3.22.4(@tiptap/extensions@3.22.5(@tiptap/core@3.22.4(@tiptap/pm@3.22.4))(@tiptap/pm@3.22.4))
version: 3.22.4(@tiptap/extensions@3.22.4(@tiptap/core@3.22.4(@tiptap/pm@3.22.4))(@tiptap/pm@3.22.4))
'@tiptap/extension-table':
specifier: 3.22.4
version: 3.22.4(@tiptap/core@3.22.4(@tiptap/pm@3.22.4))(@tiptap/pm@3.22.4)
'@tiptap/extension-task-item':
specifier: 3.22.4
version: 3.22.4(@tiptap/extension-list@3.22.5(@tiptap/core@3.22.4(@tiptap/pm@3.22.4))(@tiptap/pm@3.22.4))
version: 3.22.4(@tiptap/extension-list@3.22.4(@tiptap/core@3.22.4(@tiptap/pm@3.22.4))(@tiptap/pm@3.22.4))
'@tiptap/extension-task-list':
specifier: 3.22.4
version: 3.22.4(@tiptap/extension-list@3.22.5(@tiptap/core@3.22.4(@tiptap/pm@3.22.4))(@tiptap/pm@3.22.4))
version: 3.22.4(@tiptap/extension-list@3.22.4(@tiptap/core@3.22.4(@tiptap/pm@3.22.4))(@tiptap/pm@3.22.4))
'@tiptap/pm':
specifier: 3.22.4
version: 3.22.4
@ -238,6 +247,33 @@ importers:
posthog-js:
specifier: ^1.332.0
version: 1.332.0
prosemirror-commands:
specifier: ^1.7.1
version: 1.7.1
prosemirror-dropcursor:
specifier: ^1.8.2
version: 1.8.2
prosemirror-history:
specifier: ^1.5.0
version: 1.5.0
prosemirror-keymap:
specifier: ^1.2.3
version: 1.2.3
prosemirror-model:
specifier: ^1.25.7
version: 1.25.7
prosemirror-state:
specifier: ^1.4.4
version: 1.4.4
prosemirror-tables:
specifier: ^1.8.5
version: 1.8.5
prosemirror-transform:
specifier: ^1.12.0
version: 1.12.0
prosemirror-view:
specifier: ^1.41.8
version: 1.41.8
radix-ui:
specifier: ^1.4.3
version: 1.4.3(@types/react-dom@19.2.3(@types/react@19.2.7))(@types/react@19.2.7)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)
@ -795,6 +831,60 @@ packages:
peerDependencies:
zod: '>=3.25.76 <5'
'@eigenpal/docx-editor-agents@1.0.3':
resolution: {integrity: sha512-Bk/J9/PBnMCOxb6w4cHQiCTuN/1C4FtZM9evC9EXXcLP13yFMdqoEqsYs+Lh3HyaRRAaCZTrkfgOZyTqqyjtwQ==}
peerDependencies:
'@ai-sdk/vue': ^2.0.0
ai: ^5.0.0 || ^6.0.0
react: ^17.0.0 || ^18.0.0 || ^19.0.0
vue: ^3.0.0
peerDependenciesMeta:
'@ai-sdk/vue':
optional: true
ai:
optional: true
react:
optional: true
vue:
optional: true
'@eigenpal/docx-editor-core@1.0.3':
resolution: {integrity: sha512-etpupuln9ZlHLW4DgS7877WBdMEChsAG0D1bEZLjF70isYbyxrd2ARWax745P7XMm4GqqkAfByzxE2GGWQmJaA==}
hasBin: true
peerDependencies:
prosemirror-commands: ^1.5.2
prosemirror-dropcursor: ^1.8.2
prosemirror-history: ^1.4.0
prosemirror-keymap: ^1.2.2
prosemirror-model: ^1.19.4
prosemirror-state: ^1.4.3
prosemirror-tables: ^1.8.5
prosemirror-transform: ^1.12.0
prosemirror-view: ^1.32.7
'@eigenpal/docx-editor-i18n@1.0.3':
resolution: {integrity: sha512-zwz/S+duPOnzg/kh4bs28T3UqI8mKMzHdmFgbWgMxwtTfUkAxaUAnAVbuZgrysl1aD2scv4Hfy4EgOZcFy9NnA==}
'@eigenpal/docx-editor-react@1.0.3':
resolution: {integrity: sha512-KupDVHo6KC4KUs48bM1pMYFFbDJqkW8XyIhgsnLx+BWk2yOPU4bx2HfWB6H+JEVROA1h1AmhTAyE39gk75wg5w==}
peerDependencies:
prosemirror-commands: ^1.5.2
prosemirror-dropcursor: ^1.8.2
prosemirror-history: ^1.4.0
prosemirror-keymap: ^1.2.2
prosemirror-model: ^1.19.4
prosemirror-state: ^1.4.3
prosemirror-tables: ^1.8.5
prosemirror-transform: ^1.12.0
prosemirror-view: ^1.41.6
react: ^18.0.0 || ^19.0.0
react-dom: ^18.0.0 || ^19.0.0
peerDependenciesMeta:
react:
optional: true
react-dom:
optional: true
'@electron-forge/cli@7.11.1':
resolution: {integrity: sha512-pk8AoLsr7t7LBAt0cFD06XFA6uxtPdvtLx06xeal7O9o7GHGCbj29WGwFoJ8Br/ENM0Ho868S3PrAn1PtBXt5g==}
engines: {node: '>= 16.4.0'}
@ -1488,30 +1578,35 @@ packages:
engines: {node: '>= 10'}
cpu: [arm64]
os: [linux]
libc: [glibc]
'@napi-rs/canvas-linux-arm64-musl@0.1.80':
resolution: {integrity: sha512-1XbCOz/ymhj24lFaIXtWnwv/6eFHXDrjP0jYkc6iHQ9q8oXKzUX1Lc6bu+wuGiLhGh2GS/2JlfORC5ZcXimRcg==}
engines: {node: '>= 10'}
cpu: [arm64]
os: [linux]
libc: [musl]
'@napi-rs/canvas-linux-riscv64-gnu@0.1.80':
resolution: {integrity: sha512-XTzR125w5ZMs0lJcxRlS1K3P5RaZ9RmUsPtd1uGt+EfDyYMu4c6SEROYsxyatbbu/2+lPe7MPHOO/0a0x7L/gw==}
engines: {node: '>= 10'}
cpu: [riscv64]
os: [linux]
libc: [glibc]
'@napi-rs/canvas-linux-x64-gnu@0.1.80':
resolution: {integrity: sha512-BeXAmhKg1kX3UCrJsYbdQd3hIMDH/K6HnP/pG2LuITaXhXBiNdh//TVVVVCBbJzVQaV5gK/4ZOCMrQW9mvuTqA==}
engines: {node: '>= 10'}
cpu: [x64]
os: [linux]
libc: [glibc]
'@napi-rs/canvas-linux-x64-musl@0.1.80':
resolution: {integrity: sha512-x0XvZWdHbkgdgucJsRxprX/4o4sEed7qo9rCQA9ugiS9qE2QvP0RIiEugtZhfLH3cyI+jIRFJHV4Fuz+1BHHMg==}
engines: {node: '>= 10'}
cpu: [x64]
os: [linux]
libc: [musl]
'@napi-rs/canvas-win32-x64-msvc@0.1.80':
resolution: {integrity: sha512-Z8jPsM6df5V8B1HrCHB05+bDiCxjE9QA//3YrkKIdVDEwn5RKaqOxCJDRJkl48cJbylcrJbW4HxZbTte8juuPg==}
@ -2634,56 +2729,67 @@ packages:
resolution: {integrity: sha512-EHMUcDwhtdRGlXZsGSIuXSYwD5kOT9NVnx9sqzYiwAc91wfYOE1g1djOEDseZJKKqtHAHGwnGPQu3kytmfaXLQ==}
cpu: [arm]
os: [linux]
libc: [glibc]
'@rollup/rollup-linux-arm-musleabihf@4.54.0':
resolution: {integrity: sha512-+pBrqEjaakN2ySv5RVrj/qLytYhPKEUwk+e3SFU5jTLHIcAtqh2rLrd/OkbNuHJpsBgxsD8ccJt5ga/SeG0JmA==}
cpu: [arm]
os: [linux]
libc: [musl]
'@rollup/rollup-linux-arm64-gnu@4.54.0':
resolution: {integrity: sha512-NSqc7rE9wuUaRBsBp5ckQ5CVz5aIRKCwsoa6WMF7G01sX3/qHUw/z4pv+D+ahL1EIKy6Enpcnz1RY8pf7bjwng==}
cpu: [arm64]
os: [linux]
libc: [glibc]
'@rollup/rollup-linux-arm64-musl@4.54.0':
resolution: {integrity: sha512-gr5vDbg3Bakga5kbdpqx81m2n9IX8M6gIMlQQIXiLTNeQW6CucvuInJ91EuCJ/JYvc+rcLLsDFcfAD1K7fMofg==}
cpu: [arm64]
os: [linux]
libc: [musl]
'@rollup/rollup-linux-loong64-gnu@4.54.0':
resolution: {integrity: sha512-gsrtB1NA3ZYj2vq0Rzkylo9ylCtW/PhpLEivlgWe0bpgtX5+9j9EZa0wtZiCjgu6zmSeZWyI/e2YRX1URozpIw==}
cpu: [loong64]
os: [linux]
libc: [glibc]
'@rollup/rollup-linux-ppc64-gnu@4.54.0':
resolution: {integrity: sha512-y3qNOfTBStmFNq+t4s7Tmc9hW2ENtPg8FeUD/VShI7rKxNW7O4fFeaYbMsd3tpFlIg1Q8IapFgy7Q9i2BqeBvA==}
cpu: [ppc64]
os: [linux]
libc: [glibc]
'@rollup/rollup-linux-riscv64-gnu@4.54.0':
resolution: {integrity: sha512-89sepv7h2lIVPsFma8iwmccN7Yjjtgz0Rj/Ou6fEqg3HDhpCa+Et+YSufy27i6b0Wav69Qv4WBNl3Rs6pwhebQ==}
cpu: [riscv64]
os: [linux]
libc: [glibc]
'@rollup/rollup-linux-riscv64-musl@4.54.0':
resolution: {integrity: sha512-ZcU77ieh0M2Q8Ur7D5X7KvK+UxbXeDHwiOt/CPSBTI1fBmeDMivW0dPkdqkT4rOgDjrDDBUed9x4EgraIKoR2A==}
cpu: [riscv64]
os: [linux]
libc: [musl]
'@rollup/rollup-linux-s390x-gnu@4.54.0':
resolution: {integrity: sha512-2AdWy5RdDF5+4YfG/YesGDDtbyJlC9LHmL6rZw6FurBJ5n4vFGupsOBGfwMRjBYH7qRQowT8D/U4LoSvVwOhSQ==}
cpu: [s390x]
os: [linux]
libc: [glibc]
'@rollup/rollup-linux-x64-gnu@4.54.0':
resolution: {integrity: sha512-WGt5J8Ij/rvyqpFexxk3ffKqqbLf9AqrTBbWDk7ApGUzaIs6V+s2s84kAxklFwmMF/vBNGrVdYgbblCOFFezMQ==}
cpu: [x64]
os: [linux]
libc: [glibc]
'@rollup/rollup-linux-x64-musl@4.54.0':
resolution: {integrity: sha512-JzQmb38ATzHjxlPHuTH6tE7ojnMKM2kYNzt44LO/jJi8BpceEC8QuXYA908n8r3CNuG/B3BV8VR3Hi1rYtmPiw==}
cpu: [x64]
os: [linux]
libc: [musl]
'@rollup/rollup-openharmony-arm64@4.54.0':
resolution: {integrity: sha512-huT3fd0iC7jigGh7n3q/+lfPcXxBi+om/Rs3yiFxjvSxbSB6aohDFXbWvlspaqjeOh+hx7DDHS+5Es5qRkWkZg==}
@ -3002,24 +3108,28 @@ packages:
engines: {node: '>= 10'}
cpu: [arm64]
os: [linux]
libc: [glibc]
'@tailwindcss/oxide-linux-arm64-musl@4.1.18':
resolution: {integrity: sha512-1px92582HkPQlaaCkdRcio71p8bc8i/ap5807tPRDK/uw953cauQBT8c5tVGkOwrHMfc2Yh6UuxaH4vtTjGvHg==}
engines: {node: '>= 10'}
cpu: [arm64]
os: [linux]
libc: [musl]
'@tailwindcss/oxide-linux-x64-gnu@4.1.18':
resolution: {integrity: sha512-v3gyT0ivkfBLoZGF9LyHmts0Isc8jHZyVcbzio6Wpzifg/+5ZJpDiRiUhDLkcr7f/r38SWNe7ucxmGW3j3Kb/g==}
engines: {node: '>= 10'}
cpu: [x64]
os: [linux]
libc: [glibc]
'@tailwindcss/oxide-linux-x64-musl@4.1.18':
resolution: {integrity: sha512-bhJ2y2OQNlcRwwgOAGMY0xTFStt4/wyU6pvI6LSuZpRgKQwxTec0/3Scu91O8ir7qCR3AuepQKLU/kX99FouqQ==}
engines: {node: '>= 10'}
cpu: [x64]
os: [linux]
libc: [musl]
'@tailwindcss/oxide-wasm32-wasi@4.1.18':
resolution: {integrity: sha512-LffYTvPjODiP6PT16oNeUQJzNVyJl1cjIebq/rWWBF+3eDst5JGEFSc5cWxyRCJ0Mxl+KyIkqRxk1XPEs9x8TA==}
@ -3155,6 +3265,12 @@ packages:
peerDependencies:
'@tiptap/extension-list': 3.22.5
'@tiptap/extension-list@3.22.4':
resolution: {integrity: sha512-Xe8UFvvHmyp/c/TJsFwlwU9CWACYbBirNsluJ3U1+H8BTu1wqdrT/AXR5uIXeyCl5kiWKgX5q71eHWbYFOrqrg==}
peerDependencies:
'@tiptap/core': 3.22.4
'@tiptap/pm': 3.22.4
'@tiptap/extension-list@3.22.5':
resolution: {integrity: sha512-cVO3ZHCgxAWZ4zrFSs81FO2nyCk1wb2EHkpLpW98FzbJLkN9rDkazhW99P3HRWy/CvUldOT+8ecI1YrQtBojMg==}
peerDependencies:
@ -3207,6 +3323,12 @@ packages:
peerDependencies:
'@tiptap/core': 3.22.5
'@tiptap/extensions@3.22.4':
resolution: {integrity: sha512-fOe8VptJvLPs32bNdUYo8SRyljwqKNQVXWW056VoXIc5en/59OdJlJQVeHI0jRRciH3MtrqODi/gfJR0VHNZ8A==}
peerDependencies:
'@tiptap/core': 3.22.4
'@tiptap/pm': 3.22.4
'@tiptap/extensions@3.22.5':
resolution: {integrity: sha512-Ifg4MzKCj3uRqe3ieTwYnomu2y4p7EXr2avVSKZYfh12i2dyWe2Gkn1KuZDREANVE+gHqFlQjJRYzhJFwzSCrg==}
peerDependencies:
@ -3663,6 +3785,10 @@ packages:
engines: {node: '>=10.0.0'}
deprecated: this version has critical issues, please update to the latest version
'@xmldom/xmldom@0.9.10':
resolution: {integrity: sha512-A9gOqLdi6cV4ibazAjcQufGj0B1y/vDqYrcuP6d/6x8P27gRS8643Dj9o1dEKtB6O7fwxb2FgBmJS2mX7gpvdw==}
engines: {node: '>=14.6'}
'@xtuc/ieee754@1.2.0':
resolution: {integrity: sha512-DX8nKgqcGwsc0eJSqYt5lwP4DH5FlHnmuWWBRy7X0NcaGR0ZtuyeESgMwTYVEtxmsNGY+qit4QYT/MIYTOTPeA==}
@ -4472,6 +4598,10 @@ packages:
dir-compare@4.2.0:
resolution: {integrity: sha512-2xMCmOoMrdQIPHdsTawECdNPwlVFB9zGcz3kuhmBO6U3oU+UQjsue0i8ayLKpgBcm+hcXPMVSGUN9d+pvJ6+VQ==}
docxtemplater@3.68.7:
resolution: {integrity: sha512-FwgeAKqY2vc9eVm2V2XGg8bq25B0OQjtSDITGi9zNnvu5GbtR4WvGjM5QNld/ALB6ZbsSuHskBPK9SvPpKhsbA==}
engines: {node: '>=0.10'}
dom-serializer@0.2.2:
resolution: {integrity: sha512-2/xPb3ORsQ42nHYiSunXkDjPLBaEj/xTwUO4B7XCZQTRk7EBtTOPaygh10YAAh2OI1Qrp6NWfpAhzswj0ydt9g==}
@ -5655,24 +5785,28 @@ packages:
engines: {node: '>= 12.0.0'}
cpu: [arm64]
os: [linux]
libc: [glibc]
lightningcss-linux-arm64-musl@1.30.2:
resolution: {integrity: sha512-5Vh9dGeblpTxWHpOx8iauV02popZDsCYMPIgiuw97OJ5uaDsL86cnqSFs5LZkG3ghHoX5isLgWzMs+eD1YzrnA==}
engines: {node: '>= 12.0.0'}
cpu: [arm64]
os: [linux]
libc: [musl]
lightningcss-linux-x64-gnu@1.30.2:
resolution: {integrity: sha512-Cfd46gdmj1vQ+lR6VRTTadNHu6ALuw2pKR9lYq4FnhvgBc4zWY1EtZcAc6EffShbb1MFrIPfLDXD6Xprbnni4w==}
engines: {node: '>= 12.0.0'}
cpu: [x64]
os: [linux]
libc: [glibc]
lightningcss-linux-x64-musl@1.30.2:
resolution: {integrity: sha512-XJaLUUFXb6/QG2lGIW6aIk6jKdtjtcffUT0NKvIqhSBY3hh9Ch+1LCeH80dR9q9LBjG3ewbDjnumefsLsP6aiA==}
engines: {node: '>= 12.0.0'}
cpu: [x64]
os: [linux]
libc: [musl]
lightningcss-win32-arm64-msvc@1.30.2:
resolution: {integrity: sha512-FZn+vaj7zLv//D/192WFFVA0RgHawIcHqLX9xuWiQt7P0PtdFEVaxgF9rjM/IRYHQXNnk61/H/gb2Ei+kUQ4xQ==}
@ -6386,6 +6520,9 @@ packages:
pako@1.0.11:
resolution: {integrity: sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw==}
pako@2.1.0:
resolution: {integrity: sha512-w+eufiZ1WuJYgPXbV/PO3NCMEc3xqylkKHzp8bxp1uW4qaSNQUkwmLLEc3kKsfz8lpV1F8Ht3U1Cm+9Srog2ug==}
papaparse@5.5.3:
resolution: {integrity: sha512-5QvjGxYVjxO59MGU2lHVYpRWBBtKHnlIAcSe1uNFCkkptUh63NFRj0FJQm7nR67puEruUci/ZkjmEFrjCAyP4A==}
@ -6499,6 +6636,9 @@ packages:
resolution: {integrity: sha512-uB80kBFb/tfd68bVleG9T5GGsGPjJrLAUpR5PZIrhBnIaRTQRjqdJSsIKkOP6OAIFbj7GOrcudc5pNjZ+geV2g==}
engines: {node: '>=6'}
pizzip@3.2.0:
resolution: {integrity: sha512-X4NPNICxCfIK8VYhF6wbksn81vTiziyLbvKuORVAmolvnUzl1A1xmz9DAWKxPRq9lZg84pJOOAMq3OE61bD8IQ==}
pkce-challenge@5.0.1:
resolution: {integrity: sha512-wQ0b/W4Fr01qtpHlqSqspcj3EhBvimsdh0KlHhH8HRZnMsEa0ea2fTULOXOS9ccQr3om+GcGRk4e+isrZWV8qQ==}
engines: {node: '>=16.20.0'}
@ -6605,8 +6745,8 @@ packages:
prosemirror-markdown@1.13.2:
resolution: {integrity: sha512-FPD9rHPdA9fqzNmIIDhhnYQ6WgNoSWX9StUZ8LEKapaXU9i6XgykaHKhp6XMyXlOWetmaFgGDS/nu/w9/vUc5g==}
prosemirror-model@1.25.4:
resolution: {integrity: sha512-PIM7E43PBxKce8OQeezAs9j4TP+5yDpZVbuurd1h5phUxEKIu+G2a+EUZzIC5nS1mJktDJWzbqS23n1tsAf5QA==}
prosemirror-model@1.25.7:
resolution: {integrity: sha512-A79aN8QEFUwI6cax8Yq4Rpcx1TJZ3Kagn+ii7qLo4/V8H3mMiHrhFyhTyHHvpSnOgMPpWiDGSwM3etwrxE50ug==}
prosemirror-schema-list@1.5.1:
resolution: {integrity: sha512-927lFx/uwyQaGwJxLWCZRkjXG0p48KpMj6ueoYiu4JX05GGuGcgzAy62dfiV8eFZftgyBUvLx76RsMe20fJl+Q==}
@ -6617,11 +6757,11 @@ packages:
prosemirror-tables@1.8.5:
resolution: {integrity: sha512-V/0cDCsHKHe/tfWkeCmthNUcEp1IVO3p6vwN8XtwE9PZQLAZJigbw3QoraAdfJPir4NKJtNvOB8oYGKRl+t0Dw==}
prosemirror-transform@1.10.5:
resolution: {integrity: sha512-RPDQCxIDhIBb1o36xxwsaeAvivO8VLJcgBtzmOwQ64bMtsVFh5SSuJ6dWSxO1UsHTiTXPCgQm3PDJt7p6IOLbw==}
prosemirror-transform@1.12.0:
resolution: {integrity: sha512-GxboyN4AMIsoHNtz5uf2r2Ru551i5hWeCMD6E2Ib4Eogqoub0NflniaBPVQ4MrGE5yZ8JV9tUHg9qcZTTrcN4w==}
prosemirror-view@1.41.4:
resolution: {integrity: sha512-WkKgnyjNncri03Gjaz3IFWvCAE94XoiEgvtr0/r2Xw7R8/IjK3sKLSiDoCHWcsXSAinVaKlGRZDvMCsF1kbzjA==}
prosemirror-view@1.41.8:
resolution: {integrity: sha512-TnKDdohEatgyZNGCDWIdccOHXhYloJwbwU+phw/a23KBvJIR9lWQWW7WHHK3vBdOLDNuF7TaX98GObUZOWkOnA==}
protobufjs@7.5.4:
resolution: {integrity: sha512-CvexbZtbov6jW2eXAvLukXjXUW1TzFaivC46BpWc/3BpcCysb5Vffu+B3XHMm8lVEuy2Mm4XGex8hBSg1yapPg==}
@ -6979,6 +7119,10 @@ packages:
safer-buffer@2.1.2:
resolution: {integrity: sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==}
sax@1.6.0:
resolution: {integrity: sha512-6R3J5M4AcbtLUdZmRv2SygeVaM7IhrLXu9BmnOGmmACak8fiUtOsYNWUS4uK7upbmHIBbLBeFeI//477BKLBzA==}
engines: {node: '>=11.0.0'}
scheduler@0.25.0-rc-603e6108-20241029:
resolution: {integrity: sha512-pFwF6H1XrSdYYNLfOcGlM28/j8CGLu8IvdrxqhjWULe2bPcKiKW4CV+OWqR/9fT52mywx65l7ysNkjLKBda7eA==}
@ -7800,6 +7944,10 @@ packages:
engines: {node: '>=0.8'}
hasBin: true
xml-js@1.6.11:
resolution: {integrity: sha512-7rVi2KMfwfWFl+GpPg6m80IVMWXLRjO+PxTq7V2CDhoGak0wzYzFgUY2m4XJ47OGdXd8eLE8EmwfAmdjw7lC1g==}
hasBin: true
xmlbuilder2@2.1.2:
resolution: {integrity: sha512-PI710tmtVlQ5VmwzbRTuhmVhKnj9pM8Si+iOZCV2g2SNo3gCrpzR2Ka9wNzZtqfD+mnP+xkrqoNy0sjKZqP4Dg==}
engines: {node: '>=8.0'}
@ -8588,6 +8736,61 @@ snapshots:
dependencies:
zod: 4.2.1
'@eigenpal/docx-editor-agents@1.0.3(ai@5.0.117(zod@4.2.1))(react@19.2.3)':
dependencies:
docxtemplater: 3.68.7
jszip: 3.10.1
pizzip: 3.2.0
xml-js: 1.6.11
optionalDependencies:
ai: 5.0.117(zod@4.2.1)
react: 19.2.3
'@eigenpal/docx-editor-core@1.0.3(prosemirror-commands@1.7.1)(prosemirror-dropcursor@1.8.2)(prosemirror-history@1.5.0)(prosemirror-keymap@1.2.3)(prosemirror-model@1.25.7)(prosemirror-state@1.4.4)(prosemirror-tables@1.8.5)(prosemirror-transform@1.12.0)(prosemirror-view@1.41.8)':
dependencies:
docxtemplater: 3.68.7
jszip: 3.10.1
pizzip: 3.2.0
prosemirror-commands: 1.7.1
prosemirror-dropcursor: 1.8.2
prosemirror-history: 1.5.0
prosemirror-keymap: 1.2.3
prosemirror-model: 1.25.7
prosemirror-state: 1.4.4
prosemirror-tables: 1.8.5
prosemirror-transform: 1.12.0
prosemirror-view: 1.41.8
xml-js: 1.6.11
'@eigenpal/docx-editor-i18n@1.0.3': {}
'@eigenpal/docx-editor-react@1.0.3(@types/react-dom@19.2.3(@types/react@19.2.7))(@types/react@19.2.7)(ai@5.0.117(zod@4.2.1))(prosemirror-commands@1.7.1)(prosemirror-dropcursor@1.8.2)(prosemirror-history@1.5.0)(prosemirror-keymap@1.2.3)(prosemirror-model@1.25.7)(prosemirror-state@1.4.4)(prosemirror-tables@1.8.5)(prosemirror-transform@1.12.0)(prosemirror-view@1.41.8)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)':
dependencies:
'@eigenpal/docx-editor-agents': 1.0.3(ai@5.0.117(zod@4.2.1))(react@19.2.3)
'@eigenpal/docx-editor-core': 1.0.3(prosemirror-commands@1.7.1)(prosemirror-dropcursor@1.8.2)(prosemirror-history@1.5.0)(prosemirror-keymap@1.2.3)(prosemirror-model@1.25.7)(prosemirror-state@1.4.4)(prosemirror-tables@1.8.5)(prosemirror-transform@1.12.0)(prosemirror-view@1.41.8)
'@eigenpal/docx-editor-i18n': 1.0.3
'@radix-ui/react-select': 2.2.6(@types/react-dom@19.2.3(@types/react@19.2.7))(@types/react@19.2.7)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)
clsx: 2.1.1
prosemirror-commands: 1.7.1
prosemirror-dropcursor: 1.8.2
prosemirror-history: 1.5.0
prosemirror-keymap: 1.2.3
prosemirror-model: 1.25.7
prosemirror-state: 1.4.4
prosemirror-tables: 1.8.5
prosemirror-transform: 1.12.0
prosemirror-view: 1.41.8
sonner: 2.0.7(react-dom@19.2.3(react@19.2.3))(react@19.2.3)
optionalDependencies:
react: 19.2.3
react-dom: 19.2.3(react@19.2.3)
transitivePeerDependencies:
- '@ai-sdk/vue'
- '@types/react'
- '@types/react-dom'
- ai
- vue
'@electron-forge/cli@7.11.1(encoding@0.1.13)(esbuild@0.24.2)':
dependencies:
'@electron-forge/core': 7.11.1(encoding@0.1.13)(esbuild@0.24.2)
@ -11264,6 +11467,11 @@ snapshots:
dependencies:
'@tiptap/extension-list': 3.22.5(@tiptap/core@3.22.4(@tiptap/pm@3.22.4))(@tiptap/pm@3.22.4)
'@tiptap/extension-list@3.22.4(@tiptap/core@3.22.4(@tiptap/pm@3.22.4))(@tiptap/pm@3.22.4)':
dependencies:
'@tiptap/core': 3.22.4(@tiptap/pm@3.22.4)
'@tiptap/pm': 3.22.4
'@tiptap/extension-list@3.22.5(@tiptap/core@3.22.4(@tiptap/pm@3.22.4))(@tiptap/pm@3.22.4)':
dependencies:
'@tiptap/core': 3.22.4(@tiptap/pm@3.22.4)
@ -11277,9 +11485,9 @@ snapshots:
dependencies:
'@tiptap/core': 3.22.4(@tiptap/pm@3.22.4)
'@tiptap/extension-placeholder@3.22.4(@tiptap/extensions@3.22.5(@tiptap/core@3.22.4(@tiptap/pm@3.22.4))(@tiptap/pm@3.22.4))':
'@tiptap/extension-placeholder@3.22.4(@tiptap/extensions@3.22.4(@tiptap/core@3.22.4(@tiptap/pm@3.22.4))(@tiptap/pm@3.22.4))':
dependencies:
'@tiptap/extensions': 3.22.5(@tiptap/core@3.22.4(@tiptap/pm@3.22.4))(@tiptap/pm@3.22.4)
'@tiptap/extensions': 3.22.4(@tiptap/core@3.22.4(@tiptap/pm@3.22.4))(@tiptap/pm@3.22.4)
'@tiptap/extension-strike@3.22.5(@tiptap/core@3.22.4(@tiptap/pm@3.22.4))':
dependencies:
@ -11290,13 +11498,13 @@ snapshots:
'@tiptap/core': 3.22.4(@tiptap/pm@3.22.4)
'@tiptap/pm': 3.22.4
'@tiptap/extension-task-item@3.22.4(@tiptap/extension-list@3.22.5(@tiptap/core@3.22.4(@tiptap/pm@3.22.4))(@tiptap/pm@3.22.4))':
'@tiptap/extension-task-item@3.22.4(@tiptap/extension-list@3.22.4(@tiptap/core@3.22.4(@tiptap/pm@3.22.4))(@tiptap/pm@3.22.4))':
dependencies:
'@tiptap/extension-list': 3.22.5(@tiptap/core@3.22.4(@tiptap/pm@3.22.4))(@tiptap/pm@3.22.4)
'@tiptap/extension-list': 3.22.4(@tiptap/core@3.22.4(@tiptap/pm@3.22.4))(@tiptap/pm@3.22.4)
'@tiptap/extension-task-list@3.22.4(@tiptap/extension-list@3.22.5(@tiptap/core@3.22.4(@tiptap/pm@3.22.4))(@tiptap/pm@3.22.4))':
'@tiptap/extension-task-list@3.22.4(@tiptap/extension-list@3.22.4(@tiptap/core@3.22.4(@tiptap/pm@3.22.4))(@tiptap/pm@3.22.4))':
dependencies:
'@tiptap/extension-list': 3.22.5(@tiptap/core@3.22.4(@tiptap/pm@3.22.4))(@tiptap/pm@3.22.4)
'@tiptap/extension-list': 3.22.4(@tiptap/core@3.22.4(@tiptap/pm@3.22.4))(@tiptap/pm@3.22.4)
'@tiptap/extension-text@3.22.5(@tiptap/core@3.22.4(@tiptap/pm@3.22.4))':
dependencies:
@ -11306,6 +11514,11 @@ snapshots:
dependencies:
'@tiptap/core': 3.22.4(@tiptap/pm@3.22.4)
'@tiptap/extensions@3.22.4(@tiptap/core@3.22.4(@tiptap/pm@3.22.4))(@tiptap/pm@3.22.4)':
dependencies:
'@tiptap/core': 3.22.4(@tiptap/pm@3.22.4)
'@tiptap/pm': 3.22.4
'@tiptap/extensions@3.22.5(@tiptap/core@3.22.4(@tiptap/pm@3.22.4))(@tiptap/pm@3.22.4)':
dependencies:
'@tiptap/core': 3.22.4(@tiptap/pm@3.22.4)
@ -11319,12 +11532,12 @@ snapshots:
prosemirror-gapcursor: 1.4.0
prosemirror-history: 1.5.0
prosemirror-keymap: 1.2.3
prosemirror-model: 1.25.4
prosemirror-model: 1.25.7
prosemirror-schema-list: 1.5.1
prosemirror-state: 1.4.4
prosemirror-tables: 1.8.5
prosemirror-transform: 1.10.5
prosemirror-view: 1.41.4
prosemirror-transform: 1.12.0
prosemirror-view: 1.41.8
'@tiptap/react@3.22.4(@floating-ui/dom@1.7.4)(@tiptap/core@3.22.4(@tiptap/pm@3.22.4))(@tiptap/pm@3.22.4)(@types/react-dom@19.2.3(@types/react@19.2.7))(@types/react@19.2.7)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)':
dependencies:
@ -11939,6 +12152,8 @@ snapshots:
'@xmldom/xmldom@0.8.11': {}
'@xmldom/xmldom@0.9.10': {}
'@xtuc/ieee754@1.2.0': {}
'@xtuc/long@4.2.2': {}
@ -12763,6 +12978,10 @@ snapshots:
minimatch: 3.1.2
p-limit: 3.1.0
docxtemplater@3.68.7:
dependencies:
'@xmldom/xmldom': 0.9.10
dom-serializer@0.2.2:
dependencies:
domelementtype: 2.3.0
@ -15227,6 +15446,8 @@ snapshots:
pako@1.0.11: {}
pako@2.1.0: {}
papaparse@5.5.3: {}
parent-module@1.0.1:
@ -15324,6 +15545,10 @@ snapshots:
pify@4.0.1: {}
pizzip@3.2.0:
dependencies:
pako: 2.1.0
pkce-challenge@5.0.1: {}
pkg-types@1.3.1:
@ -15412,32 +15637,32 @@ snapshots:
prosemirror-changeset@2.3.1:
dependencies:
prosemirror-transform: 1.10.5
prosemirror-transform: 1.12.0
prosemirror-commands@1.7.1:
dependencies:
prosemirror-model: 1.25.4
prosemirror-model: 1.25.7
prosemirror-state: 1.4.4
prosemirror-transform: 1.10.5
prosemirror-transform: 1.12.0
prosemirror-dropcursor@1.8.2:
dependencies:
prosemirror-state: 1.4.4
prosemirror-transform: 1.10.5
prosemirror-view: 1.41.4
prosemirror-transform: 1.12.0
prosemirror-view: 1.41.8
prosemirror-gapcursor@1.4.0:
dependencies:
prosemirror-keymap: 1.2.3
prosemirror-model: 1.25.4
prosemirror-model: 1.25.7
prosemirror-state: 1.4.4
prosemirror-view: 1.41.4
prosemirror-view: 1.41.8
prosemirror-history@1.5.0:
dependencies:
prosemirror-state: 1.4.4
prosemirror-transform: 1.10.5
prosemirror-view: 1.41.4
prosemirror-transform: 1.12.0
prosemirror-view: 1.41.8
rope-sequence: 1.3.4
prosemirror-keymap@1.2.3:
@ -15449,41 +15674,41 @@ snapshots:
dependencies:
'@types/markdown-it': 14.1.2
markdown-it: 14.1.0
prosemirror-model: 1.25.4
prosemirror-model: 1.25.7
prosemirror-model@1.25.4:
prosemirror-model@1.25.7:
dependencies:
orderedmap: 2.1.1
prosemirror-schema-list@1.5.1:
dependencies:
prosemirror-model: 1.25.4
prosemirror-model: 1.25.7
prosemirror-state: 1.4.4
prosemirror-transform: 1.10.5
prosemirror-transform: 1.12.0
prosemirror-state@1.4.4:
dependencies:
prosemirror-model: 1.25.4
prosemirror-transform: 1.10.5
prosemirror-view: 1.41.4
prosemirror-model: 1.25.7
prosemirror-transform: 1.12.0
prosemirror-view: 1.41.8
prosemirror-tables@1.8.5:
dependencies:
prosemirror-keymap: 1.2.3
prosemirror-model: 1.25.4
prosemirror-model: 1.25.7
prosemirror-state: 1.4.4
prosemirror-transform: 1.10.5
prosemirror-view: 1.41.4
prosemirror-transform: 1.12.0
prosemirror-view: 1.41.8
prosemirror-transform@1.10.5:
prosemirror-transform@1.12.0:
dependencies:
prosemirror-model: 1.25.4
prosemirror-model: 1.25.7
prosemirror-view@1.41.4:
prosemirror-view@1.41.8:
dependencies:
prosemirror-model: 1.25.4
prosemirror-model: 1.25.7
prosemirror-state: 1.4.4
prosemirror-transform: 1.10.5
prosemirror-transform: 1.12.0
protobufjs@7.5.4:
dependencies:
@ -15986,6 +16211,8 @@ snapshots:
safer-buffer@2.1.2: {}
sax@1.6.0: {}
scheduler@0.25.0-rc-603e6108-20241029: {}
scheduler@0.27.0: {}
@ -16884,6 +17111,10 @@ snapshots:
wmf: 1.0.2
word: 0.3.0
xml-js@1.6.11:
dependencies:
sax: 1.6.0
xmlbuilder2@2.1.2:
dependencies:
'@oozcitak/dom': 1.15.5