a very good jj gui
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

Use CodeMirror for inline commit-description editing

Add an InlineEditor wrapper around CodeMirror 6 with two visual
variants:
- transparent: looks like plain text, border on focus, saves on blur
if content changed
- bordered: autoFocus + select-all on mount, modal-style border, used
for compact pop-up editing

Both bind Cmd/Ctrl+Enter to save and Escape to revert+blur. The editor
stops keydown bubbling so the global keymap doesn't fire while the user
is typing.

Used in two places:
- RevisionRow's inline edit mode (replaces the textarea)
- RevisionHeader in the diff panel (replaces the chevron-expand readonly
block; the field is now always editable except for immutable
revisions)

Plumb onDescribe through DiffPanel/PrerenderedDiffPanel so the header
can write back. RevisionRow keeps a lastSavedDescRef so the saved text
shows immediately, ahead of the collection update round-trip.

useKeyboard's input-focus guard now also skips contenteditable elements
(CodeMirror uses contenteditable), factored into a shared
isEditableElementFocused() helper.

Adds @codemirror/{commands,language,state,view} and codemirror.

+314 -86
+5
apps/desktop/package.json
··· 16 16 }, 17 17 "dependencies": { 18 18 "@base-ui/react": "^1.0.0", 19 + "@codemirror/commands": "^6.10.2", 20 + "@codemirror/language": "^6.12.1", 21 + "@codemirror/state": "^6.5.4", 22 + "@codemirror/view": "^6.39.14", 19 23 "@effect-atom/atom": "^0.4.11", 20 24 "@effect-atom/atom-react": "^0.4.4", 21 25 "@fontsource-variable/jetbrains-mono": "^5.2.8", ··· 37 41 "class-variance-authority": "^0.7.1", 38 42 "clsx": "^2.1.1", 39 43 "cmdk": "^1.1.1", 44 + "codemirror": "^6.0.2", 40 45 "effect": "^3.19.13", 41 46 "lucide-react": "^0.562.0", 42 47 "react": "19",
+1
apps/desktop/src/components/AppShell.tsx
··· 830 830 revisions={orderedRevisions} 831 831 selectedChangeId={debouncedChangeId} 832 832 revisionsPanelRef={revisionsPanelRef} 833 + onDescribe={handleDescribe} 833 834 /> 834 835 </Profiler> 835 836 </aside>
+16 -3
apps/desktop/src/components/DiffPanel.tsx
··· 24 24 changeId: string | null; 25 25 revision: Revision | null; 26 26 revisionsPanelRef: RefObject<HTMLElement | null>; 27 + onDescribe?: (changeId: string, description: string) => void; 27 28 } 28 29 29 30 interface PrerenderedDiffPanelProps { ··· 31 32 revisions: Revision[]; 32 33 selectedChangeId: string | null; 33 34 revisionsPanelRef: RefObject<HTMLElement | null>; 35 + onDescribe?: (changeId: string, description: string) => void; 34 36 } 35 37 36 38 export const PrerenderedDiffPanel = forwardRef<HTMLDivElement, PrerenderedDiffPanelProps>( 37 - function PrerenderedDiffPanel({ repoPath, revisions, selectedChangeId, revisionsPanelRef }, ref) { 39 + function PrerenderedDiffPanel({ 40 + repoPath, 41 + revisions, 42 + selectedChangeId, 43 + revisionsPanelRef, 44 + onDescribe, 45 + }, ref) { 38 46 const selectedRevision = selectedChangeId 39 47 ? (revisions.find((r) => r.change_id === selectedChangeId) ?? null) 40 48 : null; ··· 46 54 changeId={selectedChangeId} 47 55 revision={selectedRevision} 48 56 revisionsPanelRef={revisionsPanelRef} 57 + onDescribe={onDescribe} 49 58 /> 50 59 ); 51 60 }, ··· 208 217 209 218 export const DiffPanel = React.memo( 210 219 forwardRef<HTMLDivElement, DiffPanelProps>(function DiffPanel( 211 - { repoPath, changeId, revision, revisionsPanelRef }, 220 + { repoPath, changeId, revision, revisionsPanelRef, onDescribe }, 212 221 ref, 213 222 ) { 214 223 // Use changeId directly - data is prefetched and cached, so no need to defer ··· 518 527 {/* Revision header */} 519 528 {revision && ( 520 529 <div className="px-4 pt-2 pb-2 shrink-0"> 521 - <RevisionHeader revision={revision} conflictPaths={conflictPaths} /> 530 + <RevisionHeader 531 + revision={revision} 532 + conflictPaths={conflictPaths} 533 + onDescribe={onDescribe} 534 + /> 522 535 </div> 523 536 )} 524 537
+210
apps/desktop/src/components/InlineEditor.tsx
··· 1 + import { EditorState } from "@codemirror/state"; 2 + import { EditorView, keymap, placeholder as cmPlaceholder } from "@codemirror/view"; 3 + import { defaultKeymap, history, historyKeymap } from "@codemirror/commands"; 4 + import { useEffect, useRef } from "react"; 5 + 6 + interface InlineEditorProps { 7 + /** Initial document content. Also used to detect external changes. */ 8 + value: string; 9 + /** Called with the current text when the user saves (Cmd+Enter) or blurs after making changes. */ 10 + onSave: (value: string) => void; 11 + placeholder?: string; 12 + /** If true, the editor is not editable (for immutable revisions). */ 13 + readOnly?: boolean; 14 + /** 15 + * Visual mode: 16 + * - `"transparent"` (default): no border/background — looks like plain text. 17 + * - `"bordered"`: always shows a border. 18 + */ 19 + variant?: "transparent" | "bordered"; 20 + /** If true, auto-focus and select all text on mount. */ 21 + autoFocus?: boolean; 22 + /** If true, constrains height with overflow scroll (for compact contexts). */ 23 + compact?: boolean; 24 + /** Called when Escape is pressed. If not provided, Escape reverts changes and blurs. */ 25 + onCancel?: () => void; 26 + className?: string; 27 + } 28 + 29 + /** 30 + * Theme that inherits the app's CSS variables for seamless integration. 31 + * Strips all default CodeMirror chrome so it feels like native inline editing. 32 + */ 33 + const baseTheme = EditorView.theme({ 34 + "&": { 35 + fontSize: "14px", 36 + lineHeight: "20px", 37 + fontFamily: "var(--font-sans)", 38 + }, 39 + "&.cm-focused": { 40 + outline: "none", 41 + }, 42 + ".cm-scroller": { 43 + fontFamily: "inherit", 44 + lineHeight: "inherit", 45 + }, 46 + ".cm-content": { 47 + padding: "0", 48 + caretColor: "var(--foreground)", 49 + }, 50 + ".cm-line": { 51 + padding: "0", 52 + }, 53 + ".cm-cursor": { 54 + borderLeftColor: "var(--foreground)", 55 + }, 56 + "&.cm-focused .cm-selectionBackground, .cm-selectionBackground": { 57 + backgroundColor: "color-mix(in oklch, var(--accent) 30%, transparent)", 58 + }, 59 + ".cm-placeholder": { 60 + color: "var(--muted-foreground)", 61 + fontStyle: "italic", 62 + }, 63 + }); 64 + 65 + /** 66 + * A minimal CodeMirror 6 editor for inline plain-text editing (commit messages, etc). 67 + * 68 + * Two modes: 69 + * - **transparent** (default): looks like plain text, border appears on focus, 70 + * saves on blur if content changed. Ideal for always-visible document editing. 71 + * - **bordered**: always has a visible border, designed for modal/pop-up editing 72 + * (e.g. the revision graph row). Auto-focuses and selects all on mount. 73 + * 74 + * Common: Cmd/Ctrl+Enter → save, Escape → revert + blur, undo/redo, line wrapping. 75 + */ 76 + export function InlineEditor({ 77 + value, 78 + onSave, 79 + placeholder = "", 80 + readOnly = false, 81 + variant = "transparent", 82 + autoFocus = false, 83 + compact = false, 84 + onCancel, 85 + className = "", 86 + }: InlineEditorProps) { 87 + const containerRef = useRef<HTMLDivElement>(null); 88 + const viewRef = useRef<EditorView | null>(null); 89 + // Track the "committed" value so we can detect real changes and revert on Escape 90 + const committedValueRef = useRef(value); 91 + 92 + // Stable refs for callbacks 93 + const onSaveRef = useRef(onSave); 94 + const onCancelRef = useRef(onCancel); 95 + onSaveRef.current = onSave; 96 + onCancelRef.current = onCancel; 97 + 98 + // Sync external value changes into the editor (e.g. after a save round-trips through the backend) 99 + useEffect(() => { 100 + const view = viewRef.current; 101 + if (!view) return; 102 + const currentDoc = view.state.doc.toString(); 103 + if (value !== committedValueRef.current && value !== currentDoc) { 104 + committedValueRef.current = value; 105 + view.dispatch({ 106 + changes: { from: 0, to: view.state.doc.length, insert: value }, 107 + }); 108 + } 109 + }, [value]); 110 + 111 + useEffect(() => { 112 + if (!containerRef.current) return; 113 + 114 + const customKeymap = keymap.of([ 115 + { 116 + key: "Mod-Enter", 117 + run: (view) => { 118 + const text = view.state.doc.toString(); 119 + if (text !== committedValueRef.current) { 120 + committedValueRef.current = text; 121 + onSaveRef.current(text); 122 + } 123 + // Blur after explicit save 124 + view.contentDOM.blur(); 125 + return true; 126 + }, 127 + }, 128 + { 129 + key: "Escape", 130 + run: (view) => { 131 + if (onCancelRef.current) { 132 + onCancelRef.current(); 133 + } else { 134 + // Revert to committed value and blur 135 + const committed = committedValueRef.current; 136 + view.dispatch({ 137 + changes: { from: 0, to: view.state.doc.length, insert: committed }, 138 + }); 139 + view.contentDOM.blur(); 140 + } 141 + return true; 142 + }, 143 + }, 144 + ]); 145 + 146 + // Save on blur if content changed, stop keyboard events from reaching parent handlers 147 + const eventHandlers = EditorView.domEventHandlers({ 148 + blur: (_, view) => { 149 + const text = view.state.doc.toString(); 150 + if (text !== committedValueRef.current) { 151 + committedValueRef.current = text; 152 + onSaveRef.current(text); 153 + } 154 + }, 155 + keydown: (event) => { 156 + event.stopPropagation(); 157 + }, 158 + }); 159 + 160 + const state = EditorState.create({ 161 + doc: value, 162 + extensions: [ 163 + customKeymap, 164 + history(), 165 + keymap.of([...defaultKeymap, ...historyKeymap]), 166 + baseTheme, 167 + EditorView.editable.of(!readOnly), 168 + ...(placeholder ? [cmPlaceholder(placeholder)] : []), 169 + eventHandlers, 170 + ], 171 + }); 172 + 173 + const view = new EditorView({ 174 + state, 175 + parent: containerRef.current, 176 + }); 177 + 178 + viewRef.current = view; 179 + 180 + if (autoFocus) { 181 + requestAnimationFrame(() => { 182 + view.focus(); 183 + view.dispatch({ 184 + selection: { anchor: 0, head: view.state.doc.length }, 185 + }); 186 + }); 187 + } 188 + 189 + return () => { 190 + view.destroy(); 191 + viewRef.current = null; 192 + }; 193 + // Only create once on mount 194 + // eslint-disable-next-line react-hooks/exhaustive-deps 195 + }, []); 196 + 197 + const variantClasses = 198 + variant === "transparent" 199 + ? "" 200 + : "rounded border border-border bg-background focus-within:ring-1 focus-within:ring-ring"; 201 + 202 + return ( 203 + <div 204 + ref={containerRef} 205 + className={`${variantClasses} ${ 206 + compact ? "max-h-20 overflow-auto" : "" 207 + } ${className}`} 208 + /> 209 + ); 210 + }
+11 -35
apps/desktop/src/components/diff/RevisionHeader.tsx
··· 1 - import { ChevronDownIcon, ChevronRightIcon } from "lucide-react"; 2 - import { useState } from "react"; 1 + import { InlineEditor } from "@/components/InlineEditor"; 3 2 import type { Revision } from "@/tauri-commands"; 4 3 5 4 interface RevisionHeaderProps { 6 5 revision: Revision; 7 6 conflictPaths?: string[]; 7 + onDescribe?: (changeId: string, description: string) => void; 8 8 } 9 9 10 - export function RevisionHeader({ revision, conflictPaths = [] }: RevisionHeaderProps) { 10 + export function RevisionHeader({ revision, conflictPaths = [], onDescribe }: RevisionHeaderProps) { 11 11 const commitIdShort = revision.commit_id.substring(0, 12); 12 - const [isExpanded, setIsExpanded] = useState(false); 13 - 14 - // Split description into title (first line) and body (rest) 15 - const descriptionLines = revision.description?.split("\n") ?? []; 16 - const title = descriptionLines[0] ?? ""; 17 - const body = descriptionLines.slice(1).join("\n").trim(); 18 - const hasBody = body.length > 0; 19 12 20 13 return ( 21 14 <div> ··· 48 41 </div> 49 42 </div> 50 43 )} 51 - {title && ( 52 - <div className="mt-2 pt-2"> 53 - <div className="flex items-start gap-1"> 54 - {hasBody && ( 55 - <button 56 - type="button" 57 - onClick={() => setIsExpanded(!isExpanded)} 58 - className="text-muted-foreground hover:text-foreground shrink-0 transition-colors mt-0.5" 59 - > 60 - {isExpanded ? ( 61 - <ChevronDownIcon className="size-4" /> 62 - ) : ( 63 - <ChevronRightIcon className="size-4" /> 64 - )} 65 - </button> 66 - )} 67 - <span className="text-sm font-semibold text-foreground font-sans">{title}</span> 68 - </div> 69 - {hasBody && isExpanded && ( 70 - <pre className="text-xs text-muted-foreground whitespace-pre-wrap font-sans mt-2 ml-5"> 71 - {body} 72 - </pre> 73 - )} 74 - </div> 75 - )} 44 + <div className="mt-2 pt-2"> 45 + <InlineEditor 46 + value={revision.description || ""} 47 + onSave={(desc) => onDescribe?.(revision.change_id, desc)} 48 + placeholder="Enter commit description…" 49 + readOnly={revision.is_immutable} 50 + /> 51 + </div> 76 52 </div> 77 53 </div> 78 54 );
+21 -32
apps/desktop/src/components/revision-graph/RevisionRow.tsx
··· 1 1 import { useAtom } from "@effect-atom/atom-react"; 2 - import { useEffect, useRef, useState } from "react"; 2 + import { useRef, useState } from "react"; 3 3 import { draggingBookmarkAtom } from "@/atoms"; 4 4 import { getRevisionKey } from "@/db"; 5 5 import { Badge } from "@/components/ui/badge"; 6 + import { InlineEditor } from "@/components/InlineEditor"; 6 7 import type { Revision } from "@/tauri-commands"; 7 8 import { BookmarkTag } from "./BookmarkTag"; 8 9 import { ROW_HEIGHT, LANE_PADDING, LANE_WIDTH, NODE_RADIUS, laneToX, laneColor } from "./constants"; ··· 54 55 hasFocus, 55 56 }: RevisionRowProps) { 56 57 const revisionKey = getRevisionKey(revision); 57 - const firstLine = revision.description.split("\n")[0] || "(no description)"; 58 - const [draftDescription, setDraftDescription] = useState(revision.description); 58 + // Track the last description saved from the editor so we can display it 59 + // immediately when exiting edit mode, without waiting for the collection update. 60 + const lastSavedDescRef = useRef<string | null>(null); 61 + // Clear the override once the prop catches up 62 + if (lastSavedDescRef.current !== null && revision.description === lastSavedDescRef.current) { 63 + lastSavedDescRef.current = null; 64 + } 65 + const displayDescription = lastSavedDescRef.current ?? revision.description; 66 + const firstLine = displayDescription.split("\n")[0] || "(no description)"; 59 67 const [isDragOver, setIsDragOver] = useState(false); 60 68 const [showDropPlaceholder, setShowDropPlaceholder] = useState(false); 61 69 const dragOverTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null); 62 70 const dragEnterCountRef = useRef(0); 63 - const textareaRef = useRef<HTMLTextAreaElement | null>(null); 64 71 const [draggingBookmark] = useAtom(draggingBookmarkAtom); 65 72 66 73 // Calculate the node position area - leaves space for graph edges on the left ··· 69 76 const color = laneColor(lane); 70 77 71 78 const nodeSize = revision.is_working_copy ? NODE_RADIUS * 2 + 14 : NODE_RADIUS * 2 + 8; 72 - 73 - useEffect(() => { 74 - if (!isEditing) return; 75 - setDraftDescription(revision.description); 76 - requestAnimationFrame(() => { 77 - const textarea = textareaRef.current; 78 - if (!textarea) return; 79 - textarea.focus({ preventScroll: true }); 80 - textarea.select(); 81 - }); 82 - }, [isEditing, revision.description]); 83 79 84 80 return ( 85 81 // biome-ignore lint/a11y/useSemanticElements: Complex styling requires div ··· 249 245 </span> 250 246 </div> 251 247 {isEditing ? ( 252 - <textarea 253 - ref={textareaRef} 254 - value={draftDescription} 255 - onChange={(e) => setDraftDescription(e.target.value)} 256 - onClick={(e) => e.stopPropagation()} 257 - onKeyDown={(e) => { 258 - e.stopPropagation(); 259 - if (e.key === "Escape") { 260 - e.preventDefault(); 261 - onCancelDescribe(); 262 - return; 263 - } 264 - if ((e.metaKey || e.ctrlKey) && e.key === "Enter") { 265 - e.preventDefault(); 266 - onDescribe(revisionKey, draftDescription); 267 - } 248 + <InlineEditor 249 + value={revision.description} 250 + onSave={(desc) => { 251 + lastSavedDescRef.current = desc; 252 + onDescribe(revisionKey, desc); 268 253 }} 269 - className="mt-1 w-full min-h-16 rounded border border-border bg-background px-2 py-1 text-sm leading-5 resize-none focus:outline-none focus:ring-1 focus:ring-ring" 254 + onCancel={onCancelDescribe} 255 + placeholder="Enter commit description…" 256 + autoFocus 257 + compact 258 + className="mt-1" 270 259 /> 271 260 ) : ( 272 261 <div className="text-sm mt-1 truncate">{firstLine}</div>
+15 -16
apps/desktop/src/hooks/useKeyboard.ts
··· 2 2 import { getRevisionKey } from "@/db"; 3 3 import type { Revision } from "@/tauri-commands"; 4 4 5 + /** Returns true if focus is inside a text-editable element (input, textarea, or contenteditable). */ 6 + function isEditableElementFocused(): boolean { 7 + const el = document.activeElement; 8 + if (!el) return false; 9 + const tag = el.tagName; 10 + if (tag === "INPUT" || tag === "TEXTAREA") return true; 11 + if (el.getAttribute("contenteditable") === "true") return true; 12 + if (el.closest?.("[contenteditable=true]")) return true; 13 + return false; 14 + } 15 + 5 16 interface ScrollOptions { 6 17 align?: "auto" | "center"; 7 18 smooth?: boolean; ··· 74 85 75 86 useEffect(() => { 76 87 function handleKeyDown(event: KeyboardEvent) { 77 - const activeElement = document.activeElement; 78 - if (activeElement?.tagName === "INPUT" || activeElement?.tagName === "TEXTAREA") { 79 - return; 80 - } 88 + if (isEditableElementFocused()) return; 81 89 82 90 const revisions = orderedRevisionsRef.current; 83 91 const revisionKey = selectedChangeIdRef.current; ··· 198 206 if (!enabled) return; 199 207 200 208 function handleKeyDown(event: KeyboardEvent) { 201 - // Don't handle if input/textarea is focused (unless explicitly ignored) 202 - if (!ignoreInputFocus) { 203 - const activeElement = document.activeElement; 204 - if (activeElement?.tagName === "INPUT" || activeElement?.tagName === "TEXTAREA") { 205 - return; 206 - } 207 - } 209 + // Don't handle if an editable element is focused (unless explicitly ignored) 210 + if (!ignoreInputFocus && isEditableElementFocused()) return; 208 211 209 212 // Check if the key matches 210 213 if (event.key !== key) return; ··· 267 270 268 271 function handleKeyDown(event: KeyboardEvent) { 269 272 if (MODIFIER_KEYS.has(event.key)) return; 270 - 271 - const activeElement = document.activeElement; 272 - if (activeElement?.tagName === "INPUT" || activeElement?.tagName === "TEXTAREA") { 273 - return; 274 - } 273 + if (isEditableElementFocused()) return; 275 274 276 275 const now = Date.now(); 277 276 const buffer = bufferRef.current;
+35
bun.lock
··· 19 19 "version": "0.1.0", 20 20 "dependencies": { 21 21 "@base-ui/react": "^1.0.0", 22 + "@codemirror/commands": "^6.10.2", 23 + "@codemirror/language": "^6.12.1", 24 + "@codemirror/state": "^6.5.4", 25 + "@codemirror/view": "^6.39.14", 22 26 "@effect-atom/atom": "^0.4.11", 23 27 "@effect-atom/atom-react": "^0.4.4", 24 28 "@fontsource-variable/jetbrains-mono": "^5.2.8", ··· 40 44 "class-variance-authority": "^0.7.1", 41 45 "clsx": "^2.1.1", 42 46 "cmdk": "^1.1.1", 47 + "codemirror": "^6.0.2", 43 48 "effect": "^3.19.13", 44 49 "lucide-react": "^0.562.0", 45 50 "react": "19", ··· 203 208 204 209 "@biomejs/cli-win32-x64": ["@biomejs/cli-win32-x64@2.3.10", "", { "os": "win32", "cpu": "x64" }, "sha512-pHEFgq7dUEsKnqG9mx9bXihxGI49X+ar+UBrEIj3Wqj3UCZp1rNgV+OoyjFgcXsjCWpuEAF4VJdkZr3TrWdCbQ=="], 205 210 211 + "@codemirror/autocomplete": ["@codemirror/autocomplete@6.20.0", "", { "dependencies": { "@codemirror/language": "^6.0.0", "@codemirror/state": "^6.0.0", "@codemirror/view": "^6.17.0", "@lezer/common": "^1.0.0" } }, "sha512-bOwvTOIJcG5FVo5gUUupiwYh8MioPLQ4UcqbcRf7UQ98X90tCa9E1kZ3Z7tqwpZxYyOvh1YTYbmZE9RTfTp5hg=="], 212 + 213 + "@codemirror/commands": ["@codemirror/commands@6.10.2", "", { "dependencies": { "@codemirror/language": "^6.0.0", "@codemirror/state": "^6.4.0", "@codemirror/view": "^6.27.0", "@lezer/common": "^1.1.0" } }, "sha512-vvX1fsih9HledO1c9zdotZYUZnE4xV0m6i3m25s5DIfXofuprk6cRcLUZvSk3CASUbwjQX21tOGbkY2BH8TpnQ=="], 214 + 215 + "@codemirror/language": ["@codemirror/language@6.12.1", "", { "dependencies": { "@codemirror/state": "^6.0.0", "@codemirror/view": "^6.23.0", "@lezer/common": "^1.5.0", "@lezer/highlight": "^1.0.0", "@lezer/lr": "^1.0.0", "style-mod": "^4.0.0" } }, "sha512-Fa6xkSiuGKc8XC8Cn96T+TQHYj4ZZ7RdFmXA3i9xe/3hLHfwPZdM+dqfX0Cp0zQklBKhVD8Yzc8LS45rkqcwpQ=="], 216 + 217 + "@codemirror/lint": ["@codemirror/lint@6.9.4", "", { "dependencies": { "@codemirror/state": "^6.0.0", "@codemirror/view": "^6.35.0", "crelt": "^1.0.5" } }, "sha512-ABc9vJ8DEmvOWuH26P3i8FpMWPQkduD9Rvba5iwb6O3hxASgclm3T3krGo8NASXkHCidz6b++LWlzWIUfEPSWw=="], 218 + 219 + "@codemirror/search": ["@codemirror/search@6.6.0", "", { "dependencies": { "@codemirror/state": "^6.0.0", "@codemirror/view": "^6.37.0", "crelt": "^1.0.5" } }, "sha512-koFuNXcDvyyotWcgOnZGmY7LZqEOXZaaxD/j6n18TCLx2/9HieZJ5H6hs1g8FiRxBD0DNfs0nXn17g872RmYdw=="], 220 + 221 + "@codemirror/state": ["@codemirror/state@6.5.4", "", { "dependencies": { "@marijn/find-cluster-break": "^1.0.0" } }, "sha512-8y7xqG/hpB53l25CIoit9/ngxdfoG+fx+V3SHBrinnhOtLvKHRyAJJuHzkWrR4YXXLX8eXBsejgAAxHUOdW1yw=="], 222 + 223 + "@codemirror/view": ["@codemirror/view@6.39.14", "", { "dependencies": { "@codemirror/state": "^6.5.0", "crelt": "^1.0.6", "style-mod": "^4.1.0", "w3c-keyname": "^2.2.4" } }, "sha512-WJcvgHm/6Q7dvGT0YFv/6PSkoc36QlR0VCESS6x9tGsnF1lWLmmYxOgX3HH6v8fo6AvSLgpcs+H0Olre6MKXlg=="], 224 + 206 225 "@csstools/color-helpers": ["@csstools/color-helpers@6.0.1", "", {}, "sha512-NmXRccUJMk2AWA5A7e5a//3bCIMyOu2hAtdRYrhPPHjDxINuCwX1w6rnIZ4xjLcp0ayv6h8Pc3X0eJUGiAAXHQ=="], 207 226 208 227 "@csstools/css-calc": ["@csstools/css-calc@3.0.1", "", { "peerDependencies": { "@csstools/css-parser-algorithms": "^4.0.0", "@csstools/css-tokenizer": "^4.0.0" } }, "sha512-bsDKIP6f4ta2DO9t+rAbSSwv4EMESXy5ZIvzQl1afmD6Z1XHkVu9ijcG9QR/qSgQS1dVa+RaQ/MfQ7FIB/Dn1Q=="], ··· 318 337 "@jridgewell/sourcemap-codec": ["@jridgewell/sourcemap-codec@1.5.5", "", {}, "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og=="], 319 338 320 339 "@jridgewell/trace-mapping": ["@jridgewell/trace-mapping@0.3.31", "", { "dependencies": { "@jridgewell/resolve-uri": "^3.1.0", "@jridgewell/sourcemap-codec": "^1.4.14" } }, "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw=="], 340 + 341 + "@lezer/common": ["@lezer/common@1.5.1", "", {}, "sha512-6YRVG9vBkaY7p1IVxL4s44n5nUnaNnGM2/AckNgYOnxTG2kWh1vR8BMxPseWPjRNpb5VtXnMpeYAEAADoRV1Iw=="], 342 + 343 + "@lezer/highlight": ["@lezer/highlight@1.2.3", "", { "dependencies": { "@lezer/common": "^1.3.0" } }, "sha512-qXdH7UqTvGfdVBINrgKhDsVTJTxactNNxLk7+UMwZhU13lMHaOBlJe9Vqp907ya56Y3+ed2tlqzys7jDkTmW0g=="], 344 + 345 + "@lezer/lr": ["@lezer/lr@1.4.8", "", { "dependencies": { "@lezer/common": "^1.0.0" } }, "sha512-bPWa0Pgx69ylNlMlPvBPryqeLYQjyJjqPx+Aupm5zydLIF3NE+6MMLT8Yi23Bd9cif9VS00aUebn+6fDIGBcDA=="], 346 + 347 + "@marijn/find-cluster-break": ["@marijn/find-cluster-break@1.0.2", "", {}, "sha512-l0h88YhZFyKdXIFNfSWpyjStDjGHwZ/U7iobcK1cQQD8sejsONdQtTVU+1wVN1PBw40PiiHB1vA5S7VTfQiP9g=="], 321 348 322 349 "@modelcontextprotocol/sdk": ["@modelcontextprotocol/sdk@1.25.1", "", { "dependencies": { "@hono/node-server": "^1.19.7", "ajv": "^8.17.1", "ajv-formats": "^3.0.1", "content-type": "^1.0.5", "cors": "^2.8.5", "cross-spawn": "^7.0.5", "eventsource": "^3.0.2", "eventsource-parser": "^3.0.0", "express": "^5.0.1", "express-rate-limit": "^7.5.0", "jose": "^6.1.1", "json-schema-typed": "^8.0.2", "pkce-challenge": "^5.0.0", "raw-body": "^3.0.0", "zod": "^3.25 || ^4.0", "zod-to-json-schema": "^3.25.0" }, "peerDependencies": { "@cfworker/json-schema": "^4.1.1" }, "optionalPeers": ["@cfworker/json-schema"] }, "sha512-yO28oVFFC7EBoiKdAn+VqRm+plcfv4v0xp6osG/VsCB0NlPZWi87ajbCZZ8f/RvOFLEu7//rSRmuZZ7lMoe3gQ=="], 323 350 ··· 717 744 718 745 "code-block-writer": ["code-block-writer@13.0.3", "", {}, "sha512-Oofo0pq3IKnsFtuHqSF7TqBfr71aeyZDVJ0HpmqB7FBM2qEigL0iPONSCZSO9pE9dZTAxANe5XHG9Uy0YMv8cg=="], 719 746 747 + "codemirror": ["codemirror@6.0.2", "", { "dependencies": { "@codemirror/autocomplete": "^6.0.0", "@codemirror/commands": "^6.0.0", "@codemirror/language": "^6.0.0", "@codemirror/lint": "^6.0.0", "@codemirror/search": "^6.0.0", "@codemirror/state": "^6.0.0", "@codemirror/view": "^6.0.0" } }, "sha512-VhydHotNW5w1UGK0Qj96BwSk/Zqbp9WbnyK2W/eVMv4QyF41INRGpjUhFJY7/uDNuudSc33a/PKr4iDqRduvHw=="], 748 + 720 749 "color-convert": ["color-convert@2.0.1", "", { "dependencies": { "color-name": "~1.1.4" } }, "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ=="], 721 750 722 751 "color-name": ["color-name@1.1.4", "", {}, "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA=="], ··· 744 773 "cors": ["cors@2.8.5", "", { "dependencies": { "object-assign": "^4", "vary": "^1" } }, "sha512-KIHbLJqu73RGr/hnbrO9uBeixNGuvSQjul/jdFvS/KFSIH1hWVd1ng7zOHx+YrEfInLG7q4n6GHQ9cDtxv/P6g=="], 745 774 746 775 "cosmiconfig": ["cosmiconfig@9.0.0", "", { "dependencies": { "env-paths": "^2.2.1", "import-fresh": "^3.3.0", "js-yaml": "^4.1.0", "parse-json": "^5.2.0" }, "peerDependencies": { "typescript": ">=4.9.5" }, "optionalPeers": ["typescript"] }, "sha512-itvL5h8RETACmOTFc4UfIyB2RfEHi71Ax6E/PivVxq9NseKbOWpeyHEOIbmAw1rs8Ak0VursQNww7lf7YtUwzg=="], 776 + 777 + "crelt": ["crelt@1.0.6", "", {}, "sha512-VQ2MBenTq1fWZUH9DJNGti7kKv6EeAuYr3cLwxUWhIu1baTaXh4Ib5W2CqHVqib4/MqbYGJqiL3Zb8GJZr3l4g=="], 747 778 748 779 "cross-spawn": ["cross-spawn@7.0.6", "", { "dependencies": { "path-key": "^3.1.0", "shebang-command": "^2.0.0", "which": "^2.0.1" } }, "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA=="], 749 780 ··· 1335 1366 1336 1367 "strip-final-newline": ["strip-final-newline@4.0.0", "", {}, "sha512-aulFJcD6YK8V1G7iRB5tigAP4TsHBZZrOV8pjV++zdUwmeV8uzbY7yn6h9MswN62adStNZFuCIx4haBnRuMDaw=="], 1337 1368 1369 + "style-mod": ["style-mod@4.1.3", "", {}, "sha512-i/n8VsZydrugj3Iuzll8+x/00GH2vnYsk1eomD8QiRrSAeW6ItbCQDtfXCeJHd0iwiNagqjQkvpvREEPtW3IoQ=="], 1370 + 1338 1371 "sucrase": ["sucrase@3.35.1", "", { "dependencies": { "@jridgewell/gen-mapping": "^0.3.2", "commander": "^4.0.0", "lines-and-columns": "^1.1.6", "mz": "^2.7.0", "pirates": "^4.0.1", "tinyglobby": "^0.2.11", "ts-interface-checker": "^0.1.9" }, "bin": { "sucrase": "bin/sucrase", "sucrase-node": "bin/sucrase-node" } }, "sha512-DhuTmvZWux4H1UOnWMB3sk0sbaCVOoQZjv8u1rDoTV0HTdGem9hkAZtl4JZy8P2z4Bg0nT+YMeOFyVr4zcG5Tw=="], 1339 1372 1340 1373 "symbol-tree": ["symbol-tree@3.2.4", "", {}, "sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw=="], ··· 1448 1481 "vitefu": ["vitefu@1.1.1", "", { "peerDependencies": { "vite": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0-beta.0" }, "optionalPeers": ["vite"] }, "sha512-B/Fegf3i8zh0yFbpzZ21amWzHmuNlLlmJT6n7bu5e+pCHUKQIfXSYokrqOBGEMMe9UG2sostKQF9mml/vYaWJQ=="], 1449 1482 1450 1483 "vitest": ["vitest@4.0.18", "", { "dependencies": { "@vitest/expect": "4.0.18", "@vitest/mocker": "4.0.18", "@vitest/pretty-format": "4.0.18", "@vitest/runner": "4.0.18", "@vitest/snapshot": "4.0.18", "@vitest/spy": "4.0.18", "@vitest/utils": "4.0.18", "es-module-lexer": "^1.7.0", "expect-type": "^1.2.2", "magic-string": "^0.30.21", "obug": "^2.1.1", "pathe": "^2.0.3", "picomatch": "^4.0.3", "std-env": "^3.10.0", "tinybench": "^2.9.0", "tinyexec": "^1.0.2", "tinyglobby": "^0.2.15", "tinyrainbow": "^3.0.3", "vite": "^6.0.0 || ^7.0.0", "why-is-node-running": "^2.3.0" }, "peerDependencies": { "@edge-runtime/vm": "*", "@opentelemetry/api": "^1.9.0", "@types/node": "^20.0.0 || ^22.0.0 || >=24.0.0", "@vitest/browser-playwright": "4.0.18", "@vitest/browser-preview": "4.0.18", "@vitest/browser-webdriverio": "4.0.18", "@vitest/ui": "4.0.18", "happy-dom": "*", "jsdom": "*" }, "optionalPeers": ["@edge-runtime/vm", "@opentelemetry/api", "@types/node", "@vitest/browser-playwright", "@vitest/browser-preview", "@vitest/browser-webdriverio", "@vitest/ui", "happy-dom", "jsdom"], "bin": { "vitest": "vitest.mjs" } }, "sha512-hOQuK7h0FGKgBAas7v0mSAsnvrIgAvWmRFjmzpJ7SwFHH3g1k2u37JtYwOwmEKhK6ZO3v9ggDBBm0La1LCK4uQ=="], 1484 + 1485 + "w3c-keyname": ["w3c-keyname@2.2.8", "", {}, "sha512-dpojBhNsCNN7T82Tm7k26A6G9ML3NkhDsnw9n/eoxSRlVBB4CEtIQ/KTCLI2Fwf3ataSXRhYFkQi3SlnFwPvPQ=="], 1451 1486 1452 1487 "w3c-xmlserializer": ["w3c-xmlserializer@5.0.0", "", { "dependencies": { "xml-name-validator": "^5.0.0" } }, "sha512-o8qghlI8NZHU1lLPrpi2+Uq7abh4GGPpYANlalzWxyWteJOCsr/P+oPBA49TOLu5FTZO4d3F9MnWJfiMo4BkmA=="], 1453 1488