@saadnvd1/agent-os 0.3.520 → 0.3.523
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 +11 -0
- package/app/api/welcome/route.ts +22 -0
- package/app/page.tsx +6 -0
- package/app/welcome/page.tsx +6 -0
- package/components/Chat/BackgroundTasks.tsx +76 -10
- package/components/Chat/ChatPanel.tsx +90 -34
- package/components/Chat/Subagent.tsx +25 -20
- package/components/Chat/Tools.tsx +70 -39
- package/components/Chat/background.tsx +114 -0
- package/components/Projects/AddProject/AddProjectDialog.tsx +2 -2
- package/components/Settings/SettingsDialog.tsx +9 -0
- package/components/Settings/SetupSection.tsx +37 -0
- package/components/Welcome/AgentList.tsx +73 -0
- package/components/Welcome/AgentRow.tsx +102 -0
- package/components/Welcome/ProjectStep.tsx +100 -0
- package/components/Welcome/SetupTerminal.tsx +64 -0
- package/components/Welcome/WelcomeDialog.tsx +153 -0
- package/data/agents/index.ts +16 -1
- package/data/chat/useChat.ts +40 -0
- package/data/welcome/index.ts +36 -0
- package/hooks/useWelcome.ts +70 -0
- package/lib/agents/probe.ts +27 -0
- package/lib/agents/setup-commands.ts +54 -0
- package/lib/chat/activity.ts +4 -2
- package/lib/chat/background.ts +77 -0
- package/lib/chat/driver.ts +6 -0
- package/lib/chat/drivers/claude-mapper.ts +44 -6
- package/lib/chat/drivers/claude.ts +4 -0
- package/lib/chat/events.ts +14 -0
- package/lib/chat/registry.ts +2 -0
- package/lib/chat/runner.ts +20 -0
- package/lib/chat/settings.ts +1 -0
- package/lib/chat/socket.ts +9 -0
- package/lib/chat/worker/client.ts +33 -0
- package/lib/chat/worker/host.ts +13 -0
- package/lib/chat/worker/main.ts +1 -1
- package/lib/chat/worker/protocol.ts +2 -0
- package/lib/db/migrations.ts +14 -0
- package/lib/security/auth.ts +11 -1
- package/lib/settings-url.ts +1 -0
- package/lib/welcome-url.ts +33 -0
- package/lib/welcome.ts +88 -0
- package/package.json +1 -1
- package/stores/addProject.ts +19 -1
- package/stores/welcomeUi.ts +26 -0
|
@@ -41,6 +41,11 @@ export class WorkerClient {
|
|
|
41
41
|
|
|
42
42
|
private files = new Map<string, (r: FileSuggestion[] | null) => void>();
|
|
43
43
|
|
|
44
|
+
private backgrounds = new Map<
|
|
45
|
+
string,
|
|
46
|
+
(r: { ok?: boolean; error?: string }) => void
|
|
47
|
+
>();
|
|
48
|
+
|
|
44
49
|
private constructor(
|
|
45
50
|
private socket: net.Socket,
|
|
46
51
|
private handlers: WorkerHandlers
|
|
@@ -55,6 +60,9 @@ export class WorkerClient {
|
|
|
55
60
|
this.undos.clear();
|
|
56
61
|
for (const f of this.files.values()) f(null);
|
|
57
62
|
this.files.clear();
|
|
63
|
+
for (const b of this.backgrounds.values())
|
|
64
|
+
b({ error: "The chat worker went away" });
|
|
65
|
+
this.backgrounds.clear();
|
|
58
66
|
handlers.onClose(this.detached);
|
|
59
67
|
});
|
|
60
68
|
socket.on("error", () => socket.destroy());
|
|
@@ -101,6 +109,11 @@ export class WorkerClient {
|
|
|
101
109
|
else u?.reject(new Error(e.error ?? "Undo failed"));
|
|
102
110
|
return;
|
|
103
111
|
}
|
|
112
|
+
if (e.type === "background_result") {
|
|
113
|
+
this.backgrounds.get(e.reqId)?.({ ok: e.ok, error: e.error });
|
|
114
|
+
this.backgrounds.delete(e.reqId);
|
|
115
|
+
return;
|
|
116
|
+
}
|
|
104
117
|
if (e.type === "files_result") {
|
|
105
118
|
this.files.get(e.reqId)?.(e.files ?? null);
|
|
106
119
|
this.files.delete(e.reqId);
|
|
@@ -121,6 +134,26 @@ export class WorkerClient {
|
|
|
121
134
|
});
|
|
122
135
|
}
|
|
123
136
|
|
|
137
|
+
// Moves a blocking call (or all of them) to the background: whether
|
|
138
|
+
// anything moved, or why not (an error, or no answer in time).
|
|
139
|
+
background(
|
|
140
|
+
toolUseId?: string,
|
|
141
|
+
timeoutMs = 5000
|
|
142
|
+
): Promise<{ ok?: boolean; error?: string }> {
|
|
143
|
+
const reqId = randomUUID();
|
|
144
|
+
return new Promise((resolve) => {
|
|
145
|
+
const timer = setTimeout(() => {
|
|
146
|
+
this.backgrounds.delete(reqId);
|
|
147
|
+
resolve({ error: "timeout" });
|
|
148
|
+
}, timeoutMs);
|
|
149
|
+
this.backgrounds.set(reqId, (r) => {
|
|
150
|
+
clearTimeout(timer);
|
|
151
|
+
resolve(r);
|
|
152
|
+
});
|
|
153
|
+
this.command({ type: "background_task", reqId, toolUseId });
|
|
154
|
+
});
|
|
155
|
+
}
|
|
156
|
+
|
|
124
157
|
// The agent's own matches for an @mention; null when it can't say (an
|
|
125
158
|
// older worker, an error, no answer in time).
|
|
126
159
|
fileSuggestions(
|
package/lib/chat/worker/host.ts
CHANGED
|
@@ -712,6 +712,19 @@ export class ChatHost {
|
|
|
712
712
|
return this.conversation.respond(cmd.id, cmd);
|
|
713
713
|
case "stop_task":
|
|
714
714
|
return this.conversation.stopTask(cmd.taskId);
|
|
715
|
+
case "background_task":
|
|
716
|
+
try {
|
|
717
|
+
if (!this.conversation.backgroundTask) throw new Error("unsupported");
|
|
718
|
+
const ok = await this.conversation.backgroundTask(cmd.toolUseId);
|
|
719
|
+
this.emit({ type: "background_result", reqId: cmd.reqId, ok });
|
|
720
|
+
} catch (error) {
|
|
721
|
+
this.emit({
|
|
722
|
+
type: "background_result",
|
|
723
|
+
reqId: cmd.reqId,
|
|
724
|
+
error: error instanceof Error ? error.message : String(error),
|
|
725
|
+
});
|
|
726
|
+
}
|
|
727
|
+
return;
|
|
715
728
|
case "undo":
|
|
716
729
|
try {
|
|
717
730
|
const result = await this.conversation.undo(
|
package/lib/chat/worker/main.ts
CHANGED
|
@@ -76,7 +76,7 @@ async function main(sessionId: string) {
|
|
|
76
76
|
state: host.state,
|
|
77
77
|
compacting: host.compactingNow,
|
|
78
78
|
streaming: [...host.streaming.values()],
|
|
79
|
-
caps: ["plan", "queue", "retire", "fresh"],
|
|
79
|
+
caps: ["plan", "queue", "retire", "fresh", "background"],
|
|
80
80
|
} satisfies WorkerEvent)}\n`
|
|
81
81
|
);
|
|
82
82
|
client.on(
|
|
@@ -55,6 +55,7 @@ export type WorkerCommand =
|
|
|
55
55
|
| ({ type: "respond"; id: string } & ApprovalDecision)
|
|
56
56
|
| { type: "undo"; reqId: string; checkpoint: string; dryRun: boolean }
|
|
57
57
|
| { type: "stop_task"; taskId: string }
|
|
58
|
+
| { type: "background_task"; reqId: string; toolUseId?: string }
|
|
58
59
|
// It runs an older build: it takes no new message into its agent (they
|
|
59
60
|
// wait in the queue) and closes at its next turn boundary, so a current
|
|
60
61
|
// worker resumes the conversation with today's tools.
|
|
@@ -99,6 +100,7 @@ export type WorkerEvent =
|
|
|
99
100
|
// The agent's guess at the next message, or null once it's stale.
|
|
100
101
|
| { type: "suggestion"; text: string | null }
|
|
101
102
|
| { type: "undo_result"; reqId: string; result?: UndoResult; error?: string }
|
|
103
|
+
| { type: "background_result"; reqId: string; ok?: boolean; error?: string }
|
|
102
104
|
| {
|
|
103
105
|
type: "files_result";
|
|
104
106
|
reqId: string;
|
package/lib/db/migrations.ts
CHANGED
|
@@ -1754,6 +1754,20 @@ const migrations: Migration[] = [
|
|
|
1754
1754
|
db.exec(`ALTER TABLE ${table} ADD COLUMN plan_only INTEGER`);
|
|
1755
1755
|
},
|
|
1756
1756
|
},
|
|
1757
|
+
{
|
|
1758
|
+
id: 76,
|
|
1759
|
+
name: "welcome_backfill",
|
|
1760
|
+
up: (db) => {
|
|
1761
|
+
// An install that already has a session or a project of its own has
|
|
1762
|
+
// been set up; mark it so the first-run welcome never shows there.
|
|
1763
|
+
db.exec(
|
|
1764
|
+
`INSERT OR IGNORE INTO settings (key, value)
|
|
1765
|
+
SELECT 'welcome.completed_at', 'existing-install'
|
|
1766
|
+
WHERE EXISTS (SELECT 1 FROM sessions)
|
|
1767
|
+
OR EXISTS (SELECT 1 FROM projects WHERE is_uncategorized = 0)`
|
|
1768
|
+
);
|
|
1769
|
+
},
|
|
1770
|
+
},
|
|
1757
1771
|
];
|
|
1758
1772
|
|
|
1759
1773
|
// `upTo`: stop after this id (tests that start from an older database).
|
package/lib/security/auth.ts
CHANGED
|
@@ -162,8 +162,18 @@ export function authorize(req: AuthRequest, policy: AuthPolicy): AuthResult {
|
|
|
162
162
|
|
|
163
163
|
function decide(req: AuthRequest, policy: AuthPolicy): AuthResult {
|
|
164
164
|
const trusted = req.tunnel ? null : networkTrust(req, policy);
|
|
165
|
-
if (trusted) return { ok: true, via: trusted };
|
|
166
165
|
const token = readToken(req.headers);
|
|
166
|
+
if (trusted) {
|
|
167
|
+
// Identity only: network trust already decided access, so a token that
|
|
168
|
+
// is unknown, revoked or fails to resolve never turns this into a refusal.
|
|
169
|
+
let deviceId: string | undefined;
|
|
170
|
+
try {
|
|
171
|
+
deviceId = (token ? policy.lookup(token) : null)?.id;
|
|
172
|
+
} catch {}
|
|
173
|
+
return deviceId
|
|
174
|
+
? { ok: true, via: trusted, deviceId }
|
|
175
|
+
: { ok: true, via: trusted };
|
|
176
|
+
}
|
|
167
177
|
const device = token ? policy.lookup(token) : null;
|
|
168
178
|
if (device) return { ok: true, via: "device", deviceId: device.id };
|
|
169
179
|
// AGENTOS_AUTH=off is for a login proxy on this side of the network.
|
package/lib/settings-url.ts
CHANGED
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
// The welcome has its own address: `/?welcome=<step>`, beside `?settings=`
|
|
2
|
+
// and `?session=`. `/welcome` redirects here.
|
|
3
|
+
|
|
4
|
+
export const WELCOME_PARAM = "welcome";
|
|
5
|
+
|
|
6
|
+
export const WELCOME_STEPS = ["agents", "project", "finish"] as const;
|
|
7
|
+
|
|
8
|
+
export type WelcomeStep = (typeof WELCOME_STEPS)[number];
|
|
9
|
+
|
|
10
|
+
export interface WelcomeLocation {
|
|
11
|
+
open: boolean;
|
|
12
|
+
step: WelcomeStep;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
const BASE = "http://agentos.invalid";
|
|
16
|
+
|
|
17
|
+
export const isWelcomeStep = (v: unknown): v is WelcomeStep =>
|
|
18
|
+
(WELCOME_STEPS as readonly unknown[]).includes(v);
|
|
19
|
+
|
|
20
|
+
/** What the address says; a bare or unknown step starts at the first. */
|
|
21
|
+
export function welcomeFromHref(href: string): WelcomeLocation {
|
|
22
|
+
const value = new URL(href, BASE).searchParams.get(WELCOME_PARAM);
|
|
23
|
+
if (value === null) return { open: false, step: WELCOME_STEPS[0] };
|
|
24
|
+
return { open: true, step: isWelcomeStep(value) ? value : WELCOME_STEPS[0] };
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/** The same address with the welcome set (or removed), as path+query+hash. */
|
|
28
|
+
export function hrefWithWelcome(href: string, at: WelcomeLocation): string {
|
|
29
|
+
const url = new URL(href, BASE);
|
|
30
|
+
if (at.open) url.searchParams.set(WELCOME_PARAM, at.step);
|
|
31
|
+
else url.searchParams.delete(WELCOME_PARAM);
|
|
32
|
+
return url.pathname + url.search + url.hash;
|
|
33
|
+
}
|
package/lib/welcome.ts
ADDED
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* First-run welcome: shown once on a fresh install, never on one that
|
|
3
|
+
* already has work. Completion is the server's (one server, many devices),
|
|
4
|
+
* kept in `settings` as an ISO time, or "existing-install" when the install
|
|
5
|
+
* was already in use.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import type Database from "better-sqlite3";
|
|
9
|
+
import { db } from "./db";
|
|
10
|
+
|
|
11
|
+
export const WELCOME_KEY = "welcome.completed_at";
|
|
12
|
+
export const EXISTING_INSTALL = "existing-install";
|
|
13
|
+
|
|
14
|
+
export interface WelcomeGate {
|
|
15
|
+
// The welcome should open now.
|
|
16
|
+
show: boolean;
|
|
17
|
+
// No sessions and no project of the user's own.
|
|
18
|
+
fresh: boolean;
|
|
19
|
+
completedAt: string | null;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export interface GateInput {
|
|
23
|
+
completedAt: string | null;
|
|
24
|
+
sessionCount: number;
|
|
25
|
+
// Projects other than the built-in Uncategorized one.
|
|
26
|
+
projectCount: number;
|
|
27
|
+
demo: boolean;
|
|
28
|
+
skip: boolean;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export function welcomeGate(i: GateInput): WelcomeGate {
|
|
32
|
+
const fresh = i.sessionCount === 0 && i.projectCount === 0;
|
|
33
|
+
return {
|
|
34
|
+
show: !i.completedAt && fresh && !i.demo && !i.skip,
|
|
35
|
+
fresh,
|
|
36
|
+
completedAt: i.completedAt,
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export function readCompletedAt(d: Database.Database = db): string | null {
|
|
41
|
+
const row = d
|
|
42
|
+
.prepare(`SELECT value FROM settings WHERE key = ?`)
|
|
43
|
+
.get(WELCOME_KEY) as { value: string } | undefined;
|
|
44
|
+
return row?.value ?? null;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// Keeps the first stamp unless `overwrite`.
|
|
48
|
+
export function completeWelcome(
|
|
49
|
+
value: string = new Date().toISOString(),
|
|
50
|
+
d: Database.Database = db,
|
|
51
|
+
overwrite = false
|
|
52
|
+
): void {
|
|
53
|
+
d.prepare(
|
|
54
|
+
overwrite
|
|
55
|
+
? `INSERT INTO settings (key, value) VALUES (?, ?)
|
|
56
|
+
ON CONFLICT(key) DO UPDATE SET value = excluded.value`
|
|
57
|
+
: `INSERT OR IGNORE INTO settings (key, value) VALUES (?, ?)`
|
|
58
|
+
).run(WELCOME_KEY, value);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export function counts(d: Database.Database = db) {
|
|
62
|
+
const n = (sql: string) => (d.prepare(sql).get() as { n: number }).n;
|
|
63
|
+
return {
|
|
64
|
+
sessionCount: n(`SELECT COUNT(*) AS n FROM sessions`),
|
|
65
|
+
projectCount: n(
|
|
66
|
+
`SELECT COUNT(*) AS n FROM projects WHERE is_uncategorized = 0`
|
|
67
|
+
),
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/** The gate for this server. An install found in use is stamped as such. */
|
|
72
|
+
export function readWelcomeGate(
|
|
73
|
+
demo: boolean,
|
|
74
|
+
d: Database.Database = db
|
|
75
|
+
): WelcomeGate {
|
|
76
|
+
const c = counts(d);
|
|
77
|
+
let completedAt = readCompletedAt(d);
|
|
78
|
+
if (!completedAt && (c.sessionCount > 0 || c.projectCount > 0)) {
|
|
79
|
+
completeWelcome(EXISTING_INSTALL, d);
|
|
80
|
+
completedAt = readCompletedAt(d);
|
|
81
|
+
}
|
|
82
|
+
return welcomeGate({
|
|
83
|
+
completedAt,
|
|
84
|
+
...c,
|
|
85
|
+
demo,
|
|
86
|
+
skip: !!process.env.AGENTOS_SKIP_WELCOME,
|
|
87
|
+
});
|
|
88
|
+
}
|
package/package.json
CHANGED
package/stores/addProject.ts
CHANGED
|
@@ -13,14 +13,32 @@ export const addProjectUi = proxy<{
|
|
|
13
13
|
} | null;
|
|
14
14
|
}>({ open: null });
|
|
15
15
|
|
|
16
|
+
// Who asked for the dialog and wants the new project instead of the draft
|
|
17
|
+
// that opens in it. A function, so it stays out of the proxy.
|
|
18
|
+
let onProjectAdded: ((projectId: string) => void) | null = null;
|
|
19
|
+
|
|
16
20
|
export const addProjectActions = {
|
|
17
|
-
show: (
|
|
21
|
+
show: (
|
|
22
|
+
kind: AddProjectKind,
|
|
23
|
+
hostId: string,
|
|
24
|
+
onDone?: (projectId: string) => void
|
|
25
|
+
) => {
|
|
26
|
+
onProjectAdded = onDone ?? null;
|
|
18
27
|
addProjectUi.open = { kind, hostId, workspaceId: sidebarUi.workspaceId };
|
|
19
28
|
},
|
|
29
|
+
/** Called once a project is made; false if nobody took it over. */
|
|
30
|
+
finish: (projectId: string): boolean => {
|
|
31
|
+
const handler = onProjectAdded;
|
|
32
|
+
addProjectActions.close();
|
|
33
|
+
if (!handler) return false;
|
|
34
|
+
handler(projectId);
|
|
35
|
+
return true;
|
|
36
|
+
},
|
|
20
37
|
setWorkspace: (workspaceId: string | null) => {
|
|
21
38
|
if (addProjectUi.open) addProjectUi.open.workspaceId = workspaceId;
|
|
22
39
|
},
|
|
23
40
|
close: () => {
|
|
41
|
+
onProjectAdded = null;
|
|
24
42
|
addProjectUi.open = null;
|
|
25
43
|
},
|
|
26
44
|
};
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { proxy } from "valtio";
|
|
2
|
+
import type { WelcomeStep } from "@/lib/welcome-url";
|
|
3
|
+
|
|
4
|
+
// The first-run welcome: whether it is on show, the step, and the project
|
|
5
|
+
// the user made (or picked) to start in.
|
|
6
|
+
export const welcomeUi = proxy<{
|
|
7
|
+
open: boolean;
|
|
8
|
+
step: WelcomeStep;
|
|
9
|
+
projectId: string | null;
|
|
10
|
+
}>({ open: false, step: "agents", projectId: null });
|
|
11
|
+
|
|
12
|
+
export const welcomeUiActions = {
|
|
13
|
+
open: (step: WelcomeStep = "agents") => {
|
|
14
|
+
welcomeUi.step = step;
|
|
15
|
+
welcomeUi.open = true;
|
|
16
|
+
},
|
|
17
|
+
go: (step: WelcomeStep) => {
|
|
18
|
+
welcomeUi.step = step;
|
|
19
|
+
},
|
|
20
|
+
setProject: (projectId: string | null) => {
|
|
21
|
+
welcomeUi.projectId = projectId;
|
|
22
|
+
},
|
|
23
|
+
close: () => {
|
|
24
|
+
welcomeUi.open = false;
|
|
25
|
+
},
|
|
26
|
+
};
|