Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 13 additions & 0 deletions changelogs/v2.2.8.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
## What's new

### Fixes

- **Folder Preservation on Note Edits**: Fixed a bug where `patch_note` and `append_to_note` would move note files out of their current folder and into the project root. Both tools now pass the note's existing `folder` field to `writeNoteFile`, matching the behaviour of `ensure_note`, `rename_note`, and `bulk_move_notes`.
- **Kanban Drag Cancel Cleanup**: Added an `onDragCancel` handler to the board's `DndContext`. Pressing Escape during a card or column drag now properly clears all drag state (`activeCard`, `activeColumn`, `overId`, `hoverZone`) and dismisses the action zone overlay, instead of leaving stale highlights behind.

### Changes

- **Redesigned Archive/Delete Drop Zones**: The action zone overlay that appears when dragging a card has been redesigned as two compact, button-style targets centered at the bottom of the viewport. The previous full-width banner at the top of the board caused content to shift on drag start; the new design is a fixed portal overlay that doesn't affect layout. Buttons match the app's `lg` button sizing (36px tall, rounded outer corners, solid colour at 85% opacity resting and full colour on hover).
- **Kanban Card Height Fix**: Collapsed card descriptions now use `max-h-10 overflow-hidden` instead of `line-clamp-2`, which didn't reliably clip block-level markdown content. Expanded view increased to `max-h-80`. Paragraph margins from `prose-cairn` are zeroed inside cards to prevent excess vertical space.
- **Kanban Drop Target Expansion**: Columns now stretch to the board's full height via `self-stretch` and `h-full`. During an active drag-over, the card drop zone minimum height bumps from 48px to 120px, making empty or near-empty columns easy drop targets without scrolling.
- **Diff Viewer Performance**: The Agent View's Diff Viewer no longer blocks the UI thread when polling `git diff` every 5 seconds. `parseDiff` now runs on a `useDeferredValue`, so React keeps interactions responsive while the parse runs at lower priority. All diff sub-components (`FileDiff`, `UnifiedFile`, `SplitFile`, `HL`) are wrapped in `React.memo` so unchanged files skip re-rendering entirely. Split-view row building and addition/deletion counts are memoised per file.
2 changes: 2 additions & 0 deletions electron/mcp/tools/notes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -152,6 +152,7 @@ export function append_to_note(db: Database.Database, snap: Snapshot, workspaceP
createdAt: note.createdAt as string, updatedAt: updated?.updatedAt ?? new Date().toISOString(),
archivedAt: note.archivedAt as string | undefined,
projectName: proj?.name ?? note.projectId as string,
folder: (note.folder as string) ?? "",
});
return { id: noteId, title: note.title, updatedAt: updated?.updatedAt, newLength: newContent.length };
} finally {
Expand Down Expand Up @@ -195,6 +196,7 @@ export function patch_note(db: Database.Database, snap: Snapshot, workspacePath:
createdAt: note.createdAt as string, updatedAt: updated?.updatedAt ?? new Date().toISOString(),
archivedAt: note.archivedAt as string | undefined,
projectName: proj?.name ?? note.projectId as string,
folder: (note.folder as string) ?? "",
});
return { id: noteId, title: note.title, updatedAt: updated?.updatedAt, replacements: all ? count : 1 };
} finally {
Expand Down
23 changes: 23 additions & 0 deletions src/app/globals.css
Original file line number Diff line number Diff line change
Expand Up @@ -538,4 +538,27 @@ input[type="url"] {
}


/* ── Kanban drag drop zones ─────────────────────────────────────────────────── */

.zone-archive {
color: var(--warning);
background: color-mix(in srgb, var(--warning) 12%, transparent);
}
.zone-archive:hover {
background: color-mix(in srgb, var(--warning) 22%, transparent);
}
.zone-delete {
color: var(--danger);
background: color-mix(in srgb, var(--danger) 12%, transparent);
}
.zone-delete:hover {
background: color-mix(in srgb, var(--danger) 22%, transparent);
}
.zone-archive.zone-active {
background: var(--warning);
color: #1a1a1a;
}
.zone-delete.zone-active {
background: var(--danger);
color: #fff;
}
30 changes: 16 additions & 14 deletions src/components/agent/DiffFile.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,7 @@ function langFrom(filename: string): string | null {

// ── Highlighted line ──────────────────────────────────────────────────────────

function HL({ content, lang, palette }: { content: string; lang: string | null; palette: Palette }) {
const HL = React.memo(function HL({ content, lang, palette }: { content: string; lang: string | null; palette: Palette }) {
const tokens = useMemo(() => {
if (!lang || !content) return null;
try {
Expand All @@ -81,7 +81,7 @@ function HL({ content, lang, palette }: { content: string; lang: string | null;
{tokens ? tokens.map((n, i) => renderHast(n, palette, String(i))) : content}
</code>
);
}
});

// ── View mode type ────────────────────────────────────────────────────────────

Expand All @@ -103,7 +103,7 @@ function ln(change: Change, side: "old" | "new"): number | "" {

// ── Unified file view ─────────────────────────────────────────────────────────

export function UnifiedFile({ file, palette, changesOnly, hunkTop }: { file: File; palette: Palette; changesOnly: boolean; hunkTop: number }) {
export const UnifiedFile = React.memo(function UnifiedFile({ file, palette, changesOnly, hunkTop }: { file: File; palette: Palette; changesOnly: boolean; hunkTop: number }) {
const filename = file.to ?? file.from ?? "unknown";
const lang = langFrom(filename);
return (
Expand Down Expand Up @@ -162,7 +162,7 @@ export function UnifiedFile({ file, palette, changesOnly, hunkTop }: { file: Fil
})}
</>
);
}
});

// ── Split file view ───────────────────────────────────────────────────────────

Expand Down Expand Up @@ -190,11 +190,12 @@ function buildSplitRows(changes: Change[]): SplitRow[] {
return rows;
}

function SplitFile({ file, palette, hunkTop }: { file: File; palette: Palette; hunkTop: number }) {
export const SplitFile = React.memo(function SplitFile({ file, palette, hunkTop }: { file: File; palette: Palette; hunkTop: number }) {
const filename = file.to ?? file.from ?? "unknown";
const lang = langFrom(filename);
const addBg = "color-mix(in srgb, var(--success, #22c55e) 10%, transparent)";
const addBg = "color-mix(in srgb, var(--success) 10%, transparent)";
const delBg = "color-mix(in srgb, var(--danger) 10%, transparent)";
Comment thread
coderabbitai[bot] marked this conversation as resolved.
const splitRows = useMemo(() => file.chunks.map((chunk) => buildSplitRows(chunk.changes)), [file]);
return (
<>
{file.chunks.map((chunk, ci) => (
Expand All @@ -205,7 +206,7 @@ function SplitFile({ file, palette, hunkTop }: { file: File; palette: Palette; h
>
{chunk.content}
</div>
{buildSplitRows(chunk.changes).map((row, ri) => {
{splitRows[ci].map((row, ri) => {
const oldContent = row.old ? row.old.content.slice(1) : "";
const newContent = row.new ? row.new.content.slice(1) : "";
const oldBg = row.old?.type === "del" ? delBg : undefined;
Expand Down Expand Up @@ -243,14 +244,15 @@ function SplitFile({ file, palette, hunkTop }: { file: File; palette: Palette; h
))}
</>
);
}
});

// ── FileDiff — file header + collapsible content ──────────────────────────────

export function FileDiff({ file, collapsed, onToggle, mode, palette }: {
export const FileDiff = React.memo(function FileDiff({ file, fileKey, collapsed, onToggle, mode, palette }: {
file: File;
fileKey: string;
collapsed: boolean;
onToggle: () => void;
onToggle: (key: string) => void;
mode: ViewMode;
palette: Palette;
}) {
Expand All @@ -259,8 +261,8 @@ export function FileDiff({ file, collapsed, onToggle, mode, palette }: {
? `${file.from} → ${filename}`
: filename;

const additions = file.chunks.reduce((s, c) => s + c.changes.filter((ch) => ch.type === "add").length, 0);
const deletions = file.chunks.reduce((s, c) => s + c.changes.filter((ch) => ch.type === "del").length, 0);
const additions = useMemo(() => file.chunks.reduce((s, c) => s + c.changes.filter((ch) => ch.type === "add").length, 0), [file]);
const deletions = useMemo(() => file.chunks.reduce((s, c) => s + c.changes.filter((ch) => ch.type === "del").length, 0), [file]);

const headerRef = useRef<HTMLButtonElement>(null);
const [hunkTop, setHunkTop] = useState(32);
Expand All @@ -277,7 +279,7 @@ export function FileDiff({ file, collapsed, onToggle, mode, palette }: {
<div className="border-b border-[var(--border)]">
<button
ref={headerRef}
onClick={onToggle}
onClick={() => onToggle(fileKey)}
className="w-full flex items-center gap-2 px-3 py-1.5 bg-[var(--surface)] hover:bg-[var(--surface-2)] transition-colors sticky top-0 z-10 text-left border-b border-[var(--border-subtle)]"
>
{collapsed
Expand All @@ -299,4 +301,4 @@ export function FileDiff({ file, collapsed, onToggle, mode, palette }: {
)}
</div>
);
}
});
15 changes: 10 additions & 5 deletions src/components/agent/DiffViewer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@
* changes — unified but context lines hidden (adds/deletes only)
*/

import { useMemo, useState, useEffect, useCallback, useRef } from "react";
import { useMemo, useState, useEffect, useCallback, useRef, useDeferredValue } from "react";
import parseDiff from "parse-diff";
import { Copy, Check, RefreshCw, FolderGit2, ChevronRight, ChevronDown } from "lucide-react";
import { cn } from "@/lib/utils";
Expand Down Expand Up @@ -58,6 +58,10 @@ export function DiffViewer({ cwd }: DiffViewerProps) {
const [collapsed, setCollapsed] = useState<Set<string>>(new Set());
const isMounted = useRef(true);

// Defers the diff text so that parseDiff + re-render runs at lower priority,
// keeping the UI responsive while typing, scrolling, or interacting.
const deferredDiffText = useDeferredValue(diffText);

const fetchDiff = useCallback(async () => {
if (!window.electron) return;
setLoading(true);
Expand All @@ -83,9 +87,9 @@ export function DiffViewer({ cwd }: DiffViewerProps) {
}, [fetchDiff]);

const files = useMemo(() => {
if (!diffText) return [];
try { return parseDiff(diffText); } catch { return []; }
}, [diffText]);
if (!deferredDiffText) return [];
try { return parseDiff(deferredDiffText); } catch { return []; }
}, [deferredDiffText]);

const toggleCollapse = useCallback((key: string) => {
setCollapsed((prev) => {
Expand Down Expand Up @@ -224,8 +228,9 @@ export function DiffViewer({ cwd }: DiffViewerProps) {
<FileDiff
key={fileKey}
file={file}
fileKey={fileKey}
collapsed={collapsed.has(fileKey)}
onToggle={() => toggleCollapse(fileKey)}
onToggle={toggleCollapse}
mode={mode}
palette={palette}
/>
Expand Down
Loading