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
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,7 @@ export function LinkedEnvironmentsSettings({
) : links.data.links.length === 0 ? (
<SettingsRow
title="No linked environments"
description="Link another machine running T3 Code, such as a VPS, so agents here can launch and drive threads there."
description="Let agents here start and drive threads on another machine, such as a VPS."
/>
) : (
links.data.links.map((link) => (
Expand Down Expand Up @@ -260,8 +260,8 @@ function LinkEnvironmentAction({ environmentId }: { readonly environmentId: Envi
))}
</RadioGroup>
<p className="text-xs text-muted-foreground">
An agent here never gets more there than its own mode here either. Threads it starts
there cannot change that environment's own threads, projects or settings.
Agents never exceed their mode here and can't change that environment's own threads,
projects or settings.
</p>
</div>
</form>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -235,7 +235,7 @@ export function ProjectDefaultsSettings({ category }: { category: ProjectSetting
settingKeys={["worktreeAllocation"]}
mixed={mixedAllocation}
title="Worktree allocation"
description="On write starts with source tools. Attach a worktree for source changes or terminals. Explicit workspace choices still apply."
description="Attach a worktree at launch, or when a thread first changes source or opens a terminal."
control={
<Select
value={mixedAllocation ? null : settings.worktreeAllocation}
Expand Down
15 changes: 13 additions & 2 deletions apps/web/src/components/settings/SavedEnvironmentRows.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -308,8 +308,19 @@ export function SavedBackendListRow({
}
below={
<>
{enabled ? <JonesUpdateControls environmentId={environmentId} compareWithLaptop /> : null}
<FleetEnrollmentControls environmentId={environmentId} label={environment.label} />
{enabled ? (
<JonesUpdateControls
environmentId={environmentId}
compareWithLaptop
extraAction={
<FleetEnrollmentControls environmentId={environmentId} label={environment.label} />
}
/>
) : (
<div className="mt-1.5 flex justify-end empty:hidden">
<FleetEnrollmentControls environmentId={environmentId} label={environment.label} />
</div>
)}
{diagnosis ? (
<ConnectionDiagnosisSummary
diagnosis={diagnosis}
Expand Down
14 changes: 7 additions & 7 deletions apps/web/src/components/settings/SettingsPanels.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -354,7 +354,7 @@ export function AboutVersionSection() {
window.desktopBridge?.discardUpdate ? (
<SettingsRow
title="Downloaded build"
description="Discard this selection to check for another build. The active app keeps running."
description="Discard it to check for another build. The running app is unaffected."
control={
<Button
size="sm"
Expand Down Expand Up @@ -998,15 +998,15 @@ export function GeneralSettingsPanel() {
/>
<SettingsRow
title="Project and environment order"
description="Arrange projects and environments in Jones Code settings."
description="Arrange projects and environments in the sidebar."
control={
<Link
to="/settings/jones-code"
hash="project-order"
className="text-sm text-primary hover:underline"
<Button
render={<Link to="/settings/jones-code" hash="project-order" />}
size="sm"
variant="outline"
>
Manage order
</Link>
</Button>
}
/>

Expand Down
115 changes: 63 additions & 52 deletions apps/web/src/components/settings/StorageSettings.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -187,6 +187,12 @@ function lastRunSummary(
return entries.map((entry) => `${entry.label}: ${entry.counts}`).join(" · ");
}

const COVERAGE_LABEL = {
declared_cache: "Discard declared caches",
dependency_reproduction: "Discard reproducible dependencies",
format_recognized: "Discard recognized format files",
} as const;

function factsWithKeys<A>(values: readonly A[], identity: (value: A) => string) {
const occurrences = new Map<string, number>();
return values.map((value) => {
Expand Down Expand Up @@ -232,43 +238,6 @@ function CleanupResults({
<span className={`block ${entry.outcome === "failed" ? "text-destructive" : ""}`}>
{entry.reason}
</span>
{entry.retirement?.receiptId && (
<span className="block">
Receipt: {entry.retirement.receiptId} · {entry.retirement.state}
</span>
)}
{factsWithKeys(entry.retirement?.details ?? [], (detail) =>
JSON.stringify([
detail.target,
detail.root,
detail.reason,
detail.fact,
detail.missingFact,
detail.nextAction,
detail.lineage.map(lineageIdentity),
]),
).map(({ value: detail, key }) => (
<span key={key} className="block">
<code>{detail.root}</code>: {detail.fact}{" "}
{detail.missingFact ? `Missing: ${detail.missingFact}. ` : ""}
{detail.nextAction}
{factsWithKeys(detail.lineage, lineageIdentity).map(
({ value: lineage, key }) => (
<span key={key} className="block">
{Object.entries(lineage)
.map(([key, value]) => `${key}: ${value}`)
.join(" · ")}
</span>
),
)}
</span>
))}
{entry.retirement?.outputObligations.map((output) => (
<span key={output.id} className="block">
Output {output.id}: <code>{output.path}</code> · Owner {output.owner} ·{" "}
{output.state}
</span>
))}
<span className="grid grid-cols-[auto_minmax(0,1fr)] gap-x-3 gap-y-1 border-l border-border/60 pl-3">
<span>Machine</span>
<span className="text-foreground/80">{environmentLabel}</span>
Expand Down Expand Up @@ -323,7 +292,55 @@ function CleanupResults({
)}
</>
}
/>
>
{entry.retirement &&
(entry.retirement.receiptId ||
entry.retirement.details.length > 0 ||
entry.retirement.outputObligations.length > 0) && (
<details className="pb-2 text-xs text-muted-foreground wrap-anywhere select-text">
<summary className="cursor-pointer">Retirement details</summary>
<span className="mt-1 block space-y-1">
{entry.retirement.receiptId && (
<span className="block">
Receipt: {entry.retirement.receiptId} · {entry.retirement.state}
</span>
)}
{factsWithKeys(entry.retirement.details, (detail) =>
JSON.stringify([
detail.target,
detail.root,
detail.reason,
detail.fact,
detail.missingFact,
detail.nextAction,
detail.lineage.map(lineageIdentity),
]),
).map(({ value: detail, key }) => (
<span key={key} className="block">
<code>{detail.root}</code>: {detail.fact}{" "}
{detail.missingFact ? `Missing: ${detail.missingFact}. ` : ""}
{detail.nextAction}
{factsWithKeys(detail.lineage, lineageIdentity).map(
({ value: lineage, key }) => (
<span key={key} className="block">
{Object.entries(lineage)
.map(([key, value]) => `${key}: ${value}`)
.join(" · ")}
</span>
),
)}
</span>
))}
{entry.retirement.outputObligations.map((output) => (
<span key={output.id} className="block">
Output {output.id}: <code>{output.path}</code> · Owner {output.owner} ·{" "}
{output.state}
</span>
))}
</span>
</details>
)}
</SettingsRow>
);
})}
{report.omittedCount > 0 && (
Expand Down Expand Up @@ -759,7 +776,7 @@ export function StorageSettingsPanel() {
]
.filter(Boolean)
.join(". ")}
description="Retire worktrees this many days after their thread settles, its settle script durably finishes, and its final checkpoint completes. Set 0 days for the next cleanup. Branches and history are kept. Disabling this rule stops settlement retirement only. To stop all automatic checkout removal, disable every applicable cleanup cause in the effective project and environment settings."
description="Remove worktrees this many days after their thread settles and its final checkpoint completes. Branches and history are kept."
serverScoped={!isProjectScope}
control={
<RetentionControl
Expand Down Expand Up @@ -806,7 +823,7 @@ export function StorageSettingsPanel() {
<>
<SettingsRow
title="Settled checkout scope"
description="Contained allows only commits reachable from another retained reference. Branch-retained also keeps unique commits on the saved branch or recovery references."
description="Contained requires every commit to be reachable elsewhere. Branch retained also accepts the saved branch."
serverScoped={!isProjectScope}
control={
<Select
Expand All @@ -831,18 +848,12 @@ export function StorageSettingsPanel() {
).map((coverage) => (
<SettingsRow
key={coverage}
title={
coverage === "declared_cache"
? "Discard declared caches"
: coverage === "dependency_reproduction"
? "Discard reproducible dependencies"
: "Discard recognized format files"
}
description="Applies only to ignored content that passes the selected class checks. Includes additions; sensitive content and nested repositories stay protected."
title={COVERAGE_LABEL[coverage]}
description="Ignored content only. Sensitive files and nested repositories stay protected."
serverScoped={!isProjectScope}
control={
<Switch
aria-label={`Discard ${coverage}`}
aria-label={COVERAGE_LABEL[coverage]}
checked={settings.worktreeCoveredDiscard.includes(coverage)}
onCheckedChange={(checked) =>
updateWorktree({
Expand All @@ -868,8 +879,8 @@ export function StorageSettingsPanel() {
settings.worktreeKeepWhen === "any-local-files"
? "Keeps any untracked or ignored content."
: settings.worktreeKeepWhen === "tracked-changes"
? "Keeps tracked edits. Individually enumerates up to 10,000 untracked paths and ignored roots for journaled disposal. Sensitive paths require exact confirmation; nested repositories and live consumers stay protected."
: "Keeps tracked edits and untracked files. Ignored roots require producer proof, preservation or the selected coverage classes. Sensitive paths stay protected."
? "Keeps tracked edits. Removes untracked and ignored files; sensitive paths need confirmation."
: "Keeps edits and untracked files. Removes only ignored files that are safe to discard."
}
serverScoped={!isProjectScope}
control={
Expand Down Expand Up @@ -913,7 +924,7 @@ export function StorageSettingsPanel() {
<SettingsRow
title="Install retention"
status={ruleStatus("installRetention")}
description="Propose keeps all install material. Retire permits an exact approved plan after recovery checks; it does not authorize removal by itself. Active and previous recovery pairs stay protected."
description="Propose keeps old installs. Retire allows approved cleanup; recovery installs stay protected."
serverScoped
control={
<Select
Expand Down
42 changes: 40 additions & 2 deletions apps/web/src/jones/fleetUpdates/FleetEnrollmentControls.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,30 @@ vi.mock("../../components/ui/tooltip", () => ({
cloneElement(render, {}, children),
TooltipPopup: () => null,
}));
// Render the automatic-updates popover open so its controls are reachable.
vi.mock("../../components/ui/popover", () => ({
Popover: ({ children }: { children: ReactNode }) => <>{children}</>,
PopoverTrigger: ({ render, children }: { render: ReactElement; children: ReactNode }) =>
cloneElement(render, {}, children),
PopoverPopup: ({ children }: { children: ReactNode }) => <div>{children}</div>,
}));
vi.mock("../../components/ui/switch", () => ({
Switch: ({
checked,
onCheckedChange,
...props
}: ComponentProps<"button"> & { checked: boolean; onCheckedChange: (next: boolean) => void }) => (
<button
{...props}
role="switch"
aria-checked={checked}
onClick={() => onCheckedChange(!checked)}
/>
),
}));
vi.mock("../../components/ui/checkbox", () => ({
Checkbox: () => <input type="checkbox" />,
}));
import { FleetEnrollmentControls } from "./FleetEnrollmentControls";
const environmentId = EnvironmentId.make("selected-service");
const status: FleetHostStatus = {
Expand Down Expand Up @@ -72,8 +96,12 @@ async function mount() {
);
});
}
const button = (label: string) =>
tree!.root.findAllByType("button").find((node) => node.children.join("") === label)!;
const button = (label: "Turn on" | "Turn off") =>
tree!.root
.findAllByType("button")
.find(
(node) => node.props["aria-label"] === `${label} automatic updates for Selected service`,
)!;

describe("fleet enrollment settings", () => {
it("does not enroll from pairing or discovery and requires actual operation protocol support", async () => {
Expand All @@ -83,6 +111,16 @@ describe("fleet enrollment settings", () => {
expect(h.host.mock.calls.every(([, request]) => request.action === "status")).toBe(true);
expect(h.bridge).not.toHaveBeenCalled();
expect(JSON.stringify(tree!.toJSON())).toContain("host setup required");
// The row shows one compact trigger; the switch and explanations live behind it.
expect(
tree!.root
.findAllByType("button")
.some(
(node) =>
node.props["aria-label"] ===
"Automatic updates for Selected service: Off, host setup required",
),
).toBe(true);
});
it("reports an unreachable host as unavailable rather than as needing setup", async () => {
h.host.mockRejectedValue(new Error("offline"));
Expand Down
Loading
Loading