@saadnvd1/agent-os 0.3.468 → 0.3.471
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 +67 -37
- package/app/api/fresh-start/route.ts +9 -5
- package/app/api/projects/clone/route.ts +4 -2
- package/app/api/projects/init/route.test.ts +46 -0
- package/app/api/projects/init/route.ts +24 -4
- package/app/api/usage/route.ts +4 -2
- package/components/Chat/ChatPanel.tsx +5 -5
- package/components/Chat/StaleChip.tsx +7 -6
- package/components/CommandPalette/CommandPalette.tsx +3 -1
- package/components/CommandPalette/useAppCommands.ts +10 -0
- package/components/Projects/AddProject/AddProjectDialog.tsx +25 -5
- package/components/Projects/AddProject/CloneForm.tsx +3 -1
- package/components/Projects/AddProject/FolderForm.tsx +98 -4
- package/components/Projects/AddProject/NameForm.tsx +3 -1
- package/components/Projects/AddProject/WorkspacePick.tsx +56 -0
- package/components/Projects/AddProject/useAddExistingProject.ts +58 -0
- package/components/SessionList/WorkspaceSwitcher.tsx +10 -0
- package/components/Usage/UsageDialog.tsx +31 -2
- package/components/Workspaces/FreshStartForm.tsx +13 -13
- package/data/projects/add.ts +23 -3
- package/data/usage/index.ts +2 -0
- package/lib/agents/launch.ts +2 -0
- package/lib/chat/driver.ts +8 -0
- package/lib/chat/drivers/claude-approvals.ts +40 -0
- package/lib/chat/drivers/claude.ts +7 -10
- package/lib/chat/refresh.ts +31 -13
- package/lib/chat/stale.ts +25 -16
- package/lib/chat/worker/extras.ts +1 -0
- package/lib/chat/worker/host.ts +61 -22
- package/lib/db/migrations.ts +42 -0
- package/lib/orchestrator/add-project.ts +35 -0
- package/lib/orchestrator/armed.ts +1 -2
- package/lib/orchestrator/brief.ts +6 -5
- package/lib/orchestrator/claude-cli.ts +5 -0
- package/lib/orchestrator/diff.ts +6 -2
- package/lib/orchestrator/external-gates.ts +8 -43
- package/lib/orchestrator/gates.ts +10 -10
- package/lib/orchestrator/pr-wake.ts +2 -2
- package/lib/orchestrator/rebase.ts +6 -45
- package/lib/orchestrator/review.ts +18 -5
- package/lib/orchestrator/scope.ts +9 -0
- package/lib/orchestrator/serve.ts +6 -0
- package/lib/orchestrator/signoff.ts +13 -8
- package/lib/orchestrator/tool-names.ts +13 -6
- package/lib/orchestrator/tool-schemas.ts +15 -0
- package/lib/orchestrator/tools.ts +2 -0
- package/lib/orchestrator/worker.ts +4 -1
- package/lib/orchestrator/write-guard.ts +295 -0
- package/lib/project-add/index.ts +133 -14
- package/lib/project-add/name.ts +1 -0
- package/lib/secret-paths.ts +9 -0
- package/lib/session-audit/render.ts +1 -1
- package/lib/stacks/land.ts +23 -16
- package/lib/tasks/brief.ts +3 -14
- package/lib/tasks/code-review.ts +69 -107
- package/lib/tasks/gh.ts +3 -2
- package/lib/tasks/index.ts +1 -1
- package/lib/tasks/state.ts +4 -4
- package/lib/usage/check-runs.ts +167 -0
- package/lib/usage/report.ts +25 -0
- package/lib/usage/turns.ts +28 -0
- package/package.json +1 -1
- package/stores/addProject.ts +12 -3
- package/scripts/check-code-review.ts +0 -21
package/README.md
CHANGED
|
@@ -17,8 +17,8 @@ and check on all of it from your phone.
|
|
|
17
17
|
Every workspace has one orchestrator, a chat pinned at the top of its list.
|
|
18
18
|
It starts tasks, answers agents that are blocked, and reviews and merges
|
|
19
19
|
pull requests through gates: CI green and settled, an independent review of
|
|
20
|
-
the exact commit (big diffs are reviewed in parts),
|
|
21
|
-
|
|
20
|
+
the exact commit (big diffs are reviewed in parts), nothing blocked, and the
|
|
21
|
+
diff in scope. Anything that is yours to
|
|
22
22
|
decide (publishing, money, something irreversible, credentials) waits as an
|
|
23
23
|
ask with Approve, Decline and Reply. Turn on **Require my approval** and
|
|
24
24
|
sensitive or large PRs wait for you too. [More](#orchestrator)
|
|
@@ -312,25 +312,34 @@ The ring in the tab bar (in the composer on a phone) is how full the
|
|
|
312
312
|
conversation's context is, measured against the room before auto-compact:
|
|
313
313
|
amber past 60%, red past 85%. Tap it for the breakdown and **Compact now**.
|
|
314
314
|
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
315
|
+
Chats and tasks compact at 150k tokens of context by default (Settings →
|
|
316
|
+
Workspaces → Compact; off sets it to never), not near the model's window:
|
|
317
|
+
AgentOS launches Claude Code with `CLAUDE_CODE_AUTO_COMPACT_WINDOW` set to
|
|
318
|
+
that size (its floor is 100k), so the agent compacts itself mid-turn. Between
|
|
319
|
+
turns AgentOS catches what it didn't: a chat over the threshold is compacted
|
|
320
|
+
before the next message, and so is one idle past its prompt cache (5 minutes
|
|
321
|
+
or an hour, read from the agent's own usage), whose next message would
|
|
322
|
+
re-read the whole conversation at the cache-write price. A chip above the
|
|
323
|
+
composer says "Idle 47m · 412k context · Compact first?": Send compacts and
|
|
324
|
+
then sends, **Send as-is** skips it for that one message. Messages nobody is
|
|
325
|
+
typing (the orchestrator's `send`, AgentOS events and wakes, `aos send`,
|
|
326
|
+
schedules) compact first on their own, only between turns, and the chat
|
|
327
|
+
shows one quiet line with the context before and after. An orchestrator
|
|
328
|
+
isn't compacted (a summary would lose the fence around what it read from
|
|
329
|
+
tasks and CI): it starts a fresh conversation from its brief and the current
|
|
330
|
+
state at its fresh-start threshold, or earlier once idle past its cache.
|
|
327
331
|
On a desktop browser the screen button attaches a screenshot of a screen or
|
|
328
332
|
window you pick; on a phone, attach a screenshot as a photo.
|
|
329
333
|
|
|
330
334
|
**Usage** (the sidebar's ⋯ menu, or the palette) shows the account's 5-hour
|
|
331
335
|
and weekly windows, and what chats cost today, over 7 or 30 days, by day,
|
|
332
336
|
workspace and session. Costs are the agent's own estimates, recorded as each
|
|
333
|
-
turn ends.
|
|
337
|
+
turn ends. **Gate reviews** below it shows what the orchestrator's
|
|
338
|
+
independent reviewer and scope check cost, by PR: they run as `claude -p`
|
|
339
|
+
and leave no transcript, so each run's tokens, model and duration are read
|
|
340
|
+
from its JSON result into `check_runs`, and the review's verdict says them
|
|
341
|
+
too. Every compaction is kept in `chat_compactions` (who ran it, and the
|
|
342
|
+
context before and after), so cost per task can be compared by date.
|
|
334
343
|
|
|
335
344
|
**Esc** stops a running turn, as it does in Claude Code's terminal, unless a
|
|
336
345
|
menu, dialog or the command list is open (those close first). On a phone the
|
|
@@ -465,17 +474,15 @@ Starting a task returns at once and shows **Setting up** until the agent
|
|
|
465
474
|
launches; a restart in the middle resumes it. A failed setup shows on the task
|
|
466
475
|
and is in the agent's first prompt (redacted, as data).
|
|
467
476
|
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
[.claude/agents](.claude/agents) and the skill in
|
|
478
|
-
[.claude/skills](.claude/skills/README.md).
|
|
477
|
+
Tasks don't review their own PRs. The orchestrator's independent review of
|
|
478
|
+
the PR's exact head commit is the one review: `sign_off` and a stack's
|
|
479
|
+
**Land** refuse a PR without a passing one, and a PR body with AI
|
|
480
|
+
attribution. A head AgentOS rebased itself is covered by the review of the
|
|
481
|
+
head it rebased while the PR's own patch is unchanged. A task may still run
|
|
482
|
+
`/do-code-review` (the project's `.claude/skills/do-code-review`, or Claude
|
|
483
|
+
Code's `/code-review`) on a risky change before its PR; it's no longer
|
|
484
|
+
required. The review agents are in [.claude/agents](.claude/agents) and the
|
|
485
|
+
skill in [.claude/skills](.claude/skills/README.md).
|
|
479
486
|
|
|
480
487
|
Requires the GitHub CLI (`gh`) signed in, and a project with a GitHub remote.
|
|
481
488
|
|
|
@@ -872,7 +879,11 @@ it may do on its own, and the lines that always come back to you as asks.
|
|
|
872
879
|
- **Tools:** `sessions` (every session's status, activity, task, PR, CI and
|
|
873
880
|
stack position), `read` (the end of a terminal or chat) and `cards` (the
|
|
874
881
|
linked boards' cards), plus acting tools: `send`, `start_task`,
|
|
875
|
-
`start_session`, `stack`, `stack_status`, `land`, `drop`, `stop`, `done`,
|
|
882
|
+
`start_session`, `stack`, `stack_status`, `land`, `drop`, `stop`, `done`,
|
|
883
|
+
`add_project` (a git repository on the machine becomes a project of the
|
|
884
|
+
workspace, or the project already at that path moves in from no workspace;
|
|
885
|
+
folders that hold secrets and projects of another workspace are refused),
|
|
886
|
+
`note`,
|
|
876
887
|
`rule_add`, `rule_remove`, `review`, `sign_off`, `arm`, `disarm` and `ask_saad`. All are served in-process to its chat, refuse
|
|
877
888
|
any target outside its workspace, and validate their arguments. The one
|
|
878
889
|
exception is other orchestrators: `orchestrators` lists every workspace and
|
|
@@ -882,6 +893,29 @@ it may do on its own, and the lines that always come back to you as asks.
|
|
|
882
893
|
approve, answer an ask or pass a gate. The route
|
|
883
894
|
they call answers only the orchestrator's worker, by a per-orchestrator
|
|
884
895
|
secret. Its shell runs only `aos` commands that read.
|
|
896
|
+
- **Files:** it reads anywhere and writes (Write, Edit, NotebookEdit) anywhere
|
|
897
|
+
except a project repository, a task worktree (`~/.agent-os/worktrees`),
|
|
898
|
+
AgentOS's own install and state (all of `~/.agent-os` but its own folder),
|
|
899
|
+
secret files (`.env`, keys, `~/.ssh`, `~/.aws`, `gh`'s token, tokenvault and
|
|
900
|
+
the like; the list the merge gates use for secrets handling,
|
|
901
|
+
`lib/secret-paths.ts`), and files that make something run: dotfiles and
|
|
902
|
+
dot-folders at the top of its home (shell, git, Claude and tool config, the
|
|
903
|
+
usage brake's `limits.json`), `~/Library`, `/Library/LaunchAgents` and
|
|
904
|
+
`LaunchDaemons`, git's `core.hooksPath` folders (yours and each project's),
|
|
905
|
+
the folders on `PATH`, and any `.git`, `.claude`, `.mcp.json`, `CLAUDE.md`
|
|
906
|
+
or `AGENTS.md`, since writing one would give it a shell or run code in
|
|
907
|
+
AgentOS's own git; and the temporary checkouts a review or rebase runs in.
|
|
908
|
+
Code changes to a project go through a task, so its PR still passes CI,
|
|
909
|
+
review and scope. It is a list of exceptions, not of allowed places: a
|
|
910
|
+
script a hook or scheduled job runs from an ordinary folder can still be
|
|
911
|
+
written, so an orchestrator misled by text it read could run code that way.
|
|
912
|
+
That's accepted: task agents can already write anywhere, so an allowlist
|
|
913
|
+
on the orchestrator alone would buy little.
|
|
914
|
+
Its chat worker checks every write in a PreToolUse hook before it runs, on
|
|
915
|
+
the path with symlinks and `..` resolved as the system will, and refuses
|
|
916
|
+
with the reason; the repositories are read from the database on each write,
|
|
917
|
+
so a project added since counts. A worker from before an update picks the
|
|
918
|
+
new permissions up at its handover to the new build.
|
|
885
919
|
- **Brakes:** every start is refused, with the reason, while 4 sessions run in
|
|
886
920
|
the workspace, after 6 starts in an hour (`orch_max_running` and
|
|
887
921
|
`orch_max_starts_per_hour` on `workspaces`), or when the account's 5-hour
|
|
@@ -907,7 +941,7 @@ it may do on its own, and the lines that always come back to you as asks.
|
|
|
907
941
|
it (2 minutes with no new check), its review passed, nothing is
|
|
908
942
|
`BLOCKED:` or waiting, the diff stays in scope (no secrets, not only
|
|
909
943
|
lockfiles, nothing outside the repo, within the card) and its stack parent
|
|
910
|
-
has merged, and the PR body
|
|
944
|
+
has merged, and the PR body carries no AI attribution. `land` judges each item again at its own head right before
|
|
911
945
|
merging it. A gate failing again on a different commit, or a repo with no
|
|
912
946
|
CI, goes to you as an ask, and the orchestrator stops merging that task.
|
|
913
947
|
A failure counts once per task, gate and head commit, however many times
|
|
@@ -926,17 +960,15 @@ it may do on its own, and the lines that always come back to you as asks.
|
|
|
926
960
|
conflicts (read in the PR poll AgentOS already makes), `git merge-tree`
|
|
927
961
|
checks it against the base. Clean, and with the task's agent idle and
|
|
928
962
|
nothing uncommitted, AgentOS rebases it in the task's worktree and pushes
|
|
929
|
-
only its branch with `--force-with-lease
|
|
930
|
-
|
|
931
|
-
and the orchestrator's review carries over when the PR's own patch is
|
|
963
|
+
only its branch with `--force-with-lease`, and the orchestrator's review
|
|
964
|
+
carries over when the PR's own patch is
|
|
932
965
|
unchanged (same `git patch-id`; a fresh review starts otherwise). Not
|
|
933
966
|
clean: the task is asked once to rebase. Either way, one event. Main and
|
|
934
967
|
other tasks' branches are never written.
|
|
935
968
|
- **Tasks woken on CI failures and reviews:** a check failing on a task's
|
|
936
969
|
PR head messages the task's own agent once per head, with the check, the
|
|
937
970
|
failing step and about 60 lines of `gh run view --log-failed` around the
|
|
938
|
-
error, telling it to fix the root cause
|
|
939
|
-
line. A review or comment from a collaborator (not the account gh runs
|
|
971
|
+
error, telling it to fix the root cause and push. A review or comment from a collaborator (not the account gh runs
|
|
940
972
|
as, which is the task's own) wakes it once per review or comment; one
|
|
941
973
|
GraphQL query per repository a minute fingerprints the open task PRs, and
|
|
942
974
|
only a PR whose fingerprint moved has its reviews read. Comment-only wakes
|
|
@@ -949,8 +981,7 @@ it may do on its own, and the lines that always come back to you as asks.
|
|
|
949
981
|
`owner/repo#N` or URL, as long as its repository is one of the workspace's
|
|
950
982
|
projects; anything else is refused. The same gates apply, except that a
|
|
951
983
|
draft waits and a "blocked", "do not merge" or "wip" label holds it (there's
|
|
952
|
-
no session to read)
|
|
953
|
-
the orchestrator's own review of that head. `sessions` also lists tmux
|
|
984
|
+
no session to read). `sessions` also lists tmux
|
|
954
985
|
sessions AgentOS didn't start that work in a workspace repository or a
|
|
955
986
|
worktree of it, here or on another machine, with their branch's PR; `read`
|
|
956
987
|
reads them, and nothing else acts on them.
|
|
@@ -1306,8 +1337,7 @@ until `npm install` runs in them. Typecheck, lint and tests are left to CI.
|
|
|
1306
1337
|
In an emergency, `git commit --no-verify` skips it. CI runs `scripts/check --build` on every pull request and push to main,
|
|
1307
1338
|
which also holds the app's first-load JavaScript to a budget
|
|
1308
1339
|
(`scripts/check-bundle.mjs`; `npx next experimental-analyze` shows what's in
|
|
1309
|
-
it)
|
|
1310
|
-
commit (run `/do-code-review` first; see [Tasks](#tasks)).
|
|
1340
|
+
it).
|
|
1311
1341
|
|
|
1312
1342
|
### Session audits
|
|
1313
1343
|
|
|
@@ -2,17 +2,21 @@ import { NextRequest, NextResponse } from "next/server";
|
|
|
2
2
|
import { refreshSettings, setRefreshSettings } from "@/lib/chat/refresh";
|
|
3
3
|
|
|
4
4
|
// When a chat's conversation starts over (lib/chat/refresh): context
|
|
5
|
-
// tokens for orchestrators and for tasks, and
|
|
6
|
-
//
|
|
5
|
+
// tokens for orchestrators and for tasks, and at which chats and tasks
|
|
6
|
+
// compact (lib/chat/stale); 0 for never.
|
|
7
7
|
export async function GET() {
|
|
8
8
|
return NextResponse.json(refreshSettings());
|
|
9
9
|
}
|
|
10
10
|
|
|
11
11
|
export async function PUT(request: NextRequest) {
|
|
12
12
|
try {
|
|
13
|
-
const
|
|
14
|
-
|
|
15
|
-
|
|
13
|
+
const body = await request.json();
|
|
14
|
+
setRefreshSettings({
|
|
15
|
+
orchestratorTokens: body.orchestratorTokens,
|
|
16
|
+
taskTokens: body.taskTokens,
|
|
17
|
+
// Its name before it also set the agents' own auto-compact.
|
|
18
|
+
compactTokens: body.compactTokens ?? body.staleCompactTokens,
|
|
19
|
+
});
|
|
16
20
|
return NextResponse.json(refreshSettings());
|
|
17
21
|
} catch (error) {
|
|
18
22
|
const message = error instanceof Error ? error.message : String(error);
|
|
@@ -2,14 +2,16 @@ import { NextRequest, NextResponse } from "next/server";
|
|
|
2
2
|
import { startClone } from "@/lib/project-add";
|
|
3
3
|
|
|
4
4
|
// POST /api/projects/clone - starts cloning { url } into { parent } on a
|
|
5
|
-
// machine; GET /api/projects/clone/:job
|
|
5
|
+
// machine, in { workspaceId } when given; GET /api/projects/clone/:job
|
|
6
|
+
// follows it.
|
|
6
7
|
export async function POST(request: NextRequest) {
|
|
7
8
|
try {
|
|
8
9
|
const body = await request.json();
|
|
9
10
|
const job = startClone(
|
|
10
11
|
typeof body.hostId === "string" ? body.hostId : "local",
|
|
11
12
|
String(body.parent ?? "~"),
|
|
12
|
-
String(body.url ?? "").trim()
|
|
13
|
+
String(body.url ?? "").trim(),
|
|
14
|
+
typeof body.workspaceId === "string" ? body.workspaceId : null
|
|
13
15
|
);
|
|
14
16
|
return NextResponse.json({ job }, { status: 202 });
|
|
15
17
|
} catch (error) {
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import fs from "fs";
|
|
2
|
+
import os from "os";
|
|
3
|
+
import path from "path";
|
|
4
|
+
import { describe, expect, it } from "vitest";
|
|
5
|
+
import type { NextRequest } from "next/server";
|
|
6
|
+
import { getProject } from "@/lib/projects";
|
|
7
|
+
import { createWorkspace } from "@/lib/workspaces";
|
|
8
|
+
import { POST } from "./route";
|
|
9
|
+
|
|
10
|
+
const post = (body: object) =>
|
|
11
|
+
POST({ json: async () => body } as unknown as NextRequest);
|
|
12
|
+
|
|
13
|
+
describe("POST /api/projects/init", () => {
|
|
14
|
+
it("answers a folder that's already a project with 409 and it, and moves it on moveExisting", async () => {
|
|
15
|
+
const a = createWorkspace("Route A");
|
|
16
|
+
const b = createWorkspace("Route B");
|
|
17
|
+
const dir = fs.mkdtempSync(path.join(os.tmpdir(), "aos-init-route-"));
|
|
18
|
+
|
|
19
|
+
const made = await post({ path: dir, workspaceId: a.id });
|
|
20
|
+
expect(made.status).toBe(201);
|
|
21
|
+
const { project } = await made.json();
|
|
22
|
+
expect(getProject(project.id)?.workspace_id).toBe(a.id);
|
|
23
|
+
|
|
24
|
+
const again = await post({ path: dir, workspaceId: b.id });
|
|
25
|
+
expect(again.status).toBe(409);
|
|
26
|
+
expect((await again.json()).existing).toEqual({
|
|
27
|
+
id: project.id,
|
|
28
|
+
name: project.name,
|
|
29
|
+
workspaceId: a.id,
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
// Only a real true moves it.
|
|
33
|
+
expect(
|
|
34
|
+
(await post({ path: dir, workspaceId: b.id, moveExisting: "true" }))
|
|
35
|
+
.status
|
|
36
|
+
).toBe(409);
|
|
37
|
+
const moved = await post({
|
|
38
|
+
path: dir,
|
|
39
|
+
workspaceId: b.id,
|
|
40
|
+
moveExisting: true,
|
|
41
|
+
});
|
|
42
|
+
expect(moved.status).toBe(201);
|
|
43
|
+
expect((await moved.json()).project.id).toBe(project.id);
|
|
44
|
+
expect(getProject(project.id)?.workspace_id).toBe(b.id);
|
|
45
|
+
});
|
|
46
|
+
});
|
|
@@ -1,22 +1,42 @@
|
|
|
1
1
|
import { NextRequest, NextResponse } from "next/server";
|
|
2
|
-
import { addFolder, initProject } from "@/lib/project-add";
|
|
2
|
+
import { addFolder, initProject, ProjectExistsError } from "@/lib/project-add";
|
|
3
3
|
|
|
4
4
|
// POST /api/projects/init - a project from a folder that exists ({ path }),
|
|
5
|
-
// or a new one from a name ({ parent, name }: folder, git init, first commit)
|
|
5
|
+
// or a new one from a name ({ parent, name }: folder, git init, first commit),
|
|
6
|
+
// in { workspaceId } when given. A folder that's already a project answers
|
|
7
|
+
// 409 with it, unless { moveExisting } moves it into the workspace.
|
|
6
8
|
export async function POST(request: NextRequest) {
|
|
7
9
|
try {
|
|
8
10
|
const body = await request.json();
|
|
9
11
|
const hostId = typeof body.hostId === "string" ? body.hostId : "local";
|
|
12
|
+
const workspaceId =
|
|
13
|
+
typeof body.workspaceId === "string" ? body.workspaceId : null;
|
|
10
14
|
const project =
|
|
11
15
|
typeof body.path === "string"
|
|
12
|
-
?
|
|
16
|
+
? (
|
|
17
|
+
await addFolder(hostId, body.path, {
|
|
18
|
+
workspaceId,
|
|
19
|
+
moveExisting: body.moveExisting === true,
|
|
20
|
+
})
|
|
21
|
+
).project
|
|
13
22
|
: await initProject(
|
|
14
23
|
hostId,
|
|
15
24
|
String(body.parent ?? "~"),
|
|
16
|
-
String(body.name ?? "")
|
|
25
|
+
String(body.name ?? ""),
|
|
26
|
+
workspaceId
|
|
17
27
|
);
|
|
18
28
|
return NextResponse.json({ project }, { status: 201 });
|
|
19
29
|
} catch (error) {
|
|
30
|
+
if (error instanceof ProjectExistsError) {
|
|
31
|
+
const { id, name, workspace_id } = error.project;
|
|
32
|
+
return NextResponse.json(
|
|
33
|
+
{
|
|
34
|
+
error: error.message,
|
|
35
|
+
existing: { id, name, workspaceId: workspace_id },
|
|
36
|
+
},
|
|
37
|
+
{ status: 409 }
|
|
38
|
+
);
|
|
39
|
+
}
|
|
20
40
|
const message = error instanceof Error ? error.message : String(error);
|
|
21
41
|
return NextResponse.json(
|
|
22
42
|
{ error: message.trim().split("\n").pop() },
|
package/app/api/usage/route.ts
CHANGED
|
@@ -1,15 +1,17 @@
|
|
|
1
1
|
import { NextRequest, NextResponse } from "next/server";
|
|
2
2
|
import { USAGE_RANGES, type UsageRange } from "@/lib/usage/aggregate";
|
|
3
|
-
import { usageReport } from "@/lib/usage/report";
|
|
3
|
+
import { checkRunReport, usageReport } from "@/lib/usage/report";
|
|
4
4
|
import { usageWindows } from "@/lib/usage/windows";
|
|
5
5
|
|
|
6
6
|
// GET /api/usage?range=today|7d|30d - chat spend by day, session and
|
|
7
|
-
// workspace, and the account's
|
|
7
|
+
// workspace, what the orchestrator's reviews cost by PR, and the account's
|
|
8
|
+
// rolling windows
|
|
8
9
|
export async function GET(request: NextRequest) {
|
|
9
10
|
const asked = request.nextUrl.searchParams.get("range") as UsageRange | null;
|
|
10
11
|
const range = asked && USAGE_RANGES.includes(asked) ? asked : "7d";
|
|
11
12
|
return NextResponse.json({
|
|
12
13
|
report: usageReport(range),
|
|
14
|
+
reviews: checkRunReport(range),
|
|
13
15
|
windows: await usageWindows(),
|
|
14
16
|
});
|
|
15
17
|
}
|
|
@@ -285,9 +285,9 @@ export function ChatPanel({
|
|
|
285
285
|
toEnd();
|
|
286
286
|
send(text);
|
|
287
287
|
};
|
|
288
|
-
// A
|
|
289
|
-
// "Send as-is", for that send only.
|
|
290
|
-
const
|
|
288
|
+
// A chat past the compact threshold compacts before the next message
|
|
289
|
+
// unless the reader picks "Send as-is", for that send only.
|
|
290
|
+
const compactTokens = useFreshStartQuery().data?.compactTokens ?? 0;
|
|
291
291
|
const [asIsFor, setAsIsFor] = useState<string | null>(null);
|
|
292
292
|
const asIs = asIsFor === sessionId;
|
|
293
293
|
const setAsIs = (on: boolean) => setAsIsFor(on ? sessionId : null);
|
|
@@ -529,7 +529,7 @@ export function ChatPanel({
|
|
|
529
529
|
<StaleChip
|
|
530
530
|
context={context}
|
|
531
531
|
state={state}
|
|
532
|
-
thresholdTokens={
|
|
532
|
+
thresholdTokens={compactTokens}
|
|
533
533
|
asIs={asIs}
|
|
534
534
|
onAsIs={setAsIs}
|
|
535
535
|
/>
|
|
@@ -547,7 +547,7 @@ export function ChatPanel({
|
|
|
547
547
|
toEnd();
|
|
548
548
|
const compact = composerCompacts(text, context, {
|
|
549
549
|
state,
|
|
550
|
-
thresholdTokens:
|
|
550
|
+
thresholdTokens: compactTokens,
|
|
551
551
|
asIs,
|
|
552
552
|
});
|
|
553
553
|
setAsIs(false);
|
|
@@ -2,14 +2,15 @@
|
|
|
2
2
|
|
|
3
3
|
import { Minimize2 } from "lucide-react";
|
|
4
4
|
import type { ChatContext, ChatState } from "@/lib/chat/events";
|
|
5
|
-
import {
|
|
5
|
+
import { compactionDue, staleLabel } from "@/lib/chat/stale";
|
|
6
6
|
import { cn } from "@/lib/utils";
|
|
7
7
|
import { useNow } from "./useNow";
|
|
8
8
|
|
|
9
|
-
// Above the composer while the chat is
|
|
10
|
-
//
|
|
11
|
-
// re-read all of it at full price.
|
|
12
|
-
// picks "Send as-is", for that one
|
|
9
|
+
// Above the composer while the chat is due a compaction (lib/chat/stale):
|
|
10
|
+
// its context is past the compact threshold, and maybe its prompt cache has
|
|
11
|
+
// expired too, so the next message would re-read all of it at full price.
|
|
12
|
+
// Send compacts first unless the reader picks "Send as-is", for that one
|
|
13
|
+
// send only.
|
|
13
14
|
export function StaleChip({
|
|
14
15
|
context,
|
|
15
16
|
state,
|
|
@@ -25,7 +26,7 @@ export function StaleChip({
|
|
|
25
26
|
}) {
|
|
26
27
|
const watching = !!context?.cacheTtlMs && thresholdTokens > 0;
|
|
27
28
|
const now = useNow(watching);
|
|
28
|
-
const stale =
|
|
29
|
+
const stale = compactionDue(context, { state, thresholdTokens, now });
|
|
29
30
|
if (!stale) return null;
|
|
30
31
|
return (
|
|
31
32
|
<div
|
|
@@ -51,6 +51,8 @@ function PaletteBody() {
|
|
|
51
51
|
|
|
52
52
|
const results = useMemo(() => {
|
|
53
53
|
if (query.trim()) return matchCommands(all, query);
|
|
54
|
+
// A picker shows every choice; it's the whole list.
|
|
55
|
+
if (picker) return all;
|
|
54
56
|
// With no query, long groups are cut short so every group shows.
|
|
55
57
|
const seen = new Map<string, number>();
|
|
56
58
|
return all.filter((c) => {
|
|
@@ -58,7 +60,7 @@ function PaletteBody() {
|
|
|
58
60
|
seen.set(c.group, n);
|
|
59
61
|
return n <= GROUP_PREVIEW;
|
|
60
62
|
});
|
|
61
|
-
}, [all, query]);
|
|
63
|
+
}, [all, query, picker]);
|
|
62
64
|
const highlighted = Math.min(active, Math.max(results.length - 1, 0));
|
|
63
65
|
|
|
64
66
|
useEffect(() => {
|
|
@@ -29,6 +29,7 @@ import { sidebarUi, sidebarUiActions } from "@/stores/sidebarUi";
|
|
|
29
29
|
import { tasksUiActions } from "@/stores/tasksUi";
|
|
30
30
|
import { newDraft } from "@/stores/drafts";
|
|
31
31
|
import { useAddProject } from "@/components/Projects/AddProject/useAddProject";
|
|
32
|
+
import { useAddExistingProject } from "@/components/Projects/AddProject/useAddExistingProject";
|
|
32
33
|
import { usageUiActions } from "@/stores/usageUi";
|
|
33
34
|
import { settingsCommands } from "@/components/Settings/commands";
|
|
34
35
|
import { DRAFT_KEYS } from "@/lib/drafts";
|
|
@@ -66,6 +67,7 @@ export function useAppCommands({
|
|
|
66
67
|
const current = workspaces.find((w) => w.id === workspaceId) ?? null;
|
|
67
68
|
const dark = resolvedTheme?.startsWith("dark") ?? false;
|
|
68
69
|
const addProject = useAddProject();
|
|
70
|
+
const addExisting = useAddExistingProject();
|
|
69
71
|
|
|
70
72
|
const app: PaletteCommand[] = [
|
|
71
73
|
{
|
|
@@ -113,6 +115,14 @@ export function useAppCommands({
|
|
|
113
115
|
},
|
|
114
116
|
...(current
|
|
115
117
|
? [
|
|
118
|
+
{
|
|
119
|
+
id: "app.add-project-to-workspace",
|
|
120
|
+
title: `Add project to ${current.name}`,
|
|
121
|
+
group: "Actions",
|
|
122
|
+
keywords: ["existing", "move", "workspace", "project"],
|
|
123
|
+
icon: FolderPlus,
|
|
124
|
+
run: () => addExisting(current),
|
|
125
|
+
},
|
|
116
126
|
{
|
|
117
127
|
id: "app.orchestrator",
|
|
118
128
|
title: `Open ${current.name}'s orchestrator`,
|
|
@@ -14,6 +14,7 @@ import { useHostNames } from "@/data/hosts";
|
|
|
14
14
|
import { FolderForm } from "./FolderForm";
|
|
15
15
|
import { CloneForm } from "./CloneForm";
|
|
16
16
|
import { NameForm } from "./NameForm";
|
|
17
|
+
import { WorkspacePick } from "./WorkspacePick";
|
|
17
18
|
|
|
18
19
|
const TITLES = {
|
|
19
20
|
folder: "Open a folder",
|
|
@@ -21,8 +22,9 @@ const TITLES = {
|
|
|
21
22
|
name: "New project",
|
|
22
23
|
};
|
|
23
24
|
|
|
24
|
-
// The last step of "Add project" (⌘K): a folder, a clone or a name
|
|
25
|
-
// it's made, a draft
|
|
25
|
+
// The last step of "Add project" (⌘K): a folder, a clone or a name, into
|
|
26
|
+
// the sidebar's workspace unless changed here. Once it's made, a draft
|
|
27
|
+
// opens in it.
|
|
26
28
|
export function AddProjectDialog() {
|
|
27
29
|
const { open } = useSnapshot(addProjectUi);
|
|
28
30
|
const hostNames = useHostNames();
|
|
@@ -43,14 +45,32 @@ export function AddProjectDialog() {
|
|
|
43
45
|
: "this machine"}
|
|
44
46
|
</DialogDescription>
|
|
45
47
|
</DialogHeader>
|
|
48
|
+
{open && (
|
|
49
|
+
<WorkspacePick
|
|
50
|
+
workspaceId={open.workspaceId}
|
|
51
|
+
onChange={addProjectActions.setWorkspace}
|
|
52
|
+
/>
|
|
53
|
+
)}
|
|
46
54
|
{open?.kind === "folder" && (
|
|
47
|
-
<FolderForm
|
|
55
|
+
<FolderForm
|
|
56
|
+
hostId={open.hostId}
|
|
57
|
+
workspaceId={open.workspaceId}
|
|
58
|
+
onDone={done}
|
|
59
|
+
/>
|
|
48
60
|
)}
|
|
49
61
|
{open?.kind === "clone" && (
|
|
50
|
-
<CloneForm
|
|
62
|
+
<CloneForm
|
|
63
|
+
hostId={open.hostId}
|
|
64
|
+
workspaceId={open.workspaceId}
|
|
65
|
+
onDone={done}
|
|
66
|
+
/>
|
|
51
67
|
)}
|
|
52
68
|
{open?.kind === "name" && (
|
|
53
|
-
<NameForm
|
|
69
|
+
<NameForm
|
|
70
|
+
hostId={open.hostId}
|
|
71
|
+
workspaceId={open.workspaceId}
|
|
72
|
+
onDone={done}
|
|
73
|
+
/>
|
|
54
74
|
)}
|
|
55
75
|
</DialogContent>
|
|
56
76
|
</Dialog>
|
|
@@ -9,9 +9,11 @@ import { ParentPicker } from "./ParentPicker";
|
|
|
9
9
|
|
|
10
10
|
export function CloneForm({
|
|
11
11
|
hostId,
|
|
12
|
+
workspaceId,
|
|
12
13
|
onDone,
|
|
13
14
|
}: {
|
|
14
15
|
hostId: string;
|
|
16
|
+
workspaceId: string | null;
|
|
15
17
|
onDone: (projectId: string) => void;
|
|
16
18
|
}) {
|
|
17
19
|
const [url, setUrl] = useState("");
|
|
@@ -29,7 +31,7 @@ export function CloneForm({
|
|
|
29
31
|
className="space-y-3"
|
|
30
32
|
onSubmit={(e) => {
|
|
31
33
|
e.preventDefault();
|
|
32
|
-
start.mutate({ hostId, parent, url: url.trim() });
|
|
34
|
+
start.mutate({ hostId, parent, url: url.trim(), workspaceId });
|
|
33
35
|
}}
|
|
34
36
|
>
|
|
35
37
|
<Input
|