@saadnvd1/agent-os 0.3.833 → 0.3.839
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/fresh-start/route.test.ts +33 -0
- package/app/api/fresh-start/route.ts +3 -0
- package/app/api/sessions/route.ts +7 -1
- package/app/globals.css +14 -0
- package/components/Chat/ChatPanel.tsx +41 -29
- package/components/Chat/Forks.tsx +16 -1
- package/components/Chat/Messages.tsx +14 -0
- package/components/Chat/WorktreeRemovedNotice.tsx +17 -0
- package/components/Chat/composer/ComposerBody.tsx +10 -1
- package/components/Chat/composer/ForkHeader.tsx +26 -15
- package/components/Chat/fork-draft.test.ts +57 -0
- package/components/Chat/fork-draft.ts +34 -0
- package/components/Devices/DeviceRow.tsx +83 -69
- package/components/Devices/PasskeysSection.test.ts +89 -0
- package/components/Devices/PasskeysSection.tsx +36 -26
- package/components/Health/HealthPanel.tsx +41 -16
- package/components/Merge/GlobalMergeSection.tsx +2 -5
- package/components/Merge/MergeSettingsFields.tsx +39 -77
- package/components/Merge/ProjectMergeSection.tsx +2 -6
- package/components/Merge/WorktreeSweepSection.tsx +40 -14
- package/components/Models/AgentRoleCard.tsx +61 -66
- package/components/Models/AgentRolesSection.tsx +5 -8
- package/components/Models/ModelsSection.tsx +7 -18
- package/components/Models/PriceInput.tsx +59 -0
- package/components/PhoneNotify/PhoneNotifyForm.tsx +37 -58
- package/components/Settings/AppWideLimits.tsx +61 -0
- package/components/Settings/ContextSection.tsx +96 -0
- package/components/Settings/ExperimentsSection.test.ts +9 -7
- package/components/Settings/ExperimentsSection.tsx +22 -23
- package/components/Settings/InlineConfirm.tsx +51 -0
- package/components/Settings/OwnerNameRow.tsx +10 -40
- package/components/Settings/PickerSetting.tsx +59 -0
- package/components/Settings/SettingRow.tsx +131 -0
- package/components/Settings/SettingsDialog.test.ts +1 -0
- package/components/Settings/SettingsDialog.tsx +47 -2
- package/components/Settings/settings-fields.test.ts +103 -0
- package/components/Settings/settings-fields.tsx +427 -0
- package/components/Shares/SharedLinksPanel.tsx +62 -53
- package/components/Workspaces/HqFolderForm.tsx +21 -54
- package/components/Workspaces/McpServersForm.tsx +20 -19
- package/components/Workspaces/ProjectTiersForm.tsx +10 -8
- package/components/Workspaces/RulesForm.test.ts +127 -0
- package/components/Workspaces/RulesForm.tsx +54 -126
- package/components/Workspaces/WorkspaceModelsForm.tsx +27 -26
- package/components/Workspaces/WorkspaceSettings.tsx +61 -24
- package/content/whats-new.json +32 -0
- package/data/fresh-start/index.ts +1 -1
- package/data/workspaces/index.ts +9 -0
- package/hooks/useAutoSave.test.ts +116 -0
- package/hooks/useAutoSave.ts +98 -0
- package/lib/away/window-limits.ts +3 -1
- package/lib/chat/refresh.ts +10 -6
- package/lib/chat/socket.ts +7 -3
- package/lib/chat/stall-limits.ts +3 -1
- package/lib/chat/start-error.ts +13 -0
- package/lib/chat/worker/cwd.ts +77 -0
- package/lib/chat/worker/host.ts +11 -2
- package/lib/chat/worker/main.ts +11 -2
- package/lib/db/types.ts +2 -0
- package/lib/fork/note.ts +6 -0
- package/lib/fork/start.ts +6 -2
- package/lib/fork/types.ts +2 -0
- package/lib/fork/view.ts +14 -1
- package/lib/settings/defaults.ts +27 -0
- package/lib/settings/parse.ts +17 -0
- package/lib/settings-url.ts +1 -0
- package/lib/worktree-sweep/limits.ts +3 -0
- package/lib/worktree-sweep/settings.ts +4 -2
- package/package.json +1 -1
- package/components/Away/AwayWindowForm.tsx +0 -91
- package/components/Workspaces/FreshStartForm.tsx +0 -177
- package/components/Workspaces/StallForm.tsx +0 -101
- package/components/Workspaces/TaskLimitForm.tsx +0 -84
- package/components/Workspaces/fresh-start.ts +0 -19
|
@@ -5,6 +5,8 @@ import { Input } from "@/components/ui/input";
|
|
|
5
5
|
import { useSetSweepDays, useWorktreeSweep } from "@/data/worktree-sweep";
|
|
6
6
|
import { compactTimeAgo, fromSqliteTime } from "@/lib/session-meta";
|
|
7
7
|
import { formatBytes } from "@/lib/share/view";
|
|
8
|
+
import { parseSettingNumber } from "@/lib/settings/parse";
|
|
9
|
+
import { MAX_SWEEP_DAYS } from "@/lib/worktree-sweep/limits";
|
|
8
10
|
|
|
9
11
|
// Days as the box shows them; empty means "inherit".
|
|
10
12
|
function DaysInput({
|
|
@@ -19,24 +21,48 @@ function DaysInput({
|
|
|
19
21
|
onSave: (days: number | null) => void;
|
|
20
22
|
}) {
|
|
21
23
|
const [text, setText] = useState(value === null ? "" : String(value));
|
|
24
|
+
const [error, setError] = useState<string | null>(null);
|
|
22
25
|
const commit = () => {
|
|
23
26
|
const raw = text.trim();
|
|
24
|
-
|
|
25
|
-
if (
|
|
26
|
-
|
|
27
|
+
let next: number | null = null;
|
|
28
|
+
if (raw !== "") {
|
|
29
|
+
const parsed = parseSettingNumber(raw, { min: 0, max: MAX_SWEEP_DAYS });
|
|
30
|
+
if ("error" in parsed) return setError(parsed.error);
|
|
31
|
+
next = parsed.value;
|
|
32
|
+
}
|
|
33
|
+
setError(null);
|
|
34
|
+
if (next !== value) onSave(next);
|
|
27
35
|
};
|
|
28
36
|
return (
|
|
29
|
-
<
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
37
|
+
<span className="flex flex-col gap-1">
|
|
38
|
+
<Input
|
|
39
|
+
aria-label="Days before a settled worktree is removed"
|
|
40
|
+
aria-invalid={!!error}
|
|
41
|
+
inputMode="numeric"
|
|
42
|
+
value={text}
|
|
43
|
+
placeholder={placeholder}
|
|
44
|
+
disabled={disabled}
|
|
45
|
+
onChange={(e) => {
|
|
46
|
+
setText(e.target.value);
|
|
47
|
+
setError(null);
|
|
48
|
+
}}
|
|
49
|
+
onBlur={commit}
|
|
50
|
+
onKeyDown={(e) => {
|
|
51
|
+
if (e.key === "Enter") e.currentTarget.blur();
|
|
52
|
+
if (e.key === "Escape") {
|
|
53
|
+
setText(value === null ? "" : String(value));
|
|
54
|
+
setError(null);
|
|
55
|
+
e.currentTarget.blur();
|
|
56
|
+
}
|
|
57
|
+
}}
|
|
58
|
+
className="h-11 w-24 text-right tabular-nums md:h-9"
|
|
59
|
+
/>
|
|
60
|
+
{error && (
|
|
61
|
+
<span role="alert" className="text-destructive text-xs">
|
|
62
|
+
{error}
|
|
63
|
+
</span>
|
|
64
|
+
)}
|
|
65
|
+
</span>
|
|
40
66
|
);
|
|
41
67
|
}
|
|
42
68
|
|
|
@@ -4,6 +4,7 @@ import { AlertTriangle } from "lucide-react";
|
|
|
4
4
|
import type { AgentRoleView } from "@/lib/models/agent-roles-view";
|
|
5
5
|
import { analystBlock, defaultAnalystLabel } from "@/lib/models/usable";
|
|
6
6
|
import { cn } from "@/lib/utils";
|
|
7
|
+
import { PickerSetting } from "@/components/Settings/PickerSetting";
|
|
7
8
|
import { RoleModelSelect } from "./RoleModelSelect";
|
|
8
9
|
|
|
9
10
|
const SOURCE: Record<string, string> = {
|
|
@@ -16,22 +17,18 @@ const SOURCE: Record<string, string> = {
|
|
|
16
17
|
default: "Default",
|
|
17
18
|
};
|
|
18
19
|
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
// One role: what it does, the model it runs on, where that came from, and
|
|
20
|
+
// One role as settings rows: the model it runs on, where that came from, and
|
|
22
21
|
// whether the model's server answers. A role whose local model is down says so.
|
|
23
22
|
export function AgentRoleCard({
|
|
24
23
|
role,
|
|
25
24
|
scope,
|
|
26
|
-
pending,
|
|
27
25
|
onChange,
|
|
28
26
|
onFallback,
|
|
29
27
|
}: {
|
|
30
28
|
role: AgentRoleView;
|
|
31
29
|
scope: "settings" | "workspace" | "project";
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
onFallback: (ref: string | null) => void;
|
|
30
|
+
onChange: (ref: string | null) => Promise<unknown>;
|
|
31
|
+
onFallback: (ref: string | null) => Promise<unknown>;
|
|
35
32
|
}) {
|
|
36
33
|
const down = role.choice?.availability.state === "down";
|
|
37
34
|
const analyst = role.id === "analyst";
|
|
@@ -45,77 +42,75 @@ export function AgentRoleCard({
|
|
|
45
42
|
? "Same as Gate review"
|
|
46
43
|
: label
|
|
47
44
|
: "Inherit";
|
|
45
|
+
const source = role.binding
|
|
46
|
+
? `${SOURCE[role.binding.source] ?? role.binding.source}${role.choice?.endpoint ? ` · ${role.choice.endpoint}` : ""}`
|
|
47
|
+
: undefined;
|
|
48
48
|
return (
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
"bg-muted/40 space-y-2 rounded-xl px-3 py-3",
|
|
52
|
-
down && "ring-destructive/50 ring-1"
|
|
53
|
-
)}
|
|
54
|
-
>
|
|
55
|
-
<label htmlFor={`agent-role-${role.id}`} className="flex gap-2.5">
|
|
56
|
-
<span className="bg-accent text-primary grid h-8 w-8 shrink-0 place-items-center rounded-lg text-sm font-semibold">
|
|
57
|
-
{initial(role.label)}
|
|
58
|
-
</span>
|
|
59
|
-
<span className="min-w-0">
|
|
60
|
-
<span className="block text-sm font-medium">{role.label}</span>
|
|
61
|
-
<span className="text-muted-foreground block text-xs">
|
|
62
|
-
{role.purpose}
|
|
63
|
-
</span>
|
|
64
|
-
</span>
|
|
65
|
-
</label>
|
|
66
|
-
<RoleModelSelect
|
|
49
|
+
<>
|
|
50
|
+
<PickerSetting<string | null>
|
|
67
51
|
id={`agent-role-${role.id}`}
|
|
52
|
+
label={role.label}
|
|
53
|
+
help={role.purpose}
|
|
68
54
|
value={role.value}
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
55
|
+
defaultValue={null}
|
|
56
|
+
hint={source}
|
|
57
|
+
error={role.error}
|
|
58
|
+
onSave={onChange}
|
|
59
|
+
render={({ value, commit }) => (
|
|
60
|
+
<RoleModelSelect
|
|
61
|
+
id={`agent-role-${role.id}`}
|
|
62
|
+
value={value}
|
|
63
|
+
inheritLabel={
|
|
64
|
+
value === null && role.binding
|
|
65
|
+
? `${inheritLabel} · ${role.binding.ref.split("/").slice(1).join("/")}`
|
|
66
|
+
: inheritLabel
|
|
67
|
+
}
|
|
68
|
+
blockedBy={blockedBy}
|
|
69
|
+
onChange={commit}
|
|
70
|
+
/>
|
|
71
|
+
)}
|
|
72
|
+
below={
|
|
73
|
+
down && (
|
|
74
|
+
<div
|
|
75
|
+
role="alert"
|
|
76
|
+
className={cn(
|
|
77
|
+
"bg-destructive/10 text-foreground ring-destructive/35 my-1 flex gap-2 rounded-lg p-2.5 text-xs ring-1"
|
|
78
|
+
)}
|
|
79
|
+
>
|
|
80
|
+
<AlertTriangle className="text-destructive mt-0.5 h-3.5 w-3.5 shrink-0" />
|
|
81
|
+
<span>
|
|
82
|
+
<b className="font-medium">
|
|
83
|
+
This model's server isn't answering.
|
|
84
|
+
</b>{" "}
|
|
85
|
+
{role.choice?.availability.error
|
|
86
|
+
? `${role.choice.availability.error}. `
|
|
87
|
+
: ""}
|
|
88
|
+
New {role.label} sessions can't start until it is back
|
|
89
|
+
{role.fallback ? ", or you start on the fallback" : ""}.
|
|
90
|
+
</span>
|
|
91
|
+
</div>
|
|
92
|
+
)
|
|
73
93
|
}
|
|
74
|
-
blockedBy={blockedBy}
|
|
75
|
-
disabled={pending}
|
|
76
|
-
onChange={onChange}
|
|
77
94
|
/>
|
|
78
|
-
<p className="text-muted-foreground text-xs">
|
|
79
|
-
{role.error
|
|
80
|
-
? role.error
|
|
81
|
-
: role.binding
|
|
82
|
-
? `${SOURCE[role.binding.source] ?? role.binding.source}${role.choice?.endpoint ? ` · ${role.choice.endpoint}` : ""}`
|
|
83
|
-
: ""}
|
|
84
|
-
</p>
|
|
85
|
-
{down && (
|
|
86
|
-
<div
|
|
87
|
-
role="alert"
|
|
88
|
-
className="bg-destructive/10 text-foreground ring-destructive/35 flex gap-2 rounded-lg p-2.5 text-xs ring-1"
|
|
89
|
-
>
|
|
90
|
-
<AlertTriangle className="text-destructive mt-0.5 h-3.5 w-3.5 shrink-0" />
|
|
91
|
-
<span>
|
|
92
|
-
<b className="font-medium">
|
|
93
|
-
This model's server isn't answering.
|
|
94
|
-
</b>{" "}
|
|
95
|
-
{role.choice?.availability.error
|
|
96
|
-
? `${role.choice.availability.error}. `
|
|
97
|
-
: ""}
|
|
98
|
-
New {role.label} sessions can't start until it is back
|
|
99
|
-
{role.fallback ? ", or you start on the fallback" : ""}.
|
|
100
|
-
</span>
|
|
101
|
-
</div>
|
|
102
|
-
)}
|
|
103
95
|
{scope === "settings" && (
|
|
104
|
-
<
|
|
105
|
-
|
|
106
|
-
|
|
96
|
+
<PickerSetting<string | null>
|
|
97
|
+
id={`agent-role-fallback-${role.id}`}
|
|
98
|
+
label={`${role.label} fallback`}
|
|
99
|
+
value={role.fallback}
|
|
100
|
+
defaultValue={null}
|
|
101
|
+
onSave={onFallback}
|
|
102
|
+
render={({ value, commit }) => (
|
|
107
103
|
<RoleModelSelect
|
|
108
104
|
id={`agent-role-fallback-${role.id}`}
|
|
109
|
-
value={
|
|
105
|
+
value={value}
|
|
110
106
|
inheritLabel="None (don't fall back)"
|
|
111
107
|
claudeOnly={!analyst}
|
|
112
108
|
blockedBy={blockedBy}
|
|
113
|
-
|
|
114
|
-
onChange={onFallback}
|
|
109
|
+
onChange={commit}
|
|
115
110
|
/>
|
|
116
|
-
|
|
117
|
-
|
|
111
|
+
)}
|
|
112
|
+
/>
|
|
118
113
|
)}
|
|
119
|
-
|
|
114
|
+
</>
|
|
120
115
|
);
|
|
121
116
|
}
|
|
@@ -7,6 +7,7 @@ import { useWorkspacesQuery } from "@/data/workspaces";
|
|
|
7
7
|
import type { RoleMap } from "@/lib/models/config";
|
|
8
8
|
import type { RoleScope } from "@/lib/models/agent-roles-view";
|
|
9
9
|
import { cn } from "@/lib/utils";
|
|
10
|
+
import { SettingSection } from "@/components/Settings/SettingRow";
|
|
10
11
|
import { AgentRoleCard } from "./AgentRoleCard";
|
|
11
12
|
|
|
12
13
|
const SCOPES: { id: RoleScope; label: string }[] = [
|
|
@@ -44,13 +45,13 @@ export function AgentRolesSection() {
|
|
|
44
45
|
const roles: RoleMap = { ...saved("value") };
|
|
45
46
|
if (ref) roles[modelRole as keyof RoleMap] = ref;
|
|
46
47
|
else delete roles[modelRole as keyof RoleMap];
|
|
47
|
-
save.
|
|
48
|
+
return save.mutateAsync({ roles });
|
|
48
49
|
};
|
|
49
50
|
const setFallback = (modelRole: string, ref: string | null) => {
|
|
50
51
|
const fallbacks: RoleMap = { ...saved("fallback") };
|
|
51
52
|
if (ref) fallbacks[modelRole as keyof RoleMap] = ref;
|
|
52
53
|
else delete fallbacks[modelRole as keyof RoleMap];
|
|
53
|
-
save.
|
|
54
|
+
return save.mutateAsync({ roles: saved("value"), fallbacks });
|
|
54
55
|
};
|
|
55
56
|
|
|
56
57
|
return (
|
|
@@ -111,22 +112,18 @@ export function AgentRolesSection() {
|
|
|
111
112
|
) : isError ? (
|
|
112
113
|
<p className="text-destructive text-sm">{error.message}</p>
|
|
113
114
|
) : data ? (
|
|
114
|
-
<
|
|
115
|
+
<SettingSection>
|
|
115
116
|
{data.roles.map((r) => (
|
|
116
117
|
<AgentRoleCard
|
|
117
118
|
key={r.id}
|
|
118
119
|
role={r}
|
|
119
120
|
scope={scope}
|
|
120
|
-
pending={save.isPending}
|
|
121
121
|
onChange={(ref) => setRole(r.modelRole, ref)}
|
|
122
122
|
onFallback={(ref) => setFallback(r.modelRole, ref)}
|
|
123
123
|
/>
|
|
124
124
|
))}
|
|
125
|
-
</
|
|
125
|
+
</SettingSection>
|
|
126
126
|
) : null}
|
|
127
|
-
{save.isError && (
|
|
128
|
-
<p className="text-destructive text-xs">{save.error.message}</p>
|
|
129
|
-
)}
|
|
130
127
|
</div>
|
|
131
128
|
);
|
|
132
129
|
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
|
|
3
3
|
import { Button } from "@/components/ui/button";
|
|
4
|
-
import {
|
|
4
|
+
import { PriceInput } from "./PriceInput";
|
|
5
5
|
import { SettingToggle } from "@/components/Settings/SettingToggle";
|
|
6
6
|
import { useModelsQuery, useUpdateModels } from "@/data/models";
|
|
7
7
|
import type { ModelSettings } from "@/lib/models/config";
|
|
@@ -169,28 +169,17 @@ export function ModelsSection() {
|
|
|
169
169
|
<td className="py-1 pr-2 font-mono">{ref}</td>
|
|
170
170
|
{PRICE_FIELDS.map((f) => (
|
|
171
171
|
<td key={f.key} className="py-1 pr-2">
|
|
172
|
-
<
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
defaultValue={price[f.key]}
|
|
177
|
-
aria-label={`${ref} ${f.label}`}
|
|
178
|
-
className="h-11 w-20 tabular-nums md:h-8"
|
|
179
|
-
onBlur={(e) => {
|
|
180
|
-
const v = Number(e.target.value);
|
|
181
|
-
if (
|
|
182
|
-
!Number.isFinite(v) ||
|
|
183
|
-
v < 0 ||
|
|
184
|
-
v === price[f.key]
|
|
185
|
-
)
|
|
186
|
-
return;
|
|
172
|
+
<PriceInput
|
|
173
|
+
label={`${ref} ${f.label}`}
|
|
174
|
+
value={price[f.key]}
|
|
175
|
+
onSave={(v) =>
|
|
187
176
|
save({
|
|
188
177
|
prices: {
|
|
189
178
|
...saved.prices,
|
|
190
179
|
[ref]: { ...price, [f.key]: v },
|
|
191
180
|
},
|
|
192
|
-
})
|
|
193
|
-
}
|
|
181
|
+
})
|
|
182
|
+
}
|
|
194
183
|
/>
|
|
195
184
|
</td>
|
|
196
185
|
))}
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { useState } from "react";
|
|
4
|
+
import { Input } from "@/components/ui/input";
|
|
5
|
+
import { parseSettingNumber } from "@/lib/settings/parse";
|
|
6
|
+
|
|
7
|
+
// A model price in $ per million tokens, saved on blur or Enter. Blank or
|
|
8
|
+
// invalid stays in the box with a message and is never sent (a blank used to
|
|
9
|
+
// save as $0); Esc puts the saved price back.
|
|
10
|
+
export function PriceInput({
|
|
11
|
+
label,
|
|
12
|
+
value,
|
|
13
|
+
onSave,
|
|
14
|
+
}: {
|
|
15
|
+
label: string;
|
|
16
|
+
value: number;
|
|
17
|
+
onSave: (price: number) => void;
|
|
18
|
+
}) {
|
|
19
|
+
const [text, setText] = useState<string | null>(null);
|
|
20
|
+
const [error, setError] = useState<string | null>(null);
|
|
21
|
+
const revert = () => {
|
|
22
|
+
setText(null);
|
|
23
|
+
setError(null);
|
|
24
|
+
};
|
|
25
|
+
return (
|
|
26
|
+
<span className="flex flex-col gap-1">
|
|
27
|
+
<Input
|
|
28
|
+
inputMode="decimal"
|
|
29
|
+
value={text ?? String(value)}
|
|
30
|
+
aria-label={label}
|
|
31
|
+
aria-invalid={!!error}
|
|
32
|
+
className="h-11 w-20 tabular-nums md:h-8"
|
|
33
|
+
onChange={(e) => {
|
|
34
|
+
setText(e.target.value);
|
|
35
|
+
setError(null);
|
|
36
|
+
}}
|
|
37
|
+
onBlur={() => {
|
|
38
|
+
if (text === null) return;
|
|
39
|
+
const parsed = parseSettingNumber(text, { min: 0, integer: false });
|
|
40
|
+
if ("error" in parsed) return setError(parsed.error);
|
|
41
|
+
revert();
|
|
42
|
+
if (parsed.value !== value) onSave(parsed.value);
|
|
43
|
+
}}
|
|
44
|
+
onKeyDown={(e) => {
|
|
45
|
+
if (e.key === "Enter") e.currentTarget.blur();
|
|
46
|
+
if (e.key === "Escape") {
|
|
47
|
+
revert();
|
|
48
|
+
e.currentTarget.blur();
|
|
49
|
+
}
|
|
50
|
+
}}
|
|
51
|
+
/>
|
|
52
|
+
{error && (
|
|
53
|
+
<span role="alert" className="text-destructive text-xs">
|
|
54
|
+
{error}
|
|
55
|
+
</span>
|
|
56
|
+
)}
|
|
57
|
+
</span>
|
|
58
|
+
);
|
|
59
|
+
}
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
|
|
3
|
-
import { useState } from "react";
|
|
4
3
|
import { Button } from "@/components/ui/button";
|
|
5
|
-
import {
|
|
4
|
+
import { SettingSection } from "@/components/Settings/SettingRow";
|
|
5
|
+
import { TextSetting } from "@/components/Settings/settings-fields";
|
|
6
6
|
import {
|
|
7
7
|
useNotifySettings,
|
|
8
8
|
useSaveTelegram,
|
|
@@ -16,11 +16,8 @@ export function PhoneNotifyForm() {
|
|
|
16
16
|
const { data: settings, isPending } = useNotifySettings();
|
|
17
17
|
const save = useSaveTelegram();
|
|
18
18
|
const test = useTestNotify();
|
|
19
|
-
const [token, setToken] = useState("");
|
|
20
|
-
const [chatId, setChatId] = useState<string | null>(null);
|
|
21
19
|
if (isPending || !settings)
|
|
22
20
|
return <div className="bg-muted/40 h-32 animate-pulse rounded-xl" />;
|
|
23
|
-
const chat = chatId ?? settings.telegram.chatId ?? "";
|
|
24
21
|
|
|
25
22
|
return (
|
|
26
23
|
<div className="space-y-5">
|
|
@@ -48,60 +45,45 @@ export function PhoneNotifyForm() {
|
|
|
48
45
|
)}
|
|
49
46
|
</div>
|
|
50
47
|
|
|
51
|
-
<
|
|
52
|
-
className="space-y-3"
|
|
53
|
-
onSubmit={(e) => {
|
|
54
|
-
e.preventDefault();
|
|
55
|
-
save.mutate(
|
|
56
|
-
{
|
|
57
|
-
...(token.trim() && { telegramToken: token.trim() }),
|
|
58
|
-
telegramChatId: chat,
|
|
59
|
-
},
|
|
60
|
-
{ onSuccess: () => setToken("") }
|
|
61
|
-
);
|
|
62
|
-
}}
|
|
63
|
-
>
|
|
48
|
+
<div className="space-y-2">
|
|
64
49
|
<p className="label-mono text-muted-foreground">Telegram</p>
|
|
65
|
-
<
|
|
66
|
-
<
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
50
|
+
<SettingSection>
|
|
51
|
+
<TextSetting
|
|
52
|
+
id="tg-chat"
|
|
53
|
+
label="Chat id"
|
|
54
|
+
help="Where the bot sends alerts: your chat id with it, or @channel."
|
|
55
|
+
placeholder="123456789"
|
|
56
|
+
value={settings.telegram.chatId ?? ""}
|
|
57
|
+
validate={(t) =>
|
|
58
|
+
!t || /^-?\d+$|^@\w+$/.test(t)
|
|
59
|
+
? null
|
|
60
|
+
: "A chat id is a whole number like 123456789, or @channel."
|
|
61
|
+
}
|
|
62
|
+
onSave={(t) => save.mutateAsync({ telegramChatId: t || null })}
|
|
63
|
+
/>
|
|
64
|
+
<TextSetting
|
|
70
65
|
id="tg-token"
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
66
|
+
label="Bot token"
|
|
67
|
+
help="Write-only. Saved with the button, never on blur: a half-typed secret must not save."
|
|
68
|
+
secret
|
|
69
|
+
buttonLabel={settings.telegram.tokenSet ? "Replace" : "Connect"}
|
|
75
70
|
placeholder={
|
|
76
|
-
settings.telegram.tokenSet
|
|
71
|
+
settings.telegram.tokenSet
|
|
72
|
+
? "Connected; paste to replace"
|
|
73
|
+
: "123:ABC…"
|
|
77
74
|
}
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
inputMode="numeric"
|
|
88
|
-
value={chat}
|
|
89
|
-
onChange={(e) => setChatId(e.target.value)}
|
|
90
|
-
placeholder="Your chat id with the bot"
|
|
91
|
-
className="h-11 font-mono md:h-9"
|
|
75
|
+
value=""
|
|
76
|
+
validate={(t) =>
|
|
77
|
+
!t
|
|
78
|
+
? "Paste the bot token from BotFather."
|
|
79
|
+
: /\s/.test(t)
|
|
80
|
+
? "A token has no spaces."
|
|
81
|
+
: null
|
|
82
|
+
}
|
|
83
|
+
onSave={(t) => save.mutateAsync({ telegramToken: t })}
|
|
92
84
|
/>
|
|
93
|
-
</
|
|
94
|
-
{save.error && (
|
|
95
|
-
<p className="text-destructive text-sm">{save.error.message}</p>
|
|
96
|
-
)}
|
|
85
|
+
</SettingSection>
|
|
97
86
|
<div className="flex flex-wrap gap-2">
|
|
98
|
-
<Button
|
|
99
|
-
type="submit"
|
|
100
|
-
className="h-11 md:h-9"
|
|
101
|
-
disabled={save.isPending}
|
|
102
|
-
>
|
|
103
|
-
{save.isPending ? "Saving..." : "Save"}
|
|
104
|
-
</Button>
|
|
105
87
|
{settings.telegram.tokenSet && (
|
|
106
88
|
<Button
|
|
107
89
|
type="button"
|
|
@@ -109,10 +91,7 @@ export function PhoneNotifyForm() {
|
|
|
109
91
|
className="text-muted-foreground h-11 md:h-9"
|
|
110
92
|
disabled={save.isPending}
|
|
111
93
|
onClick={() =>
|
|
112
|
-
save.mutate(
|
|
113
|
-
{ telegramToken: null, telegramChatId: null },
|
|
114
|
-
{ onSuccess: () => setChatId(null) }
|
|
115
|
-
)
|
|
94
|
+
save.mutate({ telegramToken: null, telegramChatId: null })
|
|
116
95
|
}
|
|
117
96
|
>
|
|
118
97
|
Remove bot
|
|
@@ -136,7 +115,7 @@ export function PhoneNotifyForm() {
|
|
|
136
115
|
{testSummary(test.data)}
|
|
137
116
|
</p>
|
|
138
117
|
)}
|
|
139
|
-
</
|
|
118
|
+
</div>
|
|
140
119
|
</div>
|
|
141
120
|
);
|
|
142
121
|
}
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { useAwaySettingsQuery, useUpdateAwaySettings } from "@/data/away";
|
|
4
|
+
import { useStallQuery, useUpdateStall } from "@/data/stall";
|
|
5
|
+
import { MAX_STALL_MINUTES, MIN_STALL_MINUTES } from "@/lib/chat/stall-limits";
|
|
6
|
+
import {
|
|
7
|
+
MAX_WINDOW_MINUTES,
|
|
8
|
+
MIN_WINDOW_MINUTES,
|
|
9
|
+
} from "@/lib/away/window-limits";
|
|
10
|
+
import { SETTING_DEFAULTS } from "@/lib/settings/defaults";
|
|
11
|
+
import { SettingSection } from "./SettingRow";
|
|
12
|
+
import { NumberSetting } from "./settings-fields";
|
|
13
|
+
|
|
14
|
+
// Stall alerts and the Away window: app-wide, each saved on its own.
|
|
15
|
+
export function AppWideLimits() {
|
|
16
|
+
const stall = useStallQuery();
|
|
17
|
+
const away = useAwaySettingsQuery();
|
|
18
|
+
const updateStall = useUpdateStall();
|
|
19
|
+
const updateAway = useUpdateAwaySettings();
|
|
20
|
+
return (
|
|
21
|
+
<SettingSection
|
|
22
|
+
title="All workspaces"
|
|
23
|
+
help="Stall alerts and Away mode apply across every workspace."
|
|
24
|
+
>
|
|
25
|
+
{stall.data && (
|
|
26
|
+
<NumberSetting
|
|
27
|
+
id="stall-minutes"
|
|
28
|
+
label="Flag stalled turns"
|
|
29
|
+
help="Warn, and tell the orchestrator, when a turn makes no progress for this long."
|
|
30
|
+
value={stall.data.minutes}
|
|
31
|
+
defaultValue={SETTING_DEFAULTS.stallMinutes}
|
|
32
|
+
onSave={(n) => updateStall.mutateAsync(n)}
|
|
33
|
+
min={MIN_STALL_MINUTES}
|
|
34
|
+
max={MAX_STALL_MINUTES}
|
|
35
|
+
unit="minutes"
|
|
36
|
+
short=" min"
|
|
37
|
+
optional={{ off: 0, on: SETTING_DEFAULTS.stallMinutes }}
|
|
38
|
+
/>
|
|
39
|
+
)}
|
|
40
|
+
{away.data && (
|
|
41
|
+
<NumberSetting
|
|
42
|
+
id="away-window"
|
|
43
|
+
label="Away window"
|
|
44
|
+
help="Away mode only works through sessions with a real turn this recently."
|
|
45
|
+
value={away.data.windowMinutes}
|
|
46
|
+
defaultValue={SETTING_DEFAULTS.awayWindowMinutes}
|
|
47
|
+
onSave={(n) => updateAway.mutateAsync({ windowMinutes: n })}
|
|
48
|
+
min={MIN_WINDOW_MINUTES}
|
|
49
|
+
max={MAX_WINDOW_MINUTES}
|
|
50
|
+
unit="minutes"
|
|
51
|
+
short=" min"
|
|
52
|
+
/>
|
|
53
|
+
)}
|
|
54
|
+
{(stall.isError || away.isError) && (
|
|
55
|
+
<p className="text-destructive px-3 py-2 text-sm">
|
|
56
|
+
{stall.error?.message ?? away.error?.message}
|
|
57
|
+
</p>
|
|
58
|
+
)}
|
|
59
|
+
</SettingSection>
|
|
60
|
+
);
|
|
61
|
+
}
|