202 lines
9.4 KiB
JavaScript
202 lines
9.4 KiB
JavaScript
// Diff modal + Worktree modal
|
||
const { useState: useStateM, useEffect: useEffectM } = window.React;
|
||
|
||
// Fake diff hunks per task
|
||
const DIFF_HUNKS = {
|
||
t1: [
|
||
{ file: 'src/middleware/auth.ts', adds: 48, dels: 22, hunks: [
|
||
{ header: '@@ -12,7 +12,9 @@ export function authMiddleware(', lines: [
|
||
{ k: 'ctx', n1: 12, n2: 12, t: ' const session = await getSession(req);' },
|
||
{ k: 'del', n1: 13, n2: null, t: ' if (!session) return unauthorized();' },
|
||
{ k: 'del', n1: 14, n2: null, t: ' const user = await lookupUser(session.userId);' },
|
||
{ k: 'add', n1: null, n2: 13, t: ' if (!session || session.expired) {' },
|
||
{ k: 'add', n1: null, n2: 14, t: ' return unauthorized("expired_or_missing");' },
|
||
{ k: 'add', n1: null, n2: 15, t: ' }' },
|
||
{ k: 'add', n1: null, n2: 16, t: ' const user = await pool.withConnection(c => lookupUser(c, session.userId));' },
|
||
{ k: 'ctx', n1: 15, n2: 17, t: ' req.user = user;' },
|
||
{ k: 'ctx', n1: 16, n2: 18, t: ' return next();' },
|
||
]},
|
||
{ header: '@@ -42,4 +44,6 @@ export function guard(', lines: [
|
||
{ k: 'ctx', n1: 42, n2: 44, t: ' return async (req, res, next) => {' },
|
||
{ k: 'del', n1: 43, n2: null, t: ' const s = await redis.get(req.cookies.sid);' },
|
||
{ k: 'add', n1: null, n2: 45, t: ' const s = await store.get(req.cookies.sid);' },
|
||
{ k: 'add', n1: null, n2: 46, t: ' if (s) store.touch(req.cookies.sid);' },
|
||
{ k: 'ctx', n1: 44, n2: 47, t: ' next();' },
|
||
]},
|
||
]},
|
||
{ file: 'src/lib/session/index.ts', adds: 31, dels: 14, hunks: [
|
||
{ header: '@@ -1,8 +1,14 @@', lines: [
|
||
{ k: 'del', n1: 1, n2: null, t: 'import { createClient } from "redis";' },
|
||
{ k: 'add', n1: null, n2: 1, t: 'import { SessionStore } from "./store";' },
|
||
{ k: 'add', n1: null, n2: 2, t: 'import { Pool } from "./pool";' },
|
||
{ k: 'ctx', n1: 2, n2: 3, t: '' },
|
||
{ k: 'del', n1: 3, n2: null, t: 'export const redis = createClient({ url: process.env.REDIS_URL });' },
|
||
{ k: 'add', n1: null, n2: 4, t: 'export const pool = new Pool({ size: 16 });' },
|
||
{ k: 'add', n1: null, n2: 5, t: 'export const store = new SessionStore(pool);' },
|
||
]},
|
||
]},
|
||
{ file: 'src/lib/session/ttl.ts', adds: 12, dels: 4, hunks: [] },
|
||
{ file: 'src/lib/session/store.ts', adds: 38, dels: 0, hunks: [] },
|
||
],
|
||
t2: [
|
||
{ file: 'src/pages/settings.tsx', adds: 32, dels: 2, hunks: [
|
||
{ header: '@@ -4,6 +4,8 @@ import { Section } from "../ui";', lines: [
|
||
{ k: 'ctx', n1: 4, n2: 4, t: 'import { useTheme } from "../hooks/useTheme";' },
|
||
{ k: 'add', n1: null, n2: 5, t: 'import { ThemeToggle } from "../ui/ThemeToggle";' },
|
||
{ k: 'ctx', n1: 5, n2: 6, t: '' },
|
||
{ k: 'ctx', n1: 6, n2: 7, t: 'export default function Settings() {' },
|
||
{ k: 'add', n1: null, n2: 8, t: ' const [theme, setTheme] = useTheme();' },
|
||
]},
|
||
]},
|
||
{ file: 'src/hooks/useTheme.ts', adds: 24, dels: 0, hunks: [] },
|
||
{ file: 'src/theme/tokens.css', adds: 10, dels: 8, hunks: [] },
|
||
{ file: 'src/ui/ThemeToggle.tsx', adds: 26, dels: 2, hunks: [] },
|
||
],
|
||
};
|
||
|
||
const DiffModal = ({ task, onClose }) => {
|
||
useEffectM(() => {
|
||
const onKey = (e) => { if (e.key === 'Escape') onClose(); };
|
||
window.addEventListener('keydown', onKey);
|
||
return () => window.removeEventListener('keydown', onKey);
|
||
}, [onClose]);
|
||
const files = DIFF_HUNKS[task.id] || [
|
||
{ file: 'No diff available yet', adds: 0, dels: 0, hunks: [] }
|
||
];
|
||
const [activeFile, setActiveFile] = useStateM(0);
|
||
const current = files[activeFile];
|
||
|
||
return (
|
||
<div className="modal-backdrop" onClick={onClose}>
|
||
<div className="modal diff-modal" onClick={(e) => e.stopPropagation()}>
|
||
<div className="modal-head">
|
||
<div style={{ display: 'flex', alignItems: 'center', gap: 10 }}>
|
||
<Icon name="diff" size={14} />
|
||
<div>
|
||
<div className="modal-title">Diff · {task.agent.branch}</div>
|
||
<div className="modal-sub">
|
||
{task.agent.worktree} · {files.length} files ·
|
||
<span className="add" style={{ marginLeft: 6 }}>+{task.agent.diff.additions}</span>
|
||
<span className="del" style={{ marginLeft: 6 }}>−{task.agent.diff.deletions}</span>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
<div style={{ display: 'flex', gap: 6 }}>
|
||
<button className="btn"><Icon name="external" size={12} /> Open in editor</button>
|
||
<button className="btn primary"><Icon name="check" size={12} /> Approve & merge</button>
|
||
<button className="icon-btn" onClick={onClose}><Icon name="close" size={12} /></button>
|
||
</div>
|
||
</div>
|
||
<div className="modal-body diff-body">
|
||
<div className="diff-sidebar">
|
||
{files.map((f, i) => (
|
||
<div key={f.file} className={`diff-file-tab ${i === activeFile ? 'active' : ''}`} onClick={() => setActiveFile(i)}>
|
||
<div className="diff-file-name" title={f.file}>{f.file}</div>
|
||
<div className="diff-file-stats">
|
||
<span className="add">+{f.adds}</span>
|
||
<span className="del">−{f.dels}</span>
|
||
</div>
|
||
</div>
|
||
))}
|
||
</div>
|
||
<div className="diff-view">
|
||
<div className="diff-file-header">
|
||
<Icon name="note" size={12} />
|
||
<span>{current.file}</span>
|
||
<span style={{ marginLeft: 'auto' }} className="diff-stats">
|
||
<span className="add">+{current.adds}</span>
|
||
<span className="del">−{current.dels}</span>
|
||
</span>
|
||
</div>
|
||
{current.hunks.length === 0 ? (
|
||
<div style={{ padding: 40, textAlign: 'center', color: 'var(--text-faint)', fontFamily: 'var(--mono)', fontSize: 11 }}>
|
||
Select a hunk — no detail preview available for this file.
|
||
</div>
|
||
) : current.hunks.map((h, hi) => (
|
||
<div key={hi} className="diff-hunk">
|
||
<div className="diff-hunk-header">{h.header}</div>
|
||
{h.lines.map((ln, li) => (
|
||
<div key={li} className={`diff-line ${ln.k}`}>
|
||
<span className="ln">{ln.n1 ?? ''}</span>
|
||
<span className="ln">{ln.n2 ?? ''}</span>
|
||
<span className="sign">{ln.k === 'add' ? '+' : ln.k === 'del' ? '−' : ' '}</span>
|
||
<span className="t">{ln.t}</span>
|
||
</div>
|
||
))}
|
||
</div>
|
||
))}
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
);
|
||
};
|
||
|
||
const WorktreeModal = ({ task, onClose }) => {
|
||
useEffectM(() => {
|
||
const onKey = (e) => { if (e.key === 'Escape') onClose(); };
|
||
window.addEventListener('keydown', onKey);
|
||
return () => window.removeEventListener('keydown', onKey);
|
||
}, [onClose]);
|
||
|
||
const fakeTree = [
|
||
{ kind: 'dir', path: 'src', children: [
|
||
{ kind: 'dir', path: 'middleware', children: [
|
||
{ kind: 'file', path: 'auth.ts', mod: true },
|
||
]},
|
||
{ kind: 'dir', path: 'lib/session', children: [
|
||
{ kind: 'file', path: 'index.ts', mod: true },
|
||
{ kind: 'file', path: 'ttl.ts', mod: true },
|
||
{ kind: 'file', path: 'store.ts', added: true },
|
||
]},
|
||
]},
|
||
{ kind: 'file', path: 'package.json' },
|
||
{ kind: 'file', path: 'README.md' },
|
||
];
|
||
const render = (nodes, depth = 0) => nodes.map((n) => (
|
||
n.kind === 'dir' ? (
|
||
<React.Fragment key={n.path}>
|
||
<div className="tree-row" style={{ paddingLeft: 10 + depth * 14 }}>
|
||
<Icon name="folder" size={12} /> <span>{n.path}</span>
|
||
</div>
|
||
{render(n.children, depth + 1)}
|
||
</React.Fragment>
|
||
) : (
|
||
<div key={n.path} className={`tree-row ${n.mod ? 'mod' : ''} ${n.added ? 'added' : ''}`} style={{ paddingLeft: 10 + depth * 14 }}>
|
||
<Icon name="note" size={12} />
|
||
<span>{n.path}</span>
|
||
{n.mod && <span className="tree-badge mod">M</span>}
|
||
{n.added && <span className="tree-badge add">A</span>}
|
||
</div>
|
||
)
|
||
));
|
||
|
||
return (
|
||
<div className="modal-backdrop" onClick={onClose}>
|
||
<div className="modal worktree-modal" onClick={(e) => e.stopPropagation()}>
|
||
<div className="modal-head">
|
||
<div>
|
||
<div className="modal-title"><Icon name="folder-open" size={14} /> {task.agent.worktree}</div>
|
||
<div className="modal-sub">{task.agent.branch} ← {task.agent.baseBranch}</div>
|
||
</div>
|
||
<div style={{ display: 'flex', gap: 6 }}>
|
||
<button className="btn"><Icon name="terminal" size={12} /> Open terminal</button>
|
||
<button className="icon-btn" onClick={onClose}><Icon name="close" size={12} /></button>
|
||
</div>
|
||
</div>
|
||
<div className="modal-body" style={{ padding: 0, display: 'flex', flexDirection: 'column' }}>
|
||
<div style={{ padding: '10px 16px', borderBottom: '1px solid var(--line)', fontFamily: 'var(--mono)', fontSize: 11, color: 'var(--text-mute)' }}>
|
||
Filesystem preview — modified files marked <span style={{ color: 'var(--peat)' }}>M</span>, additions <span style={{ color: 'var(--moss-bright)' }}>A</span>
|
||
</div>
|
||
<div style={{ flex: 1, overflowY: 'auto', padding: 8, fontFamily: 'var(--mono)', fontSize: 12 }}>
|
||
{render(fakeTree)}
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
);
|
||
};
|
||
|
||
window.DiffModal = DiffModal;
|
||
window.WorktreeModal = WorktreeModal;
|