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
15 changes: 14 additions & 1 deletion apps/framework-editor/app/components/table/EditableCell.test.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { fireEvent, render, screen } from '@testing-library/react';
import { beforeEach, describe, expect, it, vi } from 'vitest';
import { EditableCell } from './EditableCell';
import { clearEditorSize, saveEditorSize } from './editor-size-storage';

// The ui package ships untranspiled JSX in dist; stub the bits the cell uses.
vi.mock('@trycompai/ui', () => ({
Expand Down Expand Up @@ -55,7 +56,10 @@ describe('EditableCell — non-expandable (default)', () => {
});

describe('EditableCell — expandable', () => {
beforeEach(() => vi.clearAllMocks());
beforeEach(() => {
vi.clearAllMocks();
clearEditorSize();
});

it('shows an expand affordance', () => {
setup({ expandable: true });
Expand Down Expand Up @@ -139,4 +143,13 @@ describe('EditableCell — expandable', () => {
fireEvent.click(screen.getByRole('button', { name: 'Cancel' }));
expect(onExpandedChange).toHaveBeenLastCalledWith(false);
});

it('reopens the editor at the persisted size (FRAME-3)', () => {
saveEditorSize({ width: 900, height: 500 });
setup({ expandable: true });
fireEvent.click(screen.getByRole('button', { name: /large editor/i }));
const textarea = screen.getByRole('textbox') as HTMLTextAreaElement;
expect(textarea.style.width).toBe('900px');
expect(textarea.style.height).toBe('500px');
});
});
32 changes: 29 additions & 3 deletions apps/framework-editor/app/components/table/EditableCell.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,8 @@ import {
Textarea,
} from '@trycompai/ui';
import { Maximize2 } from 'lucide-react';
import { useState } from 'react';
import { useRef, useState } from 'react';
import { loadEditorSize, saveEditorSize, type EditorSize } from './editor-size-storage';

@cubic-dev-ai cubic-dev-ai Bot Jun 12, 2026

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P3: According to linked Linear issue FRAME-3, the remembered size must be stored in a cookie. This implementation persists via localStorage instead, so it doesn’t meet the requested persistence mechanism.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At apps/framework-editor/app/components/table/EditableCell.tsx, line 14:

<comment>According to linked Linear issue FRAME-3, the remembered size must be stored in a cookie. This implementation persists via localStorage instead, so it doesn’t meet the requested persistence mechanism.</comment>

<file context>
@@ -10,7 +10,8 @@ import {
 import { Maximize2 } from 'lucide-react';
-import { useState } from 'react';
+import { useRef, useState } from 'react';
+import { loadEditorSize, saveEditorSize, type EditorSize } from './editor-size-storage';
 
 interface EditableCellProps {
</file context>
Fix with cubic


interface EditableCellProps {
value: string | null;
Expand Down Expand Up @@ -44,6 +45,10 @@ export function EditableCell({
const [editValue, setEditValue] = useState(value ?? '');
const [isExpanded, setIsExpanded] = useState(false);
const [expandValue, setExpandValue] = useState(value ?? '');
// Remembered editor size (FRAME-3): the large editor is resizable in both
// directions and reopens at the size the user last left it.
const [editorSize, setEditorSize] = useState<EditorSize | null>(null);
const textareaRef = useRef<HTMLTextAreaElement>(null);

// Keep local open state and the parent notification in lockstep so the row
// highlight tracks the dialog exactly (open icon, right-click, save, cancel,
Expand Down Expand Up @@ -78,6 +83,7 @@ export function EditableCell({
const handleOpenExpanded = () => {
if (disabled) return;
setExpandValue(value ?? '');
setEditorSize(loadEditorSize());
setExpanded(true);
};

Expand All @@ -88,6 +94,19 @@ export function EditableCell({
setExpanded(false);
};

// Persist the editor size after a resize-handle drag (fires on pointer
// release). Skipped when unchanged so plain clicks don't thrash storage.
const handleEditorResizeEnd = () => {
const el = textareaRef.current;
if (!el) return;
const next: EditorSize = { width: el.offsetWidth, height: el.offsetHeight };
if (editorSize && next.width === editorSize.width && next.height === editorSize.height) {
return;
}
setEditorSize(next);
saveEditorSize(next);
};

if (disabled) {
return (
<span className="text-muted-foreground block truncate px-2 py-1.5 text-sm">
Expand Down Expand Up @@ -149,15 +168,22 @@ export function EditableCell({
</button>

<Dialog open={isExpanded} onOpenChange={setExpanded}>
<DialogContent className="sm:max-w-[760px]">
<DialogContent className="max-h-[95vh] w-fit max-w-[95vw] overflow-y-auto sm:max-w-[95vw]">
<DialogHeader>
<DialogTitle>{expandTitle}</DialogTitle>
</DialogHeader>
<Textarea
ref={textareaRef}
value={expandValue}
onChange={(e) => setExpandValue(e.target.value)}
onMouseUp={handleEditorResizeEnd}
autoFocus
className="min-h-[260px] font-mono text-sm"
className="max-h-[80vh] max-w-[92vw] min-h-[260px] min-w-[320px] resize font-mono text-sm"
style={
editorSize
? { width: editorSize.width, height: editorSize.height }
: { width: 680 }
}
/>
<DialogFooter>
<Button variant="outline" onClick={() => setExpanded(false)}>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
// @vitest-environment jsdom
import { afterEach, describe, expect, it } from 'vitest';
import { clearEditorSize, loadEditorSize, saveEditorSize } from './editor-size-storage';

const COOKIE_NAME = 'fwk-editor-expand-editor-size';

function setRawCookie(value: string) {
document.cookie = `${COOKIE_NAME}=${encodeURIComponent(value)}; path=/`;
}

describe('editor-size-storage', () => {
afterEach(() => clearEditorSize());

it('returns null when nothing is stored', () => {
expect(loadEditorSize()).toBeNull();
});

it('round-trips a valid size', () => {
saveEditorSize({ width: 900, height: 500 });
expect(loadEditorSize()).toEqual({ width: 900, height: 500 });
});

it('returns null for malformed JSON', () => {
setRawCookie('{not json');
expect(loadEditorSize()).toBeNull();
});

it('does not store non-positive or non-numeric dimensions', () => {
saveEditorSize({ width: 0, height: 100 });
expect(loadEditorSize()).toBeNull();

setRawCookie(JSON.stringify({ width: 'x', height: 1 }));
expect(loadEditorSize()).toBeNull();
});
});
60 changes: 60 additions & 0 deletions apps/framework-editor/app/components/table/editor-size-storage.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
/**
* Persisted size for the large multi-line cell editor (FRAME-3). The user can
* resize the editor in both directions; we remember the last size so the next
* open reuses it. Stored in a cookie (per the ticket), survives reloads.
*/
export interface EditorSize {
width: number;
height: number;
}

const COOKIE_NAME = 'fwk-editor-expand-editor-size';
const ONE_YEAR_SECONDS = 60 * 60 * 24 * 365;

function isValidSize(value: unknown): value is EditorSize {
if (typeof value !== 'object' || value === null) return false;
const { width, height } = value as Record<string, unknown>;
return (
typeof width === 'number' &&
typeof height === 'number' &&
Number.isFinite(width) &&
Number.isFinite(height) &&
width > 0 &&
height > 0
);
}

function readCookie(name: string): string | null {
const match = document.cookie
.split('; ')
.find((row) => row.startsWith(`${name}=`));
return match ? match.slice(name.length + 1) : null;
}

export function loadEditorSize(): EditorSize | null {
if (typeof document === 'undefined') return null;
try {
const raw = readCookie(COOKIE_NAME);
if (!raw) return null;
const parsed: unknown = JSON.parse(decodeURIComponent(raw));
return isValidSize(parsed) ? parsed : null;
} catch {
return null;
}
}

export function saveEditorSize(size: EditorSize): void {
if (typeof document === 'undefined') return;
if (!isValidSize(size)) return;
try {
const value = encodeURIComponent(JSON.stringify(size));
document.cookie = `${COOKIE_NAME}=${value}; path=/; max-age=${ONE_YEAR_SECONDS}; samesite=lax`;
} catch {
// Ignore — resizing still works in-session even if the cookie can't be set.
}
}

export function clearEditorSize(): void {
if (typeof document === 'undefined') return;
document.cookie = `${COOKIE_NAME}=; path=/; max-age=0; samesite=lax`;
}
Loading