terminal, chat and files are granted by default; permissions screen simplified

DEFAULTS. Every role now starts with the three confined capabilities at write, seeded in
bootstrap. These are what the platform is FOR — an account that signs in and reaches none of
them is not restricted, it is useless, and making the owner grant them by hand first is a
step with no decision in it.

Seeded as real rows rather than implied by absence, which keeps the table's one rule intact:
a missing row means no access, always, with no exception to remember. Revoking one therefore
works like revoking anything else — the row goes and nothing puts it back. Done in bootstrap
because that happens exactly once per install, so seeding can never fight a later revocation.
Non-fatal: an owner whose roles hold nothing is a one-click fix, while failing bootstrap over
it leaves a platform with no account at all.

`app` capabilities are deliberately not defaulted — they reach data the owner may not intend
to share, and each needs a sidecar before it means anything.

SCREEN. Role selection is tabs rather than a dropdown: three roles are the axis you move
along, and a select hid two of them behind a click while giving no sense of which one you are
editing. Row descriptions are gone — with three rows called Terminal, Chat and Files they
explained nothing — and the "needs a Linux account" warning went with them, since every
account now gets one at creation, so it was noise about a state that no longer occurs on its
own. `needsOsAccount` is removed from the API too, not just hidden.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
2026-08-11 20:25:31 +00:00
co-authored by Claude Opus 5
parent aaeb3424ab
commit f0af7237db
4 changed files with 60 additions and 30 deletions
@@ -20,8 +20,6 @@ type CapabilityInfo = {
description: string;
routes: string[];
hasPersonalWrites: boolean;
/** Confined: the grant does nothing until the member has a Linux account on this machine. */
needsOsAccount: boolean;
};
type Grant = { role: string; capability: string; level: 'read' | 'write' };
@@ -101,21 +99,27 @@ export const PermissionsSection = () => {
return (
<div className="flex flex-col gap-5 p-1">
{/* Tabs rather than a dropdown. There are three roles and they are the axis you move along — a select
hides two of them behind a click and gives no sense of "which one am I editing" at a glance. Real
buttons, because switching role mutates a draft rather than navigating. */}
<div className="flex flex-wrap items-center justify-between gap-3">
<div className="flex items-center gap-3">
<span className="text-sm text-muted-foreground">Role</span>
<Select value={activeRole ?? undefined} onValueChange={(value) => setRole(value)}>
<SelectTrigger className="w-44">
<SelectValue />
</SelectTrigger>
<SelectContent>
{data.roles.map((r) => (
<SelectItem key={r} value={r}>
{r}
</SelectItem>
))}
</SelectContent>
</Select>
<div className="flex items-center gap-1 rounded-lg border p-1" role="tablist" aria-label="Role">
{data.roles.map((r) => (
<button
key={r}
type="button"
role="tab"
aria-selected={activeRole === r}
onClick={() => setRole(r)}
className={`rounded-md px-3 py-1.5 text-sm transition-colors ${
activeRole === r
? 'bg-accent font-medium text-accent-foreground'
: 'text-muted-foreground hover:bg-accent/50'
}`}
>
{r}
</button>
))}
</div>
<Button onClick={save} disabled={!dirty || saving}>
{saving && <Loader2 className="mr-2 h-4 w-4 animate-spin" />}
@@ -134,17 +138,11 @@ export const PermissionsSection = () => {
const level = draft[capability.key] ?? 'none';
return (
<div key={capability.key} className="flex items-center justify-between gap-4 p-3">
<div className="min-w-0">
<div className="text-sm font-medium">{capability.label}</div>
<div className="text-xs text-muted-foreground">{capability.description}</div>
{/* Said on the row rather than in a footnote, because the grant genuinely does nothing
without it and the fix is on the Accounts tab two clicks away. */}
{capability.needsOsAccount && (
<div className="mt-0.5 text-xs text-amber-500">
Needs a Linux account grant does nothing until the member has one
</div>
)}
</div>
{/* Label only. The descriptions went because with three rows called Terminal, Chat and Files
they explained nothing anyone needed — and the "needs a Linux account" line went with them:
every account gets one at creation, so warning about it on every row was noise about a state
that no longer occurs on its own. */}
<div className="min-w-0 text-sm font-medium">{capability.label}</div>
<Select
value={level}
onValueChange={(value) => setDraft((prev) => ({ ...prev, [capability.key]: value as Level }))}