Move to threads based system.

This commit is contained in:
Gabriel Brown
2026-06-22 10:37:26 -04:00
parent 8ae6c4b533
commit 206b64176b
82 changed files with 6169 additions and 1930 deletions
@@ -0,0 +1,133 @@
'use client';
import { useEffect, useRef, useState } from 'react';
import dynamic from 'next/dynamic';
import { Button, Switch } from '@spoon/ui';
const MonacoEditor = dynamic(async () => await import('@monaco-editor/react'), {
ssr: false,
});
type MonacoEditorInstance = {
getModel?: () => unknown;
};
type VimMode = {
dispose: () => void;
};
export const CodeEditor = ({
path,
content,
readOnly,
onSave,
}: {
path?: string;
content: string;
readOnly: boolean;
onSave: (content: string) => Promise<void>;
}) => {
const [value, setValue] = useState(content);
const [saving, setSaving] = useState(false);
const [vimEnabled, setVimEnabled] = useState(false);
const [dirty, setDirty] = useState(false);
const editorRef = useRef<MonacoEditorInstance | null>(null);
const vimRef = useRef<VimMode | null>(null);
const statusRef = useRef<HTMLDivElement | null>(null);
useEffect(() => {
setValue(content);
setDirty(false);
}, [content, path]);
useEffect(() => {
const editor = editorRef.current;
if (!editor) return;
vimRef.current?.dispose();
vimRef.current = null;
if (!vimEnabled) return;
void import('monaco-vim').then((module) => {
const initVimMode = module.initVimMode as unknown as (
editor: MonacoEditorInstance,
statusNode?: HTMLElement | null,
) => VimMode;
vimRef.current = initVimMode(editor, statusRef.current);
});
return () => {
vimRef.current?.dispose();
vimRef.current = null;
};
}, [vimEnabled, path]);
if (!path) {
return (
<div className='text-muted-foreground flex h-full items-center justify-center text-sm'>
Select a file to inspect or edit.
</div>
);
}
const save = async () => {
setSaving(true);
try {
await onSave(value);
setDirty(false);
} finally {
setSaving(false);
}
};
return (
<div className='flex h-full min-h-[520px] flex-col'>
<div className='border-border flex h-11 items-center justify-between gap-3 border-b px-3'>
<div className='min-w-0'>
<p className='truncate font-mono text-xs'>{path}</p>
{dirty ? (
<p className='text-muted-foreground text-xs'>Unsaved changes</p>
) : null}
</div>
<div className='flex items-center gap-3'>
<label className='flex items-center gap-2 text-xs'>
Vim
<Switch checked={vimEnabled} onCheckedChange={setVimEnabled} />
</label>
<Button
type='button'
size='sm'
disabled={readOnly || saving || !dirty}
onClick={save}
>
{saving ? 'Saving...' : 'Save'}
</Button>
</div>
</div>
<div className='min-h-0 flex-1'>
<MonacoEditor
height='520px'
path={path}
value={value}
theme='vs-dark'
options={{
readOnly,
minimap: { enabled: false },
fontSize: 13,
scrollBeyondLastLine: false,
wordWrap: 'on',
}}
onMount={(editor) => {
editorRef.current = editor as MonacoEditorInstance;
}}
onChange={(next) => {
setValue(next ?? '');
setDirty((next ?? '') !== content);
}}
/>
</div>
<div
ref={statusRef}
className='border-border text-muted-foreground h-6 border-t px-3 py-1 font-mono text-xs'
/>
</div>
);
};