@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
package/lib/fork/note.ts
ADDED
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
/** The one line the parent's chat keeps where forks were started. */
|
|
2
|
+
|
|
3
|
+
export const FORK_NOTE_PREFIX = "fork-";
|
|
4
|
+
|
|
5
|
+
export const startedText = (ns: number[], labels: string[]): string =>
|
|
6
|
+
`${ns.length === 1 ? `Fork ${ns[0]}` : `Forks ${ns.join(", ")}`} started · ${labels.join(" · ")}`;
|
package/lib/fork/start.ts
CHANGED
|
@@ -18,6 +18,7 @@ import { getProject } from "../projects";
|
|
|
18
18
|
import { limitOf, runningIn, startingIn } from "../tasks/queue-rows";
|
|
19
19
|
import { currentLevel, loadEnabled } from "../load/monitor";
|
|
20
20
|
import { childFirstMessage } from "./brief";
|
|
21
|
+
import { FORK_NOTE_PREFIX, startedText } from "./note";
|
|
21
22
|
import { ForkError, forkConversation } from "./conversation";
|
|
22
23
|
import {
|
|
23
24
|
childrenOf,
|
|
@@ -120,7 +121,7 @@ export function startFork(req: ForkRequest): ForkStarted {
|
|
|
120
121
|
instruction,
|
|
121
122
|
worktree,
|
|
122
123
|
}));
|
|
123
|
-
const noteId =
|
|
124
|
+
const noteId = `${FORK_NOTE_PREFIX}${groupId}`;
|
|
124
125
|
insertGroup({
|
|
125
126
|
id: groupId,
|
|
126
127
|
parentId: parent.id,
|
|
@@ -136,7 +137,10 @@ export function startFork(req: ForkRequest): ForkStarted {
|
|
|
136
137
|
kind: "note" as const,
|
|
137
138
|
tone: "note" as const,
|
|
138
139
|
createdAt: Date.now(),
|
|
139
|
-
text:
|
|
140
|
+
text: startedText(
|
|
141
|
+
children.map((c) => numberOf(parent.id, c.childId)),
|
|
142
|
+
children.map((c) => c.label)
|
|
143
|
+
),
|
|
140
144
|
};
|
|
141
145
|
saveItem(parent.id, item);
|
|
142
146
|
emit(parent.id, { type: "item", item });
|
package/lib/fork/types.ts
CHANGED
package/lib/fork/view.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
/** What clients and `aos children` show, and the numbers behind them. */
|
|
2
2
|
|
|
3
|
+
import { lastItems } from "../chat/store";
|
|
3
4
|
import { childrenOf, getGroup } from "./ledger";
|
|
4
5
|
import type { ChildRow, ChildView, GroupView } from "./types";
|
|
5
6
|
|
|
@@ -21,8 +22,16 @@ export const childView = (c: ChildRow): ChildView => ({
|
|
|
21
22
|
settledAt: c.settled_at,
|
|
22
23
|
delivered: c.delivered_at != null,
|
|
23
24
|
asks: c.state === "needs_input" ? c.hold : null,
|
|
25
|
+
activity: null,
|
|
24
26
|
});
|
|
25
27
|
|
|
28
|
+
// The title of the latest tool a running child used, to say what it is doing.
|
|
29
|
+
const activityOf = (c: ChildRow): string | null => {
|
|
30
|
+
if (c.state !== "running") return null;
|
|
31
|
+
const tool = lastItems(c.child_session_id, 1, "tool")[0];
|
|
32
|
+
return tool && tool.kind === "tool" ? tool.title : null;
|
|
33
|
+
};
|
|
34
|
+
|
|
26
35
|
export function childrenView(parentId: string): {
|
|
27
36
|
children: ChildView[];
|
|
28
37
|
groups: GroupView[];
|
|
@@ -45,7 +54,11 @@ export function childrenView(parentId: string): {
|
|
|
45
54
|
});
|
|
46
55
|
}
|
|
47
56
|
return {
|
|
48
|
-
children: rows.map((c, i) => ({
|
|
57
|
+
children: rows.map((c, i) => ({
|
|
58
|
+
...childView(c),
|
|
59
|
+
n: i + 1,
|
|
60
|
+
activity: activityOf(c),
|
|
61
|
+
})),
|
|
49
62
|
groups,
|
|
50
63
|
};
|
|
51
64
|
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
// Every setting's default, in one pure module the server's constants and the
|
|
2
|
+
// settings UI both import, so the two can't disagree (lib/settings/defaults.test).
|
|
3
|
+
// Context sizes are in tokens; 0 is off.
|
|
4
|
+
|
|
5
|
+
export const SETTING_DEFAULTS = {
|
|
6
|
+
fresh: {
|
|
7
|
+
orchestratorTokens: 200_000,
|
|
8
|
+
taskTokens: 0,
|
|
9
|
+
compactTokens: 150_000,
|
|
10
|
+
autoCompactTokens: 300_000,
|
|
11
|
+
interactiveAutoCompactTokens: 300_000,
|
|
12
|
+
},
|
|
13
|
+
// What a switch turns on with when the default is off.
|
|
14
|
+
freshOnValue: 200_000,
|
|
15
|
+
stallMinutes: 8,
|
|
16
|
+
awayWindowMinutes: 60,
|
|
17
|
+
// Off by default; what the switch turns on with.
|
|
18
|
+
taskLimitOn: 3,
|
|
19
|
+
sweepDays: 7,
|
|
20
|
+
} as const;
|
|
21
|
+
|
|
22
|
+
export type FreshKey = keyof typeof SETTING_DEFAULTS.fresh;
|
|
23
|
+
|
|
24
|
+
export function freshOnValue(key: FreshKey): number {
|
|
25
|
+
const d = SETTING_DEFAULTS.fresh[key];
|
|
26
|
+
return d > 0 ? d : SETTING_DEFAULTS.freshOnValue;
|
|
27
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
// A typed number, checked before anything is sent. An invalid one stays in
|
|
2
|
+
// the field with this message; nothing reaches the server.
|
|
3
|
+
export function parseSettingNumber(
|
|
4
|
+
text: string,
|
|
5
|
+
{ min, max, integer = true }: { min: number; max?: number; integer?: boolean }
|
|
6
|
+
): { value: number } | { error: string } {
|
|
7
|
+
const t = text.trim();
|
|
8
|
+
const n = Number(t);
|
|
9
|
+
const range = max === undefined ? `${min} or more` : `from ${min} to ${max}`;
|
|
10
|
+
const msg = integer
|
|
11
|
+
? `Enter a whole number ${range}.`
|
|
12
|
+
: `Enter a number ${range}.`;
|
|
13
|
+
if (!t || !Number.isFinite(n)) return { error: msg };
|
|
14
|
+
if (integer && !Number.isInteger(n)) return { error: msg };
|
|
15
|
+
if (n < min || (max !== undefined && n > max)) return { error: msg };
|
|
16
|
+
return { value: n };
|
|
17
|
+
}
|
package/lib/settings-url.ts
CHANGED
|
@@ -5,10 +5,12 @@
|
|
|
5
5
|
|
|
6
6
|
import { z } from "zod";
|
|
7
7
|
import { db } from "../db";
|
|
8
|
+
import { SETTING_DEFAULTS } from "../settings/defaults";
|
|
9
|
+
import { MAX_SWEEP_DAYS } from "./limits";
|
|
8
10
|
import type { ResolvedDays } from "./types";
|
|
9
11
|
|
|
10
|
-
export const DEFAULT_SWEEP_DAYS =
|
|
11
|
-
export
|
|
12
|
+
export const DEFAULT_SWEEP_DAYS = SETTING_DEFAULTS.sweepDays;
|
|
13
|
+
export { MAX_SWEEP_DAYS } from "./limits";
|
|
12
14
|
|
|
13
15
|
const GLOBAL_KEY = "worktree_sweep";
|
|
14
16
|
const projectKey = (projectId: string) => `worktree_sweep.project.${projectId}`;
|
package/package.json
CHANGED
|
@@ -1,91 +0,0 @@
|
|
|
1
|
-
"use client";
|
|
2
|
-
|
|
3
|
-
import { useState } from "react";
|
|
4
|
-
import { toast } from "sonner";
|
|
5
|
-
import { Button } from "@/components/ui/button";
|
|
6
|
-
import { Input } from "@/components/ui/input";
|
|
7
|
-
import {
|
|
8
|
-
useAwaySettingsQuery,
|
|
9
|
-
useUpdateAwaySettings,
|
|
10
|
-
type AwaySettings,
|
|
11
|
-
} from "@/data/away";
|
|
12
|
-
import {
|
|
13
|
-
MAX_WINDOW_MINUTES,
|
|
14
|
-
MIN_WINDOW_MINUTES,
|
|
15
|
-
parseWindow,
|
|
16
|
-
} from "@/lib/away/window-limits";
|
|
17
|
-
|
|
18
|
-
// Settings → Workspaces: how recent a session's work must be for Away mode
|
|
19
|
-
// to touch it. App-wide.
|
|
20
|
-
export function AwayWindowForm() {
|
|
21
|
-
const { data, isPending, isError, error } = useAwaySettingsQuery();
|
|
22
|
-
if (isPending)
|
|
23
|
-
return <div className="bg-muted/40 h-24 animate-pulse rounded-lg" />;
|
|
24
|
-
if (isError)
|
|
25
|
-
return <p className="text-destructive text-sm">{error.message}</p>;
|
|
26
|
-
return <Form key={data.windowMinutes} saved={data} />;
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
function Form({ saved }: { saved: AwaySettings }) {
|
|
30
|
-
const update = useUpdateAwaySettings();
|
|
31
|
-
const [text, setText] = useState(String(saved.windowMinutes));
|
|
32
|
-
const minutes = parseWindow(text);
|
|
33
|
-
const error =
|
|
34
|
-
minutes === null
|
|
35
|
-
? `A whole number from ${MIN_WINDOW_MINUTES} to ${MAX_WINDOW_MINUTES}`
|
|
36
|
-
: null;
|
|
37
|
-
return (
|
|
38
|
-
<form
|
|
39
|
-
onSubmit={(e) => {
|
|
40
|
-
e.preventDefault();
|
|
41
|
-
if (minutes === null) return;
|
|
42
|
-
update.mutate(
|
|
43
|
-
{ windowMinutes: minutes },
|
|
44
|
-
{
|
|
45
|
-
onSuccess: () => toast.success("Away mode settings saved"),
|
|
46
|
-
onError: (err) => toast.error(err.message),
|
|
47
|
-
}
|
|
48
|
-
);
|
|
49
|
-
}}
|
|
50
|
-
className="bg-muted/40 space-y-3 rounded-lg px-3 py-3"
|
|
51
|
-
>
|
|
52
|
-
<h3 className="text-sm font-medium">Away mode</h3>
|
|
53
|
-
<p className="text-muted-foreground text-sm">
|
|
54
|
-
Away mode works through orchestrators that were busy in this window, and
|
|
55
|
-
only lists sessions with a real turn inside it. Older ones are left
|
|
56
|
-
alone: a message wakes an agent and re-reads its whole conversation.
|
|
57
|
-
</p>
|
|
58
|
-
<label className="flex items-center justify-between gap-3 text-sm">
|
|
59
|
-
<span className="text-muted-foreground">Recent means the last</span>
|
|
60
|
-
<span className="flex items-center gap-2">
|
|
61
|
-
<Input
|
|
62
|
-
type="number"
|
|
63
|
-
inputMode="numeric"
|
|
64
|
-
min={MIN_WINDOW_MINUTES}
|
|
65
|
-
max={MAX_WINDOW_MINUTES}
|
|
66
|
-
step={1}
|
|
67
|
-
value={text}
|
|
68
|
-
onChange={(e) => setText(e.target.value)}
|
|
69
|
-
aria-label="Away window, minutes"
|
|
70
|
-
aria-invalid={!!error}
|
|
71
|
-
className="h-11 w-24 text-base sm:h-9 md:text-sm"
|
|
72
|
-
/>
|
|
73
|
-
<span className="text-muted-foreground">min</span>
|
|
74
|
-
</span>
|
|
75
|
-
</label>
|
|
76
|
-
{error && <p className="text-destructive text-xs">{error}</p>}
|
|
77
|
-
<div className="flex justify-end">
|
|
78
|
-
<Button
|
|
79
|
-
type="submit"
|
|
80
|
-
size="sm"
|
|
81
|
-
className="h-11 sm:h-8"
|
|
82
|
-
disabled={
|
|
83
|
-
!!error || minutes === saved.windowMinutes || update.isPending
|
|
84
|
-
}
|
|
85
|
-
>
|
|
86
|
-
Save
|
|
87
|
-
</Button>
|
|
88
|
-
</div>
|
|
89
|
-
</form>
|
|
90
|
-
);
|
|
91
|
-
}
|
|
@@ -1,177 +0,0 @@
|
|
|
1
|
-
"use client";
|
|
2
|
-
|
|
3
|
-
import { useState } from "react";
|
|
4
|
-
import { toast } from "sonner";
|
|
5
|
-
import { Button } from "@/components/ui/button";
|
|
6
|
-
import { Input } from "@/components/ui/input";
|
|
7
|
-
import { Switch } from "@/components/ui/switch";
|
|
8
|
-
import type { RefreshSettings } from "@/lib/chat/refresh";
|
|
9
|
-
import { useFreshStartQuery, useUpdateFreshStart } from "@/data/fresh-start";
|
|
10
|
-
import { parseFreshStart } from "./fresh-start";
|
|
11
|
-
|
|
12
|
-
// When a chat's agent starts a fresh conversation instead of carrying a
|
|
13
|
-
// long one: app-wide, one threshold for orchestrators and one for tasks.
|
|
14
|
-
export function FreshStartForm() {
|
|
15
|
-
const { data, isPending, isError, error } = useFreshStartQuery();
|
|
16
|
-
if (isPending)
|
|
17
|
-
return <div className="bg-muted/40 h-24 animate-pulse rounded-lg" />;
|
|
18
|
-
if (isError)
|
|
19
|
-
return <p className="text-destructive text-sm">{error.message}</p>;
|
|
20
|
-
return (
|
|
21
|
-
<Form
|
|
22
|
-
key={`${data.orchestratorTokens}:${data.taskTokens}:${data.compactTokens}:${data.autoCompactTokens}:${data.interactiveAutoCompactTokens}`}
|
|
23
|
-
saved={data}
|
|
24
|
-
/>
|
|
25
|
-
);
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
function Form({ saved }: { saved: RefreshSettings }) {
|
|
29
|
-
const update = useUpdateFreshStart();
|
|
30
|
-
const orchestrator = useThreshold(saved.orchestratorTokens, 200);
|
|
31
|
-
const task = useThreshold(saved.taskTokens, 200);
|
|
32
|
-
const compact = useThreshold(saved.compactTokens, 150);
|
|
33
|
-
const midTurn = useThreshold(saved.autoCompactTokens, 300);
|
|
34
|
-
const interactive = useThreshold(saved.interactiveAutoCompactTokens, 200);
|
|
35
|
-
const error =
|
|
36
|
-
orchestrator.error ??
|
|
37
|
-
task.error ??
|
|
38
|
-
compact.error ??
|
|
39
|
-
midTurn.error ??
|
|
40
|
-
interactive.error;
|
|
41
|
-
const dirty =
|
|
42
|
-
!error &&
|
|
43
|
-
(orchestrator.tokens !== saved.orchestratorTokens ||
|
|
44
|
-
task.tokens !== saved.taskTokens ||
|
|
45
|
-
compact.tokens !== saved.compactTokens ||
|
|
46
|
-
midTurn.tokens !== saved.autoCompactTokens ||
|
|
47
|
-
interactive.tokens !== saved.interactiveAutoCompactTokens);
|
|
48
|
-
|
|
49
|
-
return (
|
|
50
|
-
<form
|
|
51
|
-
onSubmit={(e) => {
|
|
52
|
-
e.preventDefault();
|
|
53
|
-
if (error) return;
|
|
54
|
-
update.mutate(
|
|
55
|
-
{
|
|
56
|
-
orchestratorTokens: orchestrator.tokens,
|
|
57
|
-
taskTokens: task.tokens,
|
|
58
|
-
compactTokens: compact.tokens,
|
|
59
|
-
autoCompactTokens: midTurn.tokens,
|
|
60
|
-
interactiveAutoCompactTokens: interactive.tokens,
|
|
61
|
-
},
|
|
62
|
-
{
|
|
63
|
-
onSuccess: () => toast.success("Context settings saved"),
|
|
64
|
-
onError: (err) => toast.error(err.message),
|
|
65
|
-
}
|
|
66
|
-
);
|
|
67
|
-
}}
|
|
68
|
-
className="bg-muted/40 space-y-3 rounded-lg px-3 py-3"
|
|
69
|
-
>
|
|
70
|
-
<h3 className="text-sm font-medium">Fresh starts</h3>
|
|
71
|
-
<p className="text-muted-foreground text-sm">
|
|
72
|
-
When a conversation's context passes the threshold, its agent
|
|
73
|
-
starts a new one at the next turn boundary, with its brief. An
|
|
74
|
-
orchestrator also gets its rules and the workspace's current state,
|
|
75
|
-
so nothing is lost. A task keeps only its brief and its branch.
|
|
76
|
-
</p>
|
|
77
|
-
<ThresholdRow label="Orchestrators" field={orchestrator} />
|
|
78
|
-
<ThresholdRow label="Tasks" field={task} />
|
|
79
|
-
<h3 className="pt-2 text-sm font-medium">Compact</h3>
|
|
80
|
-
<p className="text-muted-foreground text-sm">
|
|
81
|
-
Chats and tasks compact twice over. Mid-turn, Claude Code summarizes its
|
|
82
|
-
own context when a job runs long: set this high, since it happens while
|
|
83
|
-
the agent is holding its files. Between turns, nothing is in flight, so
|
|
84
|
-
AgentOS compacts a chat over the smaller size, or idle past its prompt
|
|
85
|
-
cache, before the next message; the composer offers to first. Either way
|
|
86
|
-
AgentOS hands the agent its task, scope changes, files and todos back.
|
|
87
|
-
Your own sessions, in a chat or a terminal, have their own mid-turn
|
|
88
|
-
size; a terminal can't be compacted between turns, so that is the
|
|
89
|
-
only compaction it gets. Orchestrators start fresh instead.
|
|
90
|
-
</p>
|
|
91
|
-
<ThresholdRow
|
|
92
|
-
label="Mid-turn, tasks"
|
|
93
|
-
field={midTurn}
|
|
94
|
-
switchLabel="Let a task's agent compact itself mid-turn"
|
|
95
|
-
/>
|
|
96
|
-
<ThresholdRow
|
|
97
|
-
label="Mid-turn, your sessions"
|
|
98
|
-
field={interactive}
|
|
99
|
-
switchLabel="Let your own sessions compact themselves mid-turn"
|
|
100
|
-
/>
|
|
101
|
-
<ThresholdRow
|
|
102
|
-
label="Between turns"
|
|
103
|
-
field={compact}
|
|
104
|
-
switchLabel="Compact chats and tasks between turns"
|
|
105
|
-
/>
|
|
106
|
-
{error && <p className="text-destructive text-xs">{error}</p>}
|
|
107
|
-
<div className="flex justify-end">
|
|
108
|
-
<Button
|
|
109
|
-
type="submit"
|
|
110
|
-
size="sm"
|
|
111
|
-
className="h-11 sm:h-8"
|
|
112
|
-
disabled={!!error || !dirty || update.isPending}
|
|
113
|
-
>
|
|
114
|
-
Save
|
|
115
|
-
</Button>
|
|
116
|
-
</div>
|
|
117
|
-
</form>
|
|
118
|
-
);
|
|
119
|
-
}
|
|
120
|
-
|
|
121
|
-
function useThreshold(tokens: number, fallbackK: number) {
|
|
122
|
-
const [on, setOn] = useState(tokens > 0);
|
|
123
|
-
const [text, setText] = useState(
|
|
124
|
-
String(tokens > 0 ? tokens / 1000 : fallbackK)
|
|
125
|
-
);
|
|
126
|
-
const parsed = parseFreshStart(on, text);
|
|
127
|
-
return {
|
|
128
|
-
on,
|
|
129
|
-
setOn,
|
|
130
|
-
text,
|
|
131
|
-
setText,
|
|
132
|
-
tokens: "tokens" in parsed ? parsed.tokens : tokens,
|
|
133
|
-
error: "error" in parsed ? parsed.error : null,
|
|
134
|
-
};
|
|
135
|
-
}
|
|
136
|
-
|
|
137
|
-
function ThresholdRow({
|
|
138
|
-
label,
|
|
139
|
-
field,
|
|
140
|
-
switchLabel,
|
|
141
|
-
}: {
|
|
142
|
-
label: string;
|
|
143
|
-
field: ReturnType<typeof useThreshold>;
|
|
144
|
-
switchLabel?: string;
|
|
145
|
-
}) {
|
|
146
|
-
return (
|
|
147
|
-
<div className="space-y-2">
|
|
148
|
-
<label className="flex min-h-11 items-center justify-between gap-3 text-sm md:min-h-9">
|
|
149
|
-
{label}
|
|
150
|
-
<Switch
|
|
151
|
-
checked={field.on}
|
|
152
|
-
onCheckedChange={field.setOn}
|
|
153
|
-
aria-label={switchLabel ?? `Fresh starts for ${label.toLowerCase()}`}
|
|
154
|
-
/>
|
|
155
|
-
</label>
|
|
156
|
-
{field.on && (
|
|
157
|
-
<label className="flex items-center justify-between gap-3 text-sm">
|
|
158
|
-
<span className="text-muted-foreground">At</span>
|
|
159
|
-
<span className="flex items-center gap-2">
|
|
160
|
-
<Input
|
|
161
|
-
type="number"
|
|
162
|
-
inputMode="numeric"
|
|
163
|
-
min={50}
|
|
164
|
-
step={10}
|
|
165
|
-
value={field.text}
|
|
166
|
-
onChange={(e) => field.setText(e.target.value)}
|
|
167
|
-
aria-label={`${label} threshold, thousands of tokens`}
|
|
168
|
-
aria-invalid={!!field.error}
|
|
169
|
-
className="h-11 w-24 text-base sm:h-9 md:text-sm"
|
|
170
|
-
/>
|
|
171
|
-
<span className="text-muted-foreground">k tokens</span>
|
|
172
|
-
</span>
|
|
173
|
-
</label>
|
|
174
|
-
)}
|
|
175
|
-
</div>
|
|
176
|
-
);
|
|
177
|
-
}
|
|
@@ -1,101 +0,0 @@
|
|
|
1
|
-
"use client";
|
|
2
|
-
|
|
3
|
-
import { useState } from "react";
|
|
4
|
-
import { toast } from "sonner";
|
|
5
|
-
import { Button } from "@/components/ui/button";
|
|
6
|
-
import { Input } from "@/components/ui/input";
|
|
7
|
-
import { Switch } from "@/components/ui/switch";
|
|
8
|
-
import { useStallQuery, useUpdateStall } from "@/data/stall";
|
|
9
|
-
import {
|
|
10
|
-
DEFAULT_STALL_MINUTES,
|
|
11
|
-
MAX_STALL_MINUTES,
|
|
12
|
-
MIN_STALL_MINUTES,
|
|
13
|
-
} from "@/lib/chat/stall-limits";
|
|
14
|
-
|
|
15
|
-
// How long a turn may produce nothing before it's flagged stalled and the
|
|
16
|
-
// workspace's orchestrator is told: app-wide.
|
|
17
|
-
export function StallForm() {
|
|
18
|
-
const { data, isPending, isError, error } = useStallQuery();
|
|
19
|
-
if (isPending)
|
|
20
|
-
return <div className="bg-muted/40 h-24 animate-pulse rounded-lg" />;
|
|
21
|
-
if (isError)
|
|
22
|
-
return <p className="text-destructive text-sm">{error.message}</p>;
|
|
23
|
-
return <Form key={data.minutes} saved={data.minutes} />;
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
function Form({ saved }: { saved: number }) {
|
|
27
|
-
const update = useUpdateStall();
|
|
28
|
-
const [on, setOn] = useState(saved > 0);
|
|
29
|
-
const [text, setText] = useState(
|
|
30
|
-
String(saved > 0 ? saved : DEFAULT_STALL_MINUTES)
|
|
31
|
-
);
|
|
32
|
-
const n = Number(text);
|
|
33
|
-
const valid =
|
|
34
|
-
Number.isInteger(n) && n >= MIN_STALL_MINUTES && n <= MAX_STALL_MINUTES;
|
|
35
|
-
const minutes = on ? n : 0;
|
|
36
|
-
const error =
|
|
37
|
-
on && !valid
|
|
38
|
-
? `Between ${MIN_STALL_MINUTES} and ${MAX_STALL_MINUTES} minutes`
|
|
39
|
-
: null;
|
|
40
|
-
const dirty = !error && minutes !== saved;
|
|
41
|
-
|
|
42
|
-
return (
|
|
43
|
-
<form
|
|
44
|
-
onSubmit={(e) => {
|
|
45
|
-
e.preventDefault();
|
|
46
|
-
if (error) return;
|
|
47
|
-
update.mutate(minutes, {
|
|
48
|
-
onSuccess: () => toast.success("Stall alert saved"),
|
|
49
|
-
onError: (err) => toast.error(err.message),
|
|
50
|
-
});
|
|
51
|
-
}}
|
|
52
|
-
className="bg-muted/40 space-y-3 rounded-lg px-3 py-3"
|
|
53
|
-
>
|
|
54
|
-
<h3 className="text-sm font-medium">Stalled turns</h3>
|
|
55
|
-
<p className="text-muted-foreground text-sm">
|
|
56
|
-
A turn that produces nothing for this long (no text, tool call or
|
|
57
|
-
result) shows as stalled, and the workspace's orchestrator is told
|
|
58
|
-
once. A long command that is still printing, or a background command the
|
|
59
|
-
agent is waiting on, isn't a stall.
|
|
60
|
-
</p>
|
|
61
|
-
<label className="flex min-h-11 items-center justify-between gap-3 text-sm md:min-h-9">
|
|
62
|
-
Flag stalled turns
|
|
63
|
-
<Switch
|
|
64
|
-
checked={on}
|
|
65
|
-
onCheckedChange={setOn}
|
|
66
|
-
aria-label="Flag stalled turns"
|
|
67
|
-
/>
|
|
68
|
-
</label>
|
|
69
|
-
{on && (
|
|
70
|
-
<label className="flex items-center justify-between gap-3 text-sm">
|
|
71
|
-
<span className="text-muted-foreground">After</span>
|
|
72
|
-
<span className="flex items-center gap-2">
|
|
73
|
-
<Input
|
|
74
|
-
type="number"
|
|
75
|
-
inputMode="numeric"
|
|
76
|
-
min={MIN_STALL_MINUTES}
|
|
77
|
-
max={MAX_STALL_MINUTES}
|
|
78
|
-
value={text}
|
|
79
|
-
onChange={(e) => setText(e.target.value)}
|
|
80
|
-
aria-label="Stall threshold, minutes"
|
|
81
|
-
aria-invalid={!!error}
|
|
82
|
-
className="h-11 w-24 text-base sm:h-9 md:text-sm"
|
|
83
|
-
/>
|
|
84
|
-
<span className="text-muted-foreground">minutes</span>
|
|
85
|
-
</span>
|
|
86
|
-
</label>
|
|
87
|
-
)}
|
|
88
|
-
{error && <p className="text-destructive text-xs">{error}</p>}
|
|
89
|
-
<div className="flex justify-end">
|
|
90
|
-
<Button
|
|
91
|
-
type="submit"
|
|
92
|
-
size="sm"
|
|
93
|
-
className="h-11 sm:h-8"
|
|
94
|
-
disabled={!!error || !dirty || update.isPending}
|
|
95
|
-
>
|
|
96
|
-
Save
|
|
97
|
-
</Button>
|
|
98
|
-
</div>
|
|
99
|
-
</form>
|
|
100
|
-
);
|
|
101
|
-
}
|
|
@@ -1,84 +0,0 @@
|
|
|
1
|
-
"use client";
|
|
2
|
-
|
|
3
|
-
import { useState } from "react";
|
|
4
|
-
import { toast } from "sonner";
|
|
5
|
-
import { Button } from "@/components/ui/button";
|
|
6
|
-
import { Input } from "@/components/ui/input";
|
|
7
|
-
import { Switch } from "@/components/ui/switch";
|
|
8
|
-
import type { Workspace } from "@/lib/db";
|
|
9
|
-
import { useUpdateWorkspace } from "@/data/workspaces";
|
|
10
|
-
import { parseTaskLimit } from "./task-limit";
|
|
11
|
-
|
|
12
|
-
// How many tasks the workspace runs at once. Off by default; over the limit
|
|
13
|
-
// a new task is queued and starts by itself when one finishes.
|
|
14
|
-
export function TaskLimitForm({ workspace }: { workspace: Workspace }) {
|
|
15
|
-
const update = useUpdateWorkspace();
|
|
16
|
-
const current = workspace.max_running_tasks;
|
|
17
|
-
const [on, setOn] = useState(current !== null);
|
|
18
|
-
const [text, setText] = useState(String(current ?? 3));
|
|
19
|
-
const parsed = parseTaskLimit(on, text);
|
|
20
|
-
const dirty = !("error" in parsed) && parsed.limit !== current;
|
|
21
|
-
|
|
22
|
-
return (
|
|
23
|
-
<form
|
|
24
|
-
onSubmit={(e) => {
|
|
25
|
-
e.preventDefault();
|
|
26
|
-
if ("error" in parsed) return;
|
|
27
|
-
update.mutate(
|
|
28
|
-
{ id: workspace.id, maxRunningTasks: parsed.limit },
|
|
29
|
-
{
|
|
30
|
-
onSuccess: () =>
|
|
31
|
-
toast.success(
|
|
32
|
-
parsed.limit
|
|
33
|
-
? `Up to ${parsed.limit} tasks run at once`
|
|
34
|
-
: "No running task limit"
|
|
35
|
-
),
|
|
36
|
-
onError: (error) => toast.error(error.message),
|
|
37
|
-
}
|
|
38
|
-
);
|
|
39
|
-
}}
|
|
40
|
-
className="space-y-4"
|
|
41
|
-
>
|
|
42
|
-
<label className="flex min-h-11 items-center justify-between gap-3 text-sm md:min-h-9">
|
|
43
|
-
Limit running tasks
|
|
44
|
-
<Switch
|
|
45
|
-
checked={on}
|
|
46
|
-
onCheckedChange={setOn}
|
|
47
|
-
aria-label="Limit running tasks"
|
|
48
|
-
/>
|
|
49
|
-
</label>
|
|
50
|
-
{on && (
|
|
51
|
-
<label className="flex items-center justify-between gap-3 text-sm">
|
|
52
|
-
<span>At most</span>
|
|
53
|
-
<span className="flex items-center gap-2">
|
|
54
|
-
<Input
|
|
55
|
-
type="number"
|
|
56
|
-
inputMode="numeric"
|
|
57
|
-
min={1}
|
|
58
|
-
step={1}
|
|
59
|
-
value={text}
|
|
60
|
-
onChange={(e) => setText(e.target.value)}
|
|
61
|
-
aria-label="Running task limit"
|
|
62
|
-
aria-invalid={"error" in parsed}
|
|
63
|
-
className="h-11 w-20 text-base sm:h-9 md:text-sm"
|
|
64
|
-
/>
|
|
65
|
-
<span className="text-muted-foreground">at a time</span>
|
|
66
|
-
</span>
|
|
67
|
-
</label>
|
|
68
|
-
)}
|
|
69
|
-
{"error" in parsed && (
|
|
70
|
-
<p className="text-destructive text-xs">{parsed.error}</p>
|
|
71
|
-
)}
|
|
72
|
-
<div className="flex justify-end">
|
|
73
|
-
<Button
|
|
74
|
-
type="submit"
|
|
75
|
-
size="sm"
|
|
76
|
-
className="h-11 sm:h-8"
|
|
77
|
-
disabled={"error" in parsed || !dirty || update.isPending}
|
|
78
|
-
>
|
|
79
|
-
Save
|
|
80
|
-
</Button>
|
|
81
|
-
</div>
|
|
82
|
-
</form>
|
|
83
|
-
);
|
|
84
|
-
}
|
|
@@ -1,19 +0,0 @@
|
|
|
1
|
-
import {
|
|
2
|
-
MAX_REFRESH_TOKENS,
|
|
3
|
-
MIN_REFRESH_TOKENS,
|
|
4
|
-
} from "@/lib/chat/refresh-limits";
|
|
5
|
-
|
|
6
|
-
// A threshold's input, in thousands of tokens: off is 0; on takes a whole
|
|
7
|
-
// number in the range the server holds (lib/chat/refresh).
|
|
8
|
-
export function parseFreshStart(
|
|
9
|
-
on: boolean,
|
|
10
|
-
text: string
|
|
11
|
-
): { tokens: number } | { error: string } {
|
|
12
|
-
if (!on) return { tokens: 0 };
|
|
13
|
-
const k = Number(text.trim());
|
|
14
|
-
const min = MIN_REFRESH_TOKENS / 1000;
|
|
15
|
-
const max = MAX_REFRESH_TOKENS / 1000;
|
|
16
|
-
if (!text.trim() || !Number.isInteger(k) || k < min || k > max)
|
|
17
|
-
return { error: `A whole number from ${min} to ${max}` };
|
|
18
|
-
return { tokens: k * 1000 };
|
|
19
|
-
}
|