@saadnvd1/agent-os 0.3.570 → 0.3.572
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/README.md +1 -1
- package/app/api/bus/spawn/route.ts +21 -3
- package/app/api/client-config/route.test.ts +11 -0
- package/app/api/client-config/route.ts +10 -0
- package/app/api/load/route.test.ts +32 -0
- package/app/api/load/route.ts +20 -0
- package/app/api/sessions/[id]/pause/route.ts +97 -0
- package/app/api/tasks/[id]/priority/route.ts +29 -0
- package/app/api/tasks/route.ts +17 -2
- package/components/Chat/AccessPicker.tsx +10 -18
- package/components/Chat/ChatPanel.tsx +4 -0
- package/components/Chat/ModelPicker.tsx +9 -3
- package/components/Chat/PausedBanner.tsx +54 -0
- package/components/Chat/composer/ComposerBody.tsx +3 -0
- package/components/Chat/composer/Toolbar.tsx +3 -0
- package/components/Draft/DraftChips.tsx +25 -0
- package/components/Draft/DraftPanel.tsx +1 -0
- package/components/Recipes/format.ts +19 -30
- package/components/SessionList/QueuedRow.tsx +46 -2
- package/components/SessionList/RowSubtitle.tsx +21 -3
- package/components/SessionList/SessionRowMenu.tsx +49 -1
- package/components/SessionList/useShelfActions.ts +39 -2
- package/components/Tasks/task-state-labels.ts +16 -16
- package/data/chat/useChat.ts +3 -0
- package/data/sessions/shelf.ts +50 -0
- package/data/tasks/index.ts +2 -0
- package/data/tasks/queue.ts +38 -1
- package/lib/bus/index.ts +21 -2
- package/lib/chat/access-levels.ts +24 -0
- package/lib/chat/events.ts +2 -0
- package/lib/chat/hold.ts +11 -2
- package/lib/chat/settings.ts +6 -2
- package/lib/config/client-config-types.ts +29 -0
- package/lib/config/client-config.ts +28 -0
- package/lib/db/migrations.ts +56 -1
- package/lib/db/types.ts +30 -0
- package/lib/done/index.ts +2 -0
- package/lib/drafts.ts +3 -0
- package/lib/hosts/peer-load.ts +120 -0
- package/lib/load/machines.ts +124 -0
- package/lib/load/monitor.ts +17 -2
- package/lib/load/start-gate.ts +95 -0
- package/lib/model-catalog.ts +11 -0
- package/lib/orchestrator/act.ts +78 -0
- package/lib/orchestrator/brakes.ts +2 -0
- package/lib/orchestrator/brief.ts +4 -1
- package/lib/orchestrator/digest.ts +15 -4
- package/lib/orchestrator/serve.ts +18 -0
- package/lib/orchestrator/tool-names.ts +3 -0
- package/lib/orchestrator/tool-schemas.ts +33 -0
- package/lib/orchestrator/tools.ts +7 -1
- package/lib/project-config/agentos.schema.json +13 -0
- package/lib/project-config/schema.ts +9 -0
- package/lib/recipes/status-labels.ts +27 -0
- package/lib/sidebar/paused.ts +13 -0
- package/lib/sidebar/queued.ts +34 -1
- package/lib/sidebar/shelves.ts +21 -1
- package/lib/sidebar/snooze.ts +26 -0
- package/lib/tasks/finish.ts +7 -0
- package/lib/tasks/move-carry.ts +1 -0
- package/lib/tasks/move-flow.ts +6 -0
- package/lib/tasks/pause-hold.ts +70 -0
- package/lib/tasks/pause-policy.ts +130 -0
- package/lib/tasks/pause.ts +718 -0
- package/lib/tasks/placement.ts +219 -0
- package/lib/tasks/queue.ts +522 -64
- package/lib/tasks/schedule-types.ts +64 -0
- package/lib/tasks/schedule.ts +271 -0
- package/lib/tasks/state-labels.ts +21 -0
- package/package.json +1 -1
- package/server.ts +2 -1
|
@@ -1,12 +1,13 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
|
|
3
3
|
import { Fragment } from "react";
|
|
4
|
-
import { ArrowDown, ArrowUp, Play, Trash2 } from "lucide-react";
|
|
4
|
+
import { ArrowDown, ArrowUp, Flag, Play, Trash2 } from "lucide-react";
|
|
5
5
|
import { toast } from "sonner";
|
|
6
6
|
import * as DM from "@/components/ui/dropdown-menu";
|
|
7
7
|
import * as CM from "@/components/ui/context-menu";
|
|
8
|
-
import { useQueueAction, type QueueAction } from "@/data/tasks";
|
|
8
|
+
import { useQueueAction, useSetPriority, type QueueAction } from "@/data/tasks";
|
|
9
9
|
import {
|
|
10
|
+
priorityMenu,
|
|
10
11
|
queueMenu,
|
|
11
12
|
queuedSubtitle,
|
|
12
13
|
type QueuedRow as Row,
|
|
@@ -22,6 +23,18 @@ const SEPARATOR = {
|
|
|
22
23
|
context: CM.ContextMenuSeparator,
|
|
23
24
|
};
|
|
24
25
|
const icon = "mr-2 h-3.5 w-3.5";
|
|
26
|
+
const SUB = {
|
|
27
|
+
dropdown: {
|
|
28
|
+
Sub: DM.DropdownMenuSub,
|
|
29
|
+
Trigger: DM.DropdownMenuSubTrigger,
|
|
30
|
+
Content: DM.DropdownMenuSubContent,
|
|
31
|
+
},
|
|
32
|
+
context: {
|
|
33
|
+
Sub: CM.ContextMenuSub,
|
|
34
|
+
Trigger: CM.ContextMenuSubTrigger,
|
|
35
|
+
Content: CM.ContextMenuSubContent,
|
|
36
|
+
},
|
|
37
|
+
};
|
|
25
38
|
const ICONS = { start: Play, up: ArrowUp, down: ArrowDown, remove: Trash2 };
|
|
26
39
|
|
|
27
40
|
const DONE: Record<QueueAction, string> = {
|
|
@@ -55,16 +68,47 @@ export function QueuedRow({ row }: { row: Row }) {
|
|
|
55
68
|
);
|
|
56
69
|
const failed = item.status === "failed";
|
|
57
70
|
const busy = action.isPending;
|
|
71
|
+
const setPriority = useSetPriority();
|
|
72
|
+
const priorities = priorityMenu(item);
|
|
58
73
|
|
|
59
74
|
const items = queueMenu(row);
|
|
60
75
|
const menu = items.length
|
|
61
76
|
? (kind: RowMenuKind) => {
|
|
62
77
|
const Item = ITEM[kind];
|
|
63
78
|
const Separator = SEPARATOR[kind];
|
|
79
|
+
const { Sub, Trigger, Content } = SUB[kind];
|
|
64
80
|
return items.map(({ action: a, label, disabled }) => {
|
|
65
81
|
const Icon = ICONS[a];
|
|
66
82
|
return (
|
|
67
83
|
<Fragment key={a}>
|
|
84
|
+
{a === "remove" && priorities.length > 0 && (
|
|
85
|
+
<Sub>
|
|
86
|
+
<Trigger>
|
|
87
|
+
<Flag className={icon} />
|
|
88
|
+
Priority
|
|
89
|
+
</Trigger>
|
|
90
|
+
<Content>
|
|
91
|
+
{priorities.map((p) => (
|
|
92
|
+
<Item
|
|
93
|
+
key={p.priority}
|
|
94
|
+
onClick={() =>
|
|
95
|
+
setPriority.mutate(
|
|
96
|
+
{ id: item.id, priority: p.priority },
|
|
97
|
+
{ onError: (e) => toast.error(e.message) }
|
|
98
|
+
)
|
|
99
|
+
}
|
|
100
|
+
>
|
|
101
|
+
{p.label}
|
|
102
|
+
{p.current && (
|
|
103
|
+
<span className="text-muted-foreground ml-3 text-xs">
|
|
104
|
+
current
|
|
105
|
+
</span>
|
|
106
|
+
)}
|
|
107
|
+
</Item>
|
|
108
|
+
))}
|
|
109
|
+
</Content>
|
|
110
|
+
</Sub>
|
|
111
|
+
)}
|
|
68
112
|
{a === "remove" && items.length > 1 && <Separator />}
|
|
69
113
|
<Item
|
|
70
114
|
disabled={disabled || busy}
|
|
@@ -9,6 +9,7 @@ import { headerParts } from "@/lib/orchestrator/header-line";
|
|
|
9
9
|
import { cn } from "@/lib/utils";
|
|
10
10
|
import { usageLabel, useSessionUsage } from "@/data/load";
|
|
11
11
|
import { useRowContext } from "./RowContext";
|
|
12
|
+
import { pauseSubtitle } from "@/lib/sidebar/paused";
|
|
12
13
|
|
|
13
14
|
const ATTENTION = (part: string) => part === "paused" || /\basks?$/.test(part);
|
|
14
15
|
|
|
@@ -56,9 +57,21 @@ export function RowSubtitle({
|
|
|
56
57
|
}
|
|
57
58
|
const project = ctx.projectNames.get(session.project_id ?? "");
|
|
58
59
|
const group = rowGroup(session);
|
|
60
|
+
const pause = session.pause_state;
|
|
59
61
|
return (
|
|
60
62
|
<span className="flex min-w-0 items-center gap-1.5">
|
|
61
|
-
|
|
63
|
+
{pause ? (
|
|
64
|
+
<span className="min-w-0 truncate text-xs text-amber-600 dark:text-amber-400">
|
|
65
|
+
{pauseSubtitle({
|
|
66
|
+
state: pause,
|
|
67
|
+
until: session.paused_until
|
|
68
|
+
? Date.parse(`${session.paused_until.replace(" ", "T")}Z`)
|
|
69
|
+
: null,
|
|
70
|
+
})}
|
|
71
|
+
</span>
|
|
72
|
+
) : (
|
|
73
|
+
<LiveLine {...live} />
|
|
74
|
+
)}
|
|
62
75
|
{group && (
|
|
63
76
|
<span
|
|
64
77
|
className="text-muted-foreground/70 hidden min-w-0 shrink items-center gap-1 truncate text-xs @[14rem]:flex"
|
|
@@ -72,9 +85,14 @@ export function RowSubtitle({
|
|
|
72
85
|
<span className="truncate">{group.text}</span>
|
|
73
86
|
</span>
|
|
74
87
|
)}
|
|
75
|
-
{(project || detail || usage) && (
|
|
88
|
+
{(project || detail || usage || session.placed_note) && (
|
|
76
89
|
<span className="text-muted-foreground/70 min-w-0 truncate text-xs">
|
|
77
|
-
{[
|
|
90
|
+
{[
|
|
91
|
+
usage && usageLabel(usage),
|
|
92
|
+
detail?.slice(0, 200),
|
|
93
|
+
session.placed_note,
|
|
94
|
+
project,
|
|
95
|
+
]
|
|
78
96
|
.filter(Boolean)
|
|
79
97
|
.join(" · ")}
|
|
80
98
|
</span>
|
|
@@ -17,6 +17,7 @@ import {
|
|
|
17
17
|
Loader2,
|
|
18
18
|
Pencil,
|
|
19
19
|
Pin,
|
|
20
|
+
Pause,
|
|
20
21
|
Play,
|
|
21
22
|
Repeat,
|
|
22
23
|
ChefHat,
|
|
@@ -35,7 +36,13 @@ import { rowGroup } from "@/lib/recipes/chip";
|
|
|
35
36
|
import { finishActions } from "@/lib/sidebar/row-actions";
|
|
36
37
|
import { useArchiveAction, useDoneAction } from "./useDoneAction";
|
|
37
38
|
import { useShelfActions } from "./useShelfActions";
|
|
38
|
-
import {
|
|
39
|
+
import {
|
|
40
|
+
PAUSE_PRESETS,
|
|
41
|
+
SNOOZE_PRESETS,
|
|
42
|
+
pauseUntil,
|
|
43
|
+
snoozePreset,
|
|
44
|
+
wakeLabel,
|
|
45
|
+
} from "@/lib/sidebar/snooze";
|
|
39
46
|
import type { SidebarRow } from "@/lib/sidebar/shelves";
|
|
40
47
|
import { useRowContext } from "./RowContext";
|
|
41
48
|
import { MoveMenuItems } from "@/components/Tasks/MoveMenuItems";
|
|
@@ -399,6 +406,47 @@ export function SessionRowMenu({
|
|
|
399
406
|
</Sub>
|
|
400
407
|
)
|
|
401
408
|
)}
|
|
409
|
+
{session.task_status || session.view === "chat" ? (
|
|
410
|
+
session.pause_state === "paused" ||
|
|
411
|
+
session.pause_state === "pausing" ? (
|
|
412
|
+
<Item onClick={() => shelf.resume(session)}>
|
|
413
|
+
<Play className={icon} />
|
|
414
|
+
{session.pause_state === "paused"
|
|
415
|
+
? "Resume now"
|
|
416
|
+
: "Keep running"}
|
|
417
|
+
</Item>
|
|
418
|
+
) : session.pause_state ? null : (
|
|
419
|
+
<Sub>
|
|
420
|
+
<SubTrigger>
|
|
421
|
+
<Pause className={icon} />
|
|
422
|
+
Pause
|
|
423
|
+
</SubTrigger>
|
|
424
|
+
<SubContent>
|
|
425
|
+
{PAUSE_PRESETS.map((p) => {
|
|
426
|
+
const at = pauseUntil(p.key);
|
|
427
|
+
return (
|
|
428
|
+
<Item
|
|
429
|
+
key={p.key}
|
|
430
|
+
onClick={() => shelf.pause(session, p.key)}
|
|
431
|
+
>
|
|
432
|
+
{p.label}
|
|
433
|
+
{at && (
|
|
434
|
+
<span className="text-muted-foreground ml-3 text-xs">
|
|
435
|
+
{wakeLabel(at.getTime())}
|
|
436
|
+
</span>
|
|
437
|
+
)}
|
|
438
|
+
</Item>
|
|
439
|
+
);
|
|
440
|
+
})}
|
|
441
|
+
<Item
|
|
442
|
+
onClick={() => shelf.pause(session, "until-resumed", true)}
|
|
443
|
+
>
|
|
444
|
+
Pause now (interrupt its step)
|
|
445
|
+
</Item>
|
|
446
|
+
</SubContent>
|
|
447
|
+
</Sub>
|
|
448
|
+
)
|
|
449
|
+
) : null}
|
|
402
450
|
{plain &&
|
|
403
451
|
(row.settled ? (
|
|
404
452
|
<Item onClick={() => shelf.reopen(session)}>
|
|
@@ -1,16 +1,25 @@
|
|
|
1
1
|
import { toast } from "sonner";
|
|
2
2
|
import type { Session } from "@/lib/db";
|
|
3
|
-
import { useSettleSession, useSnoozeSession } from "@/data/sessions/shelf";
|
|
4
3
|
import {
|
|
4
|
+
usePauseSession,
|
|
5
|
+
useResumeSession,
|
|
6
|
+
useSettleSession,
|
|
7
|
+
useSnoozeSession,
|
|
8
|
+
} from "@/data/sessions/shelf";
|
|
9
|
+
import {
|
|
10
|
+
pauseUntil,
|
|
5
11
|
snoozePreset,
|
|
6
12
|
wakeLabel,
|
|
13
|
+
type PausePreset,
|
|
7
14
|
type SnoozePreset,
|
|
8
15
|
} from "@/lib/sidebar/snooze";
|
|
9
16
|
|
|
10
|
-
// A row menu's Snooze, Unsnooze, Mark done and Reopen
|
|
17
|
+
// A row menu's Snooze, Unsnooze, Pause, Resume, Mark done and Reopen.
|
|
11
18
|
export function useShelfActions() {
|
|
12
19
|
const snooze = useSnoozeSession();
|
|
13
20
|
const settle = useSettleSession();
|
|
21
|
+
const pause = usePauseSession();
|
|
22
|
+
const resume = useResumeSession();
|
|
14
23
|
const unsnooze = (session: Session) =>
|
|
15
24
|
snooze.mutate({ id: session.id, until: null });
|
|
16
25
|
return {
|
|
@@ -28,6 +37,34 @@ export function useShelfActions() {
|
|
|
28
37
|
);
|
|
29
38
|
},
|
|
30
39
|
unsnooze,
|
|
40
|
+
// Stops its agent at the end of the step (or now) and starts it again at
|
|
41
|
+
// the time. A refusal (it is waiting on you, merging) is the toast.
|
|
42
|
+
pause: (session: Session, preset: PausePreset, now = false) => {
|
|
43
|
+
const until = pauseUntil(preset);
|
|
44
|
+
pause.mutate(
|
|
45
|
+
{ id: session.id, until, mode: now ? "now" : "turn" },
|
|
46
|
+
{
|
|
47
|
+
onSuccess: () =>
|
|
48
|
+
toast.success(
|
|
49
|
+
until
|
|
50
|
+
? `Pausing until ${wakeLabel(until.getTime())}`
|
|
51
|
+
: "Pausing until you resume",
|
|
52
|
+
{
|
|
53
|
+
action: {
|
|
54
|
+
label: "Keep running",
|
|
55
|
+
onClick: () => resume.mutate({ id: session.id }),
|
|
56
|
+
},
|
|
57
|
+
}
|
|
58
|
+
),
|
|
59
|
+
onError: (e) => toast.error(e.message),
|
|
60
|
+
}
|
|
61
|
+
);
|
|
62
|
+
},
|
|
63
|
+
resume: (session: Session) =>
|
|
64
|
+
resume.mutate(
|
|
65
|
+
{ id: session.id },
|
|
66
|
+
{ onError: (e) => toast.error(e.message) }
|
|
67
|
+
),
|
|
31
68
|
settle: (session: Session) =>
|
|
32
69
|
settle.mutate(
|
|
33
70
|
{ id: session.id, settled: true },
|
|
@@ -1,19 +1,19 @@
|
|
|
1
|
+
import type { Tone } from "@/lib/config/client-config-types";
|
|
1
2
|
import type { TaskState } from "@/lib/tasks/state";
|
|
3
|
+
import { TASK_STATE_LABELS } from "@/lib/tasks/state-labels";
|
|
2
4
|
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
"
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
},
|
|
10
|
-
blocked: { label: "Blocked", tone: "text-amber-600 dark:text-amber-400" },
|
|
11
|
-
"plan-ready": { label: "Plan ready", tone: "text-primary" },
|
|
12
|
-
review: { label: "Ready for review", tone: "text-primary" },
|
|
13
|
-
"checks-failing": { label: "Checks failing", tone: "text-destructive" },
|
|
14
|
-
exited: { label: "Agent exited", tone: "text-amber-600 dark:text-amber-400" },
|
|
15
|
-
merged: { label: "Merged", tone: "text-emerald-600 dark:text-emerald-400" },
|
|
16
|
-
dropped: { label: "Dropped", tone: "text-muted-foreground/60" },
|
|
17
|
-
done: { label: "Done", tone: "text-emerald-600 dark:text-emerald-400" },
|
|
18
|
-
moving: { label: "Moving", tone: "text-amber-600 dark:text-amber-400" },
|
|
5
|
+
export const TONE_TEXT: Record<Tone, string> = {
|
|
6
|
+
muted: "text-muted-foreground",
|
|
7
|
+
warning: "text-amber-600 dark:text-amber-400",
|
|
8
|
+
primary: "text-primary",
|
|
9
|
+
destructive: "text-destructive",
|
|
10
|
+
success: "text-emerald-600 dark:text-emerald-400",
|
|
19
11
|
};
|
|
12
|
+
|
|
13
|
+
// How a task's state reads, and its tone class, wherever a task is shown.
|
|
14
|
+
export const TASK_STATE = Object.fromEntries(
|
|
15
|
+
Object.entries(TASK_STATE_LABELS).map(([k, v]) => [
|
|
16
|
+
k,
|
|
17
|
+
{ label: v.label, tone: TONE_TEXT[v.tone] },
|
|
18
|
+
])
|
|
19
|
+
) as Record<TaskState, { label: string; tone: string }>;
|
package/data/chat/useChat.ts
CHANGED
|
@@ -146,6 +146,7 @@ export function useChat(
|
|
|
146
146
|
const [commands, setCommands] = useState<ChatCommand[]>([]);
|
|
147
147
|
const [models, setModels] = useState<ChatModel[]>([]);
|
|
148
148
|
const [model, setModelState] = useState("");
|
|
149
|
+
const [modelLabel, setModelLabel] = useState<string | undefined>();
|
|
149
150
|
const [refreshingCommands, setRefreshingCommands] = useState(false);
|
|
150
151
|
const refreshTimer = useRef<ReturnType<typeof setTimeout>>(undefined);
|
|
151
152
|
const [access, setAccessState] = useState<ChatAccess>("full");
|
|
@@ -251,6 +252,7 @@ export function useChat(
|
|
|
251
252
|
setCommands(m.commands);
|
|
252
253
|
setModels(m.models);
|
|
253
254
|
setModelState(m.model);
|
|
255
|
+
setModelLabel(m.modelLabel);
|
|
254
256
|
setAccessState(m.access);
|
|
255
257
|
setPlanState(m.plan);
|
|
256
258
|
setCanBackground(!!m.background);
|
|
@@ -519,6 +521,7 @@ export function useChat(
|
|
|
519
521
|
refreshingCommands,
|
|
520
522
|
models,
|
|
521
523
|
model,
|
|
524
|
+
modelLabel,
|
|
522
525
|
send,
|
|
523
526
|
interrupt,
|
|
524
527
|
setModel,
|
package/data/sessions/shelf.ts
CHANGED
|
@@ -83,3 +83,53 @@ export function useSettleSession() {
|
|
|
83
83
|
: { settled_at: null, active_at: now }
|
|
84
84
|
);
|
|
85
85
|
}
|
|
86
|
+
|
|
87
|
+
async function send(method: string, url: string, body?: object) {
|
|
88
|
+
const res = await fetch(url, {
|
|
89
|
+
method,
|
|
90
|
+
headers: { "Content-Type": "application/json" },
|
|
91
|
+
body: body ? JSON.stringify(body) : undefined,
|
|
92
|
+
});
|
|
93
|
+
if (!res.ok)
|
|
94
|
+
throw new Error(
|
|
95
|
+
(await res.json().catch(() => null))?.error ?? "Request failed"
|
|
96
|
+
);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// until null: until resumed. The row moves to "pausing" at once; the server
|
|
100
|
+
// stops the agent at the end of its step (or now) and the push settles it.
|
|
101
|
+
export function usePauseSession() {
|
|
102
|
+
return useShelfMutation<{
|
|
103
|
+
id: string;
|
|
104
|
+
until: Date | null;
|
|
105
|
+
mode: "turn" | "now";
|
|
106
|
+
}>(
|
|
107
|
+
({ id, until, mode }) =>
|
|
108
|
+
send("POST", `/api/sessions/${id}/pause`, {
|
|
109
|
+
until: until?.toISOString() ?? null,
|
|
110
|
+
mode,
|
|
111
|
+
}),
|
|
112
|
+
({ until }) => ({
|
|
113
|
+
pause_state: "pausing",
|
|
114
|
+
paused_until: until ? toSqliteTime(until) : null,
|
|
115
|
+
})
|
|
116
|
+
);
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
// Resume now, or keep running while it is still pausing.
|
|
120
|
+
export function useResumeSession() {
|
|
121
|
+
return useShelfMutation<{ id: string }>(
|
|
122
|
+
({ id }) => send("DELETE", `/api/sessions/${id}/pause`),
|
|
123
|
+
() => ({ pause_state: "resuming", paused_until: null })
|
|
124
|
+
);
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
export function useChangeResumeTime() {
|
|
128
|
+
return useShelfMutation<{ id: string; until: Date | null }>(
|
|
129
|
+
({ id, until }) =>
|
|
130
|
+
send("PATCH", `/api/sessions/${id}/pause`, {
|
|
131
|
+
until: until?.toISOString() ?? null,
|
|
132
|
+
}),
|
|
133
|
+
({ until }) => ({ paused_until: until ? toSqliteTime(until) : null })
|
|
134
|
+
);
|
|
135
|
+
}
|
package/data/tasks/index.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query";
|
|
2
2
|
import type { TaskView } from "@/lib/tasks";
|
|
3
3
|
import type { QueuedTaskView } from "@/lib/tasks/queue";
|
|
4
|
+
import type { Priority } from "@/lib/tasks/schedule-types";
|
|
4
5
|
import { sessionKeys } from "../sessions/keys";
|
|
5
6
|
import { taskKeys } from "./keys";
|
|
6
7
|
import { usePollWhenOffline } from "../push/connection";
|
|
@@ -43,6 +44,7 @@ export function useCreateTask() {
|
|
|
43
44
|
projectId: string;
|
|
44
45
|
prompt: string;
|
|
45
46
|
model?: string;
|
|
47
|
+
priority?: Priority;
|
|
46
48
|
baseBranch?: string;
|
|
47
49
|
hostId?: string;
|
|
48
50
|
view?: "chat" | "terminal";
|
package/data/tasks/queue.ts
CHANGED
|
@@ -1,6 +1,11 @@
|
|
|
1
1
|
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
|
|
2
2
|
import type { QueuedTaskView, StartOutcome } from "@/lib/tasks/queue";
|
|
3
|
-
import {
|
|
3
|
+
import {
|
|
4
|
+
priorityRequest,
|
|
5
|
+
queueRequest,
|
|
6
|
+
type QueueMenuAction,
|
|
7
|
+
} from "@/lib/sidebar/queued";
|
|
8
|
+
import type { Priority } from "@/lib/tasks/schedule-types";
|
|
4
9
|
import { sessionKeys } from "../sessions/keys";
|
|
5
10
|
import { taskKeys } from "./keys";
|
|
6
11
|
import { usePollWhenOffline } from "../push/connection";
|
|
@@ -40,3 +45,35 @@ export function useQueueAction() {
|
|
|
40
45
|
},
|
|
41
46
|
});
|
|
42
47
|
}
|
|
48
|
+
|
|
49
|
+
// Changes where a waiting task stands; the line re-ranks from the push.
|
|
50
|
+
export function useSetPriority() {
|
|
51
|
+
const queryClient = useQueryClient();
|
|
52
|
+
return useMutation({
|
|
53
|
+
mutationFn: async ({
|
|
54
|
+
id,
|
|
55
|
+
priority,
|
|
56
|
+
}: {
|
|
57
|
+
id: string;
|
|
58
|
+
priority: Priority;
|
|
59
|
+
}) => {
|
|
60
|
+
const { url, init } = priorityRequest(id, priority);
|
|
61
|
+
return json<unknown>(await fetch(url, init));
|
|
62
|
+
},
|
|
63
|
+
onMutate: async ({ id, priority }) => {
|
|
64
|
+
await queryClient.cancelQueries({ queryKey: taskKeys.queue() });
|
|
65
|
+
const previous = queryClient.getQueryData<QueuedTaskView[]>(
|
|
66
|
+
taskKeys.queue()
|
|
67
|
+
);
|
|
68
|
+
queryClient.setQueryData<QueuedTaskView[]>(taskKeys.queue(), (old) =>
|
|
69
|
+
old?.map((q) => (q.id === id ? { ...q, priority } : q))
|
|
70
|
+
);
|
|
71
|
+
return { previous };
|
|
72
|
+
},
|
|
73
|
+
onError: (_e, _v, context) => {
|
|
74
|
+
if (context?.previous)
|
|
75
|
+
queryClient.setQueryData(taskKeys.queue(), context.previous);
|
|
76
|
+
},
|
|
77
|
+
onSettled: () => queryClient.invalidateQueries({ queryKey: taskKeys.all }),
|
|
78
|
+
});
|
|
79
|
+
}
|
package/lib/bus/index.ts
CHANGED
|
@@ -15,6 +15,7 @@ import { deliverToPane, type Delivery } from "./delivery";
|
|
|
15
15
|
import { resolveRef, wasNames, type Candidate } from "./resolve";
|
|
16
16
|
import { tmuxPane } from "./tmux-pane";
|
|
17
17
|
import { hostLink } from "../hosts/remote-api";
|
|
18
|
+
import { pauseHoldOf, pausedNote } from "../tasks/pause-hold";
|
|
18
19
|
import { relayToHeldTask } from "../tasks/held-remote";
|
|
19
20
|
import { heldOrchestrator, heldTarget } from "./held-reply";
|
|
20
21
|
import { HUMAN, overRateLimit, wakeLine, type BusMessageView } from "./format";
|
|
@@ -215,6 +216,16 @@ export async function deliver(
|
|
|
215
216
|
return { state: "failed", why };
|
|
216
217
|
}
|
|
217
218
|
}
|
|
219
|
+
// A paused terminal has no agent: the message waits for the resume prompt.
|
|
220
|
+
const paused = pauseHoldOf(to.id);
|
|
221
|
+
if (paused) {
|
|
222
|
+
if (from.messageId)
|
|
223
|
+
db.prepare(`UPDATE bus_messages SET held_pause_id = ? WHERE id = ?`).run(
|
|
224
|
+
paused.pauseId,
|
|
225
|
+
from.messageId
|
|
226
|
+
);
|
|
227
|
+
return { state: "queued" };
|
|
228
|
+
}
|
|
218
229
|
if (!statusDetector.sessionExists(to.tmux_name))
|
|
219
230
|
return { state: "failed", why: "its terminal isn't running" };
|
|
220
231
|
try {
|
|
@@ -298,7 +309,10 @@ export async function sendMessage(opts: {
|
|
|
298
309
|
: opts.origin,
|
|
299
310
|
}
|
|
300
311
|
);
|
|
301
|
-
|
|
312
|
+
// A message held for a paused terminal isn't delivered until it resumes.
|
|
313
|
+
const hold = pauseHoldOf(to.id);
|
|
314
|
+
const parked = !!hold && to.view !== "chat";
|
|
315
|
+
if (delivery.state !== "failed" && !parked) {
|
|
302
316
|
db.prepare(
|
|
303
317
|
`UPDATE bus_messages SET delivered_at = datetime('now') WHERE id = ?`
|
|
304
318
|
).run(id);
|
|
@@ -308,7 +322,12 @@ export async function sendMessage(opts: {
|
|
|
308
322
|
`UPDATE bus_messages SET read_at = datetime('now') WHERE id = ?`
|
|
309
323
|
).run(id);
|
|
310
324
|
const message = toView(db.prepare(`${SELECT} WHERE m.id = ?`).get(id) as Row);
|
|
311
|
-
|
|
325
|
+
const heldNote = hold ? pausedNote(to.name, hold) : null;
|
|
326
|
+
return {
|
|
327
|
+
message,
|
|
328
|
+
delivery,
|
|
329
|
+
note: [note, heldNote].filter(Boolean).join("; ") || undefined,
|
|
330
|
+
};
|
|
312
331
|
}
|
|
313
332
|
|
|
314
333
|
function storeForHolder(
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
// What each chat access level is called and does, for every client. Synced to
|
|
2
|
+
// the native app, so it may only import synced files.
|
|
3
|
+
import type { ChatAccess } from "./events";
|
|
4
|
+
|
|
5
|
+
export const ACCESS_LEVELS: Record<
|
|
6
|
+
ChatAccess,
|
|
7
|
+
{ label: string; description: string }
|
|
8
|
+
> = {
|
|
9
|
+
ask: {
|
|
10
|
+
label: "Ask first",
|
|
11
|
+
description: "Asks before edits and commands it isn't already allowed",
|
|
12
|
+
},
|
|
13
|
+
edits: {
|
|
14
|
+
label: "Accept edits",
|
|
15
|
+
description: "Edits files on its own, asks before other commands",
|
|
16
|
+
},
|
|
17
|
+
full: {
|
|
18
|
+
label: "Full access",
|
|
19
|
+
description: "Does everything without asking",
|
|
20
|
+
},
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
// The access a new chat starts with when the client doesn't pick one.
|
|
24
|
+
export const DEFAULT_NEW_CHAT_ACCESS: ChatAccess = "edits";
|
package/lib/chat/events.ts
CHANGED
|
@@ -359,6 +359,8 @@ export type ChatServerMessage =
|
|
|
359
359
|
commands: ChatCommand[];
|
|
360
360
|
models: ChatModel[];
|
|
361
361
|
model: string;
|
|
362
|
+
// A readable name for `model` when it is not one of `models`.
|
|
363
|
+
modelLabel?: string;
|
|
362
364
|
access: ChatAccess;
|
|
363
365
|
// Plan mode: on, or null when the session can't use it.
|
|
364
366
|
plan: boolean | null;
|
package/lib/chat/hold.ts
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Holding a chat still while something outside it works on its task: a
|
|
3
3
|
* move to another machine (its row is 'moving'), or a Land of the stack it
|
|
4
|
-
* sits in (the stack is 'landing')
|
|
4
|
+
* sits in (the stack is 'landing'), or a timed pause (lib/tasks/pause.ts).
|
|
5
|
+
* All are facts in SQLite, so the worker
|
|
5
6
|
* process sees the hold too: it neither starts what's queued (queued.ts) nor
|
|
6
7
|
* a turn for a message sent meanwhile, which waits in the queue instead.
|
|
7
8
|
*
|
|
@@ -17,6 +18,8 @@ export const HELD_SQL = `(
|
|
|
17
18
|
EXISTS (SELECT 1 FROM sessions WHERE id = @sid AND task_status = 'moving')
|
|
18
19
|
OR EXISTS (SELECT 1 FROM stack_items i JOIN stacks s ON s.id = i.stack_id
|
|
19
20
|
WHERE i.session_id = @sid AND s.status = 'landing')
|
|
21
|
+
OR EXISTS (SELECT 1 FROM sessions WHERE id = @sid
|
|
22
|
+
AND pause_state IN ('pausing', 'paused'))
|
|
20
23
|
)`;
|
|
21
24
|
|
|
22
25
|
export const isHeld = (sessionId: string): boolean =>
|
|
@@ -45,12 +48,14 @@ interface HoldRow {
|
|
|
45
48
|
moved_to: string | null;
|
|
46
49
|
quick_of: string | null;
|
|
47
50
|
landing: number;
|
|
51
|
+
pause_state: string | null;
|
|
52
|
+
paused_until: string | null;
|
|
48
53
|
}
|
|
49
54
|
|
|
50
55
|
const holdRow = (sessionId: string) =>
|
|
51
56
|
db
|
|
52
57
|
.prepare(
|
|
53
|
-
`SELECT task_status, moved_to, quick_of,
|
|
58
|
+
`SELECT task_status, moved_to, quick_of, pause_state, paused_until,
|
|
54
59
|
EXISTS (SELECT 1 FROM stack_items i JOIN stacks s ON s.id = i.stack_id
|
|
55
60
|
WHERE i.session_id = @sid AND s.status = 'landing') AS landing
|
|
56
61
|
FROM sessions WHERE id = @sid`
|
|
@@ -63,6 +68,10 @@ export function chatHold(sessionId: string): string | null {
|
|
|
63
68
|
if (row?.task_status === "moving" && collecting.has(sessionId))
|
|
64
69
|
return "It's sent once the move to another machine finishes";
|
|
65
70
|
if (row?.landing) return "It's sent once its stack has landed";
|
|
71
|
+
if (row?.pause_state === "pausing" || row?.pause_state === "paused")
|
|
72
|
+
return row.paused_until
|
|
73
|
+
? "It's sent when the session resumes, with anything else held meanwhile"
|
|
74
|
+
: "It's sent when the session is resumed, with anything else held meanwhile";
|
|
66
75
|
return null;
|
|
67
76
|
}
|
|
68
77
|
|
package/lib/chat/settings.ts
CHANGED
|
@@ -7,7 +7,7 @@ import { effectiveChatAccess, noApprovalsOn } from "../approvals/no-approvals";
|
|
|
7
7
|
import os from "os";
|
|
8
8
|
import { db, type Session } from "../db";
|
|
9
9
|
import { agentEnv } from "../agents/launch";
|
|
10
|
-
import { resolveModelForAgent } from "../model-catalog";
|
|
10
|
+
import { modelDisplayName, resolveModelForAgent } from "../model-catalog";
|
|
11
11
|
import { chatDriverFor } from "./drivers";
|
|
12
12
|
import {
|
|
13
13
|
CHAT_ACCESS,
|
|
@@ -69,11 +69,15 @@ function visibleCommands(caps: Capabilities): ChatCommand[] {
|
|
|
69
69
|
export function emitCapabilities(session: Session, listener?: Listener): void {
|
|
70
70
|
const caps = registry.caps.get(capsKey(session));
|
|
71
71
|
if (!caps) return;
|
|
72
|
+
const model = chatModel(session);
|
|
72
73
|
const m: ChatServerMessage = {
|
|
73
74
|
type: "capabilities",
|
|
74
75
|
commands: visibleCommands(caps),
|
|
75
76
|
models: caps.models,
|
|
76
|
-
model
|
|
77
|
+
model,
|
|
78
|
+
modelLabel:
|
|
79
|
+
caps.models.find((o) => o.value === model)?.label ??
|
|
80
|
+
modelDisplayName(model),
|
|
77
81
|
access: effectiveChatAccess(session.chat_access),
|
|
78
82
|
plan: canPlan(session) ? !!session.chat_plan && !noApprovalsOn() : null,
|
|
79
83
|
background: chatDriverFor(session.agent_type)?.backgroundTasks === true,
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
// Read-only config a client renders with. Additive: a new field never bumps
|
|
2
|
+
// `v`; only a change that breaks an old reader does, and then the old field
|
|
3
|
+
// keeps being served beside the new one for a release. Synced to the native
|
|
4
|
+
// app, so it must stay pure: no imports.
|
|
5
|
+
export const CLIENT_CONFIG_VERSION = 1;
|
|
6
|
+
|
|
7
|
+
export type Tone = "muted" | "warning" | "primary" | "destructive" | "success";
|
|
8
|
+
|
|
9
|
+
export interface LabelTone {
|
|
10
|
+
label: string;
|
|
11
|
+
tone: Tone;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export interface AccessLevel {
|
|
15
|
+
value: string;
|
|
16
|
+
label: string;
|
|
17
|
+
description: string;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
// Keys are strings on purpose, so an older app can accept a state it doesn't
|
|
21
|
+
// know yet; each client narrows them through its own typed lookups.
|
|
22
|
+
export interface ClientConfig {
|
|
23
|
+
v: number;
|
|
24
|
+
server: { version: string | null; startedAt: string };
|
|
25
|
+
access: { levels: AccessLevel[]; default: string };
|
|
26
|
+
taskStates: Record<string, LabelTone>;
|
|
27
|
+
runStatuses: Record<string, LabelTone>;
|
|
28
|
+
runTriggers: Record<string, string>;
|
|
29
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { ACCESS_LEVELS, DEFAULT_NEW_CHAT_ACCESS } from "../chat/access-levels";
|
|
2
|
+
import { CHAT_ACCESS } from "../chat/events";
|
|
3
|
+
import {
|
|
4
|
+
RUN_STATUS_LABELS,
|
|
5
|
+
RUN_TRIGGER_LABELS,
|
|
6
|
+
} from "../recipes/status-labels";
|
|
7
|
+
import { TASK_STATE_LABELS } from "../tasks/state-labels";
|
|
8
|
+
import { cachedCheck } from "../updates/check";
|
|
9
|
+
import {
|
|
10
|
+
CLIENT_CONFIG_VERSION,
|
|
11
|
+
type ClientConfig,
|
|
12
|
+
} from "./client-config-types";
|
|
13
|
+
|
|
14
|
+
const startedAt = new Date(Date.now() - process.uptime() * 1000).toISOString();
|
|
15
|
+
|
|
16
|
+
export function clientConfig(): ClientConfig {
|
|
17
|
+
return {
|
|
18
|
+
v: CLIENT_CONFIG_VERSION,
|
|
19
|
+
server: { version: cachedCheck()?.current?.version ?? null, startedAt },
|
|
20
|
+
access: {
|
|
21
|
+
levels: CHAT_ACCESS.map((value) => ({ value, ...ACCESS_LEVELS[value] })),
|
|
22
|
+
default: DEFAULT_NEW_CHAT_ACCESS,
|
|
23
|
+
},
|
|
24
|
+
taskStates: TASK_STATE_LABELS,
|
|
25
|
+
runStatuses: RUN_STATUS_LABELS,
|
|
26
|
+
runTriggers: RUN_TRIGGER_LABELS,
|
|
27
|
+
};
|
|
28
|
+
}
|