@saadnvd1/agent-os 0.3.839 → 0.3.848
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/app/api/approvals/route.ts +5 -1
- package/app/api/health/config/route.ts +2 -0
- package/app/api/health/route.ts +4 -2
- package/app/globals.css +6 -0
- package/components/Audits/AuditsPanel.tsx +42 -35
- package/components/Chat/quote-selection.test.ts +6 -0
- package/components/Chat/quote-selection.ts +13 -10
- package/components/Devices/ConnectCard.tsx +52 -56
- package/components/Devices/DeviceGroupRow.tsx +65 -0
- package/components/Devices/DeviceRow.tsx +2 -2
- package/components/Devices/DevicesPanel.tsx +63 -49
- package/components/Devices/NetworkSection.tsx +29 -26
- package/components/Devices/TailscaleCard.tsx +9 -10
- package/components/Devices/group.test.ts +66 -0
- package/components/Devices/group.ts +67 -0
- package/components/Health/HealthPanel.tsx +5 -81
- package/components/Health/MachineChecks.tsx +20 -0
- package/components/Health/SweepRuns.tsx +46 -51
- package/components/Health/Thresholds.tsx +50 -0
- package/components/Merge/GlobalMergeSection.tsx +5 -6
- package/components/Merge/MergeApprovalToggle.tsx +20 -14
- package/components/Merge/MergeSettingsFields.tsx +0 -2
- package/components/Merge/WorktreeSweepSection.tsx +15 -34
- package/components/Models/AgentRoleCard.tsx +7 -7
- package/components/Models/ModelSelect.tsx +14 -3
- package/components/Models/ModelsSection.tsx +42 -37
- package/components/Models/ProviderCard.tsx +1 -1
- package/components/Models/RoleModelSelect.tsx +15 -3
- package/components/Models/SubagentModels.tsx +60 -45
- package/components/PhoneNotify/PhoneNotifyForm.tsx +6 -13
- package/components/Recipes/RecipesPanel.tsx +20 -9
- package/components/Recipes/RunHistory.tsx +1 -1
- package/components/Schedules/SchedulesPanel.tsx +12 -10
- package/components/SessionList/GroupShelves.tsx +8 -3
- package/components/SessionList/OrchestratorPinRow.tsx +2 -2
- package/components/SessionList/RowParts.tsx +39 -35
- package/components/SessionList/RowRename.test.ts +86 -17
- package/components/SessionList/RowShell.tsx +2 -5
- package/components/SessionList/SessionRow.tsx +0 -1
- package/components/Settings/AppWideLimits.tsx +2 -2
- package/components/Settings/{NoApprovalsSection.test.ts → ApprovalsSection.test.ts} +61 -22
- package/components/Settings/ApprovalsSection.tsx +93 -0
- package/components/Settings/ExperimentsSection.test.ts +20 -4
- package/components/Settings/ExperimentsSection.tsx +54 -34
- package/components/Settings/GeneralSection.tsx +39 -0
- package/components/Settings/ListSection.test.ts +85 -0
- package/components/Settings/ListSection.tsx +132 -0
- package/components/Settings/NotificationsSection.tsx +31 -29
- package/components/Settings/PickerSetting.tsx +5 -1
- package/components/Settings/SettingRow.tsx +91 -49
- package/components/Settings/SettingsDialog.test.ts +31 -4
- package/components/Settings/SettingsDialog.tsx +22 -17
- package/components/Settings/ValueLabel.tsx +31 -0
- package/components/Settings/settings-fields.test.ts +81 -2
- package/components/Settings/settings-fields.tsx +28 -27
- package/components/Shares/SharedLinksPanel.tsx +11 -14
- package/components/Updates/AutoUpdates.tsx +12 -8
- package/components/Updates/UpdatesSection.tsx +18 -17
- package/components/Workspaces/HqFolderForm.tsx +21 -19
- package/components/Workspaces/McpServersForm.tsx +20 -28
- package/components/Workspaces/ProjectTiersForm.tsx +25 -12
- package/components/Workspaces/RuleEditor.tsx +86 -0
- package/components/Workspaces/RulesForm.test.ts +58 -31
- package/components/Workspaces/RulesForm.tsx +129 -58
- package/components/Workspaces/WorkspaceModeForm.tsx +3 -3
- package/components/Workspaces/WorkspaceModelsForm.tsx +40 -41
- package/components/Workspaces/WorkspacePanel.tsx +73 -0
- package/components/Workspaces/WorkspacePicker.tsx +44 -0
- package/components/Workspaces/WorkspaceSettings.test.ts +153 -0
- package/components/Workspaces/WorkspaceSettings.tsx +51 -79
- package/components/Workspaces/useSettingsWorkspace.ts +47 -0
- package/components/ui/switch.tsx +1 -1
- package/content/whats-new.json +40 -0
- package/data/approvals/index.ts +2 -2
- package/data/health/index.ts +3 -0
- package/hooks/useAutoSave.ts +18 -5
- package/hooks/useSettingsUrl.ts +6 -3
- package/lib/approvals/describe.ts +64 -0
- package/lib/away/deploy.ts +34 -7
- package/lib/health/config.ts +70 -17
- package/lib/models/plan-fresh.ts +61 -0
- package/lib/models/plan-step.ts +40 -24
- package/lib/models/providers.ts +1 -1
- package/lib/orchestrator/scope.ts +10 -0
- package/lib/recipes/settle.ts +22 -0
- package/lib/settings-url.ts +14 -2
- package/lib/updates/banner.ts +2 -0
- package/lib/updates/run.ts +9 -0
- package/lib/updates/view.ts +28 -6
- package/lib/worktree-deps/paths.ts +9 -4
- package/lib/worktrees.ts +3 -0
- package/package.json +1 -1
- package/scripts/autodeploy +11 -0
- package/scripts/autodeploy.test.ts +30 -0
- package/scripts/lib/update-verify.mjs +11 -0
- package/scripts/update-run.mjs +29 -0
- package/scripts/update-run.test.ts +62 -4
- package/components/Settings/NoApprovalsSection.tsx +0 -103
- package/components/Settings/SettingToggle.tsx +0 -36
- package/components/Settings/SetupSection.tsx +0 -46
|
@@ -25,11 +25,12 @@ const w = (calls: number) => ({
|
|
|
25
25
|
costUsd: calls * 0.01,
|
|
26
26
|
scripts: 0,
|
|
27
27
|
shown: 0,
|
|
28
|
-
accepted: 1,
|
|
29
|
-
dismissed: 1,
|
|
30
|
-
acceptRate: 0.5,
|
|
28
|
+
accepted: calls ? 1 : 0,
|
|
29
|
+
dismissed: calls ? 1 : 0,
|
|
30
|
+
acceptRate: calls ? 0.5 : null,
|
|
31
31
|
});
|
|
32
32
|
const state = vi.hoisted(() => ({
|
|
33
|
+
zero: false,
|
|
33
34
|
mutateAsync: vi.fn().mockResolvedValue({}),
|
|
34
35
|
}));
|
|
35
36
|
vi.mock("@/data/experiments", () => ({
|
|
@@ -47,7 +48,9 @@ vi.mock("@/data/experiments", () => ({
|
|
|
47
48
|
status: "experimental",
|
|
48
49
|
suggests: true,
|
|
49
50
|
on: false,
|
|
50
|
-
usage:
|
|
51
|
+
usage: state.zero
|
|
52
|
+
? { today: w(0), d7: w(0), d30: w(0) }
|
|
53
|
+
: { today: w(1), d7: w(2), d30: w(3) },
|
|
51
54
|
},
|
|
52
55
|
],
|
|
53
56
|
},
|
|
@@ -62,6 +65,7 @@ let root: Root;
|
|
|
62
65
|
let host: HTMLDivElement;
|
|
63
66
|
beforeEach(() => {
|
|
64
67
|
state.mutateAsync.mockClear();
|
|
68
|
+
state.zero = false;
|
|
65
69
|
host = document.createElement("div");
|
|
66
70
|
document.body.appendChild(host);
|
|
67
71
|
root = createRoot(host);
|
|
@@ -82,6 +86,18 @@ describe("ExperimentsSection", () => {
|
|
|
82
86
|
).toBe("false");
|
|
83
87
|
});
|
|
84
88
|
|
|
89
|
+
it("doesn't tag every card Experimental", () => {
|
|
90
|
+
act(() => root.render(createElement(ExperimentsSection)));
|
|
91
|
+
expect(host.textContent).not.toContain("Experimental");
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
it("shows an empty state instead of a table of zeros", () => {
|
|
95
|
+
state.zero = true;
|
|
96
|
+
act(() => root.render(createElement(ExperimentsSection)));
|
|
97
|
+
expect(host.querySelector("table")).toBeNull();
|
|
98
|
+
expect(host.textContent).toContain("No usage yet");
|
|
99
|
+
});
|
|
100
|
+
|
|
85
101
|
it("turns one on from its toggle", () => {
|
|
86
102
|
act(() => root.render(createElement(ExperimentsSection)));
|
|
87
103
|
act(() => {
|
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
import { useState } from "react";
|
|
4
4
|
import { ArrowLeft } from "lucide-react";
|
|
5
5
|
import { Button } from "@/components/ui/button";
|
|
6
|
+
import { ScrollTable } from "@/components/Settings/ListSection";
|
|
6
7
|
import { SettingSection } from "@/components/Settings/SettingRow";
|
|
7
8
|
import { SwitchSetting } from "@/components/Settings/settings-fields";
|
|
8
9
|
import { formatTokens } from "@/lib/chat/context";
|
|
@@ -23,41 +24,60 @@ const WINDOWS = [
|
|
|
23
24
|
["d30", "30 days"],
|
|
24
25
|
] as const;
|
|
25
26
|
|
|
27
|
+
export const hasUsage = (e: ExperimentView) =>
|
|
28
|
+
WINDOWS.some(([key]) => {
|
|
29
|
+
const w = e.usage[key];
|
|
30
|
+
return (
|
|
31
|
+
w.calls > 0 ||
|
|
32
|
+
w.tokens > 0 ||
|
|
33
|
+
w.costUsd > 0 ||
|
|
34
|
+
w.accepted + w.dismissed > 0
|
|
35
|
+
);
|
|
36
|
+
});
|
|
37
|
+
|
|
26
38
|
function Usage({ e }: { e: ExperimentView }) {
|
|
39
|
+
if (!hasUsage(e))
|
|
40
|
+
return (
|
|
41
|
+
<p className="text-muted-foreground py-1 text-xs">
|
|
42
|
+
No usage yet. It shows here once it has run.
|
|
43
|
+
</p>
|
|
44
|
+
);
|
|
27
45
|
return (
|
|
28
|
-
<
|
|
29
|
-
<
|
|
30
|
-
<
|
|
31
|
-
<
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
<
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
46
|
+
<ScrollTable>
|
|
47
|
+
<table className="w-full text-xs tabular-nums">
|
|
48
|
+
<thead className="text-muted-foreground">
|
|
49
|
+
<tr>
|
|
50
|
+
<th className="py-1 text-left font-normal" />
|
|
51
|
+
<th className="py-1 text-right font-normal">Calls</th>
|
|
52
|
+
<th className="py-1 text-right font-normal">Tokens</th>
|
|
53
|
+
<th className="py-1 text-right font-normal">Cost</th>
|
|
54
|
+
{e.suggests && (
|
|
55
|
+
<th className="py-1 text-right font-normal">Accepted</th>
|
|
56
|
+
)}
|
|
57
|
+
</tr>
|
|
58
|
+
</thead>
|
|
59
|
+
<tbody>
|
|
60
|
+
{WINDOWS.map(([key, label]) => {
|
|
61
|
+
const w = e.usage[key];
|
|
62
|
+
return (
|
|
63
|
+
<tr key={key} className="border-border border-t">
|
|
64
|
+
<td className="py-1">{label}</td>
|
|
65
|
+
<td className="py-1 text-right">{w.calls}</td>
|
|
66
|
+
<td className="py-1 text-right">{formatTokens(w.tokens)}</td>
|
|
67
|
+
<td className="py-1 text-right">{usd(w.costUsd)}</td>
|
|
68
|
+
{e.suggests && (
|
|
69
|
+
<td className="py-1 text-right">
|
|
70
|
+
{pct(w.acceptRate)}
|
|
71
|
+
{w.accepted + w.dismissed > 0 &&
|
|
72
|
+
` (${w.accepted}/${w.accepted + w.dismissed})`}
|
|
73
|
+
</td>
|
|
74
|
+
)}
|
|
75
|
+
</tr>
|
|
76
|
+
);
|
|
77
|
+
})}
|
|
78
|
+
</tbody>
|
|
79
|
+
</table>
|
|
80
|
+
</ScrollTable>
|
|
61
81
|
);
|
|
62
82
|
}
|
|
63
83
|
|
|
@@ -197,7 +217,7 @@ export function ExperimentsSection() {
|
|
|
197
217
|
<SettingSection key={e.id}>
|
|
198
218
|
<SwitchSetting
|
|
199
219
|
id={`experiment-${e.id}`}
|
|
200
|
-
label={
|
|
220
|
+
label={e.status === "beta" ? `${e.name} · Beta` : e.name}
|
|
201
221
|
help={e.description}
|
|
202
222
|
checked={e.on}
|
|
203
223
|
locked={data.locked}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { Button } from "@/components/ui/button";
|
|
4
|
+
import { settingsUiActions } from "@/stores/settingsUi";
|
|
5
|
+
import { welcomeUiActions } from "@/stores/welcomeUi";
|
|
6
|
+
import { AppWideLimits } from "./AppWideLimits";
|
|
7
|
+
import { OwnerNameRow } from "./OwnerNameRow";
|
|
8
|
+
import { SettingRow, SettingSection } from "./SettingRow";
|
|
9
|
+
|
|
10
|
+
// The app-wide basics: who you are, alerts that apply everywhere, and the
|
|
11
|
+
// first-run setup again.
|
|
12
|
+
export function GeneralSection() {
|
|
13
|
+
return (
|
|
14
|
+
<div className="space-y-5">
|
|
15
|
+
<OwnerNameRow />
|
|
16
|
+
<AppWideLimits />
|
|
17
|
+
<SettingSection>
|
|
18
|
+
<SettingRow
|
|
19
|
+
id="first-run-setup"
|
|
20
|
+
label="First-run setup"
|
|
21
|
+
help="Agents, your first project, and a first session."
|
|
22
|
+
control={
|
|
23
|
+
<Button
|
|
24
|
+
size="sm"
|
|
25
|
+
variant="outline"
|
|
26
|
+
className="min-h-11 sm:min-h-8"
|
|
27
|
+
onClick={() => {
|
|
28
|
+
settingsUiActions.close();
|
|
29
|
+
welcomeUiActions.open("agents");
|
|
30
|
+
}}
|
|
31
|
+
>
|
|
32
|
+
Run setup again
|
|
33
|
+
</Button>
|
|
34
|
+
}
|
|
35
|
+
/>
|
|
36
|
+
</SettingSection>
|
|
37
|
+
</div>
|
|
38
|
+
);
|
|
39
|
+
}
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import "@/components/Chat/composer/test-dom";
|
|
2
|
+
import { act, createElement } from "react";
|
|
3
|
+
import { createRoot, type Root } from "react-dom/client";
|
|
4
|
+
import { afterEach, describe, expect, it } from "vitest";
|
|
5
|
+
import { ListSection, ScrollTable } from "./ListSection";
|
|
6
|
+
|
|
7
|
+
(
|
|
8
|
+
globalThis as { IS_REACT_ACT_ENVIRONMENT?: boolean }
|
|
9
|
+
).IS_REACT_ACT_ENVIRONMENT = true;
|
|
10
|
+
|
|
11
|
+
let root: Root;
|
|
12
|
+
let host: HTMLElement;
|
|
13
|
+
afterEach(async () => {
|
|
14
|
+
await act(async () => root.unmount());
|
|
15
|
+
host.remove();
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
const items = (n: number) => Array.from({ length: n }, (_, i) => `item ${i}`);
|
|
19
|
+
|
|
20
|
+
async function mount(
|
|
21
|
+
props: Partial<Parameters<typeof ListSection<string>>[0]>
|
|
22
|
+
) {
|
|
23
|
+
host = document.createElement("div");
|
|
24
|
+
document.body.appendChild(host);
|
|
25
|
+
root = createRoot(host);
|
|
26
|
+
await act(async () =>
|
|
27
|
+
root.render(
|
|
28
|
+
createElement(ListSection<string>, {
|
|
29
|
+
title: "Things",
|
|
30
|
+
items: [],
|
|
31
|
+
keyOf: (s) => s,
|
|
32
|
+
render: (s) => createElement("span", null, s),
|
|
33
|
+
empty: "Nothing here.",
|
|
34
|
+
...props,
|
|
35
|
+
})
|
|
36
|
+
)
|
|
37
|
+
);
|
|
38
|
+
}
|
|
39
|
+
const click = (text: string) =>
|
|
40
|
+
act(async () => {
|
|
41
|
+
[...host.querySelectorAll("button")]
|
|
42
|
+
.find((b) => b.textContent?.includes(text))!
|
|
43
|
+
.click();
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
describe("ListSection", () => {
|
|
47
|
+
it("shows the empty state and a zero count", async () => {
|
|
48
|
+
await mount({});
|
|
49
|
+
expect(host.textContent).toContain("Nothing here.");
|
|
50
|
+
expect(host.querySelector("h3")!.textContent).toBe("Things0");
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
it("shows the count and only the first 10, then Show all", async () => {
|
|
54
|
+
await mount({ items: items(25) });
|
|
55
|
+
expect(host.querySelector("h3")!.textContent).toBe("Things25");
|
|
56
|
+
expect(host.querySelectorAll("li")).toHaveLength(10);
|
|
57
|
+
await click("Show all 25");
|
|
58
|
+
expect(host.querySelectorAll("li")).toHaveLength(25);
|
|
59
|
+
await click("Show fewer");
|
|
60
|
+
expect(host.querySelectorAll("li")).toHaveLength(10);
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
it("has no Show all when everything fits", async () => {
|
|
64
|
+
await mount({ items: items(10) });
|
|
65
|
+
expect(host.querySelectorAll("button")).toHaveLength(0);
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
it("keeps older items behind their own toggle and counts them", async () => {
|
|
69
|
+
await mount({ items: items(2), older: ["old a", "old b", "old c"] });
|
|
70
|
+
expect(host.querySelector("h3")!.textContent).toBe("Things5");
|
|
71
|
+
expect(host.textContent).not.toContain("old a");
|
|
72
|
+
await click("Older (3)");
|
|
73
|
+
expect(host.textContent).toContain("old c");
|
|
74
|
+
});
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
describe("ScrollTable", () => {
|
|
78
|
+
it("scrolls in its own box", async () => {
|
|
79
|
+
host = document.createElement("div");
|
|
80
|
+
document.body.appendChild(host);
|
|
81
|
+
root = createRoot(host);
|
|
82
|
+
await act(async () => root.render(createElement(ScrollTable, null, "x")));
|
|
83
|
+
expect(host.firstElementChild!.className).toContain("overflow-x-auto");
|
|
84
|
+
});
|
|
85
|
+
});
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { useState, type ReactNode } from "react";
|
|
4
|
+
import { cn } from "@/lib/utils";
|
|
5
|
+
|
|
6
|
+
export const LIST_SECTION_LIMIT = 10;
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* A long list on a Settings page: a title with the count, the first
|
|
10
|
+
* `limit` items, "Show all" for the rest, and an empty state. Older items
|
|
11
|
+
* can go in a group of their own (`older`), collapsed until asked for.
|
|
12
|
+
*/
|
|
13
|
+
export function ListSection<T>({
|
|
14
|
+
title,
|
|
15
|
+
help,
|
|
16
|
+
items,
|
|
17
|
+
keyOf,
|
|
18
|
+
render,
|
|
19
|
+
empty,
|
|
20
|
+
limit = LIST_SECTION_LIMIT,
|
|
21
|
+
older,
|
|
22
|
+
olderLabel = "Older",
|
|
23
|
+
action,
|
|
24
|
+
className,
|
|
25
|
+
}: {
|
|
26
|
+
title: string;
|
|
27
|
+
help?: string;
|
|
28
|
+
items: readonly T[];
|
|
29
|
+
keyOf: (item: T) => string;
|
|
30
|
+
render: (item: T) => ReactNode;
|
|
31
|
+
empty: string;
|
|
32
|
+
limit?: number;
|
|
33
|
+
// Items that are shown only under their own "Show <label>" toggle.
|
|
34
|
+
older?: readonly T[];
|
|
35
|
+
olderLabel?: string;
|
|
36
|
+
action?: ReactNode;
|
|
37
|
+
className?: string;
|
|
38
|
+
}) {
|
|
39
|
+
const [all, setAll] = useState(false);
|
|
40
|
+
const [showOlder, setShowOlder] = useState(false);
|
|
41
|
+
const shown = all ? items : items.slice(0, limit);
|
|
42
|
+
const hidden = items.length - shown.length;
|
|
43
|
+
const oldCount = older?.length ?? 0;
|
|
44
|
+
const count = items.length + oldCount;
|
|
45
|
+
return (
|
|
46
|
+
<section className={cn("mb-5", className)} data-list-section={title}>
|
|
47
|
+
<div className="flex min-h-8 items-center gap-2">
|
|
48
|
+
<h3 className="text-sm font-semibold">
|
|
49
|
+
{title}
|
|
50
|
+
<span className="text-muted-foreground ml-1.5 font-normal tabular-nums">
|
|
51
|
+
{count}
|
|
52
|
+
</span>
|
|
53
|
+
</h3>
|
|
54
|
+
{action && <div className="ml-auto">{action}</div>}
|
|
55
|
+
</div>
|
|
56
|
+
{help && <p className="text-muted-foreground mb-2 text-xs">{help}</p>}
|
|
57
|
+
{count === 0 ? (
|
|
58
|
+
<p className="bg-muted/40 text-muted-foreground rounded-xl px-3 py-4 text-sm">
|
|
59
|
+
{empty}
|
|
60
|
+
</p>
|
|
61
|
+
) : (
|
|
62
|
+
<div className="bg-muted/40 overflow-hidden rounded-xl">
|
|
63
|
+
<ul className="divide-foreground/5 divide-y">
|
|
64
|
+
{shown.map((item) => (
|
|
65
|
+
<li key={keyOf(item)}>{render(item)}</li>
|
|
66
|
+
))}
|
|
67
|
+
</ul>
|
|
68
|
+
{hidden > 0 && (
|
|
69
|
+
<ShowButton onClick={() => setAll(true)}>
|
|
70
|
+
Show all {items.length}
|
|
71
|
+
</ShowButton>
|
|
72
|
+
)}
|
|
73
|
+
{all && items.length > limit && (
|
|
74
|
+
<ShowButton onClick={() => setAll(false)}>Show fewer</ShowButton>
|
|
75
|
+
)}
|
|
76
|
+
{oldCount > 0 && (
|
|
77
|
+
<>
|
|
78
|
+
<ShowButton onClick={() => setShowOlder((v) => !v)}>
|
|
79
|
+
{showOlder
|
|
80
|
+
? `Hide ${olderLabel}`
|
|
81
|
+
: `${olderLabel} (${oldCount})`}
|
|
82
|
+
</ShowButton>
|
|
83
|
+
{showOlder && (
|
|
84
|
+
<ul className="divide-foreground/5 divide-y">
|
|
85
|
+
{older!.map((item) => (
|
|
86
|
+
<li key={keyOf(item)}>{render(item)}</li>
|
|
87
|
+
))}
|
|
88
|
+
</ul>
|
|
89
|
+
)}
|
|
90
|
+
</>
|
|
91
|
+
)}
|
|
92
|
+
</div>
|
|
93
|
+
)}
|
|
94
|
+
</section>
|
|
95
|
+
);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
function ShowButton({
|
|
99
|
+
onClick,
|
|
100
|
+
children,
|
|
101
|
+
}: {
|
|
102
|
+
onClick: () => void;
|
|
103
|
+
children: ReactNode;
|
|
104
|
+
}) {
|
|
105
|
+
return (
|
|
106
|
+
<button
|
|
107
|
+
type="button"
|
|
108
|
+
onClick={onClick}
|
|
109
|
+
className="text-primary border-foreground/5 hover:bg-muted/60 min-h-11 w-full border-t px-3 text-left text-sm"
|
|
110
|
+
>
|
|
111
|
+
{children}
|
|
112
|
+
</button>
|
|
113
|
+
);
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/** A table that scrolls sideways in its own box, not the page. */
|
|
117
|
+
export function ScrollTable({
|
|
118
|
+
children,
|
|
119
|
+
className,
|
|
120
|
+
}: {
|
|
121
|
+
children: ReactNode;
|
|
122
|
+
className?: string;
|
|
123
|
+
}) {
|
|
124
|
+
return (
|
|
125
|
+
<div
|
|
126
|
+
className={cn("max-w-full overflow-x-auto", className)}
|
|
127
|
+
data-scroll-table
|
|
128
|
+
>
|
|
129
|
+
{children}
|
|
130
|
+
</div>
|
|
131
|
+
);
|
|
132
|
+
}
|
|
@@ -3,7 +3,8 @@
|
|
|
3
3
|
import { Button } from "@/components/ui/button";
|
|
4
4
|
import { PhoneNotifyForm } from "@/components/PhoneNotify/PhoneNotifyForm";
|
|
5
5
|
import type { NotificationSettings } from "@/lib/notifications";
|
|
6
|
-
import {
|
|
6
|
+
import { SettingRow, SettingSection } from "./SettingRow";
|
|
7
|
+
import { SwitchSetting } from "./settings-fields";
|
|
7
8
|
|
|
8
9
|
// This browser's alerts live in the page (useNotifications), so they come in
|
|
9
10
|
// as props.
|
|
@@ -21,44 +22,45 @@ export function NotificationsSection({
|
|
|
21
22
|
}) {
|
|
22
23
|
return (
|
|
23
24
|
<div className="space-y-5">
|
|
24
|
-
<
|
|
25
|
-
<
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
25
|
+
<SettingSection title="This browser">
|
|
26
|
+
<SwitchSetting
|
|
27
|
+
id="notify-sound"
|
|
28
|
+
label="Sound"
|
|
29
|
+
help="Play a sound when a session needs you or hits an error."
|
|
29
30
|
checked={browser.settings.sound}
|
|
30
|
-
|
|
31
|
+
defaultValue={true}
|
|
32
|
+
onSave={async (sound) => browser.updateSettings({ sound })}
|
|
31
33
|
/>
|
|
32
34
|
{browser.permissionGranted ? (
|
|
33
|
-
<
|
|
34
|
-
|
|
35
|
-
|
|
35
|
+
<SwitchSetting
|
|
36
|
+
id="notify-browser"
|
|
37
|
+
label="Browser alerts"
|
|
38
|
+
help="Show a system notification when a session needs you and AgentOS isn't in front."
|
|
36
39
|
checked={browser.settings.browserNotifications}
|
|
37
|
-
|
|
40
|
+
onSave={async (browserNotifications) =>
|
|
38
41
|
browser.updateSettings({ browserNotifications })
|
|
39
42
|
}
|
|
40
43
|
/>
|
|
41
44
|
) : (
|
|
42
|
-
<
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
</div>
|
|
45
|
+
<SettingRow
|
|
46
|
+
id="notify-browser"
|
|
47
|
+
label="Browser alerts"
|
|
48
|
+
help="This browser hasn't allowed AgentOS to show notifications."
|
|
49
|
+
control={
|
|
50
|
+
<Button
|
|
51
|
+
size="sm"
|
|
52
|
+
variant="outline"
|
|
53
|
+
className="h-11 md:h-9"
|
|
54
|
+
onClick={() => void browser.requestPermission()}
|
|
55
|
+
>
|
|
56
|
+
Allow
|
|
57
|
+
</Button>
|
|
58
|
+
}
|
|
59
|
+
/>
|
|
58
60
|
)}
|
|
59
|
-
</
|
|
61
|
+
</SettingSection>
|
|
60
62
|
<div className="space-y-2">
|
|
61
|
-
<h3 className="text-sm font-
|
|
63
|
+
<h3 className="text-sm font-semibold">Your phone</h3>
|
|
62
64
|
<PhoneNotifyForm />
|
|
63
65
|
</div>
|
|
64
66
|
</div>
|
|
@@ -18,6 +18,7 @@ export function PickerSetting<T>({
|
|
|
18
18
|
defaultValue,
|
|
19
19
|
hint,
|
|
20
20
|
error,
|
|
21
|
+
resetLabel,
|
|
21
22
|
render,
|
|
22
23
|
below,
|
|
23
24
|
}: {
|
|
@@ -30,6 +31,8 @@ export function PickerSetting<T>({
|
|
|
30
31
|
defaultValue?: T;
|
|
31
32
|
hint?: string;
|
|
32
33
|
error?: string | null;
|
|
34
|
+
// What Reset goes back to, in words.
|
|
35
|
+
resetLabel?: string;
|
|
33
36
|
render: (picker: { value: T; commit: (next: T) => void }) => ReactNode;
|
|
34
37
|
below?: ReactNode;
|
|
35
38
|
}) {
|
|
@@ -40,7 +43,8 @@ export function PickerSetting<T>({
|
|
|
40
43
|
label={label}
|
|
41
44
|
help={help}
|
|
42
45
|
hint={hint}
|
|
43
|
-
error={error}
|
|
46
|
+
error={error ?? auto.error}
|
|
47
|
+
resetLabel={resetLabel}
|
|
44
48
|
status={auto.status}
|
|
45
49
|
onUndo={auto.undo}
|
|
46
50
|
onReset={
|