Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions agent-crew/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,16 @@ that no private organization names remained in the rendered page.

![Agent Crew nudge confirmation](https://raw.githubusercontent.com/omercnet/paseo-plugins/main/agent-crew/docs/images/agent-crew-action.png)

## Auto open

When enabled in the plugin settings screen, Agent Crew opens the Explorer tab once for each new workspace observed on that host.

- Default: off.
- Existing workspaces are not opened when the setting is enabled.
- Disabling stops queued and future opens. Re-enabling resumes observation for workspaces created or updated afterward.
- Workspace claims are remembered on the daemon so reconnects and reloads do not reopen a workspace.
- A failed claim batch is retried once after two seconds, then dropped with a logged error.

## What it shows

- Every non-archived managed agent in the current workspace, organized into orchestration trees.
Expand Down
211 changes: 211 additions & 0 deletions agent-crew/client/auto-open.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,211 @@
import type { PluginClientContext } from "@getpaseo/plugin/client";
import { claimOpenedWorkspaces, MAX_AUTO_OPEN_CLAIM_BATCH } from "../shared/auto-open";
import {
agentCrewSettingsRpc,
agentCrewSettingsSchema,
DEFAULT_AUTO_OPEN_EXPLORER,
} from "../shared/settings";

const FLUSH_DELAY_MS = 400;
const RETRY_DELAY_MS = 2000;
const PAGE_LIMIT = 200;
const SETTINGS_POLL_MS = 15_000;

type ClaimJob = {
workspaceIds: string[];
retried: boolean;
};

type AutoOpenManager = {
setEnabled(enabled: boolean): void;
dispose(): void;
};

async function listWorkspaceIds(paseo: PluginClientContext["paseo"]): Promise<Set<string>> {
const workspaceIds = new Set<string>();
const seenCursors = new Set<string>();
let cursor: string | undefined;
while (true) {
const result = await paseo.workspaces.list({
page: { limit: PAGE_LIMIT, ...(cursor ? { cursor } : {}) },
});
for (const workspace of result.entries) workspaceIds.add(workspace.id);
if (!result.pageInfo.hasMore) return workspaceIds;
const nextCursor = result.pageInfo.nextCursor ?? undefined;
if (!nextCursor || seenCursors.has(nextCursor)) return workspaceIds;
seenCursors.add(nextCursor);
cursor = nextCursor;
}
}

function startAutoOpen(client: PluginClientContext): () => void {
const pending = new Set<string>();
const buffered = new Set<string>();
const jobs: ClaimJob[] = [];
let known: Set<string> | undefined;
let flushTimer: NodeJS.Timeout | undefined;
let removeObserver: (() => void) | undefined;
let releaseSubscription: (() => Promise<void>) | undefined;
let pumping = false;
let closed = false;

function schedule(delayMs: number) {
if (closed || pumping || flushTimer) return;
flushTimer = setTimeout(() => {
flushTimer = undefined;
void pump();
}, delayMs);
}

function enqueue(workspaceId: string) {
if (closed) return;
pending.add(workspaceId);
schedule(FLUSH_DELAY_MS);
}

function observe(workspaceId: string) {
if (!known) {
buffered.add(workspaceId);
return;
}
if (known.has(workspaceId)) return;
known.add(workspaceId);
enqueue(workspaceId);
}

function drainPending() {
const workspaceIds = [...pending];
pending.clear();
for (let index = 0; index < workspaceIds.length; index += MAX_AUTO_OPEN_CLAIM_BATCH) {
jobs.push({
workspaceIds: workspaceIds.slice(index, index + MAX_AUTO_OPEN_CLAIM_BATCH),
retried: false,
});
}
}

async function pump() {
if (closed || pumping) return;
pumping = true;
drainPending();
while (!closed && jobs.length > 0) {
const job = jobs.shift();
if (!job) break;
try {
const { claimed } = await client.rpc(claimOpenedWorkspaces, {
workspaceIds: job.workspaceIds,
});
for (const workspaceId of claimed) {
try {
client.openPanel("crew", { workspaceId, location: "explorer" });
} catch (error) {
console.error(
`Agent Crew auto-open could not open the panel for workspace ${workspaceId}`,
error,
);
}
}
} catch (error) {
console.error("Agent Crew auto-open claim failed", error);
if (!job.retried) {
jobs.unshift({ ...job, retried: true });
pumping = false;
schedule(RETRY_DELAY_MS);
return;
}
}
}
pumping = false;
if (!closed && pending.size > 0) schedule(FLUSH_DELAY_MS);
}

void client.paseo.workspaces
.list({ subscribe: {} })
.then(({ subscription }) => {
if (closed) return subscription.release();
releaseSubscription = () => subscription.release();
removeObserver = subscription.subscribe({
snapshot() {},
update(message) {
if (message.type !== "workspace_update" || message.payload.kind !== "upsert") return;
observe(message.payload.workspace.id);
},
});
return listWorkspaceIds(client.paseo).then((workspaceIds) => {
if (closed) return;
known = workspaceIds;
for (const workspaceId of buffered) observe(workspaceId);
buffered.clear();
});
})
.catch((error: unknown) => {
if (!closed) console.error("Agent Crew auto-open observation failed", error);
});

return () => {
closed = true;
pending.clear();
buffered.clear();
jobs.length = 0;
clearTimeout(flushTimer);
removeObserver?.();
void releaseSubscription?.();
releaseSubscription = undefined;
};
}

export function createAutoOpenManager(client: PluginClientContext): AutoOpenManager {
let enabled = DEFAULT_AUTO_OPEN_EXPLORER;
let activeCleanup: (() => void) | undefined;
let disposed = false;
let refreshRunning = false;
let generation = 0;
const pollTimer = setInterval(() => {
void refreshEnabled();
}, SETTINGS_POLL_MS);

function syncEnabled(nextEnabled: boolean) {
if (disposed || nextEnabled === enabled) return;
enabled = nextEnabled;
activeCleanup?.();
activeCleanup = enabled ? startAutoOpen(client) : undefined;
}

async function refreshEnabled() {
if (disposed || refreshRunning) return;
refreshRunning = true;
const requestGeneration = generation;
try {
const result = await client.rpc(agentCrewSettingsRpc.read, {});
if (disposed || requestGeneration !== generation) return;
if (result.status !== "ready") {
syncEnabled(DEFAULT_AUTO_OPEN_EXPLORER);
return;
}
const parsed = agentCrewSettingsSchema.safeParse(result.values);
syncEnabled(parsed.success ? parsed.data.autoOpenExplorer : DEFAULT_AUTO_OPEN_EXPLORER);
} catch (error) {
if (disposed || requestGeneration !== generation) return;
console.error("Agent Crew auto-open settings read failed", error);
syncEnabled(DEFAULT_AUTO_OPEN_EXPLORER);
} finally {
refreshRunning = false;
}
}

void refreshEnabled();

return {
setEnabled(nextEnabled) {
generation += 1;
syncEnabled(nextEnabled);
},
dispose() {
disposed = true;
generation += 1;
clearInterval(pollTimer);
activeCleanup?.();
activeCleanup = undefined;
},
};
}
78 changes: 78 additions & 0 deletions agent-crew/client/settings-screen.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
import { type PluginSurfaceProps, useSettings } from "@getpaseo/plugin/client";
import {
SettingsAction,
SettingsCard,
SettingsSection,
SettingsSwitch,
} from "@getpaseo/plugin/client/ui";
import { useMemo } from "react";
import { Text } from "react-native";
import { agentCrewSettings } from "../shared/settings";

export interface AgentCrewSettingsScreenProps extends PluginSurfaceProps {
onAutoOpenChange(enabled: boolean): void;
}

export function AgentCrewSettingsScreen({ onAutoOpenChange, theme }: AgentCrewSettingsScreenProps) {
const settings = useSettings(agentCrewSettings);
const styles = useMemo(
() => ({
text: { color: theme.colors.foreground },
error: { color: theme.colors.statusDanger },
}),
[theme],
);

if (settings.status === "loading") return <Text style={styles.text}>Loading settings…</Text>;
if (settings.status !== "ready") {
return (
<SettingsSection title="Agent Crew settings">
<Text accessibilityRole="alert" style={styles.error}>
{settings.error}
</Text>
<SettingsCard>
<SettingsAction
label="Read settings again"
actionLabel="Reload"
onPress={settings.reload}
/>
{settings.status === "invalid" ? (
<SettingsAction
label="Replace invalid data with defaults"
actionLabel="Reset"
disabled={settings.saving}
onPress={async () => {
if (await settings.reset()) onAutoOpenChange(false);
}}
/>
) : null}
</SettingsCard>
</SettingsSection>
);
}

return (
<SettingsSection title="Explorer">
<SettingsCard>
<SettingsSwitch
label="Open Agent Crew automatically"
hint="Open the Explorer panel once for each new workspace observed while enabled"
value={settings.values.autoOpenExplorer}
disabled={settings.saving}
onValueChange={async (autoOpenExplorer) => {
const saved = await settings.save(
{ ...settings.values, autoOpenExplorer },
settings.revision,
);
if (saved) onAutoOpenChange(autoOpenExplorer);
}}
/>
</SettingsCard>
{settings.saveError ? (
<Text accessibilityRole="alert" style={styles.error}>
{settings.saveError}
</Text>
) : null}
</SettingsSection>
);
}
29 changes: 25 additions & 4 deletions agent-crew/index.client.tsx
Original file line number Diff line number Diff line change
@@ -1,16 +1,31 @@
import type { PluginClientContext } from "@getpaseo/plugin/client";
import type { PluginClientContext, PluginSurfaceProps } from "@getpaseo/plugin/client";
import { createAutoOpenManager } from "./client/auto-open";
import { AgentCrew } from "./client/main";
import { AgentCrewSettingsScreen } from "./client/settings-screen";
import { agentCrewSettings } from "./shared/settings";

export default function contribute(client: PluginClientContext) {
client.addWorkspacePanel({
const autoOpen = createAutoOpenManager(client);

function SettingsSurface(props: PluginSurfaceProps) {
return <AgentCrewSettingsScreen {...props} onAutoOpenChange={autoOpen.setEnabled} />;
}

const removeSettings = client.addSettingsScreen({
id: agentCrewSettings.id,
title: "Agent Crew settings",
icon: "Settings",
Component: SettingsSurface,
});
const removeWorkspacePanel = client.addWorkspacePanel({
id: "crew",
title: "Agent Crew",
icon: "Network",
context: "workspace",
locations: ["explorer"],
Component: AgentCrew,
});
client.addCommandCenterItem({
const removeOpenCrew = client.addCommandCenterItem({
id: "open-crew",
title: "Open Agent Crew",
icon: "Network",
Expand All @@ -20,5 +35,11 @@ export default function contribute(client: PluginClientContext) {
openPanel("crew", { location: "explorer" });
},
});
return () => {};

return () => {
removeOpenCrew();
removeWorkspacePanel();
removeSettings();
autoOpen.dispose();
};
}
10 changes: 10 additions & 0 deletions agent-crew/index.server.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import type { PluginServerContext } from "@getpaseo/plugin/server";
import { createAutoOpenClaimHandler } from "./server/auto-open";
import { claimOpenedWorkspaces } from "./shared/auto-open";
import { agentCrewSettings } from "./shared/settings";

export default function contribute(server: PluginServerContext) {
server.registerSettings(agentCrewSettings);
server.handle(claimOpenedWorkspaces, createAutoOpenClaimHandler());
return () => {};
}
3 changes: 3 additions & 0 deletions agent-crew/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,10 @@
"LICENSE",
"README.md",
"client",
"shared",
"server",
"index.client.tsx",
"index.server.ts",
"paseo-plugin.json"
],
"scripts": {
Expand Down
Loading
Loading