@saadnvd1/agent-os 0.3.533 → 0.3.534
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 +68 -2
- package/app/api/features/pieces/[id]/release/route.ts +16 -0
- package/app/api/features/route.ts +8 -0
- package/app/api/recipes/[id]/carry/clear/route.ts +20 -0
- package/app/api/recipes/[id]/preview/route.ts +19 -0
- package/app/api/recipes/[id]/route.ts +51 -0
- package/app/api/recipes/[id]/run/route.ts +35 -0
- package/app/api/recipes/[id]/runs/route.ts +23 -0
- package/app/api/recipes/draft/route.ts +14 -0
- package/app/api/recipes/route.ts +35 -0
- package/app/api/tasks/[id]/plan-edits/route.ts +34 -0
- package/components/CommandPalette/useAppCommands.ts +20 -0
- package/components/Features/FeaturesSection.tsx +100 -0
- package/components/Features/index.ts +1 -0
- package/components/Recipes/RecipeForm.tsx +251 -0
- package/components/Recipes/RecipePage.tsx +127 -0
- package/components/Recipes/RecipesPanel.tsx +225 -0
- package/components/Recipes/RunChip.tsx +16 -0
- package/components/Recipes/RunHistory.tsx +88 -0
- package/components/Recipes/RunSheet.tsx +84 -0
- package/components/Recipes/TimerFields.tsx +193 -0
- package/components/Recipes/format.ts +51 -0
- package/components/Recipes/index.ts +1 -0
- package/components/Schedules/SchedulesPanel.tsx +1 -0
- package/components/SessionList/RowSubtitle.tsx +16 -0
- package/components/SessionList/SessionRowMenu.tsx +29 -0
- package/components/SessionList/SidebarHeader.tsx +5 -0
- package/components/SessionList/WorkspaceSwitcher.tsx +20 -4
- package/components/Settings/SettingsDialog.tsx +9 -0
- package/components/Tasks/PlanReview.tsx +253 -0
- package/components/Tasks/TaskRow.tsx +19 -44
- package/components/Tasks/TasksDialog.tsx +2 -0
- package/data/features/index.ts +40 -0
- package/data/push/topics.ts +8 -0
- package/data/recipes/index.ts +129 -0
- package/data/tasks/index.ts +14 -0
- package/lib/away/run.ts +4 -1
- package/lib/db/changes.ts +8 -0
- package/lib/db/migrations.ts +127 -0
- package/lib/db/queries.ts +4 -1
- package/lib/db/types.ts +6 -0
- package/lib/models/plan-step.ts +67 -6
- package/lib/notify/copy.ts +28 -10
- package/lib/notify/push/events.ts +36 -2
- package/lib/notify/push/policy.ts +2 -0
- package/lib/notify/push-types.ts +7 -3
- package/lib/orchestrator/brief.ts +13 -0
- package/lib/orchestrator/conditions.ts +3 -1
- package/lib/orchestrator/deliver.ts +2 -0
- package/lib/orchestrator/digest.ts +27 -1
- package/lib/orchestrator/plan-pieces.ts +41 -0
- package/lib/orchestrator/recipe-tools.ts +159 -0
- package/lib/orchestrator/review.ts +2 -1
- package/lib/orchestrator/serve.ts +25 -0
- package/lib/orchestrator/tool-names.ts +5 -0
- package/lib/orchestrator/tool-schemas.ts +54 -0
- package/lib/orchestrator/tools.ts +10 -0
- package/lib/plans/features.ts +200 -0
- package/lib/plans/pieces.ts +366 -0
- package/lib/plans/release.ts +323 -0
- package/lib/plans/view.ts +85 -0
- package/lib/recipes/api.ts +19 -0
- package/lib/recipes/chip.ts +40 -0
- package/lib/recipes/compose.ts +78 -0
- package/lib/recipes/draft.ts +93 -0
- package/lib/recipes/index.ts +54 -0
- package/lib/recipes/result.ts +59 -0
- package/lib/recipes/run.ts +202 -0
- package/lib/recipes/settle.ts +465 -0
- package/lib/recipes/store.ts +378 -0
- package/lib/recipes/test-fixtures.ts +49 -0
- package/lib/recipes/timer.ts +107 -0
- package/lib/recipes/types.ts +116 -0
- package/lib/schedules/index.ts +4 -1
- package/lib/schedules/start.ts +33 -9
- package/lib/schedules/store.ts +6 -2
- package/lib/security/demo.ts +1 -0
- package/lib/settings-url.ts +1 -0
- package/lib/tasks/loop-types.ts +23 -0
- package/lib/tasks/plan-view.ts +38 -1
- package/package.json +1 -1
- package/server.ts +2 -0
- package/stores/recipesUi.ts +53 -0
package/README.md
CHANGED
|
@@ -99,6 +99,11 @@ conversation in **Messages** and can join in. [More](#agent-network)
|
|
|
99
99
|
section in the agent's brief. [More](#project-config-agentosjson)
|
|
100
100
|
- **Schedules.** Start tasks, chats or orchestrator prompts on a timer, or
|
|
101
101
|
check in on a session, with run history. [More](#schedules)
|
|
102
|
+
- **Recipes.** Save a job that comes up again (a build, an audit, a report);
|
|
103
|
+
each run is a fresh task or session that is told what the last run
|
|
104
|
+
reported. [More](#recipes)
|
|
105
|
+
- **Plans in pieces.** A plan splits into several PRs only for a real reason,
|
|
106
|
+
checked by AgentOS; later pieces start by themselves. [More](#plans-in-pieces)
|
|
102
107
|
- **Stacks.** Run a board's cards as stacked PRs that merge bottom-up.
|
|
103
108
|
[More](#stacks)
|
|
104
109
|
- **Machine load.** A gauge in the sidebar, each session's share of CPU and
|
|
@@ -896,6 +901,66 @@ for a running turn, and refuses, naming the card, if it doesn't end), and
|
|
|
896
901
|
what it's sent during the land waits in its queue. A conflict stops on
|
|
897
902
|
that card with the exact command to fix it.
|
|
898
903
|
|
|
904
|
+
### Recipes
|
|
905
|
+
|
|
906
|
+
A recipe is a saved brief: a project, a kind (a **task**, which ends in a PR
|
|
907
|
+
and goes through the merge gates, or a **session**, which has no PR), and a
|
|
908
|
+
short brief that says what to run and how to judge it. The steps belong in a
|
|
909
|
+
script in the repo; the recipe says "run this, judge the result". Every run
|
|
910
|
+
is a fresh task or session, so nothing piles up in one long conversation.
|
|
911
|
+
|
|
912
|
+
Open **Settings → Recipes** (the sidebar's **⋯** menu, the workspace menu, or
|
|
913
|
+
⌘K → Recipes or "Run recipe: …"). **Run** takes an optional one-off note and
|
|
914
|
+
shows exactly what will be sent. **Save as recipe** on a session's menu or a
|
|
915
|
+
finished task prefills the form from what it did, with a suggested brief to
|
|
916
|
+
use or ignore.
|
|
917
|
+
|
|
918
|
+
- **Carried between runs.** A run ends with one line,
|
|
919
|
+
`RESULT: ok|fail key=value … | one sentence`. The next run's brief gets a
|
|
920
|
+
short block (at most 600 characters) with the last run's outcome and
|
|
921
|
+
those values, such as a build number; after a failure it also carries the
|
|
922
|
+
last good run. With no line, a cheap model summarises the run, and with
|
|
923
|
+
that unavailable the state alone is recorded. **Start the next run fresh**
|
|
924
|
+
drops the block.
|
|
925
|
+
- **One run at a time.** A run while another is going is refused, and a
|
|
926
|
+
double tap starts one run.
|
|
927
|
+
- **Timers.** A recipe can run hourly, daily, on weekdays, weekly or on a
|
|
928
|
+
cron expression, never more than once an hour; a timer that fires while a
|
|
929
|
+
run is going is skipped. Only you set timers: the orchestrator can't.
|
|
930
|
+
- **Orchestrator.** `recipes`, `run_recipe`, `recipe_save` and `recipe_update`
|
|
931
|
+
tools, and the workspace's recipes in its brief and digest. A run goes
|
|
932
|
+
through the same brakes, Pause and task queue as a task it started
|
|
933
|
+
itself, and a recipe is never an approval: it can't pass a gate. The
|
|
934
|
+
orchestrator gets an event when a run it started finishes, and for any
|
|
935
|
+
failure.
|
|
936
|
+
- **History.** Each recipe lists its runs: when, how triggered and by whom,
|
|
937
|
+
outcome, the reported values, cost, duration, and links to the session and
|
|
938
|
+
PR, with totals.
|
|
939
|
+
|
|
940
|
+
### Plans in pieces
|
|
941
|
+
|
|
942
|
+
A plan ends with a `## Pieces` block, one piece by default. AgentOS accepts
|
|
943
|
+
more only for one of four reasons, written on the piece: (a) it spans
|
|
944
|
+
repositories, (b) one part must be live (merged and deployed) before the
|
|
945
|
+
other can merge, (c) one part needs you mid-way, (d) one part must be
|
|
946
|
+
revertible on its own (a migration that rewrites rows, or an auth change
|
|
947
|
+
mixed with other work). Diff size is never a reason. A piece without a
|
|
948
|
+
standing reason is folded into the one before it, and a repository with no
|
|
949
|
+
deploy check can't claim (b); this is a deterministic check, not the model's
|
|
950
|
+
word.
|
|
951
|
+
|
|
952
|
+
A one-piece plan behaves as always. With several, piece 1 continues in the
|
|
953
|
+
planning task with only its slice, and the rest wait as rows (no worktree,
|
|
954
|
+
port or queue slot) until what they wait on has merged or deployed, then start
|
|
955
|
+
as tasks through the usual brakes and queue. A plan waiting for approval
|
|
956
|
+
shows its pieces: **Merge into ①** or **Drop** a piece (undoable until you
|
|
957
|
+
approve), **Approve with note…**, or **Reject**. Each piece's review judges
|
|
958
|
+
its slice; the last also checks the whole plan was built. **Tasks** shows
|
|
959
|
+
the feature's pieces and what each waits on, with **Release anyway** for one
|
|
960
|
+
held by a dropped or failed predecessor. A plan-only task that reaches
|
|
961
|
+
approval sends a "Plan ready: name, N pieces" push, and a finished feature
|
|
962
|
+
sends one "Feature merged" push naming it.
|
|
963
|
+
|
|
899
964
|
### Schedules
|
|
900
965
|
|
|
901
966
|
A schedule starts agent work at set times without you opening AgentOS:
|
|
@@ -1017,7 +1082,8 @@ line at the bottom of Schedules), one of:
|
|
|
1017
1082
|
|
|
1018
1083
|
A machine linked to Connect sends to the paired phones that have registered
|
|
1019
1084
|
for push, instead of Telegram. A push is generic: the title "AgentOS", a
|
|
1020
|
-
category word and a link to open, no session or task text (
|
|
1085
|
+
category word and a link to open, with no session or task text (a plan's or
|
|
1086
|
+
feature's name is the one exception, scrubbed to a single line) (the payload is a
|
|
1021
1087
|
versioned envelope, so an encrypted body can replace it later). It goes
|
|
1022
1088
|
through the Connect relay, signed with the machine key; AgentOS keeps a
|
|
1023
1089
|
`push_outbox` that retries with backoff, forgets tokens the relay reports as
|
|
@@ -1025,7 +1091,7 @@ dead, and caps alerting pushes at 10 an hour per device (one summary after).
|
|
|
1025
1091
|
|
|
1026
1092
|
- **What pushes**: an ask raised, a session waiting on you for a minute,
|
|
1027
1093
|
a task ready for review, CI or a review failing, a plan ready to approve,
|
|
1028
|
-
and the Away digest. Merged is off by default. Each device picks its
|
|
1094
|
+
a feature merged, and the Away digest. Merged is off by default. Each device picks its
|
|
1029
1095
|
categories (`PATCH /api/devices/me/push`). A task mirrored from another
|
|
1030
1096
|
machine never pushes from here.
|
|
1031
1097
|
- **Quiet rules**: waiting, review, CI and merged don't push while you're at
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { NextResponse } from "next/server";
|
|
2
|
+
import { releaseAnyway } from "@/lib/plans/release";
|
|
3
|
+
|
|
4
|
+
// A blocked piece stops waiting on what held it and starts on the next sweep.
|
|
5
|
+
export async function POST(
|
|
6
|
+
_request: Request,
|
|
7
|
+
{ params }: { params: Promise<{ id: string }> }
|
|
8
|
+
) {
|
|
9
|
+
const id = Number((await params).id);
|
|
10
|
+
if (!Number.isInteger(id) || !releaseAnyway(id))
|
|
11
|
+
return NextResponse.json(
|
|
12
|
+
{ error: "That piece isn't blocked" },
|
|
13
|
+
{ status: 409 }
|
|
14
|
+
);
|
|
15
|
+
return NextResponse.json({ success: true });
|
|
16
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { NextRequest, NextResponse } from "next/server";
|
|
2
|
+
import { featureViews } from "@/lib/plans/view";
|
|
3
|
+
|
|
4
|
+
export async function GET(request: NextRequest) {
|
|
5
|
+
return NextResponse.json({
|
|
6
|
+
features: featureViews(request.nextUrl.searchParams.get("workspace")),
|
|
7
|
+
});
|
|
8
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { NextRequest, NextResponse } from "next/server";
|
|
2
|
+
import { clearCarry } from "@/lib/recipes";
|
|
3
|
+
import { failed, resolveRecipe } from "@/lib/recipes/api";
|
|
4
|
+
|
|
5
|
+
// The next run starts without the last run's summary.
|
|
6
|
+
export async function POST(
|
|
7
|
+
request: NextRequest,
|
|
8
|
+
{ params }: { params: Promise<{ id: string }> }
|
|
9
|
+
) {
|
|
10
|
+
try {
|
|
11
|
+
const recipe = resolveRecipe(
|
|
12
|
+
(await params).id,
|
|
13
|
+
request.nextUrl.searchParams.get("workspace")
|
|
14
|
+
);
|
|
15
|
+
clearCarry(recipe.id);
|
|
16
|
+
return NextResponse.json({ ok: true });
|
|
17
|
+
} catch (error) {
|
|
18
|
+
return failed(error);
|
|
19
|
+
}
|
|
20
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { NextRequest, NextResponse } from "next/server";
|
|
2
|
+
import { previewPrompt } from "@/lib/recipes";
|
|
3
|
+
import { failed, resolveRecipe } from "@/lib/recipes/api";
|
|
4
|
+
|
|
5
|
+
// What a run started now would send, for the Run sheet.
|
|
6
|
+
export async function GET(
|
|
7
|
+
request: NextRequest,
|
|
8
|
+
{ params }: { params: Promise<{ id: string }> }
|
|
9
|
+
) {
|
|
10
|
+
try {
|
|
11
|
+
const q = request.nextUrl.searchParams;
|
|
12
|
+
const recipe = resolveRecipe((await params).id, q.get("workspace"));
|
|
13
|
+
return NextResponse.json({
|
|
14
|
+
prompt: previewPrompt(recipe.id, q.get("note")),
|
|
15
|
+
});
|
|
16
|
+
} catch (error) {
|
|
17
|
+
return failed(error, 404);
|
|
18
|
+
}
|
|
19
|
+
}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import { NextRequest, NextResponse } from "next/server";
|
|
2
|
+
import {
|
|
3
|
+
archiveRecipe,
|
|
4
|
+
listRuns,
|
|
5
|
+
recipeView,
|
|
6
|
+
setTimer,
|
|
7
|
+
updateRecipe,
|
|
8
|
+
} from "@/lib/recipes";
|
|
9
|
+
import { failed, resolveRecipe } from "@/lib/recipes/api";
|
|
10
|
+
|
|
11
|
+
type Params = { params: Promise<{ id: string }> };
|
|
12
|
+
|
|
13
|
+
const workspaceOf = (request: NextRequest) =>
|
|
14
|
+
request.nextUrl.searchParams.get("workspace");
|
|
15
|
+
|
|
16
|
+
export async function GET(request: NextRequest, { params }: Params) {
|
|
17
|
+
try {
|
|
18
|
+
const recipe = resolveRecipe((await params).id, workspaceOf(request));
|
|
19
|
+
return NextResponse.json({
|
|
20
|
+
recipe: recipeView(recipe),
|
|
21
|
+
runs: listRuns(recipe.id),
|
|
22
|
+
});
|
|
23
|
+
} catch (error) {
|
|
24
|
+
return failed(error, 404);
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
// The timer is Saad's: it can ride along with the settings here.
|
|
29
|
+
export async function PATCH(request: NextRequest, { params }: Params) {
|
|
30
|
+
try {
|
|
31
|
+
const recipe = resolveRecipe((await params).id, workspaceOf(request));
|
|
32
|
+
const body = await request.json();
|
|
33
|
+
const { timer, ...patch } = body;
|
|
34
|
+
const updated = updateRecipe(recipe.id, patch);
|
|
35
|
+
if (timer !== undefined) setTimer(updated, timer);
|
|
36
|
+
return NextResponse.json({ recipe: recipeView(updated) });
|
|
37
|
+
} catch (error) {
|
|
38
|
+
return failed(error);
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// Archives it: off the list and its timer off, its runs kept.
|
|
43
|
+
export async function DELETE(request: NextRequest, { params }: Params) {
|
|
44
|
+
try {
|
|
45
|
+
const recipe = resolveRecipe((await params).id, workspaceOf(request));
|
|
46
|
+
archiveRecipe(recipe.id);
|
|
47
|
+
return NextResponse.json({ ok: true });
|
|
48
|
+
} catch (error) {
|
|
49
|
+
return failed(error);
|
|
50
|
+
}
|
|
51
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { NextRequest, NextResponse } from "next/server";
|
|
2
|
+
import { AlreadyRunning, startRun } from "@/lib/recipes";
|
|
3
|
+
import { failed, resolveRecipe } from "@/lib/recipes/api";
|
|
4
|
+
import { BrakeRefused } from "@/lib/orchestrator/brakes";
|
|
5
|
+
|
|
6
|
+
type Params = { params: Promise<{ id: string }> };
|
|
7
|
+
|
|
8
|
+
// Run now. From the UI it is Saad's (manual); `aos recipe run` sends `from`,
|
|
9
|
+
// the session asking, and goes through the brakes like any agent's start.
|
|
10
|
+
export async function POST(request: NextRequest, { params }: Params) {
|
|
11
|
+
try {
|
|
12
|
+
const body = await request.json().catch(() => ({}));
|
|
13
|
+
const recipe = resolveRecipe(
|
|
14
|
+
(await params).id,
|
|
15
|
+
request.nextUrl.searchParams.get("workspace")
|
|
16
|
+
);
|
|
17
|
+
const from = typeof body.from === "string" ? body.from : null;
|
|
18
|
+
const { run, queued } = await startRun(recipe.id, {
|
|
19
|
+
trigger: from ? "cli" : "manual",
|
|
20
|
+
actorSessionId: from,
|
|
21
|
+
note: typeof body.note === "string" ? body.note : null,
|
|
22
|
+
requestId: typeof body.requestId === "string" ? body.requestId : null,
|
|
23
|
+
});
|
|
24
|
+
return NextResponse.json({ run, queued }, { status: 201 });
|
|
25
|
+
} catch (error) {
|
|
26
|
+
if (error instanceof AlreadyRunning)
|
|
27
|
+
return NextResponse.json(
|
|
28
|
+
{ error: "It's already running", liveRun: error.run },
|
|
29
|
+
{ status: 409 }
|
|
30
|
+
);
|
|
31
|
+
if (error instanceof BrakeRefused)
|
|
32
|
+
return NextResponse.json({ error: error.message }, { status: 429 });
|
|
33
|
+
return failed(error);
|
|
34
|
+
}
|
|
35
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { NextRequest, NextResponse } from "next/server";
|
|
2
|
+
import { listRuns } from "@/lib/recipes";
|
|
3
|
+
import { failed, resolveRecipe } from "@/lib/recipes/api";
|
|
4
|
+
|
|
5
|
+
export async function GET(
|
|
6
|
+
request: NextRequest,
|
|
7
|
+
{ params }: { params: Promise<{ id: string }> }
|
|
8
|
+
) {
|
|
9
|
+
try {
|
|
10
|
+
const recipe = resolveRecipe(
|
|
11
|
+
(await params).id,
|
|
12
|
+
request.nextUrl.searchParams.get("workspace")
|
|
13
|
+
);
|
|
14
|
+
const q = request.nextUrl.searchParams;
|
|
15
|
+
const runs = listRuns(recipe.id, {
|
|
16
|
+
before: q.get("before") ? Number(q.get("before")) : undefined,
|
|
17
|
+
limit: q.get("limit") ? Number(q.get("limit")) : undefined,
|
|
18
|
+
});
|
|
19
|
+
return NextResponse.json({ runs });
|
|
20
|
+
} catch (error) {
|
|
21
|
+
return failed(error, 404);
|
|
22
|
+
}
|
|
23
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { NextRequest, NextResponse } from "next/server";
|
|
2
|
+
import { recipeDraft } from "@/lib/recipes/draft";
|
|
3
|
+
import { failed } from "@/lib/recipes/api";
|
|
4
|
+
|
|
5
|
+
// A finished session as a recipe form: prefilled, with a drafted brief when
|
|
6
|
+
// the model could write one. Saves nothing.
|
|
7
|
+
export async function POST(request: NextRequest) {
|
|
8
|
+
try {
|
|
9
|
+
const { sessionId } = await request.json();
|
|
10
|
+
return NextResponse.json(await recipeDraft(String(sessionId ?? "")));
|
|
11
|
+
} catch (error) {
|
|
12
|
+
return failed(error);
|
|
13
|
+
}
|
|
14
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { NextRequest, NextResponse } from "next/server";
|
|
2
|
+
import {
|
|
3
|
+
createRecipe,
|
|
4
|
+
listRecipeViews,
|
|
5
|
+
recipeView,
|
|
6
|
+
setTimer,
|
|
7
|
+
} from "@/lib/recipes";
|
|
8
|
+
import { failed } from "@/lib/recipes/api";
|
|
9
|
+
|
|
10
|
+
export async function GET(request: NextRequest) {
|
|
11
|
+
const workspace = request.nextUrl.searchParams.get("workspace");
|
|
12
|
+
return NextResponse.json({ recipes: listRecipeViews(workspace) });
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export async function POST(request: NextRequest) {
|
|
16
|
+
try {
|
|
17
|
+
const body = await request.json();
|
|
18
|
+
const recipe = createRecipe({
|
|
19
|
+
workspaceId: body.workspaceId,
|
|
20
|
+
projectId: body.projectId,
|
|
21
|
+
name: body.name,
|
|
22
|
+
kind: body.kind,
|
|
23
|
+
brief: body.brief,
|
|
24
|
+
model: body.model,
|
|
25
|
+
view: body.view,
|
|
26
|
+
autoDone: body.autoDone,
|
|
27
|
+
maxMinutes: body.maxMinutes,
|
|
28
|
+
sourceSessionId: body.sourceSessionId,
|
|
29
|
+
});
|
|
30
|
+
if (body.timer) setTimer(recipe, body.timer);
|
|
31
|
+
return NextResponse.json({ recipe: recipeView(recipe) }, { status: 201 });
|
|
32
|
+
} catch (error) {
|
|
33
|
+
return failed(error);
|
|
34
|
+
}
|
|
35
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { NextRequest, NextResponse } from "next/server";
|
|
2
|
+
import { db, type Session } from "@/lib/db";
|
|
3
|
+
import { notifyStatusChanged } from "@/lib/status/hub";
|
|
4
|
+
|
|
5
|
+
// Saad's edits to a waiting plan's pieces: which to fold into the one before
|
|
6
|
+
// and which to leave out. Kept on the task and applied when the plan is
|
|
7
|
+
// approved; piece 1 can't be edited.
|
|
8
|
+
export async function PUT(
|
|
9
|
+
request: NextRequest,
|
|
10
|
+
{ params }: { params: Promise<{ id: string }> }
|
|
11
|
+
) {
|
|
12
|
+
const { id } = await params;
|
|
13
|
+
const body = await request.json().catch(() => ({}));
|
|
14
|
+
const ints = (v: unknown) =>
|
|
15
|
+
Array.isArray(v)
|
|
16
|
+
? [...new Set(v.filter((n) => Number.isInteger(n) && n >= 2 && n <= 6))]
|
|
17
|
+
: [];
|
|
18
|
+
const merge = ints(body?.merge);
|
|
19
|
+
const drop = ints(body?.drop).filter((n) => !merge.includes(n));
|
|
20
|
+
const session = db
|
|
21
|
+
.prepare(`SELECT plan_phase FROM sessions WHERE id = ?`)
|
|
22
|
+
.get(id) as Pick<Session, "plan_phase"> | undefined;
|
|
23
|
+
if (session?.plan_phase !== "awaiting")
|
|
24
|
+
return NextResponse.json(
|
|
25
|
+
{ error: "That task isn't waiting for plan approval" },
|
|
26
|
+
{ status: 409 }
|
|
27
|
+
);
|
|
28
|
+
db.prepare(`UPDATE sessions SET plan_edits = ? WHERE id = ?`).run(
|
|
29
|
+
JSON.stringify({ merge, drop }),
|
|
30
|
+
id
|
|
31
|
+
);
|
|
32
|
+
notifyStatusChanged();
|
|
33
|
+
return NextResponse.json({ success: true, merge, drop });
|
|
34
|
+
}
|
|
@@ -4,6 +4,7 @@ import { useSnapshot } from "valtio";
|
|
|
4
4
|
import { useTheme } from "next-themes";
|
|
5
5
|
import {
|
|
6
6
|
Archive,
|
|
7
|
+
ChefHat,
|
|
7
8
|
Code,
|
|
8
9
|
FolderGit2,
|
|
9
10
|
FolderPlus,
|
|
@@ -22,6 +23,8 @@ import {
|
|
|
22
23
|
import type { Session } from "@/lib/db";
|
|
23
24
|
import type { PaletteCommand } from "@/lib/palette/registry";
|
|
24
25
|
import { useWorkspacesQuery } from "@/data/workspaces";
|
|
26
|
+
import { useRecipesQuery } from "@/data/recipes";
|
|
27
|
+
import { recipesUiActions } from "@/stores/recipesUi";
|
|
25
28
|
import { usePaletteCommands } from "@/hooks/usePaletteCommands";
|
|
26
29
|
import { archivedUiActions } from "@/stores/archivedUi";
|
|
27
30
|
import { busUiActions } from "@/stores/busUi";
|
|
@@ -67,6 +70,7 @@ export function useAppCommands({
|
|
|
67
70
|
const { workspaceId } = useSnapshot(sidebarUi);
|
|
68
71
|
const current = workspaces.find((w) => w.id === workspaceId) ?? null;
|
|
69
72
|
const dark = resolvedTheme?.startsWith("dark") ?? false;
|
|
73
|
+
const { data: recipes = [] } = useRecipesQuery(workspaceId ?? null);
|
|
70
74
|
const addProject = useAddProject();
|
|
71
75
|
const addExisting = useAddExistingProject();
|
|
72
76
|
|
|
@@ -134,6 +138,22 @@ export function useAppCommands({
|
|
|
134
138
|
},
|
|
135
139
|
]
|
|
136
140
|
: []),
|
|
141
|
+
{
|
|
142
|
+
id: "app.recipes",
|
|
143
|
+
title: "Recipes",
|
|
144
|
+
group: "Go to",
|
|
145
|
+
keywords: ["saved", "jobs", "repeat", "testflight"],
|
|
146
|
+
icon: ChefHat,
|
|
147
|
+
run: () => recipesUiActions.open(workspaceId ?? null),
|
|
148
|
+
},
|
|
149
|
+
...recipes.map((r) => ({
|
|
150
|
+
id: `app.recipe.${r.id}`,
|
|
151
|
+
title: `Run recipe: ${r.name}`,
|
|
152
|
+
group: "Actions",
|
|
153
|
+
keywords: ["recipe", "run", r.projectName ?? ""],
|
|
154
|
+
icon: ChefHat,
|
|
155
|
+
run: () => recipesUiActions.openRecipe(r.id, true),
|
|
156
|
+
})),
|
|
137
157
|
{
|
|
138
158
|
id: "app.search-code",
|
|
139
159
|
title: "Search code",
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { ArrowUpRight, Check, Layers } from "lucide-react";
|
|
4
|
+
import { Button } from "@/components/ui/button";
|
|
5
|
+
import { useFeaturesQuery, useReleasePiece } from "@/data/features";
|
|
6
|
+
import type { FeatureView } from "@/lib/plans/view";
|
|
7
|
+
import { duration, money } from "@/components/Recipes/format";
|
|
8
|
+
import { sessionOpenActions } from "@/stores/sessionOpen";
|
|
9
|
+
import { tasksUiActions } from "@/stores/tasksUi";
|
|
10
|
+
import { cn } from "@/lib/utils";
|
|
11
|
+
|
|
12
|
+
function FeatureCard({ f }: { f: FeatureView }) {
|
|
13
|
+
const release = useReleasePiece();
|
|
14
|
+
if (f.status === "done")
|
|
15
|
+
return (
|
|
16
|
+
<p className="border-border/60 flex min-h-11 items-center gap-2 rounded-xl border px-3 text-sm">
|
|
17
|
+
<Check className="h-4 w-4 shrink-0 text-emerald-600" />
|
|
18
|
+
<span className="min-w-0 flex-1 truncate">
|
|
19
|
+
{f.name} · {f.total} {f.total === 1 ? "PR" : "PRs"}
|
|
20
|
+
{f.costUsd ? ` · ${money(f.costUsd)}` : ""}
|
|
21
|
+
{f.durationMs ? ` · ${duration(f.durationMs)}` : ""}
|
|
22
|
+
</span>
|
|
23
|
+
</p>
|
|
24
|
+
);
|
|
25
|
+
return (
|
|
26
|
+
<div
|
|
27
|
+
className={cn(
|
|
28
|
+
"border-border/60 space-y-1 rounded-xl border p-3",
|
|
29
|
+
f.urgent && "border-amber-500/50"
|
|
30
|
+
)}
|
|
31
|
+
>
|
|
32
|
+
<p className="text-sm font-medium">
|
|
33
|
+
{f.name}{" "}
|
|
34
|
+
<span className="text-muted-foreground font-normal">
|
|
35
|
+
{f.merged}/{f.total} merged
|
|
36
|
+
</span>
|
|
37
|
+
</p>
|
|
38
|
+
<ul>
|
|
39
|
+
{f.pieces.map((p) => (
|
|
40
|
+
<li
|
|
41
|
+
key={p.id}
|
|
42
|
+
className={cn(
|
|
43
|
+
"flex min-h-11 items-center gap-2 text-xs md:min-h-8",
|
|
44
|
+
p.status === "dropped" && "opacity-50"
|
|
45
|
+
)}
|
|
46
|
+
>
|
|
47
|
+
<span className="w-4 shrink-0 tabular-nums">{p.position}</span>
|
|
48
|
+
<span className="min-w-0 flex-1 truncate">
|
|
49
|
+
{p.name} <span className="text-muted-foreground">· {p.line}</span>
|
|
50
|
+
</span>
|
|
51
|
+
{p.status === "blocked" && (
|
|
52
|
+
<Button
|
|
53
|
+
size="sm"
|
|
54
|
+
variant="outline"
|
|
55
|
+
className="h-11 md:h-8"
|
|
56
|
+
disabled={release.isPending}
|
|
57
|
+
onClick={() => release.mutate(p.id)}
|
|
58
|
+
>
|
|
59
|
+
Release anyway
|
|
60
|
+
</Button>
|
|
61
|
+
)}
|
|
62
|
+
{p.sessionId && p.status !== "merged" && p.status !== "dropped" && (
|
|
63
|
+
<button
|
|
64
|
+
type="button"
|
|
65
|
+
aria-label={`Open ${p.name}`}
|
|
66
|
+
className="text-primary -mr-2 flex min-h-11 items-center px-2"
|
|
67
|
+
onClick={() => {
|
|
68
|
+
sessionOpenActions.request(p.sessionId!);
|
|
69
|
+
tasksUiActions.setPanelOpen(false);
|
|
70
|
+
}}
|
|
71
|
+
>
|
|
72
|
+
<ArrowUpRight className="h-3.5 w-3.5" />
|
|
73
|
+
</button>
|
|
74
|
+
)}
|
|
75
|
+
</li>
|
|
76
|
+
))}
|
|
77
|
+
</ul>
|
|
78
|
+
{release.error && (
|
|
79
|
+
<p className="text-destructive text-xs">{release.error.message}</p>
|
|
80
|
+
)}
|
|
81
|
+
</div>
|
|
82
|
+
);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// Plans that split into pieces: how far along, and what each waits on.
|
|
86
|
+
export function FeaturesSection() {
|
|
87
|
+
const { data: features = [] } = useFeaturesQuery();
|
|
88
|
+
if (!features.length) return null;
|
|
89
|
+
return (
|
|
90
|
+
<div className="space-y-2">
|
|
91
|
+
<p className="label-mono text-muted-foreground flex items-center gap-1.5 pt-2">
|
|
92
|
+
<Layers className="h-3 w-3" />
|
|
93
|
+
Plans in pieces
|
|
94
|
+
</p>
|
|
95
|
+
{features.map((f) => (
|
|
96
|
+
<FeatureCard key={f.id} f={f} />
|
|
97
|
+
))}
|
|
98
|
+
</div>
|
|
99
|
+
);
|
|
100
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { FeaturesSection } from "./FeaturesSection";
|