Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 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
6 changes: 6 additions & 0 deletions .changeset/comment-mode-copy.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
"@open-codesign/desktop": patch
"@open-codesign/i18n": patch
---

Update comment mode copy for saving comments and adding saved comments to chat.
19 changes: 19 additions & 0 deletions apps/desktop/src/main/workspace-watcher.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -142,6 +142,25 @@ describe('files-watcher subscribe / unsubscribe', () => {
expect(watchMock).toHaveBeenCalledTimes(1);
});

it('does not reject when the bound workspace folder is missing', () => {
reset();
const err = Object.assign(new Error('no such file or directory'), { code: 'ENOENT' });
watchMock.mockImplementation(() => {
throw err;
});
getDesignMock.mockReturnValue({
id: 'd1',
workspacePath: tempWorkspace('codesign-watch-missing'),
});
registerFilesWatcherIpc({} as never, () => null);
const sub = getHandler('codesign:files:v1:subscribe');

expect(sub(null, { schemaVersion: 1, designId: 'd1' })).toEqual({ ok: true });

expect(watchMock).toHaveBeenCalledTimes(1);
expect(__test.watchers.has('d1')).toBe(false);
});

it('falls back to polling when native watch is denied by permissions', () => {
reset();
const err = Object.assign(new Error('operation not permitted'), { code: 'EPERM' });
Expand Down
28 changes: 22 additions & 6 deletions apps/desktop/src/main/workspace-watcher.ts
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,10 @@ interface ActiveWatcher {
pollBusy: boolean;
}

type WatcherStartResult =
| { ok: true; entry: ActiveWatcher }
| { ok: false; reason: 'workspace-unavailable' | 'watch-failed' };

const watchers = new Map<string, ActiveWatcher>();

function isIgnored(rel: string): boolean {
Expand All @@ -66,6 +70,11 @@ function isPermissionWatchError(err: unknown): boolean {
return code === 'EPERM' || code === 'EACCES';
}

function isWorkspaceUnavailableWatchError(err: unknown): boolean {
const code = (err as NodeJS.ErrnoException).code;
return code === 'ENOENT' || code === 'ENOTDIR';
}

async function pollWorkspaceSignature(root: string): Promise<string> {
const rows: string[] = [];

Expand Down Expand Up @@ -151,7 +160,7 @@ function startWatcher(
designId: string,
workspacePath: string,
getWin: () => BrowserWindow | null,
): ActiveWatcher | null {
): WatcherStartResult {
const entry: ActiveWatcher = {
watcher: null,
workspacePath,
Expand All @@ -177,9 +186,12 @@ function startWatcher(
if (isPermissionWatchError(err)) {
watchers.set(designId, entry);
startPolling(designId, entry, getWin);
return entry;
return { ok: true, entry };
}
return null;
if (isWorkspaceUnavailableWatchError(err)) {
return { ok: false, reason: 'workspace-unavailable' };
}
return { ok: false, reason: 'watch-failed' };
}
entry.watcher = watcher;
watcher.on('error', (err) => {
Expand All @@ -196,7 +208,7 @@ function startWatcher(
startPolling(designId, active, getWin);
});
watchers.set(designId, entry);
return entry;
return { ok: true, entry };
}

function stopWatcher(designId: string): void {
Expand Down Expand Up @@ -242,10 +254,14 @@ export function registerFilesWatcherIpc(db: Database, getWin: () => BrowserWindo
return { ok: true };
}
}
const entry = startWatcher(designId, workspacePath, getWin);
if (!entry) {
const result = startWatcher(designId, workspacePath, getWin);
if (!result.ok) {
if (result.reason === 'workspace-unavailable') {
return { ok: true };
}
throw new CodesignError('Failed to watch workspace files', 'IPC_DB_ERROR');
}
const { entry } = result;
entry.refCount = 1;
return { ok: true };
});
Expand Down
73 changes: 73 additions & 0 deletions apps/desktop/src/renderer/src/components/FilesTabView.test.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import type { CommentRow } from '@open-codesign/shared';
import { describe, expect, it, vi } from 'vitest';
import { openFileTab } from '../store/slices/tabs';
import {
Expand All @@ -8,12 +9,14 @@ import {
detectedPreviewTarget,
effectivePreviewModeForDesign,
externalAppManagedFallbackPath,
findReusableWorkspaceFileCommentForSelector,
htmlRequiresWorkspaceDevServer,
isMarkdownPreviewFile,
isPreviewSourceUsableForSelectedPath,
isRenderableDesignFileKind,
previewKindForFile,
resolveReferencedWorkspacePreviewPath,
shouldEnableWorkspaceFilePreviewInteractions,
shouldShowTweakPanelForFile,
shouldUseDesignPreviewResolverForFile,
splitMarkdownFrontmatter,
Expand All @@ -23,6 +26,24 @@ import {
} from './FilesTabView';

describe('FilesTabView preview helpers', () => {
const commentRow = (overrides: Partial<CommentRow> = {}): CommentRow => ({
schemaVersion: 1,
id: overrides.id ?? 'comment-1',
designId: overrides.designId ?? 'design-1',
snapshotId: overrides.snapshotId ?? 'snapshot-1',
kind: overrides.kind ?? 'edit',
selector: overrides.selector ?? '#hero',
tag: overrides.tag ?? 'section',
outerHTML: overrides.outerHTML ?? '<section id="hero">Hello</section>',
rect: overrides.rect ?? { top: 10, left: 20, width: 100, height: 50 },
text: overrides.text ?? 'Saved note',
status: overrides.status ?? 'pending',
createdAt: overrides.createdAt ?? '2026-05-13T00:00:00.000Z',
appliedInSnapshotId: overrides.appliedInSnapshotId ?? null,
...(overrides.scope ? { scope: overrides.scope } : {}),
...(overrides.parentOuterHTML ? { parentOuterHTML: overrides.parentOuterHTML } : {}),
});

it('clamps the file browser splitter width to usable bounds', () => {
expect(clampFileBrowserWidth(120, 1280)).toBe(260);
expect(clampFileBrowserWidth(480.4, 1280)).toBe(480);
Expand Down Expand Up @@ -145,6 +166,52 @@ describe('FilesTabView preview helpers', () => {
});
});

it('prefills the existing pending comment when reselecting the same file preview element', () => {
const existing = commentRow({ id: 'comment-existing', text: 'Keep the saved note visible' });
const selectCanvasElement = vi.fn();
const openCommentBubble = vi.fn();
const applyLiveRects = vi.fn();
const pushIframeError = vi.fn();
const handlers = createWorkspaceFilePreviewMessageHandlers({
previewZoom: 100,
comments: [existing],
currentSnapshotId: existing.snapshotId,
selectCanvasElement,
openCommentBubble,
applyLiveRects,
pushIframeError,
});

handlers.onElementSelected({
__codesign: true,
type: 'ELEMENT_SELECTED',
selector: existing.selector,
tag: existing.tag,
outerHTML: existing.outerHTML,
rect: existing.rect,
});

expect(openCommentBubble).toHaveBeenCalledWith(
expect.objectContaining({
selector: existing.selector,
existingCommentId: existing.id,
initialText: existing.text,
}),
);
});

it('falls back to a pending comment for the same selector when the file preview has no current snapshot', () => {
const existing = commentRow({ id: 'comment-existing', snapshotId: 'snapshot-stale' });

expect(
findReusableWorkspaceFileCommentForSelector({
comments: [existing],
currentSnapshotId: null,
selector: existing.selector,
}),
).toBe(existing);
});

it('marks html/jsx/tsx files as renderable', () => {
expect(isRenderableDesignFileKind('html')).toBe(true);
expect(isRenderableDesignFileKind('jsx')).toBe(true);
Expand Down Expand Up @@ -219,6 +286,12 @@ describe('FilesTabView preview helpers', () => {
).toBe(false);
});

it('keeps local workspace runtime previews interactive outside dedicated file tabs', () => {
expect(shouldEnableWorkspaceFilePreviewInteractions({ previewKind: 'runtime' })).toBe(true);
expect(shouldEnableWorkspaceFilePreviewInteractions({ previewKind: 'markdown' })).toBe(false);
expect(shouldEnableWorkspaceFilePreviewInteractions({ previewKind: null })).toBe(false);
});

it('uses the design-level resolver only for generated preview fallbacks', () => {
expect(
shouldUseDesignPreviewResolverForFile({
Expand Down
Loading
Loading