- apps/desktop: electron-vite + React + Tailwind mapped onto design tokens (preflight off; tokens/base.css owns the reset); boots to a Reginald placeholder proving fonts/tokens/core wiring - packages/core: pure TS; gitea label schema (est/*, p/*, deadline/hard) with pessimistic conflict resolution + 15 unit tests - docs/design: full design handoff (tokens, 16 component contracts, interactive 14-screen prototype, Reginald voice rules) - docs/PLAN.md: product plan (purity rule, pm-state repo, deterministic scheduler + Monte Carlo, directive log) - Deliberate deviation from novelpad stack: no ElectricSQL/PGlite — local store is a rebuildable cache over gitea REST/webhooks (better-sqlite3 in main process, arriving in P1) Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
137 lines
7.5 KiB
Plaintext
137 lines
7.5 KiB
Plaintext
// Settings — gitea connection, sync, model roles, labels, rituals, appearance
|
|
function SettingsScreen({ dark, setDark }) {
|
|
const DS = window.CommiTeaDesignSystem_20e63b;
|
|
const { Card, Input, Select, Switch, Radio, Button, IconButton, Tag, Badge, Icon } = DS;
|
|
const [webhooks, setWebhooks] = React.useState(true);
|
|
const [reconcile, setReconcile] = React.useState(true);
|
|
const [poll, setPoll] = React.useState(true);
|
|
const [nag, setNag] = React.useState(true);
|
|
|
|
const Row = ({ children, style }) => (
|
|
<div style={{ display: 'flex', alignItems: 'center', gap: 12, ...style }}>{children}</div>
|
|
);
|
|
const Note = ({ children }) => (
|
|
<p style={{ font: 'var(--text-caption)', color: 'var(--ink-3)', margin: 0 }}>{children}</p>
|
|
);
|
|
|
|
return (
|
|
<div style={{ maxWidth: 720, margin: '0 auto', display: 'flex', flexDirection: 'column', gap: 16 }}>
|
|
<header style={{ borderBottom: 'var(--rule-double)', paddingBottom: 14 }}>
|
|
<h1 style={{ font: 'var(--text-display)', color: 'var(--ink-1)', margin: 0 }}>Settings</h1>
|
|
<p style={{ font: 'var(--text-data)', color: 'var(--ink-3)', margin: '6px 0 0' }}>config lives in pm-state · versioned, portable</p>
|
|
</header>
|
|
|
|
<Card overline="Gitea" title="Connection">
|
|
<div style={{ display: 'flex', flexDirection: 'column', gap: 14 }}>
|
|
<Input label="Base URL" icon="link" mono defaultValue="https://gitea.stephenmann.io" />
|
|
<Input label="Access token" icon="keyboard" mono type="password" defaultValue="ct_9f2e81c4a7d6" hint="Scopes: repo, issue. Nothing more." />
|
|
<div style={{ display: 'flex', flexDirection: 'column', gap: 8 }}>
|
|
<span style={{ font: '600 13px/1.2 var(--font-sans)', color: 'var(--ink-1)' }}>Managed repos</span>
|
|
<Row style={{ padding: '8px 12px', background: 'var(--paper-0)', border: '1px solid var(--line-1)', borderRadius: 'var(--radius-2)' }}>
|
|
<Icon name="git-branch" size={14} style={{ color: 'var(--ink-3)' }} />
|
|
<span style={{ font: 'var(--text-data)', color: 'var(--ink-1)', flex: 1 }}>stephen/commitea</span>
|
|
<Badge tone="ok" dot>syncing</Badge>
|
|
<IconButton icon="x" label="Stop managing" size="sm" />
|
|
</Row>
|
|
<Row style={{ padding: '8px 12px', background: 'var(--paper-0)', border: '1px solid var(--line-1)', borderRadius: 'var(--radius-2)' }}>
|
|
<Icon name="layers" size={14} style={{ color: 'var(--ink-3)' }} />
|
|
<span style={{ font: 'var(--text-data)', color: 'var(--ink-1)', flex: 1 }}>stephen/pm-state</span>
|
|
<Badge>sidecar</Badge>
|
|
</Row>
|
|
<Note>The sidecar holds machine-derived state only. Delete it and resync — no truth is lost.</Note>
|
|
<Button variant="secondary" size="sm" icon="plus" style={{ alignSelf: 'flex-start' }}>Add repo</Button>
|
|
</div>
|
|
</div>
|
|
</Card>
|
|
|
|
<Card overline="Sync" title="Staying current">
|
|
<div style={{ display: 'flex', flexDirection: 'column', gap: 12 }}>
|
|
<Row>
|
|
<Switch label="Webhooks while running" checked={webhooks} onChange={(e) => setWebhooks(e.target.checked)} />
|
|
<span style={{ font: '400 11.5px var(--font-mono)', color: 'var(--ink-3)', marginLeft: 'auto' }}>endpoint :48731 · healthy</span>
|
|
</Row>
|
|
<Switch label="Full reconcile on launch" checked={reconcile} onChange={(e) => setReconcile(e.target.checked)} />
|
|
<Row>
|
|
<Switch label="Poll fallback" checked={poll} onChange={(e) => setPoll(e.target.checked)} />
|
|
<div style={{ marginLeft: 'auto', width: 140 }}>
|
|
<Select options={[{ value: '2', label: 'every 2 min' }, { value: '5', label: 'every 5 min' }, { value: '15', label: 'every 15 min' }]} defaultValue="5" />
|
|
</div>
|
|
</Row>
|
|
<Note>last reconcile 3.2s · 500 issues · nothing lost</Note>
|
|
</div>
|
|
</Card>
|
|
|
|
<Card overline="Models" title="The router">
|
|
<div style={{ display: 'flex', flexDirection: 'column', gap: 14 }}>
|
|
<div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: '12px 14px' }}>
|
|
<Select label="Prose & rituals" options={[
|
|
{ value: 'gemma-4b', label: 'gemma-4b · local' },
|
|
{ value: 'qwen-7b', label: 'qwen-7b · local' },
|
|
]} defaultValue="gemma-4b" />
|
|
<Input label="Base URL" mono defaultValue="http://localhost:1234/v1" />
|
|
<Select label="Decomposition & negotiation" options={[
|
|
{ value: 'qwen-72b', label: 'qwen-72b · lm-studio box' },
|
|
{ value: 'gpt-4o', label: 'gpt-4o · OpenAI API' },
|
|
]} defaultValue="qwen-72b" />
|
|
<Input label="Base URL" mono defaultValue="http://10.0.0.42:1234/v1" />
|
|
</div>
|
|
<Row>
|
|
<span style={{ font: '400 11.5px var(--font-mono)', color: 'var(--ink-3)' }}>hot memory ≤ 2k tokens · math is never delegated to either</span>
|
|
</Row>
|
|
<p style={{ font: 'var(--text-agent)', color: 'var(--ink-2)', margin: 0 }}>
|
|
The small one writes my standup; the large one argues with your estimates. Neither is allowed near the arithmetic.
|
|
</p>
|
|
</div>
|
|
</Card>
|
|
|
|
<Card overline="Labels" title="Schema">
|
|
<div style={{ display: 'flex', flexDirection: 'column', gap: 10 }}>
|
|
<Row style={{ flexWrap: 'wrap', gap: 6 }}>
|
|
{['est/1d', 'est/2d', 'est/3d', 'est/5d', 'est/8d'].map((l) => <Tag key={l} label={l} />)}
|
|
</Row>
|
|
<Row style={{ flexWrap: 'wrap', gap: 6 }}>
|
|
{['p/1', 'p/2', 'p/3', 'p/4'].map((l) => <Tag key={l} label={l} />)}
|
|
<Tag label="deadline/hard" />
|
|
</Row>
|
|
<Note>Fixed sets, human-meaningful, visible in gitea. Not configurable — that is rather the point.</Note>
|
|
</div>
|
|
</Card>
|
|
|
|
<Card overline="Rituals" title="Reginald's calendar">
|
|
<div style={{ display: 'flex', flexDirection: 'column', gap: 12 }}>
|
|
<Row>
|
|
<span style={{ font: 'var(--text-body)', color: 'var(--ink-1)' }}>Morning standup</span>
|
|
<div style={{ marginLeft: 'auto', width: 120 }}>
|
|
<Select options={[{ value: '0630', label: '06:30' }, { value: '0700', label: '07:00' }, { value: '0800', label: '08:00' }]} defaultValue="0700" />
|
|
</div>
|
|
</Row>
|
|
<Row>
|
|
<Switch label="Stale-blocker nagging" checked={nag} onChange={(e) => setNag(e.target.checked)} />
|
|
<div style={{ marginLeft: 'auto', width: 140 }}>
|
|
<Select options={[{ value: '2', label: 'after 2 days' }, { value: '3', label: 'after 3 days' }, { value: '5', label: 'after 5 days' }]} defaultValue="3" />
|
|
</div>
|
|
</Row>
|
|
</div>
|
|
</Card>
|
|
|
|
<Card overline="Appearance" title="Service">
|
|
<div style={{ display: 'flex', gap: 20 }}>
|
|
<Radio name="theme" label="Morning (light)" checked={!dark} onChange={() => setDark(false)} />
|
|
<Radio name="theme" label="Evening (dark)" checked={dark} onChange={() => setDark(true)} />
|
|
</div>
|
|
</Card>
|
|
|
|
<Card>
|
|
<Row>
|
|
<div style={{ flex: 1 }}>
|
|
<div style={{ font: 'var(--text-body-strong)', color: 'var(--ink-1)' }}>Forget this gitea</div>
|
|
<Note>Removes the connection and the local cache. Gitea itself is untouched.</Note>
|
|
</div>
|
|
<Button variant="danger">Forget</Button>
|
|
</Row>
|
|
</Card>
|
|
</div>
|
|
);
|
|
}
|
|
Object.assign(window, { SettingsScreen });
|