new plugin
This commit is contained in:
106
plugin-agent-inbox-config/README.md
Normal file
106
plugin-agent-inbox-config/README.md
Normal file
@@ -0,0 +1,106 @@
|
||||
# @paperclipai/plugin-agent-inbox-config
|
||||
|
||||
Plugin for configuring per-agent inbox-lite filter settings via UI.
|
||||
|
||||
## What It Does
|
||||
|
||||
This plugin provides a UI for configuring which issues appear in each agent's inbox via the `inbox-lite` endpoint. Instead of using query parameters or code changes, users can use a simple interface with toggles to control:
|
||||
|
||||
- **Issue statuses** - Which issue statuses (todo, in_progress, blocked, etc.) should appear in the inbox
|
||||
- **Project filter** - Optionally limit to issues from a specific project
|
||||
- **Goal filter** - Optionally limit to issues linked to a specific goal
|
||||
- **Search query** - Optional text search to filter issues by title, description, or comments
|
||||
- **Include backlog** - Toggle to include/exclude backlog issues
|
||||
|
||||
## UI Slots
|
||||
|
||||
The plugin registers three UI slots:
|
||||
|
||||
1. **Sidebar entry** (`agent-inbox-config-sidebar`) - Quick access to select an agent and navigate to their inbox config
|
||||
2. **Page** (`agent-inbox-config-page`) - Overview page showing all agents with their current inbox configuration summaries
|
||||
3. **Agent detail tab** (`agent-inbox-tab`) - Full configuration form on each agent's detail page
|
||||
|
||||
## Configuration
|
||||
|
||||
### Instance Config Schema
|
||||
|
||||
The plugin supports the following instance-level configuration:
|
||||
|
||||
```json
|
||||
{
|
||||
"defaultStatuses": "todo,in_progress,blocked",
|
||||
"includeBacklog": false,
|
||||
"maxIssuesPerAgent": 50
|
||||
}
|
||||
```
|
||||
|
||||
- `defaultStatuses`: Comma-separated list of statuses to use as default for new agents
|
||||
- `includeBacklog`: Whether to include backlog issues by default
|
||||
- `maxIssuesPerAgent`: Maximum number of issues returned per agent inbox (1-500)
|
||||
|
||||
### Per-Agent Config Storage
|
||||
|
||||
Each agent's inbox configuration is stored in two places:
|
||||
|
||||
1. **Plugin state** - Under the key `agentInboxConfigs` scoped to `instance`. This allows the plugin to quickly read/write configs without querying the database.
|
||||
|
||||
2. **Agent runtimeConfig** - The config is also persisted to the agent's `runtimeConfig.inboxConfig` field for durability and to be available even if the plugin is uninstalled.
|
||||
|
||||
## Data Handlers
|
||||
|
||||
The plugin registers the following data handlers for UI components:
|
||||
|
||||
- `agents` - Lists all agents in a company
|
||||
- `getAgentInboxConfig` - Gets the inbox config for a specific agent
|
||||
- `listAgentInboxConfigs` - Lists all agent inbox configs
|
||||
|
||||
## Action Handlers
|
||||
|
||||
The plugin registers the following action handlers:
|
||||
|
||||
- `setAgentInboxConfig` - Sets or updates an agent's inbox configuration
|
||||
- `resetAgentInboxConfig` - Resets an agent's config to defaults
|
||||
|
||||
## Events
|
||||
|
||||
The plugin subscribes to:
|
||||
|
||||
- `agent.created` - Automatically sets default inbox config for new agents
|
||||
|
||||
## Capabilities Required
|
||||
|
||||
```json
|
||||
[
|
||||
"agents.read",
|
||||
"projects.read",
|
||||
"goals.read",
|
||||
"issues.read",
|
||||
"plugin.state.read",
|
||||
"plugin.state.write",
|
||||
"ui.sidebar.register",
|
||||
"ui.page.register"
|
||||
]
|
||||
```
|
||||
|
||||
## Local Install (Dev)
|
||||
|
||||
From the repo root, build the plugin and install it:
|
||||
|
||||
```bash
|
||||
pnpm --filter @paperclipai/plugin-agent-inbox-config build
|
||||
pnpm paperclipai plugin install ./packages/plugins/plugin-agent-inbox-config
|
||||
```
|
||||
|
||||
## Usage
|
||||
|
||||
1. Navigate to an agent's detail page in Paperclip
|
||||
2. Click the "Inbox Settings" tab
|
||||
3. Configure the filters:
|
||||
- Check/uncheck issue statuses
|
||||
- Select a project filter (optional)
|
||||
- Select a goal filter (optional)
|
||||
- Enter a search query (optional)
|
||||
- Toggle include backlog
|
||||
4. Click "Save Configuration"
|
||||
|
||||
The agent's inbox-lite endpoint will now return issues matching these filters.
|
||||
20
plugin-agent-inbox-config/dist/constants.d.ts
vendored
Normal file
20
plugin-agent-inbox-config/dist/constants.d.ts
vendored
Normal file
@@ -0,0 +1,20 @@
|
||||
export declare const PLUGIN_ID = "paperclip.plugin-agent-inbox-config";
|
||||
export declare const PLUGIN_VERSION = "0.1.0";
|
||||
export declare const SIDEBAR_SLOT_ID = "agent-inbox-config-sidebar";
|
||||
export declare const PAGE_SLOT_ID = "agent-inbox-config-page";
|
||||
export declare const AGENT_TAB_SLOT_ID = "agent-inbox-tab";
|
||||
export declare const EXPORT_NAMES: {
|
||||
readonly sidebar: "InboxConfigSidebar";
|
||||
readonly page: "InboxConfigPage";
|
||||
readonly agentTab: "AgentInboxSettingsTab";
|
||||
};
|
||||
export declare const DEFAULT_INBOX_CONFIG: {
|
||||
statuses: string;
|
||||
includeBacklog: boolean;
|
||||
projectId: string | null;
|
||||
goalId: string | null;
|
||||
labelIds: string[];
|
||||
query: string | null;
|
||||
};
|
||||
export type AgentInboxConfig = typeof DEFAULT_INBOX_CONFIG;
|
||||
//# sourceMappingURL=constants.d.ts.map
|
||||
1
plugin-agent-inbox-config/dist/constants.d.ts.map
vendored
Normal file
1
plugin-agent-inbox-config/dist/constants.d.ts.map
vendored
Normal file
@@ -0,0 +1 @@
|
||||
{"version":3,"file":"constants.d.ts","sourceRoot":"","sources":["../src/constants.ts"],"names":[],"mappings":"AAAA,eAAO,MAAM,SAAS,wCAAwC,CAAC;AAC/D,eAAO,MAAM,cAAc,UAAU,CAAC;AAGtC,eAAO,MAAM,eAAe,+BAA+B,CAAC;AAC5D,eAAO,MAAM,YAAY,4BAA4B,CAAC;AACtD,eAAO,MAAM,iBAAiB,oBAAoB,CAAC;AAGnD,eAAO,MAAM,YAAY;;;;CAIf,CAAC;AAGX,eAAO,MAAM,oBAAoB;;;eAGZ,MAAM,GAAG,IAAI;YAChB,MAAM,GAAG,IAAI;cACb,MAAM,EAAE;WACT,MAAM,GAAG,IAAI;CAC7B,CAAC;AAEF,MAAM,MAAM,gBAAgB,GAAG,OAAO,oBAAoB,CAAC"}
|
||||
22
plugin-agent-inbox-config/dist/constants.js
vendored
Normal file
22
plugin-agent-inbox-config/dist/constants.js
vendored
Normal file
@@ -0,0 +1,22 @@
|
||||
export const PLUGIN_ID = "paperclip.plugin-agent-inbox-config";
|
||||
export const PLUGIN_VERSION = "0.1.0";
|
||||
// Slot IDs
|
||||
export const SIDEBAR_SLOT_ID = "agent-inbox-config-sidebar";
|
||||
export const PAGE_SLOT_ID = "agent-inbox-config-page";
|
||||
export const AGENT_TAB_SLOT_ID = "agent-inbox-tab";
|
||||
// Export names
|
||||
export const EXPORT_NAMES = {
|
||||
sidebar: "InboxConfigSidebar",
|
||||
page: "InboxConfigPage",
|
||||
agentTab: "AgentInboxSettingsTab",
|
||||
};
|
||||
// Default config
|
||||
export const DEFAULT_INBOX_CONFIG = {
|
||||
statuses: "todo,in_progress,blocked",
|
||||
includeBacklog: false,
|
||||
projectId: null,
|
||||
goalId: null,
|
||||
labelIds: [],
|
||||
query: null,
|
||||
};
|
||||
//# sourceMappingURL=constants.js.map
|
||||
1
plugin-agent-inbox-config/dist/constants.js.map
vendored
Normal file
1
plugin-agent-inbox-config/dist/constants.js.map
vendored
Normal file
@@ -0,0 +1 @@
|
||||
{"version":3,"file":"constants.js","sourceRoot":"","sources":["../src/constants.ts"],"names":[],"mappings":"AAAA,MAAM,CAAC,MAAM,SAAS,GAAG,qCAAqC,CAAC;AAC/D,MAAM,CAAC,MAAM,cAAc,GAAG,OAAO,CAAC;AAEtC,WAAW;AACX,MAAM,CAAC,MAAM,eAAe,GAAG,4BAA4B,CAAC;AAC5D,MAAM,CAAC,MAAM,YAAY,GAAG,yBAAyB,CAAC;AACtD,MAAM,CAAC,MAAM,iBAAiB,GAAG,iBAAiB,CAAC;AAEnD,eAAe;AACf,MAAM,CAAC,MAAM,YAAY,GAAG;IAC1B,OAAO,EAAE,oBAAoB;IAC7B,IAAI,EAAE,iBAAiB;IACvB,QAAQ,EAAE,uBAAuB;CACzB,CAAC;AAEX,iBAAiB;AACjB,MAAM,CAAC,MAAM,oBAAoB,GAAG;IAClC,QAAQ,EAAE,0BAA0B;IACpC,cAAc,EAAE,KAAK;IACrB,SAAS,EAAE,IAAqB;IAChC,MAAM,EAAE,IAAqB;IAC7B,QAAQ,EAAE,EAAc;IACxB,KAAK,EAAE,IAAqB;CAC7B,CAAC"}
|
||||
3
plugin-agent-inbox-config/dist/index.d.ts
vendored
Normal file
3
plugin-agent-inbox-config/dist/index.d.ts
vendored
Normal file
@@ -0,0 +1,3 @@
|
||||
export { default as manifest } from "./manifest.js";
|
||||
export { default as worker } from "./worker.js";
|
||||
//# sourceMappingURL=index.d.ts.map
|
||||
1
plugin-agent-inbox-config/dist/index.d.ts.map
vendored
Normal file
1
plugin-agent-inbox-config/dist/index.d.ts.map
vendored
Normal file
@@ -0,0 +1 @@
|
||||
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,IAAI,QAAQ,EAAE,MAAM,eAAe,CAAC;AACpD,OAAO,EAAE,OAAO,IAAI,MAAM,EAAE,MAAM,aAAa,CAAC"}
|
||||
3
plugin-agent-inbox-config/dist/index.js
vendored
Normal file
3
plugin-agent-inbox-config/dist/index.js
vendored
Normal file
@@ -0,0 +1,3 @@
|
||||
export { default as manifest } from "./manifest.js";
|
||||
export { default as worker } from "./worker.js";
|
||||
//# sourceMappingURL=index.js.map
|
||||
1
plugin-agent-inbox-config/dist/index.js.map
vendored
Normal file
1
plugin-agent-inbox-config/dist/index.js.map
vendored
Normal file
@@ -0,0 +1 @@
|
||||
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,IAAI,QAAQ,EAAE,MAAM,eAAe,CAAC;AACpD,OAAO,EAAE,OAAO,IAAI,MAAM,EAAE,MAAM,aAAa,CAAC"}
|
||||
8
plugin-agent-inbox-config/dist/manifest.d.ts
vendored
Normal file
8
plugin-agent-inbox-config/dist/manifest.d.ts
vendored
Normal file
@@ -0,0 +1,8 @@
|
||||
import type { PaperclipPluginManifestV1 } from "@paperclipai/plugin-sdk";
|
||||
/**
|
||||
* Plugin that provides per-agent inbox-lite configuration via UI toggles.
|
||||
* Allows configuring which issues appear in each agent's inbox without code changes.
|
||||
*/
|
||||
declare const manifest: PaperclipPluginManifestV1;
|
||||
export default manifest;
|
||||
//# sourceMappingURL=manifest.d.ts.map
|
||||
1
plugin-agent-inbox-config/dist/manifest.d.ts.map
vendored
Normal file
1
plugin-agent-inbox-config/dist/manifest.d.ts.map
vendored
Normal file
@@ -0,0 +1 @@
|
||||
{"version":3,"file":"manifest.d.ts","sourceRoot":"","sources":["../src/manifest.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,yBAAyB,EAAE,MAAM,yBAAyB,CAAC;AAKzE;;;GAGG;AACH,QAAA,MAAM,QAAQ,EAAE,yBAoEf,CAAC;AAEF,eAAe,QAAQ,CAAC"}
|
||||
77
plugin-agent-inbox-config/dist/manifest.js
vendored
Normal file
77
plugin-agent-inbox-config/dist/manifest.js
vendored
Normal file
@@ -0,0 +1,77 @@
|
||||
const PLUGIN_ID = "paperclip.plugin-agent-inbox-config";
|
||||
const PLUGIN_VERSION = "0.1.0";
|
||||
/**
|
||||
* Plugin that provides per-agent inbox-lite configuration via UI toggles.
|
||||
* Allows configuring which issues appear in each agent's inbox without code changes.
|
||||
*/
|
||||
const manifest = {
|
||||
id: PLUGIN_ID,
|
||||
apiVersion: 1,
|
||||
version: PLUGIN_VERSION,
|
||||
displayName: "Agent Inbox Configuration",
|
||||
description: "Configure per-agent inbox-lite filters via UI toggles. Control which issues appear in each agent's inbox based on status, project, goal, labels, and search queries.",
|
||||
author: "Paperclip",
|
||||
categories: ["ui", "automation"],
|
||||
capabilities: [
|
||||
"agents.read",
|
||||
"projects.read",
|
||||
"goals.read",
|
||||
"issues.read",
|
||||
"plugin.state.read",
|
||||
"plugin.state.write",
|
||||
"http.outbound",
|
||||
"ui.detailTab.register",
|
||||
"ui.page.register",
|
||||
],
|
||||
entrypoints: {
|
||||
worker: "./dist/worker.js",
|
||||
ui: "./dist/ui",
|
||||
},
|
||||
instanceConfigSchema: {
|
||||
type: "object",
|
||||
title: "Agent Inbox Config Plugin Settings",
|
||||
description: "Default inbox configuration settings for agents.",
|
||||
properties: {
|
||||
defaultStatuses: {
|
||||
type: "string",
|
||||
title: "Default Statuses",
|
||||
description: "Default statuses to include in inbox (comma-separated). Override per-agent via agent settings.",
|
||||
default: "todo,in_progress,blocked",
|
||||
},
|
||||
includeBacklog: {
|
||||
type: "boolean",
|
||||
title: "Include Backlog by Default",
|
||||
description: "Whether backlog issues are included by default.",
|
||||
default: false,
|
||||
},
|
||||
maxIssuesPerAgent: {
|
||||
type: "integer",
|
||||
title: "Max Issues Per Agent",
|
||||
description: "Maximum number of issues to return per agent inbox.",
|
||||
default: 50,
|
||||
minimum: 1,
|
||||
maximum: 500,
|
||||
},
|
||||
},
|
||||
},
|
||||
ui: {
|
||||
slots: [
|
||||
{
|
||||
type: "page",
|
||||
id: "agent-inbox-config-page",
|
||||
displayName: "Agent Inbox Configuration",
|
||||
exportName: "InboxConfigPage",
|
||||
routePath: "inbox-config",
|
||||
},
|
||||
{
|
||||
type: "detailTab",
|
||||
id: "agent-inbox-tab",
|
||||
displayName: "Inbox Settings",
|
||||
exportName: "AgentInboxSettingsTab",
|
||||
entityTypes: ["agent"],
|
||||
},
|
||||
],
|
||||
},
|
||||
};
|
||||
export default manifest;
|
||||
//# sourceMappingURL=manifest.js.map
|
||||
1
plugin-agent-inbox-config/dist/manifest.js.map
vendored
Normal file
1
plugin-agent-inbox-config/dist/manifest.js.map
vendored
Normal file
@@ -0,0 +1 @@
|
||||
{"version":3,"file":"manifest.js","sourceRoot":"","sources":["../src/manifest.ts"],"names":[],"mappings":"AAEA,MAAM,SAAS,GAAG,qCAAqC,CAAC;AACxD,MAAM,cAAc,GAAG,OAAO,CAAC;AAE/B;;;GAGG;AACH,MAAM,QAAQ,GAA8B;IAC1C,EAAE,EAAE,SAAS;IACb,UAAU,EAAE,CAAC;IACb,OAAO,EAAE,cAAc;IACvB,WAAW,EAAE,2BAA2B;IACxC,WAAW,EAAE,sKAAsK;IACnL,MAAM,EAAE,WAAW;IACnB,UAAU,EAAE,CAAC,IAAI,EAAE,YAAY,CAAC;IAChC,YAAY,EAAE;QACZ,aAAa;QACb,eAAe;QACf,YAAY;QACZ,aAAa;QACb,mBAAmB;QACnB,oBAAoB;QACpB,eAAe;QACf,uBAAuB;QACvB,kBAAkB;KACnB;IACD,WAAW,EAAE;QACX,MAAM,EAAE,kBAAkB;QAC1B,EAAE,EAAE,WAAW;KAChB;IACD,oBAAoB,EAAE;QACpB,IAAI,EAAE,QAAQ;QACd,KAAK,EAAE,oCAAoC;QAC3C,WAAW,EAAE,kDAAkD;QAC/D,UAAU,EAAE;YACV,eAAe,EAAE;gBACf,IAAI,EAAE,QAAQ;gBACd,KAAK,EAAE,kBAAkB;gBACzB,WAAW,EAAE,gGAAgG;gBAC7G,OAAO,EAAE,0BAA0B;aACpC;YACD,cAAc,EAAE;gBACd,IAAI,EAAE,SAAS;gBACf,KAAK,EAAE,4BAA4B;gBACnC,WAAW,EAAE,iDAAiD;gBAC9D,OAAO,EAAE,KAAK;aACf;YACD,iBAAiB,EAAE;gBACjB,IAAI,EAAE,SAAS;gBACf,KAAK,EAAE,sBAAsB;gBAC7B,WAAW,EAAE,qDAAqD;gBAClE,OAAO,EAAE,EAAE;gBACX,OAAO,EAAE,CAAC;gBACV,OAAO,EAAE,GAAG;aACb;SACF;KACF;IACD,EAAE,EAAE;QACF,KAAK,EAAE;YACL;gBACE,IAAI,EAAE,MAAM;gBACZ,EAAE,EAAE,yBAAyB;gBAC7B,WAAW,EAAE,2BAA2B;gBACxC,UAAU,EAAE,iBAAiB;gBAC7B,SAAS,EAAE,cAAc;aAC1B;YACD;gBACE,IAAI,EAAE,WAAW;gBACjB,EAAE,EAAE,iBAAiB;gBACrB,WAAW,EAAE,gBAAgB;gBAC7B,UAAU,EAAE,uBAAuB;gBACnC,WAAW,EAAE,CAAC,OAAO,CAAC;aACvB;SACF;KACF;CACF,CAAC;AAEF,eAAe,QAAQ,CAAC"}
|
||||
4
plugin-agent-inbox-config/dist/ui/index.d.ts
vendored
Normal file
4
plugin-agent-inbox-config/dist/ui/index.d.ts
vendored
Normal file
@@ -0,0 +1,4 @@
|
||||
import { type PluginPageProps, type PluginDetailTabProps } from "@paperclipai/plugin-sdk/ui";
|
||||
export declare function InboxConfigPage({ context }: PluginPageProps): import("react/jsx-runtime").JSX.Element;
|
||||
export declare function AgentInboxSettingsTab({ context }: PluginDetailTabProps): import("react/jsx-runtime").JSX.Element;
|
||||
//# sourceMappingURL=index.d.ts.map
|
||||
1
plugin-agent-inbox-config/dist/ui/index.d.ts.map
vendored
Normal file
1
plugin-agent-inbox-config/dist/ui/index.d.ts.map
vendored
Normal file
@@ -0,0 +1 @@
|
||||
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/ui/index.tsx"],"names":[],"mappings":"AACA,OAAO,EAKL,KAAK,eAAe,EACpB,KAAK,oBAAoB,EAC1B,MAAM,4BAA4B,CAAC;AAuHpC,wBAAgB,eAAe,CAAC,EAAE,OAAO,EAAE,EAAE,eAAe,2CA+E3D;AA+CD,wBAAgB,qBAAqB,CAAC,EAAE,OAAO,EAAE,EAAE,oBAAoB,2CA4OtE"}
|
||||
251
plugin-agent-inbox-config/dist/ui/index.js
vendored
Normal file
251
plugin-agent-inbox-config/dist/ui/index.js
vendored
Normal file
@@ -0,0 +1,251 @@
|
||||
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
||||
import { useState, useEffect } from "react";
|
||||
import { usePluginAction, usePluginData, usePluginToast, } from "@paperclipai/plugin-sdk/ui";
|
||||
import { DEFAULT_INBOX_CONFIG, PAGE_SLOT_ID, AGENT_TAB_SLOT_ID, } from "../constants.js";
|
||||
// -----------------------------------------------------------------------------
|
||||
// Styles
|
||||
// -----------------------------------------------------------------------------
|
||||
const containerStyle = {
|
||||
display: "grid",
|
||||
gap: "16px",
|
||||
};
|
||||
const cardStyle = {
|
||||
border: "1px solid var(--border, #e5e7eb)",
|
||||
borderRadius: "12px",
|
||||
padding: "16px",
|
||||
background: "var(--card, white)",
|
||||
};
|
||||
const sectionHeaderStyle = {
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
justifyContent: "space-between",
|
||||
marginBottom: "12px",
|
||||
};
|
||||
const buttonStyle = {
|
||||
appearance: "none",
|
||||
border: "1px solid var(--border, #e5e7eb)",
|
||||
borderRadius: "8px",
|
||||
background: "transparent",
|
||||
color: "inherit",
|
||||
padding: "8px 14px",
|
||||
fontSize: "13px",
|
||||
cursor: "pointer",
|
||||
};
|
||||
const primaryButtonStyle = {
|
||||
...buttonStyle,
|
||||
background: "var(--foreground, #1f2937)",
|
||||
color: "var(--background, white)",
|
||||
};
|
||||
const inputStyle = {
|
||||
width: "100%",
|
||||
border: "1px solid var(--border, #e5e7eb)",
|
||||
borderRadius: "8px",
|
||||
padding: "10px 12px",
|
||||
background: "transparent",
|
||||
color: "inherit",
|
||||
fontSize: "13px",
|
||||
};
|
||||
const selectStyle = {
|
||||
...inputStyle,
|
||||
};
|
||||
const checkboxStyle = {
|
||||
width: "18px",
|
||||
height: "18px",
|
||||
accentColor: "var(--foreground, #1f2937)",
|
||||
};
|
||||
const rowStyle = {
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
gap: "10px",
|
||||
marginBottom: "12px",
|
||||
};
|
||||
const mutedTextStyle = {
|
||||
fontSize: "12px",
|
||||
opacity: 0.7,
|
||||
lineHeight: 1.5,
|
||||
};
|
||||
// -----------------------------------------------------------------------------
|
||||
// Helper functions
|
||||
// -----------------------------------------------------------------------------
|
||||
function getStatusOptions() {
|
||||
return [
|
||||
{ value: "todo", label: "Todo" },
|
||||
{ value: "in_progress", label: "In Progress" },
|
||||
{ value: "blocked", label: "Blocked" },
|
||||
{ value: "backlog", label: "Backlog" },
|
||||
{ value: "in_review", label: "In Review" },
|
||||
{ value: "done", label: "Done" },
|
||||
{ value: "cancelled", label: "Cancelled" },
|
||||
];
|
||||
}
|
||||
function getStatusLabel(value) {
|
||||
const option = getStatusOptions().find((o) => o.value === value);
|
||||
return option?.label || value;
|
||||
}
|
||||
// -----------------------------------------------------------------------------
|
||||
// Page Component - Shows all agents and their inbox configs
|
||||
// -----------------------------------------------------------------------------
|
||||
export function InboxConfigPage({ context }) {
|
||||
const [agents, setAgents] = useState([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [error, setError] = useState(null);
|
||||
useEffect(() => {
|
||||
async function loadAgents() {
|
||||
if (!context.companyId)
|
||||
return;
|
||||
setLoading(true);
|
||||
try {
|
||||
const response = await fetch(`/api/companies/${context.companyId}/agents`, {
|
||||
credentials: "include",
|
||||
});
|
||||
if (!response.ok)
|
||||
throw new Error(`Failed to load agents: ${response.status}`);
|
||||
const data = await response.json();
|
||||
setAgents(Array.isArray(data) ? data : []);
|
||||
setError(null);
|
||||
}
|
||||
catch (err) {
|
||||
setError(err instanceof Error ? err.message : String(err));
|
||||
}
|
||||
finally {
|
||||
setLoading(false);
|
||||
}
|
||||
}
|
||||
void loadAgents();
|
||||
}, [context.companyId]);
|
||||
const agentPath = (agentId) => {
|
||||
const prefix = context.companyPrefix || "";
|
||||
return `${prefix ? "/${prefix}" : "/"}/agents/${agentId}`;
|
||||
};
|
||||
if (!context.companyId) {
|
||||
return (_jsx("div", { style: { padding: "24px", fontSize: "14px", opacity: 0.7 }, children: "Select a company to configure agent inboxes." }));
|
||||
}
|
||||
if (loading) {
|
||||
return _jsx("div", { style: { padding: "24px", fontSize: "14px" }, children: "Loading agents\u2026" });
|
||||
}
|
||||
return (_jsxs("div", { style: { padding: "24px", maxWidth: "1000px", margin: "0 auto" }, children: [_jsx("h1", { style: { fontSize: "24px", fontWeight: 700, marginBottom: "8px" }, children: "Agent Inbox Configuration" }), _jsx("p", { style: { ...mutedTextStyle, marginBottom: "24px", fontSize: "14px" }, children: "Configure which issues appear in each agent's inbox via the inbox-lite endpoint." }), error ? (_jsx("div", { style: { ...cardStyle, color: "var(--destructive, #dc2626)" }, children: error })) : (_jsxs("div", { style: { display: "grid", gap: "16px" }, children: [agents.map((agent) => (_jsxs("div", { style: cardStyle, children: [_jsxs("div", { style: sectionHeaderStyle, children: [_jsx("strong", { style: { fontSize: "15px" }, children: agent.name }), _jsx("a", { href: `${agentPath(agent.id)}?tab=plugin:${PAGE_SLOT_ID}:${AGENT_TAB_SLOT_ID}`, style: { fontSize: "12px", textDecoration: "underline", cursor: "pointer" }, children: "Configure \u2192" })] }), _jsx("div", { style: { display: "grid", gap: "8px", fontSize: "13px" }, children: _jsx(AgentInboxSummary, { agentId: agent.id }) })] }, agent.id))), agents.length === 0 && (_jsx("div", { style: { ...cardStyle, padding: "24px", textAlign: "center", opacity: 0.7 }, children: "No agents found in this company." }))] }))] }));
|
||||
}
|
||||
// -----------------------------------------------------------------------------
|
||||
// Agent Inbox Summary Component (used in page view)
|
||||
// -----------------------------------------------------------------------------
|
||||
function AgentInboxSummary({ agentId }) {
|
||||
const config = usePluginData("getAgentInboxConfig", { agentId });
|
||||
if (config.loading)
|
||||
return _jsx("div", { style: { fontSize: "12px", opacity: 0.6 }, children: "Loading\u2026" });
|
||||
if (config.error)
|
||||
return _jsx("div", { style: { fontSize: "12px", color: "var(--destructive, #dc2626)" }, children: "Error loading config" });
|
||||
const c = config.data || DEFAULT_INBOX_CONFIG;
|
||||
return (_jsxs("div", { style: { display: "grid", gap: "6px", fontSize: "12px" }, children: [_jsxs("div", { children: [_jsx("strong", { children: "Statuses:" }), " ", c.statuses] }), c.projectId ? (_jsxs("div", { children: [_jsx("strong", { children: "Project:" }), " Filtered"] })) : null, c.goalId ? (_jsxs("div", { children: [_jsx("strong", { children: "Goal:" }), " Filtered"] })) : null, c.labelIds && c.labelIds.length > 0 ? (_jsxs("div", { children: [_jsx("strong", { children: "Labels:" }), " ", c.labelIds.length, " filter(s)"] })) : null, c.query ? (_jsxs("div", { children: [_jsx("strong", { children: "Search:" }), " \"", c.query, "\""] })) : null] }));
|
||||
}
|
||||
// -----------------------------------------------------------------------------
|
||||
// Agent Detail Tab Component - Configure inbox for a specific agent
|
||||
// -----------------------------------------------------------------------------
|
||||
export function AgentInboxSettingsTab({ context }) {
|
||||
const { entityId, entityType } = context;
|
||||
if (entityType !== "agent") {
|
||||
return (_jsx("div", { style: { padding: "24px", fontSize: "13px", opacity: 0.7 }, children: "This tab is only available on agent detail pages." }));
|
||||
}
|
||||
const agentId = entityId;
|
||||
const toast = usePluginToast();
|
||||
// Fetch current config
|
||||
const configData = usePluginData("getAgentInboxConfig", { agentId });
|
||||
// Fetch projects for dropdown
|
||||
const [projects, setProjects] = useState([]);
|
||||
const [goals, setGoals] = useState([]);
|
||||
const [loadingOptions, setLoadingOptions] = useState(true);
|
||||
// Form state
|
||||
const [formState, setFormState] = useState({
|
||||
...DEFAULT_INBOX_CONFIG,
|
||||
statuses: configData.data?.statuses || DEFAULT_INBOX_CONFIG.statuses,
|
||||
includeBacklog: configData.data?.includeBacklog || DEFAULT_INBOX_CONFIG.includeBacklog,
|
||||
projectId: configData.data?.projectId || null,
|
||||
goalId: configData.data?.goalId || null,
|
||||
labelIds: configData.data?.labelIds || [],
|
||||
query: configData.data?.query || null,
|
||||
});
|
||||
const [saving, setSaving] = useState(false);
|
||||
// Load projects and goals
|
||||
useEffect(() => {
|
||||
async function loadOptions() {
|
||||
if (!context.companyId)
|
||||
return;
|
||||
setLoadingOptions(true);
|
||||
try {
|
||||
const [projectsRes, goalsRes] = await Promise.all([
|
||||
fetch(`/api/companies/${context.companyId}/projects`, { credentials: "include" }),
|
||||
fetch(`/api/companies/${context.companyId}/goals`, { credentials: "include" }),
|
||||
]);
|
||||
if (projectsRes.ok) {
|
||||
const data = await projectsRes.json();
|
||||
setProjects(Array.isArray(data) ? data : []);
|
||||
}
|
||||
if (goalsRes.ok) {
|
||||
const data = await goalsRes.json();
|
||||
setGoals(Array.isArray(data) ? data : []);
|
||||
}
|
||||
}
|
||||
catch (err) {
|
||||
console.error("Failed to load options:", err);
|
||||
}
|
||||
finally {
|
||||
setLoadingOptions(false);
|
||||
}
|
||||
}
|
||||
void loadOptions();
|
||||
}, [context.companyId]);
|
||||
// Update form when config data loads
|
||||
useEffect(() => {
|
||||
if (configData.data) {
|
||||
setFormState({ ...DEFAULT_INBOX_CONFIG, ...configData.data });
|
||||
}
|
||||
}, [configData.data]);
|
||||
const saveConfig = usePluginAction("setAgentInboxConfig");
|
||||
async function handleSave(e) {
|
||||
e.preventDefault();
|
||||
if (!agentId)
|
||||
return;
|
||||
setSaving(true);
|
||||
try {
|
||||
await saveConfig({
|
||||
agentId,
|
||||
config: {
|
||||
statuses: formState.statuses,
|
||||
includeBacklog: formState.includeBacklog,
|
||||
projectId: formState.projectId,
|
||||
goalId: formState.goalId,
|
||||
labelIds: formState.labelIds,
|
||||
query: formState.query,
|
||||
},
|
||||
});
|
||||
toast({
|
||||
title: "Inbox config saved",
|
||||
body: `Configuration updated for this agent.`,
|
||||
tone: "success",
|
||||
});
|
||||
}
|
||||
catch (err) {
|
||||
const message = err instanceof Error ? err.message : String(err);
|
||||
toast({
|
||||
title: "Failed to save config",
|
||||
body: message,
|
||||
tone: "error",
|
||||
});
|
||||
}
|
||||
finally {
|
||||
setSaving(false);
|
||||
}
|
||||
}
|
||||
const statusOptions = getStatusOptions();
|
||||
const selectedStatuses = formState.statuses.split(",");
|
||||
return (_jsxs("div", { style: { padding: "20px", maxWidth: "700px" }, children: [_jsx("h2", { style: { fontSize: "18px", fontWeight: 600, marginBottom: "8px" }, children: "Inbox Configuration" }), _jsx("p", { style: { ...mutedTextStyle, marginBottom: "20px", fontSize: "13px" }, children: "Configure which issues appear in this agent's inbox via the inbox-lite endpoint." }), _jsxs("form", { onSubmit: handleSave, style: { display: "grid", gap: "20px" }, children: [_jsxs("div", { style: cardStyle, children: [_jsx("div", { style: { fontWeight: 600, marginBottom: "12px", fontSize: "14px" }, children: "Issue Statuses" }), _jsx("p", { style: { ...mutedTextStyle, marginBottom: "12px", fontSize: "12px" }, children: "Select which issue statuses should appear in this agent's inbox." }), _jsx("div", { style: { display: "grid", gap: "8px" }, children: statusOptions.map((option) => (_jsxs("label", { style: { display: "flex", alignItems: "center", gap: "10px", fontSize: "13px" }, children: [_jsx("input", { type: "checkbox", checked: selectedStatuses.includes(option.value), onChange: (e) => {
|
||||
const isChecked = e.target.checked;
|
||||
const current = formState.statuses.split(",");
|
||||
const next = isChecked
|
||||
? [...current, option.value].filter(Boolean)
|
||||
: current.filter((s) => s !== option.value);
|
||||
setFormState({ ...formState, statuses: next.join(",") });
|
||||
}, style: checkboxStyle }), _jsx("span", { children: option.label })] }, option.value))) })] }), _jsxs("div", { style: cardStyle, children: [_jsx("div", { style: { fontWeight: 600, marginBottom: "12px", fontSize: "14px" }, children: "Project Filter" }), _jsx("p", { style: { ...mutedTextStyle, marginBottom: "12px", fontSize: "12px" }, children: "Optionally limit inbox to issues from a specific project." }), _jsxs("select", { style: selectStyle, value: formState.projectId || "", onChange: (e) => setFormState({ ...formState, projectId: e.target.value || null }), disabled: loadingOptions, children: [_jsx("option", { value: "", children: "No filter (all projects)" }), projects.map((project) => (_jsx("option", { value: project.id, children: project.name }, project.id)))] })] }), _jsxs("div", { style: cardStyle, children: [_jsx("div", { style: { fontWeight: 600, marginBottom: "12px", fontSize: "14px" }, children: "Goal Filter" }), _jsx("p", { style: { ...mutedTextStyle, marginBottom: "12px", fontSize: "12px" }, children: "Optionally limit inbox to issues linked to a specific goal." }), _jsxs("select", { style: selectStyle, value: formState.goalId || "", onChange: (e) => setFormState({ ...formState, goalId: e.target.value || null }), disabled: loadingOptions, children: [_jsx("option", { value: "", children: "No filter (all goals)" }), goals.map((goal) => (_jsx("option", { value: goal.id, children: goal.title }, goal.id)))] })] }), _jsxs("div", { style: cardStyle, children: [_jsx("div", { style: { fontWeight: 600, marginBottom: "12px", fontSize: "14px" }, children: "Search Query" }), _jsx("p", { style: { ...mutedTextStyle, marginBottom: "12px", fontSize: "12px" }, children: "Optional search query to filter issues by title, description, or comments." }), _jsx("input", { type: "text", style: inputStyle, value: formState.query || "", onChange: (e) => setFormState({ ...formState, query: e.target.value || null }), placeholder: "e.g., docker, deployment, urgent" })] }), _jsxs("div", { style: cardStyle, children: [_jsxs("label", { style: { display: "flex", alignItems: "center", gap: "10px" }, children: [_jsx("input", { type: "checkbox", checked: formState.includeBacklog, onChange: (e) => setFormState({ ...formState, includeBacklog: e.target.checked }), style: checkboxStyle }), _jsx("span", { style: { fontSize: "13px" }, children: "Include backlog issues" })] }), _jsx("p", { style: { ...mutedTextStyle, marginTop: "6px", fontSize: "12px" }, children: "When enabled, issues with status \"backlog\" will be included in the inbox." })] }), _jsxs("div", { style: { display: "flex", gap: "10px", justifyContent: "flex-end" }, children: [_jsx("button", { type: "button", style: buttonStyle, onClick: () => {
|
||||
setFormState({ ...DEFAULT_INBOX_CONFIG });
|
||||
}, children: "Reset to Defaults" }), _jsx("button", { type: "submit", style: primaryButtonStyle, disabled: saving || configData.loading || loadingOptions, children: saving ? "Saving…" : "Save Configuration" })] })] })] }));
|
||||
}
|
||||
//# sourceMappingURL=index.js.map
|
||||
1
plugin-agent-inbox-config/dist/ui/index.js.map
vendored
Normal file
1
plugin-agent-inbox-config/dist/ui/index.js.map
vendored
Normal file
File diff suppressed because one or more lines are too long
4
plugin-agent-inbox-config/dist/worker.d.ts
vendored
Normal file
4
plugin-agent-inbox-config/dist/worker.d.ts
vendored
Normal file
@@ -0,0 +1,4 @@
|
||||
import { type PaperclipPlugin } from "@paperclipai/plugin-sdk";
|
||||
declare const plugin: PaperclipPlugin;
|
||||
export default plugin;
|
||||
//# sourceMappingURL=worker.d.ts.map
|
||||
1
plugin-agent-inbox-config/dist/worker.d.ts.map
vendored
Normal file
1
plugin-agent-inbox-config/dist/worker.d.ts.map
vendored
Normal file
@@ -0,0 +1 @@
|
||||
{"version":3,"file":"worker.d.ts","sourceRoot":"","sources":["../src/worker.ts"],"names":[],"mappings":"AAAA,OAAO,EAA2B,KAAK,eAAe,EAAE,MAAM,yBAAyB,CAAC;AAUxF,QAAA,MAAM,MAAM,EAAE,eAqLZ,CAAC;AAEH,eAAe,MAAM,CAAC"}
|
||||
165
plugin-agent-inbox-config/dist/worker.js
vendored
Normal file
165
plugin-agent-inbox-config/dist/worker.js
vendored
Normal file
@@ -0,0 +1,165 @@
|
||||
import { definePlugin, runWorker } from "@paperclipai/plugin-sdk";
|
||||
import { DEFAULT_INBOX_CONFIG, PLUGIN_ID, PLUGIN_VERSION, } from "./constants.js";
|
||||
const WORKER_KEY = "agent-inbox-config-worker";
|
||||
const plugin = definePlugin({
|
||||
async setup(ctx) {
|
||||
ctx.logger.info(`${PLUGIN_ID} v${PLUGIN_VERSION} starting...`);
|
||||
// Initialize plugin state with default inbox configs per agent
|
||||
const existingState = await ctx.state.get({ scopeKind: "instance", stateKey: "agentInboxConfigs" });
|
||||
if (!existingState) {
|
||||
await ctx.state.set({ scopeKind: "instance", stateKey: "agentInboxConfigs" }, {});
|
||||
}
|
||||
const versionState = await ctx.state.get({ scopeKind: "instance", stateKey: "version" });
|
||||
if (!versionState) {
|
||||
await ctx.state.set({ scopeKind: "instance", stateKey: "version" }, PLUGIN_VERSION);
|
||||
}
|
||||
// Register data handlers for UI to fetch data
|
||||
ctx.data.register("agents", async (params) => {
|
||||
const companyId = typeof params?.companyId === "string" ? params.companyId : "";
|
||||
if (!companyId)
|
||||
return [];
|
||||
return await ctx.agents.list({ companyId, limit: 100, offset: 0 });
|
||||
});
|
||||
ctx.data.register("getAgentInboxConfig", async (params) => {
|
||||
const agentId = typeof params?.agentId === "string" ? params.agentId : "";
|
||||
if (!agentId)
|
||||
return null;
|
||||
const configs = (await ctx.state.get({ scopeKind: "instance", stateKey: "agentInboxConfigs" })) || {};
|
||||
return configs[agentId] || null;
|
||||
});
|
||||
ctx.data.register("listAgentInboxConfigs", async () => {
|
||||
const configs = (await ctx.state.get({ scopeKind: "instance", stateKey: "agentInboxConfigs" })) || {};
|
||||
return Object.entries(configs).map(([agentId, config]) => ({ agentId, config }));
|
||||
});
|
||||
// Register action handlers for UI to perform actions
|
||||
ctx.actions.register("setAgentInboxConfig", async (params) => {
|
||||
const agentId = typeof params?.agentId === "string" ? params.agentId : "";
|
||||
const config = params?.config;
|
||||
if (!agentId) {
|
||||
throw new Error("agentId is required");
|
||||
}
|
||||
const configs = (await ctx.state.get({ scopeKind: "instance", stateKey: "agentInboxConfigs" })) || {};
|
||||
// Merge with defaults
|
||||
const mergedConfig = {
|
||||
...DEFAULT_INBOX_CONFIG,
|
||||
statuses: config?.statuses || DEFAULT_INBOX_CONFIG.statuses,
|
||||
includeBacklog: config?.includeBacklog ?? DEFAULT_INBOX_CONFIG.includeBacklog,
|
||||
projectId: config?.projectId ?? null,
|
||||
goalId: config?.goalId ?? null,
|
||||
labelIds: config?.labelIds || [],
|
||||
query: config?.query ?? null,
|
||||
};
|
||||
configs[agentId] = mergedConfig;
|
||||
await ctx.state.set({ scopeKind: "instance", stateKey: "agentInboxConfigs" }, configs);
|
||||
// Also persist to agent's runtimeConfig for durability and server-side access
|
||||
try {
|
||||
const agentResponse = await ctx.http.fetch(`/api/agents/${agentId}`, {
|
||||
method: "GET",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
});
|
||||
if (agentResponse.ok) {
|
||||
const agentData = await agentResponse.json();
|
||||
const currentRuntimeConfig = agentData.runtimeConfig || {};
|
||||
const agentUpdateResponse = await ctx.http.fetch(`/api/agents/${agentId}`, {
|
||||
method: "PATCH",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({
|
||||
runtimeConfig: {
|
||||
...currentRuntimeConfig,
|
||||
inboxConfig: mergedConfig,
|
||||
},
|
||||
}),
|
||||
});
|
||||
if (!agentUpdateResponse.ok) {
|
||||
ctx.logger.warn("Failed to update agent runtimeConfig with inbox config", { agentId, status: agentUpdateResponse.status });
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (err) {
|
||||
ctx.logger.warn("Failed to persist inbox config to agent runtimeConfig", { agentId, error: err });
|
||||
}
|
||||
ctx.logger.info("Inbox config updated for agent", { agentId });
|
||||
return { success: true, agentId, config: mergedConfig };
|
||||
});
|
||||
ctx.actions.register("resetAgentInboxConfig", async (params) => {
|
||||
const agentId = typeof params?.agentId === "string" ? params.agentId : "";
|
||||
if (!agentId) {
|
||||
throw new Error("agentId is required");
|
||||
}
|
||||
const configs = (await ctx.state.get({ scopeKind: "instance", stateKey: "agentInboxConfigs" })) || {};
|
||||
delete configs[agentId];
|
||||
await ctx.state.set({ scopeKind: "instance", stateKey: "agentInboxConfigs" }, configs);
|
||||
// Also clear inboxConfig from agent's runtimeConfig
|
||||
try {
|
||||
const agentResponse = await ctx.http.fetch(`/api/agents/${agentId}`, {
|
||||
method: "GET",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
});
|
||||
if (agentResponse.ok) {
|
||||
const agentData = await agentResponse.json();
|
||||
const currentRuntimeConfig = agentData.runtimeConfig || {};
|
||||
// Remove inboxConfig from runtimeConfig
|
||||
const updatedRuntimeConfig = { ...currentRuntimeConfig };
|
||||
delete updatedRuntimeConfig.inboxConfig;
|
||||
const agentUpdateResponse = await ctx.http.fetch(`/api/agents/${agentId}`, {
|
||||
method: "PATCH",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({
|
||||
runtimeConfig: updatedRuntimeConfig,
|
||||
}),
|
||||
});
|
||||
if (!agentUpdateResponse.ok) {
|
||||
ctx.logger.warn("Failed to clear agent runtimeConfig inbox config", { agentId, status: agentUpdateResponse.status });
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (err) {
|
||||
ctx.logger.warn("Failed to clear inbox config from agent runtimeConfig", { agentId, error: err });
|
||||
}
|
||||
ctx.logger.info("Inbox config reset for agent", { agentId });
|
||||
return { success: true, agentId };
|
||||
});
|
||||
// Event handler for agent creation - set default inbox config
|
||||
ctx.events.on("agent.created", async (event) => {
|
||||
const agentId = event.entityId || "";
|
||||
if (!agentId)
|
||||
return;
|
||||
const configs = (await ctx.state.get({ scopeKind: "instance", stateKey: "agentInboxConfigs" })) || {};
|
||||
// Set default config for new agent in plugin state
|
||||
configs[agentId] = {
|
||||
...DEFAULT_INBOX_CONFIG,
|
||||
};
|
||||
await ctx.state.set({ scopeKind: "instance", stateKey: "agentInboxConfigs" }, configs);
|
||||
// Also set default inboxConfig in agent's runtimeConfig
|
||||
try {
|
||||
const agentResponse = await ctx.http.fetch(`/api/agents/${agentId}`, {
|
||||
method: "PATCH",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({
|
||||
runtimeConfig: {
|
||||
inboxConfig: { ...DEFAULT_INBOX_CONFIG },
|
||||
},
|
||||
}),
|
||||
});
|
||||
if (!agentResponse.ok) {
|
||||
ctx.logger.warn("Failed to set default inbox config on new agent", { agentId, status: agentResponse.status });
|
||||
}
|
||||
}
|
||||
catch (err) {
|
||||
ctx.logger.warn("Failed to set default inbox config on new agent runtimeConfig", { agentId, error: err });
|
||||
}
|
||||
ctx.logger.info("Default inbox config set for new agent", { agentId });
|
||||
});
|
||||
ctx.logger.info(`${PLUGIN_ID} initialized successfully`);
|
||||
},
|
||||
async onHealth() {
|
||||
return {
|
||||
status: "ok",
|
||||
message: "Agent Inbox Config plugin ready",
|
||||
details: {},
|
||||
};
|
||||
},
|
||||
});
|
||||
export default plugin;
|
||||
runWorker(plugin, import.meta.url);
|
||||
//# sourceMappingURL=worker.js.map
|
||||
1
plugin-agent-inbox-config/dist/worker.js.map
vendored
Normal file
1
plugin-agent-inbox-config/dist/worker.js.map
vendored
Normal file
File diff suppressed because one or more lines are too long
17
plugin-agent-inbox-config/node_modules/.bin/paperclip-plugin-dev-server
generated
vendored
Executable file
17
plugin-agent-inbox-config/node_modules/.bin/paperclip-plugin-dev-server
generated
vendored
Executable file
@@ -0,0 +1,17 @@
|
||||
#!/bin/sh
|
||||
basedir=$(dirname "$(echo "$0" | sed -e 's,\\,/,g')")
|
||||
|
||||
case `uname` in
|
||||
*CYGWIN*) basedir=`cygpath -w "$basedir"`;;
|
||||
esac
|
||||
|
||||
if [ -z "$NODE_PATH" ]; then
|
||||
export NODE_PATH="/home/mike/code/paperclip/packages/plugins/sdk/dist/node_modules:/home/mike/code/paperclip/packages/plugins/sdk/node_modules:/home/mike/code/paperclip/packages/plugins/node_modules:/home/mike/code/paperclip/packages/node_modules:/home/mike/code/paperclip/node_modules:/home/mike/code/node_modules:/home/mike/node_modules:/home/node_modules:/node_modules:/home/mike/code/paperclip/node_modules/.pnpm/node_modules"
|
||||
else
|
||||
export NODE_PATH="/home/mike/code/paperclip/packages/plugins/sdk/dist/node_modules:/home/mike/code/paperclip/packages/plugins/sdk/node_modules:/home/mike/code/paperclip/packages/plugins/node_modules:/home/mike/code/paperclip/packages/node_modules:/home/mike/code/paperclip/node_modules:/home/mike/code/node_modules:/home/mike/node_modules:/home/node_modules:/node_modules:/home/mike/code/paperclip/node_modules/.pnpm/node_modules:$NODE_PATH"
|
||||
fi
|
||||
if [ -x "$basedir/node" ]; then
|
||||
exec "$basedir/node" "$basedir/../@paperclipai/plugin-sdk/dist/dev-cli.js" "$@"
|
||||
else
|
||||
exec node "$basedir/../@paperclipai/plugin-sdk/dist/dev-cli.js" "$@"
|
||||
fi
|
||||
17
plugin-agent-inbox-config/node_modules/.bin/tsc
generated
vendored
Executable file
17
plugin-agent-inbox-config/node_modules/.bin/tsc
generated
vendored
Executable file
@@ -0,0 +1,17 @@
|
||||
#!/bin/sh
|
||||
basedir=$(dirname "$(echo "$0" | sed -e 's,\\,/,g')")
|
||||
|
||||
case `uname` in
|
||||
*CYGWIN*) basedir=`cygpath -w "$basedir"`;;
|
||||
esac
|
||||
|
||||
if [ -z "$NODE_PATH" ]; then
|
||||
export NODE_PATH="/home/mike/code/paperclip/node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/bin/node_modules:/home/mike/code/paperclip/node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/node_modules:/home/mike/code/paperclip/node_modules/.pnpm/typescript@5.9.3/node_modules:/home/mike/code/paperclip/node_modules/.pnpm/node_modules"
|
||||
else
|
||||
export NODE_PATH="/home/mike/code/paperclip/node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/bin/node_modules:/home/mike/code/paperclip/node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/node_modules:/home/mike/code/paperclip/node_modules/.pnpm/typescript@5.9.3/node_modules:/home/mike/code/paperclip/node_modules/.pnpm/node_modules:$NODE_PATH"
|
||||
fi
|
||||
if [ -x "$basedir/node" ]; then
|
||||
exec "$basedir/node" "$basedir/../typescript/bin/tsc" "$@"
|
||||
else
|
||||
exec node "$basedir/../typescript/bin/tsc" "$@"
|
||||
fi
|
||||
17
plugin-agent-inbox-config/node_modules/.bin/tsserver
generated
vendored
Executable file
17
plugin-agent-inbox-config/node_modules/.bin/tsserver
generated
vendored
Executable file
@@ -0,0 +1,17 @@
|
||||
#!/bin/sh
|
||||
basedir=$(dirname "$(echo "$0" | sed -e 's,\\,/,g')")
|
||||
|
||||
case `uname` in
|
||||
*CYGWIN*) basedir=`cygpath -w "$basedir"`;;
|
||||
esac
|
||||
|
||||
if [ -z "$NODE_PATH" ]; then
|
||||
export NODE_PATH="/home/mike/code/paperclip/node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/bin/node_modules:/home/mike/code/paperclip/node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/node_modules:/home/mike/code/paperclip/node_modules/.pnpm/typescript@5.9.3/node_modules:/home/mike/code/paperclip/node_modules/.pnpm/node_modules"
|
||||
else
|
||||
export NODE_PATH="/home/mike/code/paperclip/node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/bin/node_modules:/home/mike/code/paperclip/node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/node_modules:/home/mike/code/paperclip/node_modules/.pnpm/typescript@5.9.3/node_modules:/home/mike/code/paperclip/node_modules/.pnpm/node_modules:$NODE_PATH"
|
||||
fi
|
||||
if [ -x "$basedir/node" ]; then
|
||||
exec "$basedir/node" "$basedir/../typescript/bin/tsserver" "$@"
|
||||
else
|
||||
exec node "$basedir/../typescript/bin/tsserver" "$@"
|
||||
fi
|
||||
1
plugin-agent-inbox-config/node_modules/@paperclipai/plugin-sdk
generated
vendored
Symbolic link
1
plugin-agent-inbox-config/node_modules/@paperclipai/plugin-sdk
generated
vendored
Symbolic link
@@ -0,0 +1 @@
|
||||
../../../sdk
|
||||
1
plugin-agent-inbox-config/node_modules/@types/node
generated
vendored
Symbolic link
1
plugin-agent-inbox-config/node_modules/@types/node
generated
vendored
Symbolic link
@@ -0,0 +1 @@
|
||||
../../../../../node_modules/.pnpm/@types+node@24.12.0/node_modules/@types/node
|
||||
1
plugin-agent-inbox-config/node_modules/@types/react
generated
vendored
Symbolic link
1
plugin-agent-inbox-config/node_modules/@types/react
generated
vendored
Symbolic link
@@ -0,0 +1 @@
|
||||
../../../../../node_modules/.pnpm/@types+react@19.2.14/node_modules/@types/react
|
||||
1
plugin-agent-inbox-config/node_modules/@types/react-dom
generated
vendored
Symbolic link
1
plugin-agent-inbox-config/node_modules/@types/react-dom
generated
vendored
Symbolic link
@@ -0,0 +1 @@
|
||||
../../../../../node_modules/.pnpm/@types+react-dom@19.2.3_@types+react@19.2.14/node_modules/@types/react-dom
|
||||
1
plugin-agent-inbox-config/node_modules/react
generated
vendored
Symbolic link
1
plugin-agent-inbox-config/node_modules/react
generated
vendored
Symbolic link
@@ -0,0 +1 @@
|
||||
../../../../node_modules/.pnpm/react@19.2.4/node_modules/react
|
||||
1
plugin-agent-inbox-config/node_modules/react-dom
generated
vendored
Symbolic link
1
plugin-agent-inbox-config/node_modules/react-dom
generated
vendored
Symbolic link
@@ -0,0 +1 @@
|
||||
../../../../node_modules/.pnpm/react-dom@19.2.4_react@19.2.4/node_modules/react-dom
|
||||
1
plugin-agent-inbox-config/node_modules/typescript
generated
vendored
Symbolic link
1
plugin-agent-inbox-config/node_modules/typescript
generated
vendored
Symbolic link
@@ -0,0 +1 @@
|
||||
../../../../node_modules/.pnpm/typescript@5.9.3/node_modules/typescript
|
||||
36
plugin-agent-inbox-config/package.json
Normal file
36
plugin-agent-inbox-config/package.json
Normal file
@@ -0,0 +1,36 @@
|
||||
{
|
||||
"name": "@paperclipai/plugin-agent-inbox-config",
|
||||
"version": "0.1.0",
|
||||
"description": "Plugin for configuring per-agent inbox-lite filter settings via UI",
|
||||
"type": "module",
|
||||
"type": "module",
|
||||
"private": true,
|
||||
"exports": {
|
||||
".": "./src/index.ts"
|
||||
},
|
||||
"paperclipPlugin": {
|
||||
"manifest": "./dist/manifest.js",
|
||||
"worker": "./dist/worker.js",
|
||||
"ui": "./dist/ui/"
|
||||
},
|
||||
"scripts": {
|
||||
"prebuild": "node ../../../scripts/ensure-plugin-build-deps.mjs",
|
||||
"build": "tsc",
|
||||
"clean": "rm -rf dist",
|
||||
"typecheck": "pnpm --filter @paperclipai/plugin-sdk build && tsc --noEmit"
|
||||
},
|
||||
"dependencies": {
|
||||
"@paperclipai/plugin-sdk": "workspace:*"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "^24.6.0",
|
||||
"@types/react": "^19.0.8",
|
||||
"@types/react-dom": "^19.0.3",
|
||||
"react": "^19.0.0",
|
||||
"react-dom": "^19.0.0",
|
||||
"typescript": "^5.7.3"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": ">=18"
|
||||
}
|
||||
}
|
||||
26
plugin-agent-inbox-config/src/constants.ts
Normal file
26
plugin-agent-inbox-config/src/constants.ts
Normal file
@@ -0,0 +1,26 @@
|
||||
export const PLUGIN_ID = "paperclip.plugin-agent-inbox-config";
|
||||
export const PLUGIN_VERSION = "0.1.0";
|
||||
|
||||
// Slot IDs
|
||||
export const SIDEBAR_SLOT_ID = "agent-inbox-config-sidebar";
|
||||
export const PAGE_SLOT_ID = "agent-inbox-config-page";
|
||||
export const AGENT_TAB_SLOT_ID = "agent-inbox-tab";
|
||||
|
||||
// Export names
|
||||
export const EXPORT_NAMES = {
|
||||
sidebar: "InboxConfigSidebar",
|
||||
page: "InboxConfigPage",
|
||||
agentTab: "AgentInboxSettingsTab",
|
||||
} as const;
|
||||
|
||||
// Default config
|
||||
export const DEFAULT_INBOX_CONFIG = {
|
||||
statuses: "todo,in_progress,blocked",
|
||||
includeBacklog: false,
|
||||
projectId: null as string | null,
|
||||
goalId: null as string | null,
|
||||
labelIds: [] as string[],
|
||||
query: null as string | null,
|
||||
};
|
||||
|
||||
export type AgentInboxConfig = typeof DEFAULT_INBOX_CONFIG;
|
||||
2
plugin-agent-inbox-config/src/index.ts
Normal file
2
plugin-agent-inbox-config/src/index.ts
Normal file
@@ -0,0 +1,2 @@
|
||||
export { default as manifest } from "./manifest.js";
|
||||
export { default as worker } from "./worker.js";
|
||||
80
plugin-agent-inbox-config/src/manifest.ts
Normal file
80
plugin-agent-inbox-config/src/manifest.ts
Normal file
@@ -0,0 +1,80 @@
|
||||
import type { PaperclipPluginManifestV1 } from "@paperclipai/plugin-sdk";
|
||||
|
||||
const PLUGIN_ID = "paperclip.plugin-agent-inbox-config";
|
||||
const PLUGIN_VERSION = "0.1.0";
|
||||
|
||||
/**
|
||||
* Plugin that provides per-agent inbox-lite configuration via UI toggles.
|
||||
* Allows configuring which issues appear in each agent's inbox without code changes.
|
||||
*/
|
||||
const manifest: PaperclipPluginManifestV1 = {
|
||||
id: PLUGIN_ID,
|
||||
apiVersion: 1,
|
||||
version: PLUGIN_VERSION,
|
||||
displayName: "Agent Inbox Configuration",
|
||||
description: "Configure per-agent inbox-lite filters via UI toggles. Control which issues appear in each agent's inbox based on status, project, goal, labels, and search queries.",
|
||||
author: "Paperclip",
|
||||
categories: ["ui", "automation"],
|
||||
capabilities: [
|
||||
"agents.read",
|
||||
"projects.read",
|
||||
"goals.read",
|
||||
"issues.read",
|
||||
"plugin.state.read",
|
||||
"plugin.state.write",
|
||||
"http.outbound",
|
||||
"ui.detailTab.register",
|
||||
"ui.page.register",
|
||||
],
|
||||
entrypoints: {
|
||||
worker: "./dist/worker.js",
|
||||
ui: "./dist/ui",
|
||||
},
|
||||
instanceConfigSchema: {
|
||||
type: "object",
|
||||
title: "Agent Inbox Config Plugin Settings",
|
||||
description: "Default inbox configuration settings for agents.",
|
||||
properties: {
|
||||
defaultStatuses: {
|
||||
type: "string",
|
||||
title: "Default Statuses",
|
||||
description: "Default statuses to include in inbox (comma-separated). Override per-agent via agent settings.",
|
||||
default: "todo,in_progress,blocked",
|
||||
},
|
||||
includeBacklog: {
|
||||
type: "boolean",
|
||||
title: "Include Backlog by Default",
|
||||
description: "Whether backlog issues are included by default.",
|
||||
default: false,
|
||||
},
|
||||
maxIssuesPerAgent: {
|
||||
type: "integer",
|
||||
title: "Max Issues Per Agent",
|
||||
description: "Maximum number of issues to return per agent inbox.",
|
||||
default: 50,
|
||||
minimum: 1,
|
||||
maximum: 500,
|
||||
},
|
||||
},
|
||||
},
|
||||
ui: {
|
||||
slots: [
|
||||
{
|
||||
type: "page",
|
||||
id: "agent-inbox-config-page",
|
||||
displayName: "Agent Inbox Configuration",
|
||||
exportName: "InboxConfigPage",
|
||||
routePath: "inbox-config",
|
||||
},
|
||||
{
|
||||
type: "detailTab",
|
||||
id: "agent-inbox-tab",
|
||||
displayName: "Inbox Settings",
|
||||
exportName: "AgentInboxSettingsTab",
|
||||
entityTypes: ["agent"],
|
||||
},
|
||||
],
|
||||
},
|
||||
};
|
||||
|
||||
export default manifest;
|
||||
490
plugin-agent-inbox-config/src/ui/index.tsx
Normal file
490
plugin-agent-inbox-config/src/ui/index.tsx
Normal file
@@ -0,0 +1,490 @@
|
||||
import { useState, useEffect, type FormEvent } from "react";
|
||||
import {
|
||||
useHostContext,
|
||||
usePluginAction,
|
||||
usePluginData,
|
||||
usePluginToast,
|
||||
type PluginPageProps,
|
||||
type PluginDetailTabProps,
|
||||
} from "@paperclipai/plugin-sdk/ui";
|
||||
|
||||
import {
|
||||
DEFAULT_INBOX_CONFIG,
|
||||
EXPORT_NAMES,
|
||||
PAGE_SLOT_ID,
|
||||
AGENT_TAB_SLOT_ID,
|
||||
type AgentInboxConfig,
|
||||
} from "../constants.js";
|
||||
|
||||
// -----------------------------------------------------------------------------
|
||||
// Types
|
||||
// -----------------------------------------------------------------------------
|
||||
|
||||
type AgentRecord = {
|
||||
id: string;
|
||||
name: string;
|
||||
status: string;
|
||||
urlKey?: string;
|
||||
};
|
||||
|
||||
// -----------------------------------------------------------------------------
|
||||
// Styles
|
||||
// -----------------------------------------------------------------------------
|
||||
|
||||
const containerStyle: React.CSSProperties = {
|
||||
display: "grid",
|
||||
gap: "16px",
|
||||
};
|
||||
|
||||
const cardStyle: React.CSSProperties = {
|
||||
border: "1px solid var(--border, #e5e7eb)",
|
||||
borderRadius: "12px",
|
||||
padding: "16px",
|
||||
background: "var(--card, white)",
|
||||
};
|
||||
|
||||
const sectionHeaderStyle: React.CSSProperties = {
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
justifyContent: "space-between",
|
||||
marginBottom: "12px",
|
||||
};
|
||||
|
||||
const buttonStyle: React.CSSProperties = {
|
||||
appearance: "none",
|
||||
border: "1px solid var(--border, #e5e7eb)",
|
||||
borderRadius: "8px",
|
||||
background: "transparent",
|
||||
color: "inherit",
|
||||
padding: "8px 14px",
|
||||
fontSize: "13px",
|
||||
cursor: "pointer",
|
||||
};
|
||||
|
||||
const primaryButtonStyle: React.CSSProperties = {
|
||||
...buttonStyle,
|
||||
background: "var(--foreground, #1f2937)",
|
||||
color: "var(--background, white)",
|
||||
};
|
||||
|
||||
const inputStyle: React.CSSProperties = {
|
||||
width: "100%",
|
||||
border: "1px solid var(--border, #e5e7eb)",
|
||||
borderRadius: "8px",
|
||||
padding: "10px 12px",
|
||||
background: "transparent",
|
||||
color: "inherit",
|
||||
fontSize: "13px",
|
||||
};
|
||||
|
||||
const selectStyle: React.CSSProperties = {
|
||||
...inputStyle,
|
||||
};
|
||||
|
||||
const checkboxStyle: React.CSSProperties = {
|
||||
width: "18px",
|
||||
height: "18px",
|
||||
accentColor: "var(--foreground, #1f2937)",
|
||||
};
|
||||
|
||||
const rowStyle: React.CSSProperties = {
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
gap: "10px",
|
||||
marginBottom: "12px",
|
||||
};
|
||||
|
||||
const mutedTextStyle: React.CSSProperties = {
|
||||
fontSize: "12px",
|
||||
opacity: 0.7,
|
||||
lineHeight: 1.5,
|
||||
};
|
||||
|
||||
// -----------------------------------------------------------------------------
|
||||
// Helper functions
|
||||
// -----------------------------------------------------------------------------
|
||||
|
||||
function getStatusOptions(): Array<{ value: string; label: string }> {
|
||||
return [
|
||||
{ value: "todo", label: "Todo" },
|
||||
{ value: "in_progress", label: "In Progress" },
|
||||
{ value: "blocked", label: "Blocked" },
|
||||
{ value: "backlog", label: "Backlog" },
|
||||
{ value: "in_review", label: "In Review" },
|
||||
{ value: "done", label: "Done" },
|
||||
{ value: "cancelled", label: "Cancelled" },
|
||||
];
|
||||
}
|
||||
|
||||
function getStatusLabel(value: string): string {
|
||||
const option = getStatusOptions().find((o) => o.value === value);
|
||||
return option?.label || value;
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------------------
|
||||
// Page Component - Shows all agents and their inbox configs
|
||||
// -----------------------------------------------------------------------------
|
||||
|
||||
export function InboxConfigPage({ context }: PluginPageProps) {
|
||||
const [agents, setAgents] = useState<AgentRecord[]>([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
async function loadAgents() {
|
||||
if (!context.companyId) return;
|
||||
setLoading(true);
|
||||
try {
|
||||
const response = await fetch(`/api/companies/${context.companyId}/agents`, {
|
||||
credentials: "include",
|
||||
});
|
||||
if (!response.ok) throw new Error(`Failed to load agents: ${response.status}`);
|
||||
const data = await response.json();
|
||||
setAgents(Array.isArray(data) ? data : []);
|
||||
setError(null);
|
||||
} catch (err) {
|
||||
setError(err instanceof Error ? err.message : String(err));
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
}
|
||||
void loadAgents();
|
||||
}, [context.companyId]);
|
||||
|
||||
const agentPath = (agentId: string) => {
|
||||
const prefix = context.companyPrefix || "";
|
||||
return `${prefix ? "/${prefix}" : "/"}/agents/${agentId}`;
|
||||
};
|
||||
|
||||
if (!context.companyId) {
|
||||
return (
|
||||
<div style={{ padding: "24px", fontSize: "14px", opacity: 0.7 }}>
|
||||
Select a company to configure agent inboxes.
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (loading) {
|
||||
return <div style={{ padding: "24px", fontSize: "14px" }}>Loading agents…</div>;
|
||||
}
|
||||
|
||||
return (
|
||||
<div style={{ padding: "24px", maxWidth: "1000px", margin: "0 auto" }}>
|
||||
<h1 style={{ fontSize: "24px", fontWeight: 700, marginBottom: "8px" }}>Agent Inbox Configuration</h1>
|
||||
<p style={{ ...mutedTextStyle, marginBottom: "24px", fontSize: "14px" }}>
|
||||
Configure which issues appear in each agent's inbox via the inbox-lite endpoint.
|
||||
</p>
|
||||
|
||||
{error ? (
|
||||
<div style={{ ...cardStyle, color: "var(--destructive, #dc2626)" }}>{error}</div>
|
||||
) : (
|
||||
<div style={{ display: "grid", gap: "16px" }}>
|
||||
{agents.map((agent) => (
|
||||
<div key={agent.id} style={cardStyle}>
|
||||
<div style={sectionHeaderStyle}>
|
||||
<strong style={{ fontSize: "15px" }}>{agent.name}</strong>
|
||||
<a
|
||||
href={`${agentPath(agent.id)}?tab=plugin:${PAGE_SLOT_ID}:${AGENT_TAB_SLOT_ID}`}
|
||||
style={{ fontSize: "12px", textDecoration: "underline", cursor: "pointer" }}
|
||||
>
|
||||
Configure →
|
||||
</a>
|
||||
</div>
|
||||
<div style={{ display: "grid", gap: "8px", fontSize: "13px" }}>
|
||||
<AgentInboxSummary agentId={agent.id} />
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
{agents.length === 0 && (
|
||||
<div style={{ ...cardStyle, padding: "24px", textAlign: "center", opacity: 0.7 }}>
|
||||
No agents found in this company.
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------------------
|
||||
// Agent Inbox Summary Component (used in page view)
|
||||
// -----------------------------------------------------------------------------
|
||||
|
||||
function AgentInboxSummary({ agentId }: { agentId: string }) {
|
||||
const config = usePluginData<AgentInboxConfig | null>("getAgentInboxConfig", { agentId });
|
||||
|
||||
if (config.loading) return <div style={{ fontSize: "12px", opacity: 0.6 }}>Loading…</div>;
|
||||
if (config.error) return <div style={{ fontSize: "12px", color: "var(--destructive, #dc2626)" }}>Error loading config</div>;
|
||||
|
||||
const c = config.data || DEFAULT_INBOX_CONFIG;
|
||||
|
||||
return (
|
||||
<div style={{ display: "grid", gap: "6px", fontSize: "12px" }}>
|
||||
<div>
|
||||
<strong>Statuses:</strong> {c.statuses}
|
||||
</div>
|
||||
{c.projectId ? (
|
||||
<div>
|
||||
<strong>Project:</strong> Filtered
|
||||
</div>
|
||||
) : null}
|
||||
{c.goalId ? (
|
||||
<div>
|
||||
<strong>Goal:</strong> Filtered
|
||||
</div>
|
||||
) : null}
|
||||
{c.labelIds && c.labelIds.length > 0 ? (
|
||||
<div>
|
||||
<strong>Labels:</strong> {c.labelIds.length} filter(s)
|
||||
</div>
|
||||
) : null}
|
||||
{c.query ? (
|
||||
<div>
|
||||
<strong>Search:</strong> "{c.query}"
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------------------
|
||||
// Agent Detail Tab Component - Configure inbox for a specific agent
|
||||
// -----------------------------------------------------------------------------
|
||||
|
||||
export function AgentInboxSettingsTab({ context }: PluginDetailTabProps) {
|
||||
const { entityId, entityType } = context;
|
||||
|
||||
if (entityType !== "agent") {
|
||||
return (
|
||||
<div style={{ padding: "24px", fontSize: "13px", opacity: 0.7 }}>
|
||||
This tab is only available on agent detail pages.
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const agentId = entityId;
|
||||
const toast = usePluginToast();
|
||||
|
||||
// Fetch current config
|
||||
const configData = usePluginData<AgentInboxConfig | null>("getAgentInboxConfig", { agentId });
|
||||
|
||||
// Fetch projects for dropdown
|
||||
const [projects, setProjects] = useState<Array<{ id: string; name: string }>>([]);
|
||||
const [goals, setGoals] = useState<Array<{ id: string; title: string }>>([]);
|
||||
const [loadingOptions, setLoadingOptions] = useState(true);
|
||||
|
||||
// Form state
|
||||
const [formState, setFormState] = useState<AgentInboxConfig>({
|
||||
...DEFAULT_INBOX_CONFIG,
|
||||
statuses: configData.data?.statuses || DEFAULT_INBOX_CONFIG.statuses,
|
||||
includeBacklog: configData.data?.includeBacklog || DEFAULT_INBOX_CONFIG.includeBacklog,
|
||||
projectId: configData.data?.projectId || null,
|
||||
goalId: configData.data?.goalId || null,
|
||||
labelIds: configData.data?.labelIds || [],
|
||||
query: configData.data?.query || null,
|
||||
});
|
||||
|
||||
const [saving, setSaving] = useState(false);
|
||||
|
||||
// Load projects and goals
|
||||
useEffect(() => {
|
||||
async function loadOptions() {
|
||||
if (!context.companyId) return;
|
||||
setLoadingOptions(true);
|
||||
try {
|
||||
const [projectsRes, goalsRes] = await Promise.all([
|
||||
fetch(`/api/companies/${context.companyId}/projects`, { credentials: "include" }),
|
||||
fetch(`/api/companies/${context.companyId}/goals`, { credentials: "include" }),
|
||||
]);
|
||||
|
||||
if (projectsRes.ok) {
|
||||
const data = await projectsRes.json();
|
||||
setProjects(Array.isArray(data) ? data : []);
|
||||
}
|
||||
if (goalsRes.ok) {
|
||||
const data = await goalsRes.json();
|
||||
setGoals(Array.isArray(data) ? data : []);
|
||||
}
|
||||
} catch (err) {
|
||||
console.error("Failed to load options:", err);
|
||||
} finally {
|
||||
setLoadingOptions(false);
|
||||
}
|
||||
}
|
||||
void loadOptions();
|
||||
}, [context.companyId]);
|
||||
|
||||
// Update form when config data loads
|
||||
useEffect(() => {
|
||||
if (configData.data) {
|
||||
setFormState({ ...DEFAULT_INBOX_CONFIG, ...configData.data });
|
||||
}
|
||||
}, [configData.data]);
|
||||
|
||||
const saveConfig = usePluginAction("setAgentInboxConfig");
|
||||
|
||||
async function handleSave(e: FormEvent) {
|
||||
e.preventDefault();
|
||||
if (!agentId) return;
|
||||
|
||||
setSaving(true);
|
||||
try {
|
||||
await saveConfig({
|
||||
agentId,
|
||||
config: {
|
||||
statuses: formState.statuses,
|
||||
includeBacklog: formState.includeBacklog,
|
||||
projectId: formState.projectId,
|
||||
goalId: formState.goalId,
|
||||
labelIds: formState.labelIds,
|
||||
query: formState.query,
|
||||
},
|
||||
});
|
||||
toast({
|
||||
title: "Inbox config saved",
|
||||
body: `Configuration updated for this agent.`,
|
||||
tone: "success",
|
||||
});
|
||||
} catch (err) {
|
||||
const message = err instanceof Error ? err.message : String(err);
|
||||
toast({
|
||||
title: "Failed to save config",
|
||||
body: message,
|
||||
tone: "error",
|
||||
});
|
||||
} finally {
|
||||
setSaving(false);
|
||||
}
|
||||
}
|
||||
|
||||
const statusOptions = getStatusOptions();
|
||||
const selectedStatuses = formState.statuses.split(",");
|
||||
|
||||
return (
|
||||
<div style={{ padding: "20px", maxWidth: "700px" }}>
|
||||
<h2 style={{ fontSize: "18px", fontWeight: 600, marginBottom: "8px" }}>Inbox Configuration</h2>
|
||||
<p style={{ ...mutedTextStyle, marginBottom: "20px", fontSize: "13px" }}>
|
||||
Configure which issues appear in this agent's inbox via the inbox-lite endpoint.
|
||||
</p>
|
||||
|
||||
<form onSubmit={handleSave} style={{ display: "grid", gap: "20px" }}>
|
||||
{/* Statuses */}
|
||||
<div style={cardStyle}>
|
||||
<div style={{ fontWeight: 600, marginBottom: "12px", fontSize: "14px" }}>Issue Statuses</div>
|
||||
<p style={{ ...mutedTextStyle, marginBottom: "12px", fontSize: "12px" }}>
|
||||
Select which issue statuses should appear in this agent's inbox.
|
||||
</p>
|
||||
<div style={{ display: "grid", gap: "8px" }}>
|
||||
{statusOptions.map((option) => (
|
||||
<label key={option.value} style={{ display: "flex", alignItems: "center", gap: "10px", fontSize: "13px" }}>
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={selectedStatuses.includes(option.value)}
|
||||
onChange={(e) => {
|
||||
const isChecked = e.target.checked;
|
||||
const current = formState.statuses.split(",");
|
||||
const next = isChecked
|
||||
? [...current, option.value].filter(Boolean)
|
||||
: current.filter((s) => s !== option.value);
|
||||
setFormState({ ...formState, statuses: next.join(",") });
|
||||
}}
|
||||
style={checkboxStyle}
|
||||
/>
|
||||
<span>{option.label}</span>
|
||||
</label>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Project Filter */}
|
||||
<div style={cardStyle}>
|
||||
<div style={{ fontWeight: 600, marginBottom: "12px", fontSize: "14px" }}>Project Filter</div>
|
||||
<p style={{ ...mutedTextStyle, marginBottom: "12px", fontSize: "12px" }}>
|
||||
Optionally limit inbox to issues from a specific project.
|
||||
</p>
|
||||
<select
|
||||
style={selectStyle}
|
||||
value={formState.projectId || ""}
|
||||
onChange={(e) => setFormState({ ...formState, projectId: e.target.value || null })}
|
||||
disabled={loadingOptions}
|
||||
>
|
||||
<option value="">No filter (all projects)</option>
|
||||
{projects.map((project) => (
|
||||
<option key={project.id} value={project.id}>{project.name}</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
|
||||
{/* Goal Filter */}
|
||||
<div style={cardStyle}>
|
||||
<div style={{ fontWeight: 600, marginBottom: "12px", fontSize: "14px" }}>Goal Filter</div>
|
||||
<p style={{ ...mutedTextStyle, marginBottom: "12px", fontSize: "12px" }}>
|
||||
Optionally limit inbox to issues linked to a specific goal.
|
||||
</p>
|
||||
<select
|
||||
style={selectStyle}
|
||||
value={formState.goalId || ""}
|
||||
onChange={(e) => setFormState({ ...formState, goalId: e.target.value || null })}
|
||||
disabled={loadingOptions}
|
||||
>
|
||||
<option value="">No filter (all goals)</option>
|
||||
{goals.map((goal) => (
|
||||
<option key={goal.id} value={goal.id}>{goal.title}</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
|
||||
{/* Search Query */}
|
||||
<div style={cardStyle}>
|
||||
<div style={{ fontWeight: 600, marginBottom: "12px", fontSize: "14px" }}>Search Query</div>
|
||||
<p style={{ ...mutedTextStyle, marginBottom: "12px", fontSize: "12px" }}>
|
||||
Optional search query to filter issues by title, description, or comments.
|
||||
</p>
|
||||
<input
|
||||
type="text"
|
||||
style={inputStyle}
|
||||
value={formState.query || ""}
|
||||
onChange={(e) => setFormState({ ...formState, query: e.target.value || null })}
|
||||
placeholder="e.g., docker, deployment, urgent"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Include Backlog Toggle */}
|
||||
<div style={cardStyle}>
|
||||
<label style={{ display: "flex", alignItems: "center", gap: "10px" }}>
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={formState.includeBacklog}
|
||||
onChange={(e) => setFormState({ ...formState, includeBacklog: e.target.checked })}
|
||||
style={checkboxStyle}
|
||||
/>
|
||||
<span style={{ fontSize: "13px" }}>Include backlog issues</span>
|
||||
</label>
|
||||
<p style={{ ...mutedTextStyle, marginTop: "6px", fontSize: "12px" }}>
|
||||
When enabled, issues with status "backlog" will be included in the inbox.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Actions */}
|
||||
<div style={{ display: "flex", gap: "10px", justifyContent: "flex-end" }}>
|
||||
<button
|
||||
type="button"
|
||||
style={buttonStyle}
|
||||
onClick={() => {
|
||||
setFormState({ ...DEFAULT_INBOX_CONFIG });
|
||||
}}
|
||||
>
|
||||
Reset to Defaults
|
||||
</button>
|
||||
<button
|
||||
type="submit"
|
||||
style={primaryButtonStyle}
|
||||
disabled={saving || configData.loading || loadingOptions}
|
||||
>
|
||||
{saving ? "Saving…" : "Save Configuration"}
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
195
plugin-agent-inbox-config/src/worker.ts
Normal file
195
plugin-agent-inbox-config/src/worker.ts
Normal file
@@ -0,0 +1,195 @@
|
||||
import { definePlugin, runWorker, type PaperclipPlugin } from "@paperclipai/plugin-sdk";
|
||||
import {
|
||||
DEFAULT_INBOX_CONFIG,
|
||||
PLUGIN_ID,
|
||||
PLUGIN_VERSION,
|
||||
type AgentInboxConfig,
|
||||
} from "./constants.js";
|
||||
|
||||
const WORKER_KEY = "agent-inbox-config-worker";
|
||||
|
||||
const plugin: PaperclipPlugin = definePlugin({
|
||||
async setup(ctx) {
|
||||
ctx.logger.info(`${PLUGIN_ID} v${PLUGIN_VERSION} starting...`);
|
||||
|
||||
// Initialize plugin state with default inbox configs per agent
|
||||
const existingState = await ctx.state.get({ scopeKind: "instance", stateKey: "agentInboxConfigs" });
|
||||
if (!existingState) {
|
||||
await ctx.state.set({ scopeKind: "instance", stateKey: "agentInboxConfigs" }, {});
|
||||
}
|
||||
|
||||
const versionState = await ctx.state.get({ scopeKind: "instance", stateKey: "version" });
|
||||
if (!versionState) {
|
||||
await ctx.state.set({ scopeKind: "instance", stateKey: "version" }, PLUGIN_VERSION);
|
||||
}
|
||||
|
||||
// Register data handlers for UI to fetch data
|
||||
ctx.data.register("agents", async (params) => {
|
||||
const companyId = typeof params?.companyId === "string" ? params.companyId : "";
|
||||
if (!companyId) return [];
|
||||
return await ctx.agents.list({ companyId, limit: 100, offset: 0 });
|
||||
});
|
||||
|
||||
ctx.data.register("getAgentInboxConfig", async (params) => {
|
||||
const agentId = typeof params?.agentId === "string" ? params.agentId : "";
|
||||
if (!agentId) return null;
|
||||
|
||||
const configs = (await ctx.state.get({ scopeKind: "instance", stateKey: "agentInboxConfigs" })) || {};
|
||||
return (configs as Record<string, AgentInboxConfig>)[agentId] || null;
|
||||
});
|
||||
|
||||
ctx.data.register("listAgentInboxConfigs", async () => {
|
||||
const configs = (await ctx.state.get({ scopeKind: "instance", stateKey: "agentInboxConfigs" })) || {};
|
||||
return Object.entries(configs).map(([agentId, config]) => ({ agentId, config }));
|
||||
});
|
||||
|
||||
// Register action handlers for UI to perform actions
|
||||
ctx.actions.register("setAgentInboxConfig", async (params) => {
|
||||
const agentId = typeof params?.agentId === "string" ? params.agentId : "";
|
||||
const config = params?.config as Partial<AgentInboxConfig>;
|
||||
|
||||
if (!agentId) {
|
||||
throw new Error("agentId is required");
|
||||
}
|
||||
|
||||
const configs = (await ctx.state.get({ scopeKind: "instance", stateKey: "agentInboxConfigs" })) || {};
|
||||
|
||||
// Merge with defaults
|
||||
const mergedConfig: AgentInboxConfig = {
|
||||
...DEFAULT_INBOX_CONFIG,
|
||||
statuses: config?.statuses || DEFAULT_INBOX_CONFIG.statuses,
|
||||
includeBacklog: config?.includeBacklog ?? DEFAULT_INBOX_CONFIG.includeBacklog,
|
||||
projectId: config?.projectId ?? null,
|
||||
goalId: config?.goalId ?? null,
|
||||
labelIds: config?.labelIds || [],
|
||||
query: config?.query ?? null,
|
||||
};
|
||||
|
||||
(configs as Record<string, AgentInboxConfig>)[agentId] = mergedConfig;
|
||||
await ctx.state.set({ scopeKind: "instance", stateKey: "agentInboxConfigs" }, configs);
|
||||
|
||||
// Also persist to agent's runtimeConfig for durability and server-side access
|
||||
try {
|
||||
const agentResponse = await ctx.http.fetch(`/api/agents/${agentId}`, {
|
||||
method: "GET",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
});
|
||||
if (agentResponse.ok) {
|
||||
const agentData = await agentResponse.json();
|
||||
const currentRuntimeConfig = agentData.runtimeConfig || {};
|
||||
|
||||
const agentUpdateResponse = await ctx.http.fetch(`/api/agents/${agentId}`, {
|
||||
method: "PATCH",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({
|
||||
runtimeConfig: {
|
||||
...currentRuntimeConfig,
|
||||
inboxConfig: mergedConfig,
|
||||
},
|
||||
}),
|
||||
});
|
||||
if (!agentUpdateResponse.ok) {
|
||||
ctx.logger.warn("Failed to update agent runtimeConfig with inbox config", { agentId, status: agentUpdateResponse.status });
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
ctx.logger.warn("Failed to persist inbox config to agent runtimeConfig", { agentId, error: err });
|
||||
}
|
||||
|
||||
ctx.logger.info("Inbox config updated for agent", { agentId });
|
||||
return { success: true, agentId, config: mergedConfig };
|
||||
});
|
||||
|
||||
ctx.actions.register("resetAgentInboxConfig", async (params) => {
|
||||
const agentId = typeof params?.agentId === "string" ? params.agentId : "";
|
||||
|
||||
if (!agentId) {
|
||||
throw new Error("agentId is required");
|
||||
}
|
||||
|
||||
const configs = (await ctx.state.get({ scopeKind: "instance", stateKey: "agentInboxConfigs" })) || {};
|
||||
delete (configs as Record<string, AgentInboxConfig>)[agentId];
|
||||
await ctx.state.set({ scopeKind: "instance", stateKey: "agentInboxConfigs" }, configs);
|
||||
|
||||
// Also clear inboxConfig from agent's runtimeConfig
|
||||
try {
|
||||
const agentResponse = await ctx.http.fetch(`/api/agents/${agentId}`, {
|
||||
method: "GET",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
});
|
||||
if (agentResponse.ok) {
|
||||
const agentData = await agentResponse.json();
|
||||
const currentRuntimeConfig = agentData.runtimeConfig || {};
|
||||
|
||||
// Remove inboxConfig from runtimeConfig
|
||||
const updatedRuntimeConfig = { ...currentRuntimeConfig };
|
||||
delete updatedRuntimeConfig.inboxConfig;
|
||||
|
||||
const agentUpdateResponse = await ctx.http.fetch(`/api/agents/${agentId}`, {
|
||||
method: "PATCH",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({
|
||||
runtimeConfig: updatedRuntimeConfig,
|
||||
}),
|
||||
});
|
||||
if (!agentUpdateResponse.ok) {
|
||||
ctx.logger.warn("Failed to clear agent runtimeConfig inbox config", { agentId, status: agentUpdateResponse.status });
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
ctx.logger.warn("Failed to clear inbox config from agent runtimeConfig", { agentId, error: err });
|
||||
}
|
||||
|
||||
ctx.logger.info("Inbox config reset for agent", { agentId });
|
||||
return { success: true, agentId };
|
||||
});
|
||||
|
||||
// Event handler for agent creation - set default inbox config
|
||||
ctx.events.on("agent.created", async (event) => {
|
||||
const agentId = event.entityId || "";
|
||||
if (!agentId) return;
|
||||
|
||||
const configs = (await ctx.state.get({ scopeKind: "instance", stateKey: "agentInboxConfigs" })) || {};
|
||||
|
||||
// Set default config for new agent in plugin state
|
||||
(configs as Record<string, AgentInboxConfig>)[agentId] = {
|
||||
...DEFAULT_INBOX_CONFIG,
|
||||
};
|
||||
|
||||
await ctx.state.set({ scopeKind: "instance", stateKey: "agentInboxConfigs" }, configs);
|
||||
|
||||
// Also set default inboxConfig in agent's runtimeConfig
|
||||
try {
|
||||
const agentResponse = await ctx.http.fetch(`/api/agents/${agentId}`, {
|
||||
method: "PATCH",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({
|
||||
runtimeConfig: {
|
||||
inboxConfig: { ...DEFAULT_INBOX_CONFIG },
|
||||
},
|
||||
}),
|
||||
});
|
||||
if (!agentResponse.ok) {
|
||||
ctx.logger.warn("Failed to set default inbox config on new agent", { agentId, status: agentResponse.status });
|
||||
}
|
||||
} catch (err) {
|
||||
ctx.logger.warn("Failed to set default inbox config on new agent runtimeConfig", { agentId, error: err });
|
||||
}
|
||||
|
||||
ctx.logger.info("Default inbox config set for new agent", { agentId });
|
||||
});
|
||||
|
||||
ctx.logger.info(`${PLUGIN_ID} initialized successfully`);
|
||||
},
|
||||
|
||||
async onHealth() {
|
||||
return {
|
||||
status: "ok",
|
||||
message: "Agent Inbox Config plugin ready",
|
||||
details: {},
|
||||
};
|
||||
},
|
||||
});
|
||||
|
||||
export default plugin;
|
||||
runWorker(plugin, import.meta.url);
|
||||
10
plugin-agent-inbox-config/tsconfig.json
Normal file
10
plugin-agent-inbox-config/tsconfig.json
Normal file
@@ -0,0 +1,10 @@
|
||||
{
|
||||
"extends": "../../../tsconfig.json",
|
||||
"compilerOptions": {
|
||||
"outDir": "dist",
|
||||
"rootDir": "src",
|
||||
"lib": ["ES2023", "DOM"],
|
||||
"jsx": "react-jsx"
|
||||
},
|
||||
"include": ["src"]
|
||||
}
|
||||
Reference in New Issue
Block a user