@saadnvd1/agent-os 0.3.571 → 0.3.573
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/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/ChatPanel.tsx +2 -0
- package/components/Chat/PausedBanner.tsx +54 -0
- package/components/Draft/DraftChips.tsx +25 -0
- package/components/Draft/DraftPanel.tsx +1 -0
- 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/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/hold.ts +11 -2
- 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/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/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/package.json +1 -1
- package/server.ts +2 -1
package/README.md
CHANGED
|
@@ -13,7 +13,7 @@ and check on all of it from your phone.
|
|
|
13
13
|
## What it does
|
|
14
14
|
|
|
15
15
|
- **An orchestrator for each workspace.** A chat that starts tasks, answers blocked agents, and merges pull requests only through gates. Anything that is yours to decide waits as an ask. [More](docs/reference/orchestrator.md)
|
|
16
|
-
- **Tasks that end in a pull request.** Each gets its own worktree, branch and ports, runs the project's code review, and waits for your sign-off. [More](docs/guides/tasks.md)
|
|
16
|
+
- **Tasks that end in a pull request.** Each gets its own worktree, branch and ports, runs the project's code review, and waits for your sign-off. A queue ranks waiting tasks by priority and machine load, can start one on a linked machine when this one is busy, and a running task can be paused for a set time and resumes by itself. [More](docs/guides/tasks.md)
|
|
17
17
|
- **Each kind of work on the right model.** Planning, review, implementation and titles each run on their own model, set in Settings → Models. [More](docs/reference/model-routing.md)
|
|
18
18
|
- **Nothing starts until you send.** ⌥N opens a draft with project, machine, agent and branch as chips. [More](docs/guides/sessions.md)
|
|
19
19
|
- **Other machines in the same list.** Link another machine that runs AgentOS and start or move work there. [More](docs/reference/machines.md)
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import { NextRequest, NextResponse } from "next/server";
|
|
2
2
|
import { spawnSession, findProject } from "@/lib/agents/spawn";
|
|
3
3
|
import { createTask } from "@/lib/tasks";
|
|
4
|
-
import { startOrQueue } from "@/lib/tasks/queue";
|
|
4
|
+
import { startOrQueue, stampPriority } from "@/lib/tasks/queue";
|
|
5
|
+
import { isPriority } from "@/lib/tasks/schedule-types";
|
|
5
6
|
import { hostIdNamed } from "@/lib/hosts";
|
|
6
7
|
import { startView } from "@/lib/sessions/launch";
|
|
7
8
|
import { db } from "@/lib/db";
|
|
@@ -30,6 +31,8 @@ export async function POST(request: NextRequest) {
|
|
|
30
31
|
after,
|
|
31
32
|
view,
|
|
32
33
|
planOnly,
|
|
34
|
+
priority,
|
|
35
|
+
machine,
|
|
33
36
|
from,
|
|
34
37
|
} = await request.json();
|
|
35
38
|
const startedBy = orchestratorFrom(from);
|
|
@@ -38,6 +41,12 @@ export async function POST(request: NextRequest) {
|
|
|
38
41
|
throw new Error("--after works for tasks only");
|
|
39
42
|
if (planOnly !== undefined && mode !== "task")
|
|
40
43
|
throw new Error("--plan-only works for tasks only");
|
|
44
|
+
if ((priority !== undefined || machine !== undefined) && mode !== "task")
|
|
45
|
+
throw new Error("--priority and --on-mac work for tasks only");
|
|
46
|
+
if (priority !== undefined && !isPriority(priority))
|
|
47
|
+
throw new Error("--priority is urgent, blocking, normal or background");
|
|
48
|
+
if (machine !== undefined && machine !== "mac" && machine !== "any")
|
|
49
|
+
throw new Error('machine is "mac" or "any"');
|
|
41
50
|
if (mode === "task") {
|
|
42
51
|
const task = {
|
|
43
52
|
projectId: findProject(String(project ?? "")).id,
|
|
@@ -50,8 +59,17 @@ export async function POST(request: NextRequest) {
|
|
|
50
59
|
startedBy,
|
|
51
60
|
};
|
|
52
61
|
const out = await startOrQueue(
|
|
53
|
-
{
|
|
54
|
-
|
|
62
|
+
{
|
|
63
|
+
...task,
|
|
64
|
+
after: typeof after === "string" ? after : undefined,
|
|
65
|
+
priority,
|
|
66
|
+
machine,
|
|
67
|
+
},
|
|
68
|
+
async () => {
|
|
69
|
+
const session = await createTask(task);
|
|
70
|
+
stampPriority(session.id, priority);
|
|
71
|
+
return session;
|
|
72
|
+
}
|
|
55
73
|
);
|
|
56
74
|
return "queued" in out
|
|
57
75
|
? NextResponse.json({ queued: out.queued }, { status: 202 })
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { describe, expect, it, vi } from "vitest";
|
|
2
|
+
|
|
3
|
+
const facts = vi.hoisted(() => ({ level: null as string | null }));
|
|
4
|
+
vi.mock("@/lib/load/machines", () => ({
|
|
5
|
+
localFacts: () => ({
|
|
6
|
+
level: facts.level,
|
|
7
|
+
load1: 1,
|
|
8
|
+
cores: 8,
|
|
9
|
+
memUsedPct: 40,
|
|
10
|
+
booted: 0,
|
|
11
|
+
capabilities: ["linux"],
|
|
12
|
+
}),
|
|
13
|
+
}));
|
|
14
|
+
|
|
15
|
+
import { GET } from "./route";
|
|
16
|
+
import { parseReading } from "@/lib/hosts/peer-load";
|
|
17
|
+
|
|
18
|
+
describe("GET /api/load", () => {
|
|
19
|
+
it("reports no level before the first sample, and the asker reads that as no reading", async () => {
|
|
20
|
+
facts.level = null;
|
|
21
|
+
const body = await GET().json();
|
|
22
|
+
expect(body.level).toBeNull();
|
|
23
|
+
expect(parseReading(body, 1)).toBeNull();
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
it("reports the level once it has one", async () => {
|
|
27
|
+
facts.level = "amber";
|
|
28
|
+
const body = await GET().json();
|
|
29
|
+
expect(body).toMatchObject({ level: "amber", cores: 8 });
|
|
30
|
+
expect(parseReading(body, 1)).toMatchObject({ level: "amber" });
|
|
31
|
+
});
|
|
32
|
+
});
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { NextResponse } from "next/server";
|
|
2
|
+
import { localFacts } from "@/lib/load/machines";
|
|
3
|
+
|
|
4
|
+
export const dynamic = "force-dynamic";
|
|
5
|
+
|
|
6
|
+
// This machine's load as the scheduler reads it, for another machine's
|
|
7
|
+
// AgentOS deciding whether to start a task here. No session names or paths.
|
|
8
|
+
// Before the monitor's first sample the level is null, which the asker
|
|
9
|
+
// reads as no reading (held), never as green.
|
|
10
|
+
export function GET() {
|
|
11
|
+
const f = localFacts();
|
|
12
|
+
return NextResponse.json({
|
|
13
|
+
level: f.level,
|
|
14
|
+
load1: f.load1,
|
|
15
|
+
cores: f.cores,
|
|
16
|
+
memUsedPct: f.memUsedPct,
|
|
17
|
+
booted: f.booted,
|
|
18
|
+
capabilities: f.capabilities,
|
|
19
|
+
});
|
|
20
|
+
}
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
import { NextResponse } from "next/server";
|
|
2
|
+
import {
|
|
3
|
+
PauseRefused,
|
|
4
|
+
pauseViewOf,
|
|
5
|
+
requestPause,
|
|
6
|
+
resumeNow,
|
|
7
|
+
setResumeTime,
|
|
8
|
+
} from "@/lib/tasks/pause";
|
|
9
|
+
|
|
10
|
+
const MAX_AHEAD_MS = 60 * 24 * 3600_000;
|
|
11
|
+
|
|
12
|
+
function parseUntil(v: unknown): Date | null | "bad" {
|
|
13
|
+
if (v === null) return null;
|
|
14
|
+
const at = typeof v === "string" ? new Date(v) : new Date(NaN);
|
|
15
|
+
const ahead = at.getTime() - Date.now();
|
|
16
|
+
return Number.isNaN(ahead) || ahead < 0 || ahead > MAX_AHEAD_MS ? "bad" : at;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
const fail = (error: unknown) =>
|
|
20
|
+
NextResponse.json(
|
|
21
|
+
{ error: error instanceof Error ? error.message : String(error) },
|
|
22
|
+
{ status: error instanceof PauseRefused ? 409 : 500 }
|
|
23
|
+
);
|
|
24
|
+
|
|
25
|
+
// POST /api/sessions/:id/pause { until, mode } - pauses it: until an ISO time
|
|
26
|
+
// (the reader's own 9am, worked out on their device), or null for until it is
|
|
27
|
+
// resumed. mode "turn" (the default) stops it at the end of its step; "now"
|
|
28
|
+
// interrupts the step. Refused with 409 and the reason when it can't be.
|
|
29
|
+
export async function POST(
|
|
30
|
+
request: Request,
|
|
31
|
+
{ params }: { params: Promise<{ id: string }> }
|
|
32
|
+
) {
|
|
33
|
+
const { id } = await params;
|
|
34
|
+
const body = (await request.json().catch(() => null)) as {
|
|
35
|
+
until?: unknown;
|
|
36
|
+
mode?: unknown;
|
|
37
|
+
} | null;
|
|
38
|
+
if (body?.until === undefined)
|
|
39
|
+
return NextResponse.json({ error: "until is required" }, { status: 400 });
|
|
40
|
+
const until = parseUntil(body.until);
|
|
41
|
+
if (until === "bad")
|
|
42
|
+
return NextResponse.json(
|
|
43
|
+
{ error: "until must be a time within the next 60 days" },
|
|
44
|
+
{ status: 400 }
|
|
45
|
+
);
|
|
46
|
+
if (body.mode !== undefined && body.mode !== "turn" && body.mode !== "now")
|
|
47
|
+
return NextResponse.json(
|
|
48
|
+
{ error: 'mode is "turn" or "now"' },
|
|
49
|
+
{ status: 400 }
|
|
50
|
+
);
|
|
51
|
+
try {
|
|
52
|
+
requestPause(id, { until, mode: body.mode ?? "turn", by: "person" });
|
|
53
|
+
return NextResponse.json({ pause: pauseViewOf(id) }, { status: 202 });
|
|
54
|
+
} catch (error) {
|
|
55
|
+
return fail(error);
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// PATCH /api/sessions/:id/pause { until } - changes when it resumes.
|
|
60
|
+
export async function PATCH(
|
|
61
|
+
request: Request,
|
|
62
|
+
{ params }: { params: Promise<{ id: string }> }
|
|
63
|
+
) {
|
|
64
|
+
const { id } = await params;
|
|
65
|
+
const body = (await request.json().catch(() => null)) as {
|
|
66
|
+
until?: unknown;
|
|
67
|
+
} | null;
|
|
68
|
+
if (body?.until === undefined)
|
|
69
|
+
return NextResponse.json({ error: "until is required" }, { status: 400 });
|
|
70
|
+
const until = parseUntil(body.until);
|
|
71
|
+
if (until === "bad")
|
|
72
|
+
return NextResponse.json(
|
|
73
|
+
{ error: "until must be a time within the next 60 days" },
|
|
74
|
+
{ status: 400 }
|
|
75
|
+
);
|
|
76
|
+
try {
|
|
77
|
+
setResumeTime(id, until);
|
|
78
|
+
return NextResponse.json({ pause: pauseViewOf(id) });
|
|
79
|
+
} catch (error) {
|
|
80
|
+
return fail(error);
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// DELETE /api/sessions/:id/pause - resume now (past the task limit), or keep
|
|
85
|
+
// running when it is still pausing.
|
|
86
|
+
export async function DELETE(
|
|
87
|
+
_request: Request,
|
|
88
|
+
{ params }: { params: Promise<{ id: string }> }
|
|
89
|
+
) {
|
|
90
|
+
const { id } = await params;
|
|
91
|
+
try {
|
|
92
|
+
await resumeNow(id, "person");
|
|
93
|
+
return NextResponse.json({ pause: pauseViewOf(id) });
|
|
94
|
+
} catch (error) {
|
|
95
|
+
return fail(error);
|
|
96
|
+
}
|
|
97
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { NextResponse } from "next/server";
|
|
2
|
+
import { setPriority } from "@/lib/tasks/queue";
|
|
3
|
+
import { isPriority } from "@/lib/tasks/schedule-types";
|
|
4
|
+
|
|
5
|
+
// PATCH /api/tasks/:id/priority { priority } - urgent, blocking, normal or
|
|
6
|
+
// background, for a queued task or a running one. A queued task keeps its age.
|
|
7
|
+
export async function PATCH(
|
|
8
|
+
request: Request,
|
|
9
|
+
{ params }: { params: Promise<{ id: string }> }
|
|
10
|
+
) {
|
|
11
|
+
const { id } = await params;
|
|
12
|
+
const body = (await request.json().catch(() => null)) as {
|
|
13
|
+
priority?: unknown;
|
|
14
|
+
} | null;
|
|
15
|
+
if (!isPriority(body?.priority))
|
|
16
|
+
return NextResponse.json(
|
|
17
|
+
{ error: "priority is one of: urgent, blocking, normal, background" },
|
|
18
|
+
{ status: 400 }
|
|
19
|
+
);
|
|
20
|
+
try {
|
|
21
|
+
setPriority(id, body.priority);
|
|
22
|
+
} catch (error) {
|
|
23
|
+
return NextResponse.json(
|
|
24
|
+
{ error: error instanceof Error ? error.message : String(error) },
|
|
25
|
+
{ status: 404 }
|
|
26
|
+
);
|
|
27
|
+
}
|
|
28
|
+
return NextResponse.json({ ok: true, priority: body.priority });
|
|
29
|
+
}
|
package/app/api/tasks/route.ts
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import { parseOverride } from "@/lib/models/resolve";
|
|
2
2
|
import { NextRequest, NextResponse } from "next/server";
|
|
3
3
|
import { createTask, listTasks, movedTasks } from "@/lib/tasks";
|
|
4
|
-
import { startOrQueue } from "@/lib/tasks/queue";
|
|
4
|
+
import { stampPriority, startOrQueue } from "@/lib/tasks/queue";
|
|
5
|
+
import { isPriority } from "@/lib/tasks/schedule-types";
|
|
5
6
|
import { TASK_CAPABILITIES } from "@/lib/tasks/remote";
|
|
6
7
|
import { ensureProject, type ProjectRef } from "@/lib/tasks/project-ref";
|
|
7
8
|
import { isSessionId, statusFor } from "@/lib/tasks/move-bundle";
|
|
@@ -40,12 +41,20 @@ export async function POST(request: NextRequest) {
|
|
|
40
41
|
plan,
|
|
41
42
|
plan_reason,
|
|
42
43
|
plan_only,
|
|
44
|
+
priority,
|
|
45
|
+
machine,
|
|
43
46
|
} = await request.json();
|
|
44
47
|
if (id !== undefined && !isSessionId(id)) throw new Error("Bad task id");
|
|
45
48
|
const pid =
|
|
46
49
|
project && typeof project === "object"
|
|
47
50
|
? (await ensureProject(project as ProjectRef)).id
|
|
48
51
|
: projectId;
|
|
52
|
+
if (priority !== undefined && !isPriority(priority))
|
|
53
|
+
throw new Error(
|
|
54
|
+
"Priority is one of: urgent, blocking, normal, background"
|
|
55
|
+
);
|
|
56
|
+
if (machine !== undefined && machine !== "any" && machine !== "mac")
|
|
57
|
+
throw new Error('Machine is "any" or "mac"');
|
|
49
58
|
if (agent !== undefined && !isValidAgentType(agent))
|
|
50
59
|
throw new Error("Unknown agent");
|
|
51
60
|
if (agent && agent !== "claude" && model != null && model !== "")
|
|
@@ -76,12 +85,18 @@ export async function POST(request: NextRequest) {
|
|
|
76
85
|
return NextResponse.json({ session }, { status: 201 });
|
|
77
86
|
}
|
|
78
87
|
const out = await startOrQueue(
|
|
79
|
-
{
|
|
88
|
+
{
|
|
89
|
+
...opts,
|
|
90
|
+
after: typeof after === "string" ? after : undefined,
|
|
91
|
+
priority,
|
|
92
|
+
machine,
|
|
93
|
+
},
|
|
80
94
|
() => createTask(opts)
|
|
81
95
|
);
|
|
82
96
|
if ("queued" in out)
|
|
83
97
|
return NextResponse.json({ queued: out.queued }, { status: 202 });
|
|
84
98
|
const session = out.started;
|
|
99
|
+
stampPriority(session.id, priority);
|
|
85
100
|
return NextResponse.json({ session }, { status: 201 });
|
|
86
101
|
} catch (error) {
|
|
87
102
|
const message = error instanceof Error ? error.message : String(error);
|
|
@@ -12,6 +12,7 @@ import {
|
|
|
12
12
|
import { ArrowDown, Loader2 } from "lucide-react";
|
|
13
13
|
import type { ClipboardEvent } from "react";
|
|
14
14
|
import { useChat } from "@/data/chat/useChat";
|
|
15
|
+
import { PausedBanner } from "./PausedBanner";
|
|
15
16
|
import {
|
|
16
17
|
groupTimeline,
|
|
17
18
|
sameBlock,
|
|
@@ -716,6 +717,7 @@ export function ChatPanel({
|
|
|
716
717
|
setInsert({ text: quoteMarkdown(text), at: Date.now() })
|
|
717
718
|
}
|
|
718
719
|
/>
|
|
720
|
+
<PausedBanner sessionId={sessionId} />
|
|
719
721
|
<Queue
|
|
720
722
|
queue={queue}
|
|
721
723
|
running={running}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { Pause } from "lucide-react";
|
|
4
|
+
import { toast } from "sonner";
|
|
5
|
+
import { useSessionsQuery } from "@/data/sessions";
|
|
6
|
+
import { useResumeSession } from "@/data/sessions/shelf";
|
|
7
|
+
import { pauseSubtitle } from "@/lib/sidebar/paused";
|
|
8
|
+
import { Button } from "@/components/ui/button";
|
|
9
|
+
|
|
10
|
+
// Above the composer while the session is paused: when it resumes, and a
|
|
11
|
+
// button to resume it now. What is sent meanwhile waits and goes with it.
|
|
12
|
+
export function PausedBanner({ sessionId }: { sessionId: string }) {
|
|
13
|
+
const { data } = useSessionsQuery();
|
|
14
|
+
const resume = useResumeSession();
|
|
15
|
+
const session = data?.sessions.find((s) => s.id === sessionId);
|
|
16
|
+
const state = session?.pause_state;
|
|
17
|
+
if (!session || !state) return null;
|
|
18
|
+
const until = session.paused_until
|
|
19
|
+
? Date.parse(`${session.paused_until.replace(" ", "T")}Z`)
|
|
20
|
+
: null;
|
|
21
|
+
return (
|
|
22
|
+
<section
|
|
23
|
+
aria-label="Paused"
|
|
24
|
+
className="bg-card popover-surface mb-2 flex min-h-11 items-center gap-2 rounded-2xl px-3 py-1.5"
|
|
25
|
+
>
|
|
26
|
+
<Pause
|
|
27
|
+
aria-hidden
|
|
28
|
+
className="h-4 w-4 shrink-0 text-amber-600 dark:text-amber-400"
|
|
29
|
+
/>
|
|
30
|
+
<p className="min-w-0 flex-1 text-sm">
|
|
31
|
+
{pauseSubtitle({ state, until })}
|
|
32
|
+
<span className="text-muted-foreground block text-xs">
|
|
33
|
+
Messages you send now are held and delivered together when it resumes.
|
|
34
|
+
</span>
|
|
35
|
+
</p>
|
|
36
|
+
{(state === "paused" || state === "pausing") && (
|
|
37
|
+
<Button
|
|
38
|
+
size="sm"
|
|
39
|
+
variant="secondary"
|
|
40
|
+
className="min-h-11 shrink-0"
|
|
41
|
+
disabled={resume.isPending}
|
|
42
|
+
onClick={() =>
|
|
43
|
+
resume.mutate(
|
|
44
|
+
{ id: sessionId },
|
|
45
|
+
{ onError: (e) => toast.error(e.message) }
|
|
46
|
+
)
|
|
47
|
+
}
|
|
48
|
+
>
|
|
49
|
+
{state === "paused" ? "Resume now" : "Keep running"}
|
|
50
|
+
</Button>
|
|
51
|
+
)}
|
|
52
|
+
</section>
|
|
53
|
+
);
|
|
54
|
+
}
|
|
@@ -6,6 +6,7 @@ import {
|
|
|
6
6
|
Cpu,
|
|
7
7
|
FolderGit2,
|
|
8
8
|
GitBranch,
|
|
9
|
+
Flag,
|
|
9
10
|
GitPullRequest,
|
|
10
11
|
Monitor,
|
|
11
12
|
SquareTerminal,
|
|
@@ -21,6 +22,12 @@ import { useProjectMachinesQuery } from "@/data/projects";
|
|
|
21
22
|
import { modelChoices, useModelsQuery } from "@/data/models";
|
|
22
23
|
import type { AgentProbe } from "@/lib/agents/probe";
|
|
23
24
|
import { ChipHeading, ChipItem, ChipMenu, ChipToggle } from "./Chip";
|
|
25
|
+
import {
|
|
26
|
+
DEFAULT_PRIORITY,
|
|
27
|
+
PRIORITIES,
|
|
28
|
+
PRIORITY_HINT,
|
|
29
|
+
PRIORITY_LABEL,
|
|
30
|
+
} from "@/lib/tasks/schedule-types";
|
|
24
31
|
|
|
25
32
|
// What stands between an agent and its first prompt, if anything.
|
|
26
33
|
export function agentBlocker(probe?: AgentProbe): string | null {
|
|
@@ -260,6 +267,24 @@ export function DraftChips({
|
|
|
260
267
|
))}
|
|
261
268
|
</ChipMenu>
|
|
262
269
|
)}
|
|
270
|
+
{project && draft.openPr && (
|
|
271
|
+
<ChipMenu
|
|
272
|
+
icon={Flag}
|
|
273
|
+
name="Priority"
|
|
274
|
+
label={PRIORITY_LABEL[draft.taskPriority ?? DEFAULT_PRIORITY]}
|
|
275
|
+
>
|
|
276
|
+
{PRIORITIES.map((p) => (
|
|
277
|
+
<ChipItem
|
|
278
|
+
key={p}
|
|
279
|
+
checked={p === (draft.taskPriority ?? DEFAULT_PRIORITY)}
|
|
280
|
+
onSelect={() => onChange({ taskPriority: p })}
|
|
281
|
+
hint={PRIORITY_HINT[p]}
|
|
282
|
+
>
|
|
283
|
+
{PRIORITY_LABEL[p]}
|
|
284
|
+
</ChipItem>
|
|
285
|
+
))}
|
|
286
|
+
</ChipMenu>
|
|
287
|
+
)}
|
|
263
288
|
{project && draft.openPr && (
|
|
264
289
|
<ChipToggle
|
|
265
290
|
icon={SquareTerminal}
|
|
@@ -66,6 +66,7 @@ export function DraftPanel({
|
|
|
66
66
|
projectId: project.id,
|
|
67
67
|
prompt: text,
|
|
68
68
|
model: draft.taskModel ?? undefined,
|
|
69
|
+
priority: draft.taskPriority,
|
|
69
70
|
baseBranch: draft.baseBranch ?? undefined,
|
|
70
71
|
hostId: draft.hostId !== project.host_id ? draft.hostId : undefined,
|
|
71
72
|
view: draft.terminal ? "terminal" : undefined,
|
|
@@ -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)}>
|