@saadnvd1/agent-os 0.3.692 → 0.3.693
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/sessions/route.ts +1 -0
- package/components/Draft/DraftChips.tsx +10 -5
- package/components/Draft/DraftPanel.tsx +3 -0
- package/data/sessions/queries.ts +2 -0
- package/hooks/useDraftRequests.ts +9 -3
- package/lib/bus/read-scope.ts +2 -1
- package/lib/drafts.ts +13 -3
- package/lib/projects.ts +27 -6
- package/lib/sessions/launch.ts +35 -2
- package/lib/sidebar/shelves.ts +5 -4
- package/lib/workspaces.ts +6 -0
- package/package.json +1 -1
|
@@ -61,6 +61,7 @@ export async function POST(request: NextRequest) {
|
|
|
61
61
|
const { session, initialPrompt, repeat } = await launchSession({
|
|
62
62
|
id: str(body.id),
|
|
63
63
|
projectId: str(body.projectId),
|
|
64
|
+
workspaceId: str(body.workspaceId),
|
|
64
65
|
project: projectRefOf(body.project),
|
|
65
66
|
workingDirectory: str(body.workingDirectory),
|
|
66
67
|
agentType:
|
|
@@ -98,11 +98,16 @@ export function DraftChips({
|
|
|
98
98
|
}, [hostGone, onChange]);
|
|
99
99
|
|
|
100
100
|
const pickProject = (id: string | null) => {
|
|
101
|
-
const next = newDraft(
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
101
|
+
const next = newDraft(
|
|
102
|
+
draft.id,
|
|
103
|
+
real.find((p) => p.id === id) ?? null,
|
|
104
|
+
{
|
|
105
|
+
agentType: draft.agentType,
|
|
106
|
+
model: draft.model,
|
|
107
|
+
access: draft.access,
|
|
108
|
+
},
|
|
109
|
+
{ workspaceId: draft.workspaceId }
|
|
110
|
+
);
|
|
106
111
|
onChange({
|
|
107
112
|
...next,
|
|
108
113
|
openPr: !!id && draft.openPr,
|
|
@@ -12,6 +12,7 @@ import { discoversModels } from "@/lib/model-catalog";
|
|
|
12
12
|
import { useDiscoveredModels } from "@/data/agents";
|
|
13
13
|
import { useHostsQuery } from "@/data/hosts";
|
|
14
14
|
import { useGitCheck } from "@/data/git/queries";
|
|
15
|
+
import { useSelectedWorkspace } from "@/hooks/useSelectedWorkspace";
|
|
15
16
|
import { useLaunchSession } from "@/data/sessions";
|
|
16
17
|
import { useCreateTask } from "@/data/tasks";
|
|
17
18
|
import { usePanes } from "@/contexts/PaneContext";
|
|
@@ -50,6 +51,7 @@ export function DraftPanel({
|
|
|
50
51
|
discovers
|
|
51
52
|
);
|
|
52
53
|
const launch = useLaunchSession();
|
|
54
|
+
const { workspace } = useSelectedWorkspace();
|
|
53
55
|
const createTask = useCreateTask();
|
|
54
56
|
const [prefill, setPrefill] = useState<{ text: string; at: number }>();
|
|
55
57
|
const pending = launch.isPending || createTask.isPending;
|
|
@@ -93,6 +95,7 @@ export function DraftPanel({
|
|
|
93
95
|
}
|
|
94
96
|
const { session, initialPrompt, repeat } = await launch.mutateAsync({
|
|
95
97
|
projectId: project?.id ?? null,
|
|
98
|
+
workspaceId: draft.workspaceId ?? workspace?.id ?? null,
|
|
96
99
|
id: draft.id,
|
|
97
100
|
hostId:
|
|
98
101
|
!project || draft.hostId !== project.host_id
|
package/data/sessions/queries.ts
CHANGED
|
@@ -182,6 +182,8 @@ export interface LaunchSessionInput {
|
|
|
182
182
|
// The draft's id: the session's, and a resend's key to the same one.
|
|
183
183
|
id: string;
|
|
184
184
|
projectId: string | null;
|
|
185
|
+
// The workspace selected: where a session in no project is filed.
|
|
186
|
+
workspaceId?: string | null;
|
|
185
187
|
// A scratch chat's machine; a project's sessions run where it lives, or
|
|
186
188
|
// on a linked machine.
|
|
187
189
|
hostId?: string;
|
|
@@ -46,9 +46,15 @@ export function useDraftRequests(options: Options) {
|
|
|
46
46
|
useEffect(() => {
|
|
47
47
|
hydrateDrafts();
|
|
48
48
|
const open = (projectId: string | null, openPr = false) => {
|
|
49
|
-
const { projects, viewing, show } = latest.current;
|
|
49
|
+
const { projects, viewing, show, workspaceId } = latest.current;
|
|
50
50
|
const drafts = Object.values(draftsStore.drafts) as Draft[];
|
|
51
|
-
const found = reusableDraft(
|
|
51
|
+
const found = reusableDraft(
|
|
52
|
+
drafts,
|
|
53
|
+
projectId,
|
|
54
|
+
draftHasText,
|
|
55
|
+
openPr,
|
|
56
|
+
workspaceId
|
|
57
|
+
);
|
|
52
58
|
if (found) return show(found.id);
|
|
53
59
|
const from = viewing.draftId ? draftsStore.drafts[viewing.draftId] : null;
|
|
54
60
|
const s = viewing.session;
|
|
@@ -58,7 +64,7 @@ export function useDraftRequests(options: Options) {
|
|
|
58
64
|
? { agentType: s.agent_type, model: s.model, access: s.chat_access }
|
|
59
65
|
: {};
|
|
60
66
|
const project = projects.find((p) => p.id === projectId) ?? null;
|
|
61
|
-
const draft = makeDraft(uuid(), project, carry, { openPr });
|
|
67
|
+
const draft = makeDraft(uuid(), project, carry, { openPr, workspaceId });
|
|
62
68
|
draftsActions.put(draft);
|
|
63
69
|
show(draft.id);
|
|
64
70
|
};
|
package/lib/bus/read-scope.ts
CHANGED
|
@@ -60,7 +60,8 @@ function inScope(s: ReadableSession, scope: ReaderScope): boolean {
|
|
|
60
60
|
if (s.role) return false;
|
|
61
61
|
if (scope.workspaceId) {
|
|
62
62
|
const p = s.project_id ? getProject(s.project_id) : null;
|
|
63
|
-
|
|
63
|
+
// A session in no project is in the workspace it was started in.
|
|
64
|
+
return (p?.workspace_id ?? s.workspace_id) === scope.workspaceId;
|
|
64
65
|
}
|
|
65
66
|
return s.project_id === scope.projectId;
|
|
66
67
|
}
|
package/lib/drafts.ts
CHANGED
|
@@ -13,6 +13,8 @@ export interface Draft {
|
|
|
13
13
|
id: string;
|
|
14
14
|
// null: a scratch chat, in no project.
|
|
15
15
|
projectId: string | null;
|
|
16
|
+
// The workspace selected when it was made: where a scratch chat is filed.
|
|
17
|
+
workspaceId?: string | null;
|
|
16
18
|
hostId: string;
|
|
17
19
|
agentType: AgentType;
|
|
18
20
|
model: string;
|
|
@@ -55,13 +57,14 @@ export function newDraft(
|
|
|
55
57
|
id: string,
|
|
56
58
|
project: DraftProject | null,
|
|
57
59
|
carry: Carry,
|
|
58
|
-
opts: { openPr?: boolean; now?: number } = {}
|
|
60
|
+
opts: { openPr?: boolean; now?: number; workspaceId?: string | null } = {}
|
|
59
61
|
): Draft {
|
|
60
62
|
const real = project && !project.is_uncategorized ? project : null;
|
|
61
63
|
const agentType = real?.agent_type ?? carry.agentType ?? "claude";
|
|
62
64
|
return {
|
|
63
65
|
id,
|
|
64
66
|
projectId: real?.id ?? null,
|
|
67
|
+
workspaceId: real ? null : (opts.workspaceId ?? null),
|
|
65
68
|
hostId: real?.host_id || "local",
|
|
66
69
|
agentType,
|
|
67
70
|
model: resolveModelForAgent(
|
|
@@ -82,11 +85,18 @@ export function reusableDraft(
|
|
|
82
85
|
drafts: Draft[],
|
|
83
86
|
projectId: string | null,
|
|
84
87
|
hasText: (draftId: string) => boolean,
|
|
85
|
-
openPr = false
|
|
88
|
+
openPr = false,
|
|
89
|
+
workspaceId: string | null = null
|
|
86
90
|
): Draft | null {
|
|
91
|
+
// An empty scratch chat from another workspace would file this one's
|
|
92
|
+
// session there.
|
|
87
93
|
return (
|
|
88
94
|
drafts.find(
|
|
89
|
-
(d) =>
|
|
95
|
+
(d) =>
|
|
96
|
+
d.projectId === projectId &&
|
|
97
|
+
d.openPr === openPr &&
|
|
98
|
+
(projectId !== null || (d.workspaceId ?? null) === workspaceId) &&
|
|
99
|
+
!hasText(d.id)
|
|
90
100
|
) ?? null
|
|
91
101
|
);
|
|
92
102
|
}
|
package/lib/projects.ts
CHANGED
|
@@ -278,11 +278,12 @@ export function deleteProject(id: string): boolean {
|
|
|
278
278
|
const project = getProject(id);
|
|
279
279
|
if (!project || project.is_uncategorized) return false;
|
|
280
280
|
|
|
281
|
-
//
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
281
|
+
// Its sessions go to Uncategorized, staying in the project's workspace.
|
|
282
|
+
db.prepare(
|
|
283
|
+
`UPDATE sessions SET project_id = 'uncategorized', updated_at = datetime('now'),
|
|
284
|
+
workspace_id = CASE WHEN role IS NULL THEN ? ELSE workspace_id END
|
|
285
|
+
WHERE project_id = ?`
|
|
286
|
+
).run(project.workspace_id ?? null, id);
|
|
286
287
|
|
|
287
288
|
// Delete dev server instances
|
|
288
289
|
queries.deleteDevServersByProject(db).run(id);
|
|
@@ -312,7 +313,27 @@ export function moveSessionToProject(
|
|
|
312
313
|
sessionId: string,
|
|
313
314
|
projectId: string
|
|
314
315
|
): void {
|
|
315
|
-
|
|
316
|
+
const to = getProject(projectId);
|
|
317
|
+
const from = db
|
|
318
|
+
.prepare(
|
|
319
|
+
`SELECT p.workspace_id AS workspace_id, s.workspace_id AS own
|
|
320
|
+
FROM sessions s LEFT JOIN projects p ON p.id = s.project_id WHERE s.id = ?`
|
|
321
|
+
)
|
|
322
|
+
.get(sessionId) as
|
|
323
|
+
{ workspace_id: string | null; own: string | null } | undefined;
|
|
324
|
+
db.transaction(() => {
|
|
325
|
+
queries.updateSessionProject(db).run(projectId, sessionId);
|
|
326
|
+
// A project's session follows the project; one left in none keeps the
|
|
327
|
+
// workspace it was in.
|
|
328
|
+
db.prepare(
|
|
329
|
+
`UPDATE sessions SET workspace_id = ? WHERE id = ? AND role IS NULL`
|
|
330
|
+
).run(
|
|
331
|
+
to && !to.is_uncategorized
|
|
332
|
+
? null
|
|
333
|
+
: (from?.workspace_id ?? from?.own ?? null),
|
|
334
|
+
sessionId
|
|
335
|
+
);
|
|
336
|
+
})();
|
|
316
337
|
}
|
|
317
338
|
|
|
318
339
|
/**
|
package/lib/sessions/launch.ts
CHANGED
|
@@ -16,6 +16,7 @@ import { isDiscoveredModel } from "../chat/discovered-models";
|
|
|
16
16
|
import { parseOverride, resolveRole, setSessionModel } from "../models/resolve";
|
|
17
17
|
import { ANTHROPIC } from "../models/roles";
|
|
18
18
|
import { getProject } from "../projects";
|
|
19
|
+
import { getWorkspace } from "../workspaces";
|
|
19
20
|
import { getHost, isRemoteHost } from "../hosts";
|
|
20
21
|
import { hostLink } from "../hosts/remote-api";
|
|
21
22
|
import { startOnPeer } from "../hosts/peer-actions";
|
|
@@ -78,6 +79,9 @@ export interface LaunchInput {
|
|
|
78
79
|
// role's model (or `model`), not the interactive one's.
|
|
79
80
|
startedBy?: string | null;
|
|
80
81
|
startedVia?: StartVia;
|
|
82
|
+
// The workspace selected when it was started. A session in a project
|
|
83
|
+
// follows the project; one in none keeps this (see inWorkspace).
|
|
84
|
+
workspaceId?: string | null;
|
|
81
85
|
groupPath?: string;
|
|
82
86
|
systemPrompt?: string | null;
|
|
83
87
|
}
|
|
@@ -178,6 +182,29 @@ export function launchSession(input: LaunchInput): Promise<Launched> {
|
|
|
178
182
|
return p;
|
|
179
183
|
}
|
|
180
184
|
|
|
185
|
+
// The workspace a project-less session is filed under: the one it was started
|
|
186
|
+
// from, else that of the orchestrator starting it. A workspace deleted since
|
|
187
|
+
// the draft was made is dropped rather than failing the send.
|
|
188
|
+
function homeWorkspace(input: LaunchInput): string | null {
|
|
189
|
+
const picked = input.workspaceId?.trim();
|
|
190
|
+
if (picked) return getWorkspace(picked) ? picked : null;
|
|
191
|
+
if (!input.startedBy) return null;
|
|
192
|
+
const by = db
|
|
193
|
+
.prepare(
|
|
194
|
+
`SELECT workspace_id FROM sessions WHERE id = ? AND role = 'orchestrator'`
|
|
195
|
+
)
|
|
196
|
+
.get(input.startedBy) as { workspace_id: string | null } | undefined;
|
|
197
|
+
return by?.workspace_id ?? null;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
function setHome(id: string, workspaceId: string | null): void {
|
|
201
|
+
if (workspaceId)
|
|
202
|
+
db.prepare(`UPDATE sessions SET workspace_id = ? WHERE id = ?`).run(
|
|
203
|
+
workspaceId,
|
|
204
|
+
id
|
|
205
|
+
);
|
|
206
|
+
}
|
|
207
|
+
|
|
181
208
|
async function launch(input: LaunchInput, id: string): Promise<Launched> {
|
|
182
209
|
const project = input.project
|
|
183
210
|
? await ensureProject(input.project)
|
|
@@ -185,6 +212,7 @@ async function launch(input: LaunchInput, id: string): Promise<Launched> {
|
|
|
185
212
|
? getProject(input.projectId)
|
|
186
213
|
: undefined;
|
|
187
214
|
const scratch = !project || project.is_uncategorized;
|
|
215
|
+
const home = scratch ? homeWorkspace(input) : null;
|
|
188
216
|
const hostId = (scratch ? input.hostId : null) || project?.host_id || "local";
|
|
189
217
|
// A project here can start on a linked machine, which finds or clones it.
|
|
190
218
|
const carried =
|
|
@@ -210,7 +238,7 @@ async function launch(input: LaunchInput, id: string): Promise<Launched> {
|
|
|
210
238
|
if (link) {
|
|
211
239
|
if (input.images?.length || input.files?.length)
|
|
212
240
|
throw new Error(`Images and files can't be sent to ${link.hostName} yet`);
|
|
213
|
-
|
|
241
|
+
const started = await startOnPeer(link, {
|
|
214
242
|
id,
|
|
215
243
|
project: scratch ? null : (project ?? null),
|
|
216
244
|
ref: carried && project ? await projectRef(project) : undefined,
|
|
@@ -222,6 +250,10 @@ async function launch(input: LaunchInput, id: string): Promise<Launched> {
|
|
|
222
250
|
prompt: input.prompt?.trim(),
|
|
223
251
|
view: input.view ?? DEFAULT_START_VIEW,
|
|
224
252
|
});
|
|
253
|
+
setHome(started.session.id, home);
|
|
254
|
+
return home
|
|
255
|
+
? { ...started, session: { ...started.session, workspace_id: home } }
|
|
256
|
+
: started;
|
|
225
257
|
}
|
|
226
258
|
const projectPath = expand(folder);
|
|
227
259
|
if (scratch && projectPath === SCRATCH_DIR)
|
|
@@ -242,7 +274,7 @@ async function launch(input: LaunchInput, id: string): Promise<Launched> {
|
|
|
242
274
|
const routed =
|
|
243
275
|
agentType === "claude" && (picked || !input.model?.trim()) && !remote
|
|
244
276
|
? resolveRole(role, {
|
|
245
|
-
workspaceId: project?.workspace_id,
|
|
277
|
+
workspaceId: project?.workspace_id ?? home,
|
|
246
278
|
override: picked,
|
|
247
279
|
})
|
|
248
280
|
: null;
|
|
@@ -302,6 +334,7 @@ async function launch(input: LaunchInput, id: string): Promise<Launched> {
|
|
|
302
334
|
input.startedVia ?? (input.startedBy ? "orchestrator" : null),
|
|
303
335
|
id
|
|
304
336
|
);
|
|
337
|
+
setHome(id, home);
|
|
305
338
|
if (interactive) setSessionModel(id, interactive);
|
|
306
339
|
if (input.access)
|
|
307
340
|
db.prepare(`UPDATE sessions SET chat_access = ? WHERE id = ?`).run(
|
package/lib/sidebar/shelves.ts
CHANGED
|
@@ -382,7 +382,9 @@ export function doneSummary(
|
|
|
382
382
|
}
|
|
383
383
|
|
|
384
384
|
// A workspace holds its projects' sessions and its own orchestrator; no
|
|
385
|
-
// workspace selected means every session.
|
|
385
|
+
// workspace selected means every session. A session in a project follows the
|
|
386
|
+
// project, so moving it moves them; one in no project (scratch) belongs to the
|
|
387
|
+
// workspace it was started in, kept on the session.
|
|
386
388
|
export function inWorkspace(
|
|
387
389
|
session: Pick<Session, "project_id" | "workspace_id" | "role">,
|
|
388
390
|
workspaceOf: (projectId: string) => string | null | undefined,
|
|
@@ -391,9 +393,8 @@ export function inWorkspace(
|
|
|
391
393
|
if (!workspaceId) return true;
|
|
392
394
|
if (session.role === "orchestrator")
|
|
393
395
|
return session.workspace_id === workspaceId;
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
);
|
|
396
|
+
const byProject = session.project_id ? workspaceOf(session.project_id) : null;
|
|
397
|
+
return (byProject ?? session.workspace_id) === workspaceId;
|
|
397
398
|
}
|
|
398
399
|
|
|
399
400
|
// A workspace's projects, or every project when none is selected.
|
package/lib/workspaces.ts
CHANGED
|
@@ -115,6 +115,11 @@ export function deleteWorkspace(id: string): void {
|
|
|
115
115
|
`UPDATE projects SET workspace_id = NULL WHERE workspace_id = ?`
|
|
116
116
|
).run(id);
|
|
117
117
|
db.prepare(`DELETE FROM workspaces WHERE id = ?`).run(id);
|
|
118
|
+
// Sessions filed here by being started here go back to "All workspaces".
|
|
119
|
+
db.prepare(
|
|
120
|
+
`UPDATE sessions SET workspace_id = NULL
|
|
121
|
+
WHERE role IS NULL AND workspace_id = ?`
|
|
122
|
+
).run(id);
|
|
118
123
|
// Its orchestrator goes with it; a running worker idles out on its own.
|
|
119
124
|
db.prepare(
|
|
120
125
|
`DELETE FROM sessions WHERE role = 'orchestrator' AND workspace_id = ?`
|
|
@@ -128,6 +133,7 @@ export function deleteWorkspace(id: string): void {
|
|
|
128
133
|
})();
|
|
129
134
|
}
|
|
130
135
|
|
|
136
|
+
// A project's sessions follow it, so moving one needs no change to them.
|
|
131
137
|
export function setProjectWorkspace(
|
|
132
138
|
projectId: string,
|
|
133
139
|
workspaceId: string | null
|