@saadnvd1/agent-os 0.3.646 → 0.3.649
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/app/api/bus/peers/route.ts +1 -1
- package/components/SessionList/OrchestratorPinRow.tsx +6 -2
- package/components/SessionList/RowParts.tsx +32 -7
- package/components/SessionList/RowRename.test.ts +102 -0
- package/components/SessionList/RowShell.tsx +9 -1
- package/components/SessionList/SessionRow.tsx +1 -0
- package/lib/agents/brief.ts +1 -1
- package/lib/bus/index.ts +77 -11
- package/package.json +2 -2
- package/scripts/check +1 -1
- package/scripts/test +11 -0
|
@@ -12,7 +12,7 @@ export async function GET(request: NextRequest) {
|
|
|
12
12
|
if (held)
|
|
13
13
|
return NextResponse.json({ peer: { id: held.id, name: held.name } });
|
|
14
14
|
try {
|
|
15
|
-
const { session, note } = resolveTarget(ref);
|
|
15
|
+
const { session, note } = resolveTarget(ref, from || null);
|
|
16
16
|
return NextResponse.json({
|
|
17
17
|
peer: { id: session.id, name: session.name },
|
|
18
18
|
note,
|
|
@@ -15,7 +15,7 @@ import { orchestratorOpenActions } from "@/stores/orchestratorOpen";
|
|
|
15
15
|
import { useRowContext } from "./RowContext";
|
|
16
16
|
import { SessionRowMenu } from "./SessionRowMenu";
|
|
17
17
|
import { ActiveTick, ROW_MENU_CONTENT } from "./RowShell";
|
|
18
|
-
import { RowRename, UnreadDot } from "./RowParts";
|
|
18
|
+
import { keepRenameFocus, RowRename, UnreadDot } from "./RowParts";
|
|
19
19
|
import { QuickChangeRow } from "./QuickChangeRow";
|
|
20
20
|
import { NestRail } from "./RowParts";
|
|
21
21
|
|
|
@@ -159,6 +159,7 @@ export const OrchestratorPinRow = memo(function OrchestratorPinRow({
|
|
|
159
159
|
<DM.DropdownMenuContent
|
|
160
160
|
align="end"
|
|
161
161
|
className={ROW_MENU_CONTENT}
|
|
162
|
+
onCloseAutoFocus={keepRenameFocus(renaming)}
|
|
162
163
|
onClick={(e) => e.stopPropagation()}
|
|
163
164
|
>
|
|
164
165
|
{menu("dropdown")}
|
|
@@ -171,7 +172,10 @@ export const OrchestratorPinRow = memo(function OrchestratorPinRow({
|
|
|
171
172
|
<>
|
|
172
173
|
<CM.ContextMenu>
|
|
173
174
|
<CM.ContextMenuTrigger asChild>{content}</CM.ContextMenuTrigger>
|
|
174
|
-
<CM.ContextMenuContent
|
|
175
|
+
<CM.ContextMenuContent
|
|
176
|
+
className={ROW_MENU_CONTENT}
|
|
177
|
+
onCloseAutoFocus={keepRenameFocus(renaming)}
|
|
178
|
+
>
|
|
175
179
|
{menu("context")}
|
|
176
180
|
</CM.ContextMenuContent>
|
|
177
181
|
</CM.ContextMenu>
|
|
@@ -61,6 +61,12 @@ export function CardBadge({ count }: { count: number }) {
|
|
|
61
61
|
);
|
|
62
62
|
}
|
|
63
63
|
|
|
64
|
+
// For a menu's onCloseAutoFocus: while a rename is open, the input keeps the
|
|
65
|
+
// focus instead of the menu's trigger getting it back.
|
|
66
|
+
export const keepRenameFocus = (renaming: boolean) => (e: Event) => {
|
|
67
|
+
if (renaming) e.preventDefault();
|
|
68
|
+
};
|
|
69
|
+
|
|
64
70
|
export function RowRename({
|
|
65
71
|
initial,
|
|
66
72
|
onDone,
|
|
@@ -70,13 +76,26 @@ export function RowRename({
|
|
|
70
76
|
}) {
|
|
71
77
|
const [value, setValue] = useState(initial);
|
|
72
78
|
const ref = useRef<HTMLInputElement>(null);
|
|
73
|
-
|
|
79
|
+
const hadFocus = useRef(false);
|
|
80
|
+
// The menu closing hands focus back to its trigger (RowShell stops that
|
|
81
|
+
// while renaming); keep taking it until we hold it, then leave it alone.
|
|
74
82
|
useEffect(() => {
|
|
75
|
-
const
|
|
76
|
-
ref.current
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
83
|
+
const take = () => {
|
|
84
|
+
const el = ref.current;
|
|
85
|
+
if (!el || hadFocus.current) return;
|
|
86
|
+
el.focus();
|
|
87
|
+
if (document.activeElement === el) {
|
|
88
|
+
hadFocus.current = true;
|
|
89
|
+
el.select();
|
|
90
|
+
}
|
|
91
|
+
};
|
|
92
|
+
take();
|
|
93
|
+
const t = setInterval(take, 50);
|
|
94
|
+
const stop = setTimeout(() => clearInterval(t), 1000);
|
|
95
|
+
return () => {
|
|
96
|
+
clearInterval(t);
|
|
97
|
+
clearTimeout(stop);
|
|
98
|
+
};
|
|
80
99
|
}, []);
|
|
81
100
|
return (
|
|
82
101
|
<input
|
|
@@ -84,7 +103,13 @@ export function RowRename({
|
|
|
84
103
|
value={value}
|
|
85
104
|
aria-label="Session name"
|
|
86
105
|
onChange={(e) => setValue(e.target.value)}
|
|
87
|
-
|
|
106
|
+
onFocus={() => {
|
|
107
|
+
hadFocus.current = true;
|
|
108
|
+
}}
|
|
109
|
+
onBlur={() => {
|
|
110
|
+
// Only once we've held focus: the menu closing hands it to its trigger.
|
|
111
|
+
if (hadFocus.current) onDone(value.trim() || null);
|
|
112
|
+
}}
|
|
88
113
|
onKeyDown={(e) => {
|
|
89
114
|
e.stopPropagation();
|
|
90
115
|
if (e.key === "Enter") onDone(value.trim() || null);
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
import "@/components/Chat/composer/test-dom";
|
|
2
|
+
import { act, createElement, useState } from "react";
|
|
3
|
+
import { createRoot, type Root } from "react-dom/client";
|
|
4
|
+
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
|
5
|
+
import { keepRenameFocus, RowRename } from "./RowParts";
|
|
6
|
+
|
|
7
|
+
(
|
|
8
|
+
globalThis as { IS_REACT_ACT_ENVIRONMENT?: boolean }
|
|
9
|
+
).IS_REACT_ACT_ENVIRONMENT = true;
|
|
10
|
+
|
|
11
|
+
let root: Root;
|
|
12
|
+
let host: HTMLElement;
|
|
13
|
+
const input = () => host.querySelector("input")!;
|
|
14
|
+
|
|
15
|
+
beforeEach(() => {
|
|
16
|
+
vi.useFakeTimers();
|
|
17
|
+
host = document.createElement("div");
|
|
18
|
+
document.body.appendChild(host);
|
|
19
|
+
root = createRoot(host);
|
|
20
|
+
});
|
|
21
|
+
afterEach(() => {
|
|
22
|
+
act(() => root.unmount());
|
|
23
|
+
host.remove();
|
|
24
|
+
vi.useRealTimers();
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
function mount(onDone: (n: string | null) => void, initial = "old") {
|
|
28
|
+
act(() => root.render(createElement(RowRename, { initial, onDone })));
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
describe("RowRename", () => {
|
|
32
|
+
it("retries focus the menu stole, and ignores blur until it has it", () => {
|
|
33
|
+
const real = window.HTMLElement.prototype.focus;
|
|
34
|
+
let calls = 0;
|
|
35
|
+
// The first focus attempts land while the menu still traps focus.
|
|
36
|
+
vi.spyOn(window.HTMLElement.prototype, "focus").mockImplementation(
|
|
37
|
+
function (this: HTMLElement) {
|
|
38
|
+
if (++calls > 2) real.call(this);
|
|
39
|
+
}
|
|
40
|
+
);
|
|
41
|
+
const done = vi.fn();
|
|
42
|
+
mount(done);
|
|
43
|
+
act(() => input().dispatchEvent(new window.FocusEvent("blur")));
|
|
44
|
+
expect(done).not.toHaveBeenCalled();
|
|
45
|
+
act(() => vi.advanceTimersByTime(150));
|
|
46
|
+
expect(document.activeElement).toBe(input());
|
|
47
|
+
vi.restoreAllMocks();
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
it("keeps typed text across a parent re-render (list refresh)", () => {
|
|
51
|
+
function Parent() {
|
|
52
|
+
const [n, setN] = useState(0);
|
|
53
|
+
return createElement(
|
|
54
|
+
"div",
|
|
55
|
+
null,
|
|
56
|
+
createElement("button", { onClick: () => setN(n + 1) }),
|
|
57
|
+
createElement(RowRename, { initial: "old", onDone: () => {} })
|
|
58
|
+
);
|
|
59
|
+
}
|
|
60
|
+
act(() => root.render(createElement(Parent)));
|
|
61
|
+
const el = input();
|
|
62
|
+
act(() => {
|
|
63
|
+
const set = Object.getOwnPropertyDescriptor(
|
|
64
|
+
window.HTMLInputElement.prototype,
|
|
65
|
+
"value"
|
|
66
|
+
)!.set!;
|
|
67
|
+
set.call(el, "typed");
|
|
68
|
+
el.dispatchEvent(new window.Event("input", { bubbles: true }));
|
|
69
|
+
});
|
|
70
|
+
act(() => host.querySelector("button")!.click());
|
|
71
|
+
expect(input()).toBe(el);
|
|
72
|
+
expect(input().value).toBe("typed");
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
it("saves on Enter, cancels on Escape, saves on blur once focused", () => {
|
|
76
|
+
const done = vi.fn();
|
|
77
|
+
mount(done);
|
|
78
|
+
act(() => vi.advanceTimersByTime(60));
|
|
79
|
+
const key = (k: string) =>
|
|
80
|
+
act(() => {
|
|
81
|
+
input().dispatchEvent(
|
|
82
|
+
new window.KeyboardEvent("keydown", { key: k, bubbles: true })
|
|
83
|
+
);
|
|
84
|
+
});
|
|
85
|
+
key("Enter");
|
|
86
|
+
expect(done).toHaveBeenLastCalledWith("old");
|
|
87
|
+
key("Escape");
|
|
88
|
+
expect(done).toHaveBeenLastCalledWith(null);
|
|
89
|
+
act(() => input().blur());
|
|
90
|
+
expect(done).toHaveBeenCalledTimes(3);
|
|
91
|
+
});
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
describe("keepRenameFocus", () => {
|
|
95
|
+
it("stops the menu returning focus only while renaming", () => {
|
|
96
|
+
const e = { preventDefault: vi.fn() } as unknown as Event;
|
|
97
|
+
keepRenameFocus(false)(e);
|
|
98
|
+
expect(e.preventDefault).not.toHaveBeenCalled();
|
|
99
|
+
keepRenameFocus(true)(e);
|
|
100
|
+
expect(e.preventDefault).toHaveBeenCalled();
|
|
101
|
+
});
|
|
102
|
+
});
|
|
@@ -6,6 +6,7 @@ import { Button } from "@/components/ui/button";
|
|
|
6
6
|
import * as DM from "@/components/ui/dropdown-menu";
|
|
7
7
|
import * as CM from "@/components/ui/context-menu";
|
|
8
8
|
import { cn } from "@/lib/utils";
|
|
9
|
+
import { keepRenameFocus } from "./RowParts";
|
|
9
10
|
|
|
10
11
|
// The open row's accent: a thin rounded tick at the left edge, inside the
|
|
11
12
|
// row so the list's overflow can't clip it.
|
|
@@ -41,6 +42,7 @@ export function RowShell({
|
|
|
41
42
|
menu,
|
|
42
43
|
menuLabel,
|
|
43
44
|
menuButton = true,
|
|
45
|
+
renaming = false,
|
|
44
46
|
}: {
|
|
45
47
|
title: string;
|
|
46
48
|
tipKey?: string;
|
|
@@ -60,6 +62,8 @@ export function RowShell({
|
|
|
60
62
|
menuLabel: string;
|
|
61
63
|
// False keeps the right-click menu but hides the ⋯ (while selecting).
|
|
62
64
|
menuButton?: boolean;
|
|
65
|
+
// A rename input is open: closing the menu mustn't move focus off it.
|
|
66
|
+
renaming?: boolean;
|
|
63
67
|
}) {
|
|
64
68
|
const content = (
|
|
65
69
|
<div
|
|
@@ -114,6 +118,7 @@ export function RowShell({
|
|
|
114
118
|
<DM.DropdownMenuContent
|
|
115
119
|
align="end"
|
|
116
120
|
className={ROW_MENU_CONTENT}
|
|
121
|
+
onCloseAutoFocus={keepRenameFocus(renaming)}
|
|
117
122
|
onClick={(e) => e.stopPropagation()}
|
|
118
123
|
>
|
|
119
124
|
{menu("dropdown")}
|
|
@@ -126,7 +131,10 @@ export function RowShell({
|
|
|
126
131
|
return (
|
|
127
132
|
<CM.ContextMenu>
|
|
128
133
|
<CM.ContextMenuTrigger asChild>{content}</CM.ContextMenuTrigger>
|
|
129
|
-
<CM.ContextMenuContent
|
|
134
|
+
<CM.ContextMenuContent
|
|
135
|
+
className={ROW_MENU_CONTENT}
|
|
136
|
+
onCloseAutoFocus={keepRenameFocus(renaming)}
|
|
137
|
+
>
|
|
130
138
|
{menu("context")}
|
|
131
139
|
</CM.ContextMenuContent>
|
|
132
140
|
</CM.ContextMenu>
|
package/lib/agents/brief.ts
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
export const BUS_BRIEF = `You are one of several coding agents running in AgentOS. Other sessions may be working on related projects at the same time, and you can talk to them with the \`aos\` command:
|
|
3
3
|
|
|
4
4
|
- \`aos peers\`: list sessions, their project and what each is doing
|
|
5
|
-
- \`aos send <session> "<message>"\`: message a session by name (or project/name, an old name, or its id); it prints delivered, queued or FAILED
|
|
5
|
+
- \`aos send <session> "<message>"\`: message a session by name (or project/name, an old name, or its id; \`orchestrator\` is your workspace's orchestrator); it prints delivered, queued or FAILED
|
|
6
6
|
- \`aos inbox\`: read messages sent to you
|
|
7
7
|
- \`aos history <session>\`: your conversation with a session
|
|
8
8
|
- \`aos spawn <project> "<prompt>"\`: start a new agent session in a project
|
package/lib/bus/index.ts
CHANGED
|
@@ -7,7 +7,9 @@
|
|
|
7
7
|
import { db, type Session } from "../db";
|
|
8
8
|
import { getProject } from "../projects";
|
|
9
9
|
import { statusDetector } from "../status-detector";
|
|
10
|
-
import { sendChatConfirmed } from "../chat/runner";
|
|
10
|
+
import { chatState, sendChatConfirmed } from "../chat/runner";
|
|
11
|
+
import { chatActivityLine } from "../chat/activity";
|
|
12
|
+
import { getWorkspace } from "../workspaces";
|
|
11
13
|
import type { ChatOrigin, PeerMessage } from "../chat/events";
|
|
12
14
|
import { sessionRowInfo } from "../session-meta";
|
|
13
15
|
import { previousNames } from "../session-names";
|
|
@@ -38,6 +40,10 @@ export interface Peer {
|
|
|
38
40
|
hostId: string;
|
|
39
41
|
running: boolean;
|
|
40
42
|
doing: string | null;
|
|
43
|
+
// The workspace it is the orchestrator of, else null.
|
|
44
|
+
orchestratorOf: string | null;
|
|
45
|
+
// The asking session's own workspace orchestrator.
|
|
46
|
+
yours?: boolean;
|
|
41
47
|
}
|
|
42
48
|
|
|
43
49
|
function liveSessions(): Session[] {
|
|
@@ -68,19 +74,41 @@ const sessionById = (id: string | null | undefined) =>
|
|
|
68
74
|
| undefined)
|
|
69
75
|
: undefined;
|
|
70
76
|
|
|
77
|
+
// A chat has no pane: its worker is the state, and it can always take a
|
|
78
|
+
// message (queued as its next prompt), so it is never "not running".
|
|
79
|
+
async function peerState(
|
|
80
|
+
s: Session
|
|
81
|
+
): Promise<{ running: boolean; subtitle: string | null }> {
|
|
82
|
+
if (s.view === "chat") {
|
|
83
|
+
const busy = chatState(s.id) === "running";
|
|
84
|
+
return {
|
|
85
|
+
running: true,
|
|
86
|
+
subtitle: chatActivityLine(s.id) ?? (busy ? "working" : "idle"),
|
|
87
|
+
};
|
|
88
|
+
}
|
|
89
|
+
const status = statusDetector.sessionExists(s.tmux_name)
|
|
90
|
+
? await statusDetector.getStatus(s.tmux_name)
|
|
91
|
+
: "dead";
|
|
92
|
+
const info = sessionRowInfo(status, statusDetector.titleFor(s.tmux_name));
|
|
93
|
+
return { running: info.running, subtitle: info.subtitle };
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
const orchestratorOf = (s: Session) =>
|
|
97
|
+
s.role === "orchestrator" && s.workspace_id
|
|
98
|
+
? (getWorkspace(s.workspace_id)?.name ?? "a workspace")
|
|
99
|
+
: null;
|
|
100
|
+
|
|
71
101
|
// `callerId`: the asking session. A task its orchestrator moved here also
|
|
72
102
|
// sees that orchestrator, which it can message though it runs elsewhere.
|
|
73
103
|
export async function listPeers(callerId?: string): Promise<Peer[]> {
|
|
74
104
|
await statusDetector.refreshCache();
|
|
75
105
|
const sessions = liveSessions();
|
|
76
106
|
const named = candidates(sessions);
|
|
77
|
-
const
|
|
107
|
+
const caller = sessionById(callerId);
|
|
108
|
+
const orchestrator = heldOrchestrator(caller);
|
|
78
109
|
const listed = await Promise.all(
|
|
79
110
|
sessions.map(async (s, i) => {
|
|
80
|
-
const
|
|
81
|
-
? await statusDetector.getStatus(s.tmux_name)
|
|
82
|
-
: "dead";
|
|
83
|
-
const info = sessionRowInfo(status, statusDetector.titleFor(s.tmux_name));
|
|
111
|
+
const info = await peerState(s);
|
|
84
112
|
return {
|
|
85
113
|
id: s.id,
|
|
86
114
|
name: s.name,
|
|
@@ -90,6 +118,10 @@ export async function listPeers(callerId?: string): Promise<Peer[]> {
|
|
|
90
118
|
hostId: s.host_id,
|
|
91
119
|
running: info.running,
|
|
92
120
|
doing: info.subtitle,
|
|
121
|
+
orchestratorOf: orchestratorOf(s),
|
|
122
|
+
...(caller?.workspace_id &&
|
|
123
|
+
s.role === "orchestrator" &&
|
|
124
|
+
s.workspace_id === caller.workspace_id && { yours: true }),
|
|
93
125
|
};
|
|
94
126
|
})
|
|
95
127
|
);
|
|
@@ -105,6 +137,8 @@ export async function listPeers(callerId?: string): Promise<Peer[]> {
|
|
|
105
137
|
hostId: "remote",
|
|
106
138
|
running: true,
|
|
107
139
|
doing: `your orchestrator, on ${orchestrator.heldFrom}`,
|
|
140
|
+
orchestratorOf: null,
|
|
141
|
+
yours: true,
|
|
108
142
|
},
|
|
109
143
|
];
|
|
110
144
|
}
|
|
@@ -116,28 +150,60 @@ export function resolveHeldOrchestrator(callerId: string, ref: string) {
|
|
|
116
150
|
|
|
117
151
|
// A live session by name, old name, project/name, id or id prefix. The note
|
|
118
152
|
// says when the name meant something other than its current owner.
|
|
119
|
-
export function resolveTarget(
|
|
153
|
+
export function resolveTarget(
|
|
154
|
+
ref: string,
|
|
155
|
+
callerId?: string | null
|
|
156
|
+
): {
|
|
120
157
|
session: Session;
|
|
121
158
|
note?: string;
|
|
122
159
|
} {
|
|
123
|
-
const found = tryResolveTarget(ref);
|
|
160
|
+
const found = tryResolveTarget(ref, callerId);
|
|
124
161
|
if (!found.ok) throw new Error(found.error);
|
|
125
162
|
return found;
|
|
126
163
|
}
|
|
127
164
|
|
|
165
|
+
// "orchestrator" / "my orchestrator": the caller's own workspace orchestrator.
|
|
166
|
+
function ownOrchestrator(
|
|
167
|
+
ref: string,
|
|
168
|
+
sessions: Session[],
|
|
169
|
+
callerId?: string | null
|
|
170
|
+
): Session | undefined {
|
|
171
|
+
if (!/^(my )?orchestrator$/i.test(ref.trim())) return undefined;
|
|
172
|
+
const workspace = sessionById(callerId)?.workspace_id;
|
|
173
|
+
if (!workspace) return undefined;
|
|
174
|
+
return sessions.find(
|
|
175
|
+
(s) => s.role === "orchestrator" && s.workspace_id === workspace
|
|
176
|
+
);
|
|
177
|
+
}
|
|
178
|
+
|
|
128
179
|
function tryResolveTarget(
|
|
129
|
-
ref: string
|
|
180
|
+
ref: string,
|
|
181
|
+
callerId?: string | null
|
|
130
182
|
):
|
|
131
183
|
| { ok: true; session: Session; note?: string }
|
|
132
184
|
| { ok: false; none: boolean; error: string } {
|
|
133
185
|
const sessions = liveSessions();
|
|
186
|
+
const own = ownOrchestrator(ref, sessions, callerId);
|
|
187
|
+
if (own) return { ok: true, session: own };
|
|
134
188
|
const r = resolveRef(ref, candidates(sessions));
|
|
135
|
-
if (!r.ok)
|
|
189
|
+
if (!r.ok) {
|
|
190
|
+
const orchestrators = /^(my )?orchestrator$/i.test(ref.trim())
|
|
191
|
+
? sessions.filter((s) => s.role === "orchestrator")
|
|
192
|
+
: [];
|
|
193
|
+
if (orchestrators.length)
|
|
194
|
+
return {
|
|
195
|
+
ok: false,
|
|
196
|
+
none: true,
|
|
197
|
+
error: `You are in no workspace, so "${ref}" is ambiguous. Orchestrators: ${orchestrators
|
|
198
|
+
.map((s) => `${s.name} (id ${s.id.slice(0, 8)})`)
|
|
199
|
+
.join(", ")}`,
|
|
200
|
+
};
|
|
136
201
|
return {
|
|
137
202
|
ok: false,
|
|
138
203
|
none: r.reason === "none",
|
|
139
204
|
error: r.reason === "none" ? `${r.error}. Run: aos peers` : r.error,
|
|
140
205
|
};
|
|
206
|
+
}
|
|
141
207
|
return {
|
|
142
208
|
ok: true,
|
|
143
209
|
session: sessions.find((s) => s.id === r.id)!,
|
|
@@ -287,7 +353,7 @@ export async function sendMessage(opts: {
|
|
|
287
353
|
// A held task's message to its orchestrator is kept for the holder to pull.
|
|
288
354
|
const held = heldTarget(from, opts.to);
|
|
289
355
|
if (from && held) return storeForHolder(from, held, body);
|
|
290
|
-
const target = tryResolveTarget(opts.to);
|
|
356
|
+
const target = tryResolveTarget(opts.to, opts.fromId);
|
|
291
357
|
if (!target.ok) {
|
|
292
358
|
// A task still in the start queue has no session yet: hold it.
|
|
293
359
|
const queued = target.none ? findQueuedTarget(opts.to) : null;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@saadnvd1/agent-os",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.649",
|
|
4
4
|
"description": "Self-hosted web UI for managing Claude Code sessions",
|
|
5
5
|
"bin": {
|
|
6
6
|
"agent-os": "scripts/agent-os"
|
|
@@ -48,7 +48,7 @@
|
|
|
48
48
|
"lint": "eslint .",
|
|
49
49
|
"format": "prettier --write .",
|
|
50
50
|
"format:check": "prettier --check .",
|
|
51
|
-
"test": "
|
|
51
|
+
"test": "scripts/test",
|
|
52
52
|
"test:watch": "vitest",
|
|
53
53
|
"check": "scripts/check",
|
|
54
54
|
"screenshots": "tsx scripts/screenshots/run.ts",
|
package/scripts/check
CHANGED
|
@@ -56,7 +56,7 @@ if has static; then
|
|
|
56
56
|
fi
|
|
57
57
|
fi
|
|
58
58
|
if has test; then
|
|
59
|
-
step "test";
|
|
59
|
+
step "test"; scripts/test ${vitest_args[@]+"${vitest_args[@]}"}
|
|
60
60
|
fi
|
|
61
61
|
if has build; then
|
|
62
62
|
# The build imports the generated migrations index, so it needs it even
|
package/scripts/test
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# The full suite, kindly: through `aos heavy` (so other sessions are told) and
|
|
3
|
+
# at low priority on a Mac. CI and machines without aos run plain vitest.
|
|
4
|
+
set -euo pipefail
|
|
5
|
+
cd "$(dirname "$0")/.."
|
|
6
|
+
cmd=(npx vitest run "$@")
|
|
7
|
+
[ "$(uname)" = Darwin ] && [ -z "${CI:-}" ] && cmd=(nice -n 10 "${cmd[@]}")
|
|
8
|
+
if [ -z "${CI:-}" ] && command -v aos >/dev/null 2>&1; then
|
|
9
|
+
exec aos heavy -- "${cmd[@]}"
|
|
10
|
+
fi
|
|
11
|
+
exec "${cmd[@]}"
|