diff --git a/catalog.json b/catalog.json index dfed967..9b20b61 100644 --- a/catalog.json +++ b/catalog.json @@ -2,7 +2,7 @@ "schemaVersion": 1, "providerId": "official", "name": "PI-Desktop Official Plugins", - "updatedAt": "2026-09-14T04:38:39Z", + "updatedAt": "2026-09-18T16:03:32Z", "homepage": "https://github.com/vastsa/pi-desktop-plugins", "plugins": [ { @@ -789,19 +789,18 @@ "downloads": 0, "homepage": "https://github.com/vastsa/pi-desktop-plugins/tree/main/plugins/pi.session-orchestrator", "repository": "https://github.com/vastsa/pi-desktop-plugins", - "readmeMarkdown": "# Session Orchestrator\n\nSession Orchestrator coordinates real PI-Desktop sessions through one\nhigh-risk Agent tool, `SessionTask`. Every session is addressed by its existing\ndurable `sessionId`; follow-up messages keep that session's context and model.\nThe host owns delivery, queue admission, execution state, results, and completion\nnotifications. The plugin does not infer success from assistant text.\n\n## A normal workflow\n\n1. Call `models` to inspect the user's ready configured models when a task needs\n a particular model. Then `spawn(task, title?, model?)` to create a worker.\n Keep the returned `sessionId` and delivery `messageId`.\n2. Work in parallel. By default the host sends a completion message back to the\n sending session when the target turn settles; repeated polling is unnecessary.\n3. Use `send(sessionId, message)` for follow-up work in the same session, or to\n communicate with any other existing session. Workers can reply to their\n initiating session and peers by their real Session IDs.\n4. Inspect `status` or `result` when needed. Pass `messageId` or `turnId` to\n retrieve one specific delivery instead of whichever message is latest.\n5. Review the actual result. `accept` optionally records that review for the\n exact completed message. `cancel` stops collaboration work and retains the\n durable session and history.\n\nA received message is explicitly identified as another session's communication,\nnot human input or new user authorization. Completion notices need no\nacknowledgement unless further work is necessary. Use\n`notifyOnCompletion: false` for informational messages to avoid unnecessary\nreturn messages. Host-generated completion notices never request another\nautomatic completion notice.\n\n## Actions\n\n| Action | Behavior |\n| --- | --- |\n| `spawn(task, title?, model?)` | Atomically creates a real worker and admits its first delivery through the host. |\n| `send(sessionId, message, kind?)` | Sends a task or message to an existing session, including a busy session's host queue. Does not reselect its model. |\n| `models()` | Lists ready model keys, aliases, reasoning metadata, AI-delegation eligibility, and the default. |\n| `status(sessionIds?)` | Reads host-owned live summaries; omission selects this caller's recent session references. |\n| `list()` | Reads a bounded host-backed directory of communicable Agent sessions, including independent top-level sessions. The response also retains a legacy `workers` field for recent-reference callers. |\n| `result(sessionId, messageId?, turnId?)` | Returns the host delivery and exact turn outcome. Failed or interrupted work is never accepted as a successful report. |\n| `wait(sessionIds, timeoutMs?)` | Explicit polling fallback, 25 seconds by default and at most 45 seconds. The entire read budget observes the deadline and cancellation. |\n| `supervise(sessionIds, message)` | Sends up to four follow-ups in parallel; returns successful receipts and any individual failures. |\n| `accept(sessionId|sessionIds, messageId?, note?)` | Stores review metadata keyed by the exact completed delivery. It never changes host execution state. |\n| `cancel(sessionId, messageId?)` | Cancels target collaboration work without deleting the session. |\n\n`result.ready` means the delivery has settled, including failure, cancellation,\nor interruption. Successful work requires `message.status === \"completed\"`;\nthe compatibility `worker.report` field is populated only for that outcome.\n\n`spawn` and `send` accept `notifyOnCompletion` (default true) and an optional\n`idempotencyKey` for retrying the same request. Use a different key for a new\nrequest. For batch supervision requiring retry identities, call `send` separately\nwith one key per target. Session identity and delivery identity have separate\npurposes: `messageId` names a ledger entry, not a second worker system.\n\n## Model selection\n\nAn omitted `model` first selects from ready bindings with\n`availableForSubagents: true`. A default within that set takes precedence;\notherwise the first configured eligible row provides a stable selection.\nOnly when that set is empty does the configured ready default apply. An empty\ncatalog or unavailable default produces a clear configuration error.\n\nThe spawn response records `requestedModel` and its selection policy; the\nactual session model is read from `status.modelKey`, including after an\nidempotent retry.\n\nAn explicit request resolves against existing model keys, IDs, aliases and\nnames. Matching handles case, spacing, punctuation, common English/Chinese\nfamily names, and supported reasoning/non-reasoning intent. Numeric version\norder is preserved. Multiple matches return bounded exact-key candidates;\nno match is an error. The plugin never invents a model ID or silently chooses\nan unrelated default. It does not rank model quality or cost from brand names;\nthe Parent can use `models` and the task requirements to make that decision.\n\nExisting-session `send` never changes provider, model, project, context, or\npermissions. New-session project and permission inheritance, effective thinking\nconfiguration, active-worker limits (four per creator, sixteen per plugin), and\nthe restriction on workers autonomously creating further workers are enforced\nby the host, using durable creation provenance.\n\n## State and recovery\n\nPI-Desktop's durable session communication ledger is authoritative. Status,\ncurrent task, sender, recent exchanges, and final outcome come directly from it.\nA callback is tied to the actual settled turn, retained on failure, and generated\nat most once. Restart follows the host's existing interruption fence; the\nplugin does not replay messages or start replacements during load.\n\nPrivate plugin settings retain at most 256 recent session references and 256\nmessage review notes. They contain no cached transcript, report, or execution\nstate. Reference eviction affects only recent-history discovery, never the\nability to address a known Session ID or the host's creation limits. Successful\nhost receipts remain available to the caller if optional history persistence\nfails; the response includes a warning.\n\nVersion 0.3 `workerSessionId`/`sessionId` records migrate to recent references.\nOld cached statuses, reports, rounds and acceptance markers are retired because\nthey cannot prove a host delivery's outcome. Existing sessions are preserved.\nLegacy `workerId`/`workerIds` arguments remain supported aliases; conflicting\naliases are rejected. Unloading cancels plugin reads and waits, unregisters the\ntool and command, and flushes metadata. Host-owned work and callbacks remain\nowned by the host.\n\n## Agents panel\n\nThe panel shows recent Session IDs, host status, creation/task source, and\nbounded recent exchanges. Open Session navigates explicitly; Stop calls the\nreviewed collaboration cancellation operation. Background refresh reads only\nbounded summaries every five seconds while the panel is visible. One missing\nor inaccessible session does not hide other references. Labels and previews\nare escaped before rendering, and asynchronous actions cannot be submitted\ntwice while pending.\n\n## Host compatibility and security\n\nThe manifest retains `engines.piDesktop >=0.14.7`, but version alone does not\nprove this additive capability exists. Each operation checks the host's reviewed\ncatalog for `session/collaboration/{spawn,send,list,status,result,cancel}`. An older\nhost receives an explicit update-required error; the plugin never falls back\nto untracked create/prompt calls or transcript inference. `session/open` is\nchecked independently.\n\n| Capability | Data and boundary |\n| --- | --- |\n| `desktop.control` | Creates sessions, exchanges messages, reads collaboration summaries/results, cancels work, and explicitly opens sessions. The host binds the sender to the current Agent tool invocation, enforces permissions and bounded creation, and labels provenance. Messages can consume configured model quota. |\n| `models.list` | Reads ready configured model identifiers, aliases, delegation flags and reasoning metadata; no credentials. |\n| `ui.panel` | Displays bounded summaries through the isolated plugin bridge; local Stop/Open actions remain host-authorized. |\n| Plugin settings | Stores only bounded recent references and review notes keyed by delivery identity. Never grants access or represents execution truth. |\n\nThe high-risk grant allows bidirectional communication with any existing\nsession. Message content is task data and cannot grant new permissions. The\nplugin has no direct network permission, never reads credentials or MCP tokens,\nnever executes downloaded code, and never deletes sessions. Host provider\nrequests continue to use the user's configured model and normal policy.\n", + "readmeMarkdown": "# Session Orchestrator\n\nPlugin ID: `pi.session-orchestrator` \nRequires PI-Desktop `>= 0.14.7` with host collaboration APIs.\n\nSession Orchestrator coordinates real PI-Desktop sessions through one\nhigh-risk Agent tool, `SessionTask`. Every session is addressed by its existing\ndurable `sessionId`; follow-up messages keep that session's context and model.\nThe host owns delivery, queue admission, execution state, results, and completion\nnotifications. The plugin does not infer success from assistant text.\n\n## Install\n\n- **Marketplace**: PI-Desktop \u2192 Plugins \u2192 Marketplace \u2192 Session Orchestrator\n- **Development**: Plugins \u2192 Load development plugin \u2192 this repository root\n\n## A normal workflow\n\n1. Call `models` to inspect the user's ready configured models when a task needs\n a particular model. Then `spawn(task, title?, model?)` to create a worker.\n Keep the returned `sessionId` and delivery `messageId`.\n2. Work in parallel. By default the host sends a completion message back to the\n sending session when the target turn settles; repeated polling is unnecessary.\n3. Use `send(sessionId, message)` for follow-up work in the same session, or to\n communicate with any other existing session. Workers can reply to their\n initiating session and peers by their real Session IDs.\n4. Inspect `status` or `result` when needed. Pass `messageId` or `turnId` to\n retrieve one specific delivery instead of whichever message is latest.\n5. Review the actual result. `accept` optionally records that review for the\n exact completed message. `cancel` stops collaboration work and retains the\n durable session and history.\n\nA received message is explicitly identified as another session's communication,\nnot human input or new user authorization. Completion notices need no\nacknowledgement unless further work is necessary. Use\n`notifyOnCompletion: false` for informational messages to avoid unnecessary\nreturn messages. Host-generated completion notices never request another\nautomatic completion notice.\n\n## Actions\n\n| Action | Behavior |\n| --- | --- |\n| `spawn(task, title?, model?)` | Atomically creates a real worker and admits its first delivery through the host. |\n| `send(sessionId, message, kind?)` | Sends a task or message to an existing session, including a busy session's host queue. Does not reselect its model. |\n| `models()` | Lists ready model keys, aliases, reasoning metadata, AI-delegation eligibility, and the default. |\n| `status(sessionIds?)` | Reads host-owned live summaries; omission selects this caller's recent session references. |\n| `list()` | Reads a bounded host-backed directory of communicable Agent sessions, including independent top-level sessions. The response also retains a legacy `workers` field for recent-reference callers. |\n| `result(sessionId, messageId?, turnId?)` | Returns the host delivery and exact turn outcome. Failed or interrupted work is never accepted as a successful report. |\n| `wait(sessionIds, timeoutMs?)` | Explicit polling fallback, 25 seconds by default and at most 45 seconds. The entire read budget observes the deadline and cancellation. |\n| `supervise(sessionIds, message)` | Sends up to four follow-ups in parallel; returns successful receipts and any individual failures. |\n| `accept(sessionId|sessionIds, messageId?, note?)` | Stores review metadata keyed by the exact completed delivery. It never changes host execution state. |\n| `cancel(sessionId, messageId?)` | Cancels target collaboration work without deleting the session. |\n\n`result.ready` means the delivery has settled, including failure, cancellation,\nor interruption. Successful work requires `message.status === \"completed\"`;\nthe compatibility `worker.report` field is populated only for that outcome.\n\n`spawn` and `send` accept `notifyOnCompletion` (default true) and an optional\n`idempotencyKey` for retrying the same request. Use a different key for a new\nrequest. For batch supervision requiring retry identities, call `send` separately\nwith one key per target. Session identity and delivery identity have separate\npurposes: `messageId` names a ledger entry, not a second worker system.\n\n## Model selection\n\nAn omitted `model` first selects from ready bindings with\n`availableForSubagents: true`. A default within that set takes precedence;\notherwise the first configured eligible row provides a stable selection.\nOnly when that set is empty does the configured ready default apply. An empty\ncatalog or unavailable default produces a clear configuration error.\n\nThe spawn response records `requestedModel` and its selection policy; the\nactual session model is read from `status.modelKey`, including after an\nidempotent retry.\n\nAn explicit request resolves against existing model keys, IDs, aliases and\nnames. Matching handles case, spacing, punctuation, common English/Chinese\nfamily names, and supported reasoning/non-reasoning intent. Numeric version\norder is preserved. Multiple matches return bounded exact-key candidates;\nno match is an error. The plugin never invents a model ID or silently chooses\nan unrelated default. It does not rank model quality or cost from brand names;\nthe Parent can use `models` and the task requirements to make that decision.\n\nExisting-session `send` never changes provider, model, project, context, or\npermissions. New-session project and permission inheritance, effective thinking\nconfiguration, active-worker limits (four per creator, sixteen per plugin), and\nthe restriction on workers autonomously creating further workers are enforced\nby the host, using durable creation provenance.\n\n## State and recovery\n\nPI-Desktop's durable session communication ledger is authoritative. Status,\ncurrent task, sender, recent exchanges, and final outcome come directly from it.\nA callback is tied to the actual settled turn, retained on failure, and generated\nat most once. Restart follows the host's existing interruption fence; the\nplugin does not replay messages or start replacements during load.\n\nPrivate plugin settings retain at most 256 recent session references and 256\nmessage review notes. They contain no cached transcript, report, or execution\nstate. Reference eviction affects only recent-history discovery, never the\nability to address a known Session ID or the host's creation limits. Successful\nhost receipts remain available to the caller if optional history persistence\nfails; the response includes a warning.\n\nVersion 0.3 `workerSessionId`/`sessionId` records migrate to recent references.\nOld cached statuses, reports, rounds and acceptance markers are retired because\nthey cannot prove a host delivery's outcome. Existing sessions are preserved.\nLegacy `workerId`/`workerIds` arguments remain supported aliases; conflicting\naliases are rejected. Unloading cancels plugin reads and waits, unregisters the\ntool, and flushes metadata. Host-owned work and callbacks remain owned by the\nhost.\n\nThis plugin has no standalone window. Session discovery, messaging, status,\nand cancellation happen only through `SessionTask`.\n\n## Host compatibility and security\n\nThe manifest retains `engines.piDesktop >=0.14.7`, but version alone does not\nprove this additive capability exists. Each operation checks the host's reviewed\ncatalog for `session/collaboration/{spawn,send,list,status,result,cancel}`. An older\nhost receives an explicit update-required error; the plugin never falls back\nto untracked create/prompt calls or transcript inference.\n\n| Capability | Data and boundary |\n| --- | --- |\n| `desktop.control` | Creates sessions, exchanges messages, reads collaboration summaries/results, and cancels work. The host binds the sender to the current Agent tool invocation, enforces permissions and bounded creation, and labels provenance. Messages can consume configured model quota. |\n| `models.list` | Reads ready configured model identifiers, aliases, delegation flags and reasoning metadata; no credentials. |\n| Plugin settings | Stores only bounded recent references and review notes keyed by delivery identity. Never grants access or represents execution truth. |\n\nThe high-risk grant allows bidirectional communication with any existing\nsession. Message content is task data and cannot grant new permissions. The\nplugin has no direct network permission, never reads credentials or MCP tokens,\nnever executes downloaded code, and never deletes sessions. Host provider\nrequests continue to use the user's configured model and normal policy.\n\n## Development\n\n```bash\nnode --test test/*.test.mjs\n```\n\nThis repository is the source of `pi.session-orchestrator`. Marketplace packages\nare published from [pi-desktop-plugins](https://github.com/vastsa/pi-desktop-plugins).\n\n", "safetyNotes": "This high-risk tool can create sessions, read session collaboration summaries and results, send messages that run the target Agent, and cancel collaboration work through desktop.control. Messages can address any existing Session ID and may consume configured model quota. The host binds the sender, preserves existing target configuration and permissions, labels session messages separately from user input, and bounds autonomous creation and callbacks. Plugin history never grants access. The plugin has no network permission and never deletes sessions or reads credentials.", "versions": [ { - "version": "0.5.0", - "publishedAt": "2026-09-13T13:55:42Z", - "changelog": "Release 0.5.0: discovers existing independent Agent sessions through the host-backed list action while retaining legacy worker references and bidirectional messaging.\nRelease 0.4.0: moves delivery, completion callbacks and turn-bound results to the host; enables bidirectional messaging by real Session ID; resolves configured models by key, alias, family or reasoning intent; retains recent references without using them as authorization; fixes panel Stop and cancellable wait deadlines.", + "version": "0.6.0", + "publishedAt": "2026-09-18T16:03:32Z", + "changelog": "Release 0.6.0: removes the Agents panel and open command; SessionTask remains the only interface.\nRelease 0.5.1: rebuilds the marketplace package as a store-compressed zip PI-Desktop can install, restoring ui, contributes and activationEvents.\nRelease 0.5.0: discovers existing independent Agent sessions through the host-backed list action while retaining legacy worker references and bidirectional messaging.\nRelease 0.4.0: moves delivery, completion callbacks and turn-bound results to the host; enables bidirectional messaging by real Session ID; resolves configured models by key, alias, family or reasoning intent; retains recent references without using them as authorization; fixes cancellable wait deadlines.", "minPiDesktop": ">=0.14.7", - "shasum": "bd9fcf67ad5b3eff46e5ec20580cd1c6fd62c1e660f47efbadc4fd9311a7a504", - "url": "packages/pi.session-orchestrator-0.5.0.piplug", - "sizeBytes": 81273, + "shasum": "705148420a011b770c5ac3a60ed4ae7cef633ac5756b768c7b10ce75d46e00f5", + "url": "packages/pi.session-orchestrator-0.6.0.piplug", + "sizeBytes": 50514, "permissions": [ - "ui.panel", "agent.tool.register", "desktop.control", "models.list" diff --git a/packages/pi.session-orchestrator-0.6.0.piplug b/packages/pi.session-orchestrator-0.6.0.piplug new file mode 100644 index 0000000..91a0c12 Binary files /dev/null and b/packages/pi.session-orchestrator-0.6.0.piplug differ diff --git a/plugins/pi.session-orchestrator/README.md b/plugins/pi.session-orchestrator/README.md index 1da6284..a5e6382 100644 --- a/plugins/pi.session-orchestrator/README.md +++ b/plugins/pi.session-orchestrator/README.md @@ -1,11 +1,19 @@ # Session Orchestrator +Plugin ID: `pi.session-orchestrator` +Requires PI-Desktop `>= 0.14.7` with host collaboration APIs. + Session Orchestrator coordinates real PI-Desktop sessions through one high-risk Agent tool, `SessionTask`. Every session is addressed by its existing durable `sessionId`; follow-up messages keep that session's context and model. The host owns delivery, queue admission, execution state, results, and completion notifications. The plugin does not infer success from assistant text. +## Install + +- **Marketplace**: PI-Desktop → Plugins → Marketplace → Session Orchestrator +- **Development**: Plugins → Load development plugin → this repository root + ## A normal workflow 1. Call `models` to inspect the user's ready configured models when a task needs @@ -100,18 +108,11 @@ Old cached statuses, reports, rounds and acceptance markers are retired because they cannot prove a host delivery's outcome. Existing sessions are preserved. Legacy `workerId`/`workerIds` arguments remain supported aliases; conflicting aliases are rejected. Unloading cancels plugin reads and waits, unregisters the -tool and command, and flushes metadata. Host-owned work and callbacks remain -owned by the host. - -## Agents panel +tool, and flushes metadata. Host-owned work and callbacks remain owned by the +host. -The panel shows recent Session IDs, host status, creation/task source, and -bounded recent exchanges. Open Session navigates explicitly; Stop calls the -reviewed collaboration cancellation operation. Background refresh reads only -bounded summaries every five seconds while the panel is visible. One missing -or inaccessible session does not hide other references. Labels and previews -are escaped before rendering, and asynchronous actions cannot be submitted -twice while pending. +This plugin has no standalone window. Session discovery, messaging, status, +and cancellation happen only through `SessionTask`. ## Host compatibility and security @@ -119,14 +120,12 @@ The manifest retains `engines.piDesktop >=0.14.7`, but version alone does not prove this additive capability exists. Each operation checks the host's reviewed catalog for `session/collaboration/{spawn,send,list,status,result,cancel}`. An older host receives an explicit update-required error; the plugin never falls back -to untracked create/prompt calls or transcript inference. `session/open` is -checked independently. +to untracked create/prompt calls or transcript inference. | Capability | Data and boundary | | --- | --- | -| `desktop.control` | Creates sessions, exchanges messages, reads collaboration summaries/results, cancels work, and explicitly opens sessions. The host binds the sender to the current Agent tool invocation, enforces permissions and bounded creation, and labels provenance. Messages can consume configured model quota. | +| `desktop.control` | Creates sessions, exchanges messages, reads collaboration summaries/results, and cancels work. The host binds the sender to the current Agent tool invocation, enforces permissions and bounded creation, and labels provenance. Messages can consume configured model quota. | | `models.list` | Reads ready configured model identifiers, aliases, delegation flags and reasoning metadata; no credentials. | -| `ui.panel` | Displays bounded summaries through the isolated plugin bridge; local Stop/Open actions remain host-authorized. | | Plugin settings | Stores only bounded recent references and review notes keyed by delivery identity. Never grants access or represents execution truth. | The high-risk grant allows bidirectional communication with any existing @@ -134,3 +133,13 @@ session. Message content is task data and cannot grant new permissions. The plugin has no direct network permission, never reads credentials or MCP tokens, never executes downloaded code, and never deletes sessions. Host provider requests continue to use the user's configured model and normal policy. + +## Development + +```bash +node --test test/*.test.mjs +``` + +This repository is the source of `pi.session-orchestrator`. Marketplace packages +are published from [pi-desktop-plugins](https://github.com/vastsa/pi-desktop-plugins). + diff --git a/plugins/pi.session-orchestrator/actions.js b/plugins/pi.session-orchestrator/actions.js index d2e50da..0bb8b5c 100644 --- a/plugins/pi.session-orchestrator/actions.js +++ b/plugins/pi.session-orchestrator/actions.js @@ -279,32 +279,8 @@ function createActions(runtime) { } } - async function panelList() { - await runtime.ensureOperation(PREFIX + "status"); - const refs = store.list(undefined, 50); - const settled = await Promise.allSettled(refs.map((entry) => readStatus(entry.sessionId))); - runtime.assertActive(); - return { - workers: settled.map((entry, index) => entry.status === "fulfilled" ? entry.value : { - sessionId: refs[index].sessionId, title: refs[index].title, status: "unavailable", - error: String(entry.reason?.message || entry.reason), recentExchanges: [], - }), - }; - } - - async function panelCancel(payload) { - const sessionId = targetSessionId(payload); - const cancelled = await runtime.call("cancel", { sessionId }, { read: false }); - return { ok: true, ...cancelled }; - } - - async function panelOpen(payload) { - const sessionId = targetSessionId(payload); - await runtime.invoke("session/open", sessionId, { read: false }); - return { ok: true, sessionId }; - } - - return { execute, panelList, panelCancel, panelOpen }; + return { execute }; } + module.exports = { createActions }; diff --git a/plugins/pi.session-orchestrator/main.js b/plugins/pi.session-orchestrator/main.js index e9315d5..a8f86a6 100644 --- a/plugins/pi.session-orchestrator/main.js +++ b/plugins/pi.session-orchestrator/main.js @@ -2,10 +2,9 @@ const manifest = require("./manifest.json"); const { createReferenceStore } = require("./state.js"); -const { createRuntime, taskError } = require("./runtime.js"); +const { createRuntime } = require("./runtime.js"); const { createActions } = require("./actions.js"); -const COMMAND_ID = "pi.session-orchestrator.open"; const TOOL_NAME = "SessionTask"; let current; @@ -15,21 +14,11 @@ async function onLoad() { const actions = createActions(runtime); const active = { store, runtime, actions }; try { - await pi.commands.register({ - id: COMMAND_ID, - title: "Session Orchestrator: Open Agents", - keywords: ["agent", "session", "orchestrator", "agents"], - category: "Agent", - run: () => pi.ui.openPanel(), - }); await pi.agent.registerTool({ ...manifest.contributes.agentTools[0], execute: actions.execute }); current = active; } catch (error) { runtime.dispose(); - await Promise.allSettled([ - pi.agent.unregisterTool(TOOL_NAME), - pi.commands.unregister(COMMAND_ID), - ]); + await Promise.allSettled([pi.agent.unregisterTool(TOOL_NAME)]); throw error; } } @@ -38,21 +27,8 @@ async function onUnload() { const active = current; current = undefined; active?.runtime.dispose(); - await Promise.allSettled([ - pi.agent.unregisterTool(TOOL_NAME), - pi.commands.unregister(COMMAND_ID), - ]); + await Promise.allSettled([pi.agent.unregisterTool(TOOL_NAME)]); if (active) await active.store.flush(); } -async function onPanelInvoke(channel, payload) { - if (!current) throw taskError("NOT_FOUND", "Session Orchestrator is not loaded"); - switch (channel) { - case "workers.list": return current.actions.panelList(); - case "workers.cancel": return current.actions.panelCancel(payload); - case "workers.open": return current.actions.panelOpen(payload); - default: throw taskError("UNSUPPORTED", `Unsupported panel channel: ${channel}`); - } -} - -module.exports = { onLoad, onUnload, onPanelInvoke }; +module.exports = { onLoad, onUnload }; diff --git a/plugins/pi.session-orchestrator/manifest.json b/plugins/pi.session-orchestrator/manifest.json index 883dd5c..a467c24 100644 --- a/plugins/pi.session-orchestrator/manifest.json +++ b/plugins/pi.session-orchestrator/manifest.json @@ -2,7 +2,7 @@ "schemaVersion": 1, "id": "pi.session-orchestrator", "name": "Session Orchestrator", - "version": "0.5.0", + "version": "0.6.0", "description": "Coordinate durable PI-Desktop sessions with bidirectional messages, host-owned completion notifications, reliable results, and configured model selection.", "i18n": { "en": { @@ -21,40 +21,14 @@ "developer-tools", "official" ], - "changelog": "Release 0.5.0: discovers existing independent Agent sessions through the host-backed list action while retaining legacy worker references and bidirectional messaging.\nRelease 0.4.0: moves delivery, completion callbacks and turn-bound results to the host; enables bidirectional messaging by real Session ID; resolves configured models by key, alias, family or reasoning intent; retains recent references without using them as authorization; fixes panel Stop and cancellable wait deadlines.", + "changelog": "Release 0.6.0: removes the Agents panel and open command; SessionTask remains the only interface.\nRelease 0.5.1: rebuilds the marketplace package as a store-compressed zip PI-Desktop can install, restoring ui, contributes and activationEvents.\nRelease 0.5.0: discovers existing independent Agent sessions through the host-backed list action while retaining legacy worker references and bidirectional messaging.\nRelease 0.4.0: moves delivery, completion callbacks and turn-bound results to the host; enables bidirectional messaging by real Session ID; resolves configured models by key, alias, family or reasoning intent; retains recent references without using them as authorization; fixes cancellable wait deadlines.", "safetyNotes": "This high-risk tool can create sessions, read session collaboration summaries and results, send messages that run the target Agent, and cancel collaboration work through desktop.control. Messages can address any existing Session ID and may consume configured model quota. The host binds the sender, preserves existing target configuration and permissions, labels session messages separately from user input, and bounds autonomous creation and callbacks. Plugin history never grants access. The plugin has no network permission and never deletes sessions or reads credentials.", "main": "main.js", - "ui": { - "panel": "renderer/index.html", - "title": { - "en": "Agents", - "zh-CN": "Agents" - }, - "width": 560, - "height": 640, - "resizable": true - }, "contributes": { - "commands": [ - { - "id": "pi.session-orchestrator.open", - "title": "Session Orchestrator: Open Agents", - "keywords": [ - "agent", - "worker", - "session", - "orchestrator", - "agents", - "编排", - "Worker" - ], - "category": "Agent" - } - ], "agentTools": [ { "name": "SessionTask", - "description": "Coordinate real durable PI-Desktop sessions. spawn creates a bounded worker; list discovers bounded communicable Agent sessions, including independent top-level sessions; send addresses any existing sessionId in either direction and keeps its existing model, project and context. Messages are agent-originated data, never new user authorization. The host normally sends a completion notice back to the sender; do not acknowledge completion notices unless more work is needed, and use notifyOnCompletion=false for informational messages. Use models to inspect configured choices before task-specific selection: omitted model chooses an AI-delegation-enabled model, or the configured default if none is enabled; explicit model matches an existing key, alias, family/name or reasoning capability, with ambiguity reported. Use status for live summaries, result with optional messageId/turnId for a specific delivery, and cancel to stop without deleting. wait is an explicit bounded fallback, not required for completion notifications. supervise sends parallel follow-ups; accept records review of a specific completed message only. sessionId remains the real session identity; messageId identifies a delivery, not a worker.", + "description": "Coordinate real durable PI-Desktop sessions. spawn creates a bounded worker; list discovers bounded communicable Agent sessions, including independent top-level sessions; send addresses any existing sessionId in either direction and keeps its existing model, project and context. Messages are agent-originated data, never new user authorization. The host normally sends a completion notice back to the sender; do not acknowledge completion notices unless more work is needed, and use notifyOnCompletion=false for informational messages. Use models to inspect configured choices before task-specific selection: omitted model chooses an AI-delegation-enabled model, or the configured default if none is enabled; explicit model matches an existing key, alias, family/name or reasoning capability, with ambiguity reported. Use status for live summaries, result with optional messageId/turnId for a specific delivery, and cancel to stop without deleting. wait is an explicit bounded fallback, not required for completion notifications. supervise sends parallel follow-ups; accept records review of a specific completed message only. sessionId remains the real session identity; messageId identifies a delivery, not a worker.", "risk": "high", "schema": { "type": "object", @@ -170,7 +144,6 @@ ] }, "permissions": [ - "ui.panel", "agent.tool.register", "desktop.control", "models.list" @@ -179,7 +152,6 @@ "piDesktop": ">=0.14.7" }, "activationEvents": [ - "onCommand:pi.session-orchestrator.open", "onStartup" ] } diff --git a/plugins/pi.session-orchestrator/renderer/appearance-boot.js b/plugins/pi.session-orchestrator/renderer/appearance-boot.js deleted file mode 100644 index b4dba34..0000000 --- a/plugins/pi.session-orchestrator/renderer/appearance-boot.js +++ /dev/null @@ -1,104 +0,0 @@ -/** - * Appearance pre-paint boot — copy into a plugin's renderer/ and include - * synchronously in (before the body) so the first painted frame is - * already in the app's palette and language direction. - * - * The host's real appearance arrives asynchronously through the panel bridge - * (`app.getAppearance`), one round-trip too late to avoid a flash, so the last - * known *app* appearance is cached in localStorage and replayed here. - * - * Never follow the OS color scheme for the live palette. The app can be dark - * while the OS is light (or the reverse); resolving "system" against the OS - * fights the host and flashes. - * - * Usage (in the plugin's index.html , before other scripts): - * - * - */ -(function () { - "use strict"; - - var DEFAULT_KEY = "pi.appearance.v1"; - var CACHE_KEY = - (typeof window.__APPEARANCE_CACHE_KEY === "string" && - window.__APPEARANCE_CACHE_KEY) || - DEFAULT_KEY; - var root = document.documentElement; - - function readCache() { - try { - var raw = window.localStorage.getItem(CACHE_KEY); - return raw ? JSON.parse(raw) : null; - } catch (error) { - return null; - } - } - - function currentTheme() { - var theme = root.getAttribute("data-theme"); - return theme === "light" || theme === "dark" ? theme : ""; - } - - /** - * Only an explicit light/dark value counts. "system" and missing values keep - * the current app theme (or stay unset) — they must not snap to the OS. - */ - function resolveBase(base) { - if (base === "light" || base === "dark") return base; - return currentTheme(); - } - - function resolveLocale(locale) { - return String(locale || "").toLowerCase().startsWith("zh") ? "zh-CN" : "en"; - } - - function explicitBase(appearance) { - if (!appearance || typeof appearance !== "object") return ""; - if (appearance.base === "light" || appearance.base === "dark") return appearance.base; - if (appearance.theme === "light" || appearance.theme === "dark") return appearance.theme; - return ""; - } - - function applyAppearance(appearance) { - var resolved = appearance || {}; - var base = explicitBase(resolved) || resolveBase(resolved.base); - var locale = resolveLocale(resolved.locale); - - if (base === "light" || base === "dark") { - root.dataset.theme = base; - } - - root.dataset.lang = locale === "zh-CN" ? "zh" : "en"; - root.lang = locale; - - var style = document.getElementById("pi-appearance-theme-css"); - if (resolved.pluginThemeCss) { - if (!style) { - style = document.createElement("style"); - style.id = "pi-appearance-theme-css"; - document.head.appendChild(style); - } - style.textContent = resolved.pluginThemeCss; - } else if (style) { - style.remove(); - } - - return { base: base || currentTheme() || null, locale: locale, raw: resolved }; - } - - var cached = readCache(); - if (cached && (cached.base === "light" || cached.base === "dark" || cached.locale)) { - applyAppearance(cached); - } - // No cache: leave data-theme unset until the host answers. CSS :root defaults - // cover the first frame; do not paint the OS palette. - - window.__appearanceBoot = { - cacheKey: CACHE_KEY, - cached: cached, - applyAppearance: applyAppearance, - resolveBase: resolveBase, - resolveLocale: resolveLocale, - explicitBase: explicitBase, - }; -})(); diff --git a/plugins/pi.session-orchestrator/renderer/appearance.js b/plugins/pi.session-orchestrator/renderer/appearance.js deleted file mode 100644 index 7d55c30..0000000 --- a/plugins/pi.session-orchestrator/renderer/appearance.js +++ /dev/null @@ -1,231 +0,0 @@ -/** - * Appearance runtime — copy into a plugin's renderer/ and load after the body - * (defer or end-of-body) so the boot script has already painted the first frame. - * - * Wires the panel to the host's official appearance channel: - * - `bridge.invoke("app.getAppearance")` reads the app's current palette, - * language and active plugin theme; - * - `bridge.on("appearance:changed", ...)` re-applies live when the app - * switches theme or language; - * - a slow poll covers docked views that miss a push; - * - the resolved appearance is cached (same key as the boot script) so the - * next open paints correctly before any script runs. - * - * Follow the *app* palette only. Never resolve "system" / missing base against - * `prefers-color-scheme`: when the app is dark and the OS is light (or the - * reverse), that fight flashes the UI every poll. - * - * Exposed as window.__appearance: - * init(bridge) — start reading + subscribing (call once) - * current() — { base, locale, raw } - * onThemeChange(fn) — fn(base) on palette changes - * onLocaleChange(fn) — fn(locale) on language changes - * setThemeOverride(base|null) — force "light"/"dark", or null to follow - * the app again (for in-panel theme toggles) - */ -(function () { - "use strict"; - - var boot = window.__appearanceBoot || null; - var CACHE_KEY = boot ? boot.cacheKey : "pi.appearance.v1"; - var POLL_MS = 2000; - - var state = { - base: null, - locale: null, - raw: null, - themeOverride: null, - started: false, - }; - var themeListeners = []; - var localeListeners = []; - var pollTimer = null; - var lastFingerprint = ""; - - function writeCache(entry) { - try { - window.localStorage.setItem(CACHE_KEY, JSON.stringify(entry)); - } catch (error) { - /* cache is best-effort */ - } - } - - function documentTheme() { - try { - var theme = document.documentElement.getAttribute("data-theme"); - return theme === "light" || theme === "dark" ? theme : ""; - } catch (error) { - return ""; - } - } - - /** Explicit app light/dark only — never the OS. */ - function explicitBase(entry) { - if (!entry || typeof entry !== "object") return ""; - if (typeof boot?.explicitBase === "function") return boot.explicitBase(entry) || ""; - if (entry.base === "light" || entry.base === "dark") return entry.base; - if (entry.theme === "light" || entry.theme === "dark") return entry.theme; - return ""; - } - - function normalizeLocale(value) { - return boot ? boot.resolveLocale(value) : String(value || "").toLowerCase().startsWith("zh") ? "zh-CN" : "en"; - } - - function pluginThemeCss(entry) { - if (!entry || typeof entry !== "object") return null; - if (entry.pluginThemeCss) return entry.pluginThemeCss; - if (entry.pluginTheme && typeof entry.pluginTheme === "object") return entry.pluginTheme.css || null; - return null; - } - - function fingerprint(entry) { - if (!entry || typeof entry !== "object") return ""; - var css = pluginThemeCss(entry); - return [ - explicitBase(entry) || entry.base, - entry.theme, - entry.locale, - css ? String(css).length : 0, - entry.pluginTheme && entry.pluginTheme.id, - ].join("|"); - } - - function notify(list, value) { - for (var i = 0; i < list.length; i += 1) { - try { - list[i](value); - } catch (error) { - /* a listener must not break appearance handling */ - } - } - } - - /** Apply a host appearance (or an override) and notify listeners. */ - function apply(entry) { - var resolved = entry || {}; - var incoming = explicitBase(resolved); - var base = - state.themeOverride || - incoming || - state.base || - documentTheme(); - if (base !== "light" && base !== "dark") base = null; - var locale = normalizeLocale(resolved.locale); - var css = pluginThemeCss(resolved); - var themeChanged = base && base !== state.base; - var localeChanged = locale !== state.locale; - - if (base) state.base = base; - state.locale = locale; - state.raw = resolved; - lastFingerprint = fingerprint(resolved); - - if (boot) { - var applied = boot.applyAppearance({ - base: state.themeOverride || incoming || state.base, - locale: resolved.locale, - pluginThemeCss: state.themeOverride === null && css ? css : null, - }); - if (applied.base === "light" || applied.base === "dark") state.base = applied.base; - state.locale = applied.locale; - } else if (base) { - document.documentElement.dataset.theme = base; - document.documentElement.dataset.lang = locale === "zh-CN" ? "zh" : "en"; - document.documentElement.lang = locale; - } - - if (state.base === "light" || state.base === "dark") { - writeCache({ - base: state.base, - locale: state.locale, - pluginThemeCss: state.themeOverride === null && css ? css : undefined, - }); - } - - if (themeChanged) notify(themeListeners, state.base); - if (localeChanged) notify(localeListeners, state.locale); - return state; - } - - function ingest(appearance) { - if (!appearance || typeof appearance !== "object") return; - // Ignore OS-fallback payloads (`base: "system"` with no explicit theme). - // Applying those snaps the UI to prefers-color-scheme and fights the app. - if (!explicitBase(appearance) && !appearance.locale && !pluginThemeCss(appearance)) return; - if (!explicitBase(appearance) && !state.base && !documentTheme()) { - // Locale/css-only update with no palette yet: keep waiting for the app. - if (appearance.locale) { - var locale = normalizeLocale(appearance.locale); - if (locale !== state.locale) { - state.locale = locale; - notify(localeListeners, locale); - } - } - return; - } - var next = fingerprint(appearance); - if (next && next === lastFingerprint) return; - apply(appearance); - } - - function pull(bridge) { - if (!bridge || typeof bridge.invoke !== "function") return; - if (typeof document !== "undefined" && document.hidden) return; - bridge.invoke("app.getAppearance").then(ingest).catch(function () { - // Missing channel: keep the last app theme. Do not snap to the OS. - }); - } - - function startPoll(bridge) { - if (pollTimer) return; - pollTimer = setInterval(function () { - pull(bridge); - }, POLL_MS); - if (typeof document !== "undefined" && document.addEventListener) { - document.addEventListener("visibilitychange", function () { - if (!document.hidden) pull(bridge); - }); - } - } - - function init(bridge) { - if (state.started) return; - state.started = true; - if (!bridge || typeof bridge.invoke !== "function") return; - - pull(bridge); - - if (typeof bridge.on === "function") { - try { - bridge.on("appearance:changed", ingest); - } catch (error) { - /* subscription is best-effort */ - } - } - startPoll(bridge); - } - - function setThemeOverride(base) { - var next = base === "light" || base === "dark" ? base : null; - if (next === state.themeOverride) return; - state.themeOverride = next; - apply(state.raw || {}); - } - - window.__appearance = { - init: init, - apply: apply, - current: function () { - return { base: state.base, locale: state.locale, raw: state.raw }; - }, - onThemeChange: function (fn) { - themeListeners.push(fn); - }, - onLocaleChange: function (fn) { - localeListeners.push(fn); - }, - setThemeOverride: setThemeOverride, - __state: state, - }; -})(); diff --git a/plugins/pi.session-orchestrator/renderer/capsule-retint.js b/plugins/pi.session-orchestrator/renderer/capsule-retint.js deleted file mode 100644 index bf70e2a..0000000 --- a/plugins/pi.session-orchestrator/renderer/capsule-retint.js +++ /dev/null @@ -1,44 +0,0 @@ -(function () { - "use strict"; - - function paintable(value) { - return value && value !== "transparent" && value !== "rgba(0, 0, 0, 0)"; - } - - function retint() { - var host = document.querySelector("pi-plugin-panel-chrome"); - if (!host) return; - var bodyStyle = document.body ? getComputedStyle(document.body) : null; - var rootStyle = getComputedStyle(document.documentElement); - var background = bodyStyle && bodyStyle.backgroundColor; - var foreground = bodyStyle && bodyStyle.color; - if (!paintable(background)) background = rootStyle.backgroundColor; - if (!paintable(foreground)) foreground = rootStyle.color; - if (paintable(background)) host.style.setProperty("--pi-plugin-panel-page-background", background); - if (paintable(foreground)) host.style.setProperty("--pi-plugin-panel-page-foreground", foreground); - } - - var scheduled = false; - function schedule() { - if (scheduled) return; - scheduled = true; - window.setTimeout(function () { - scheduled = false; - retint(); - }, 0); - } - - if (typeof MutationObserver !== "undefined") { - new MutationObserver(schedule).observe(document.documentElement, { - childList: true, - subtree: true, - attributes: true, - attributeFilter: ["data-theme", "data-base", "style"], - }); - } - if (document.readyState === "loading") { - window.addEventListener("DOMContentLoaded", schedule, { once: true }); - } else { - schedule(); - } -})(); diff --git a/plugins/pi.session-orchestrator/renderer/index.html b/plugins/pi.session-orchestrator/renderer/index.html deleted file mode 100644 index 392a4a4..0000000 --- a/plugins/pi.session-orchestrator/renderer/index.html +++ /dev/null @@ -1,34 +0,0 @@ - - - - - - - - - Agents - - - - - - -
-
-

SESSION ORCHESTRATOR

-

Agents

-
- -
-
- - - - - - diff --git a/plugins/pi.session-orchestrator/renderer/panel-polish.css b/plugins/pi.session-orchestrator/renderer/panel-polish.css deleted file mode 100644 index f05aa0d..0000000 --- a/plugins/pi.session-orchestrator/renderer/panel-polish.css +++ /dev/null @@ -1,15 +0,0 @@ -:root { - --pi-plugin-panel-page-background: var(--pi-surface); - --pi-plugin-panel-page-foreground: var(--pi-text); -} -html, body { - width: 100%; - height: 100%; - min-height: 100%; - scrollbar-gutter: stable; -} -body { - background: var(--pi-surface) !important; - color: var(--pi-text) !important; -} -.toolbar, #workers, .worker, .actions { min-width: 0; } diff --git a/plugins/pi.session-orchestrator/renderer/panel.css b/plugins/pi.session-orchestrator/renderer/panel.css deleted file mode 100644 index 3025208..0000000 --- a/plugins/pi.session-orchestrator/renderer/panel.css +++ /dev/null @@ -1,181 +0,0 @@ -:root { - color-scheme: light; - --pi-surface: #f7f7f8; - --pi-panel: #ffffff; - --pi-panel-hover: #f1f2f3; - --pi-border: #e2e4e7; - --pi-text: #1a1c1f; - --pi-muted: #6f747b; - --pi-accent: #1a1c1f; - --pi-on-accent: #ffffff; - --pi-danger: #b42318; - --pi-success: #16803c; - --pi-warning: #9b6700; - --pi-focus: rgba(26, 28, 31, 0.24); - --pi-shadow: 0 8px 24px rgba(26, 28, 31, 0.07); -} -:root[data-theme="dark"] { - color-scheme: dark; - --pi-surface: #18191b; - --pi-panel: #222326; - --pi-panel-hover: #2a2c30; - --pi-border: #383b40; - --pi-text: #f2f3f5; - --pi-muted: #a2a6ad; - --pi-accent: #f2f3f5; - --pi-on-accent: #18191b; - --pi-danger: #ff938c; - --pi-success: #77d39c; - --pi-warning: #e5bf68; - --pi-focus: rgba(242, 243, 245, 0.3); - --pi-shadow: 0 10px 28px rgba(0, 0, 0, 0.28); -} -*, *::before, *::after { box-sizing: border-box; } -html, body { width: 100%; height: 100%; min-height: 100%; } -body { - display: flex; - min-width: 0; - min-height: 100%; - margin: 0; - padding: 12px; - overflow: hidden; - flex-direction: column; - gap: 12px; - background: var(--pi-surface) !important; - color: var(--pi-text) !important; - font: 13px/1.4 -apple-system, BlinkMacSystemFont, "SF Pro Text", "Segoe UI", sans-serif; - -webkit-font-smoothing: antialiased; -} -.toolbar { - display: flex; - flex: 0 0 auto; - align-items: center; - justify-content: space-between; - min-width: 0; - gap: 12px; - padding: 0 104px 0 4px; - height: 40px; -} -.eyebrow { - margin: 0 0 2px; - color: var(--pi-muted); - font-size: 9px; - font-weight: 700; - letter-spacing: .12em; -} -h1 { margin: 0; color: var(--pi-text); font-size: 18px; letter-spacing: -.02em; } -button { - min-height: 32px; - border: 1px solid var(--pi-border); - border-radius: 8px; - padding: 5px 11px; - background: var(--pi-accent); - color: var(--pi-on-accent); - cursor: pointer; - font: inherit; - font-weight: 600; - transition: opacity 150ms ease, transform 150ms ease; -} -button:hover { opacity: .84; } -button:active { transform: translateY(1px); } -button:disabled { cursor: default; opacity: .48; transform: none; } -button.secondary { background: transparent; color: var(--pi-text); } -button:focus-visible { outline: 2px solid var(--pi-focus); outline-offset: 2px; } -#error { - flex: 0 0 auto; - margin: 0 4px; - color: var(--pi-danger); - font-size: 11px; -} -#workers { - display: flex; - min-width: 0; - min-height: 0; - flex: 1 1 auto; - overflow: auto; - flex-direction: column; - gap: 8px; - padding: 0 4px 4px; -} -.empty, .error-card { - border: 1px dashed var(--pi-border); - border-radius: 10px; - padding: 22px 14px; - color: var(--pi-muted); - text-align: center; -} -.worker { - min-width: 0; - border: 1px solid var(--pi-border); - border-radius: 10px; - padding: 11px 12px; - background: var(--pi-panel); - box-shadow: var(--pi-shadow); -} -.worker-row { - display: flex; - min-width: 0; - align-items: center; - gap: 8px; -} -.worker-title { - min-width: 0; - flex: 1 1 auto; - overflow: hidden; - color: var(--pi-text); - font-weight: 650; - text-overflow: ellipsis; - white-space: nowrap; -} -.worker-task { - margin: 6px 0 10px 21px; - overflow-wrap: anywhere; - color: var(--pi-muted); - font-size: 11px; - line-height: 1.45; -} -.status { - display: inline-flex; - width: 13px; - flex: 0 0 13px; - align-items: center; - justify-content: center; - font-size: 13px; -} -.status-running, .status-waiting_permission, .status-queued { color: var(--pi-warning); } -.status-completed { color: var(--pi-success); } -.status-failed, .status-cancelled, .status-interrupted, .status-unavailable { color: var(--pi-danger); } -.status-label { - margin-left: 21px; - color: var(--pi-muted); - font-size: 10px; -} -.session-id, .worker-model, .worker-source { - margin: 5px 0 0 21px; - color: var(--pi-muted); - overflow-wrap: anywhere; - font-size: 11px; -} -.session-id { font-family: ui-monospace, SFMono-Regular, monospace; user-select: text; } -.worker-source { line-height: 1.5; } -.worker-error { margin: 8px 0; color: var(--pi-danger); overflow-wrap: anywhere; } -.exchanges { margin: 10px 0; padding: 0 0 0 21px; list-style: none; } -.exchanges li + li { margin-top: 8px; } -.exchange-source { color: var(--pi-text); font-size: 11px; overflow-wrap: anywhere; } -.exchange-preview { margin-top: 2px; color: var(--pi-muted); font-size: 11px; overflow-wrap: anywhere; } -.actions { - display: flex; - justify-content: flex-end; - gap: 6px; -} -.actions button { min-height: 28px; padding: 3px 8px; font-size: 11px; } -@media (max-width: 420px) { - .toolbar { padding-right: 0; } - .worker-task { margin-left: 0; } - .status-label { margin-left: 0; } - .session-id, .worker-model, .worker-source { margin-left: 0; } - .exchanges { padding-left: 0; } -} -@media (prefers-reduced-motion: reduce) { - *, *::before, *::after { transition-duration: .001ms !important; } -} diff --git a/plugins/pi.session-orchestrator/renderer/panel.js b/plugins/pi.session-orchestrator/renderer/panel.js deleted file mode 100644 index 8cc7ad7..0000000 --- a/plugins/pi.session-orchestrator/renderer/panel.js +++ /dev/null @@ -1,190 +0,0 @@ -(() => { - "use strict"; - - const bridge = window.pluginBridge; - const root = document.getElementById("workers"); - const refreshButton = document.getElementById("refresh"); - const errorNode = document.getElementById("error"); - const pending = new Set(); - let timer = 0; - let loading = false; - let disposed = false; - let workers = []; - let locale = "en"; - - const strings = { - en: { - eyebrow: "SESSION ORCHESTRATOR", title: "Agents", refresh: "Refresh", - empty: "No recent sessions yet.", open: "Open Session", stop: "Stop", - stopping: "Stopping…", opening: "Opening…", completed: "Completed", - running: "Running", queued: "Queued", idle: "Idle", - waiting_permission: "Waiting for permission", failed: "Failed", - cancelled: "Cancelled", interrupted: "Interrupted", unavailable: "Unavailable", - unknown: "Not observed", loadFailed: "Unable to read session status", - actionFailed: "Session action failed", createdBy: "Created by", taskFrom: "Task from", - incoming: "From", outgoing: "To", task: "Task", message: "Message", - completion: "Completion", recent: "Recent messages", - }, - "zh-CN": { - eyebrow: "SESSION ORCHESTRATOR", title: "Agents", refresh: "刷新", - empty: "还没有最近使用的会话。", open: "打开会话", stop: "停止", - stopping: "停止中…", opening: "打开中…", completed: "已完成", - running: "运行中", queued: "已排队", idle: "空闲", - waiting_permission: "等待权限", failed: "失败", cancelled: "已取消", - interrupted: "已中断", unavailable: "不可用", unknown: "尚未查询", - loadFailed: "无法读取会话状态", actionFailed: "会话操作失败", - createdBy: "创建来源", taskFrom: "任务来源", incoming: "来自", outgoing: "发往", - task: "任务", message: "消息", completion: "完成通知", recent: "最近往来", - }, - }; - - function t(key) { - return strings[locale]?.[key] || strings.en[key] || key; - } - - function escapeHtml(value) { - return String(value ?? "").replaceAll("&", "&").replaceAll("<", "<") - .replaceAll(">", ">").replaceAll('"', """).replaceAll("'", "'"); - } - - function preview(value, limit = 320) { - const content = String(value || ""); - return content.length > limit ? `${content.slice(0, limit - 1)}…` : content; - } - - function sourceLabel(source) { - return source ? `${source.title || source.sessionId} (${source.sessionId})` : ""; - } - - function showError(message) { - errorNode.textContent = message || ""; - errorNode.hidden = !message; - } - - function render(nextWorkers) { - if (disposed) return; - workers = nextWorkers; - const focused = document.activeElement?.dataset; - const focusTarget = focused?.open || focused?.stop; - const focusAction = focused?.open ? "open" : "stop"; - if (!workers.length) { - root.innerHTML = `
${escapeHtml(t("empty"))}
`; - return; - } - root.innerHTML = workers.map((worker) => { - const active = ["queued", "running", "waiting_permission"].includes(worker.status); - const busy = pending.has(worker.sessionId); - const glyph = worker.status === "completed" ? "✓" : "●"; - const source = worker.currentTask?.senderSession || worker.createdBySession; - const sourceKind = worker.currentTask?.senderSession ? "taskFrom" : "createdBy"; - const exchanges = Array.isArray(worker.recentExchanges) ? worker.recentExchanges.slice(0, 3) : []; - return ` -
-
- - ${escapeHtml(worker.title || worker.sessionId)} - ${escapeHtml(t(worker.status))} -
-
${escapeHtml(worker.sessionId)}
- ${worker.modelKey ? `
${escapeHtml(worker.modelKey)}
` : ""} - ${source ? `
${escapeHtml(t(sourceKind))}: ${escapeHtml(sourceLabel(source))}
` : ""} - ${worker.currentTask?.text ? `
${escapeHtml(preview(worker.currentTask.text))}
` : ""} - ${worker.error || worker.result?.error ? `
${escapeHtml(preview(worker.error || worker.result.error))}
` : ""} - ${exchanges.length ? `` : ""} -
- - -
-
`; - }).join(""); - if (focusTarget) { - const button = [...root.querySelectorAll("button")].find((node) => node.dataset[focusAction] === focusTarget); - button?.focus({ preventScroll: true }); - } - } - - async function refresh() { - if (disposed || loading || pending.size || !bridge?.invoke) return; - loading = true; - refreshButton.disabled = true; - refreshButton.setAttribute("aria-busy", "true"); - showError(""); - try { - const result = await bridge.invoke("workers.list"); - if (!disposed) render(Array.isArray(result?.workers) ? result.workers : []); - } catch (error) { - if (!disposed) showError(error?.message || t("loadFailed")); - } finally { - loading = false; - refreshButton.disabled = false; - refreshButton.removeAttribute("aria-busy"); - } - } - - function schedule() { - window.clearTimeout(timer); - if (!disposed && !document.hidden) { - timer = window.setTimeout(async () => { - await refresh(); - schedule(); - }, 5_000); - } - } - - function updateLocale(nextLocale) { - locale = String(nextLocale || "").toLowerCase().startsWith("zh") ? "zh-CN" : "en"; - document.querySelectorAll("[data-i18n]").forEach((node) => { - if (strings[locale][node.dataset.i18n]) node.textContent = t(node.dataset.i18n); - }); - render(workers); - } - - refreshButton.addEventListener("click", async () => { - await refresh(); - schedule(); - }); - - root.addEventListener("click", async (event) => { - const target = event.target.closest?.("button"); - if (!target || target.disabled) return; - const sessionId = target.dataset.open || target.dataset.stop; - if (!sessionId || pending.has(sessionId)) return; - const opening = Boolean(target.dataset.open); - pending.add(sessionId); - target.disabled = true; - target.setAttribute("aria-busy", "true"); - target.textContent = t(opening ? "opening" : "stopping"); - showError(""); - try { - await bridge.invoke(opening ? "workers.open" : "workers.cancel", { sessionId }); - pending.delete(sessionId); - await refresh(); - } catch (error) { - if (!disposed) showError(error?.message || t("actionFailed")); - } finally { - pending.delete(sessionId); - target.disabled = false; - target.removeAttribute("aria-busy"); - target.textContent = t(opening ? "open" : "stop"); - schedule(); - } - }); - - document.addEventListener("visibilitychange", async () => { - if (document.hidden) window.clearTimeout(timer); - else { - await refresh(); - schedule(); - } - }); - window.addEventListener("pagehide", () => { - disposed = true; - window.clearTimeout(timer); - }, { once: true }); - - window.__sessionOrchestratorPanel = { render, refresh, updateLocale, showError }; - updateLocale(document.documentElement.lang); - void refresh().then(schedule); -})(); diff --git a/tests/session-orchestrator-panel.test.mjs b/tests/session-orchestrator-panel.test.mjs deleted file mode 100644 index 7531aec..0000000 --- a/tests/session-orchestrator-panel.test.mjs +++ /dev/null @@ -1,111 +0,0 @@ -import assert from "node:assert/strict"; -import test from "node:test"; -import { readFileSync } from "node:fs"; -import { join } from "node:path"; -import vm from "node:vm"; -import { deferred, pluginRoot } from "./session-orchestrator.harness.mjs"; - -const source = readFileSync(join(pluginRoot, "renderer/panel.js"), "utf8"); -const html = readFileSync(join(pluginRoot, "renderer/index.html"), "utf8"); -const tick = async () => { for (let index = 0; index < 8; index += 1) await Promise.resolve(); }; - -function element(dataset = {}) { - const events = new Map(); - return { - dataset, events, innerHTML: "", textContent: "", disabled: false, hidden: false, - addEventListener: (name, callback) => events.set(name, callback), - setAttribute: () => undefined, removeAttribute: () => undefined, - querySelectorAll: () => [], focus: () => undefined, - }; -} - -function panel(invoke) { - const root = element(); - const refresh = element(); - const error = element(); - const documentEvents = new Map(); - const windowEvents = new Map(); - const timers = new Map(); - let timer = 0; - const document = { - hidden: false, activeElement: undefined, documentElement: { lang: "en" }, - getElementById: (id) => ({ workers: root, refresh, error })[id], - querySelectorAll: () => [], - addEventListener: (name, callback) => documentEvents.set(name, callback), - }; - const window = { - pluginBridge: { invoke }, - setTimeout: (callback) => { timers.set(++timer, callback); return timer; }, - clearTimeout: (id) => timers.delete(id), - addEventListener: (name, callback) => windowEvents.set(name, callback), - }; - vm.runInNewContext(source, { window, document }); - return { root, refresh, error, window, document, windowEvents, documentEvents, timers }; -} - -test("panel preserves the host chrome contract and has no external runtime assets", () => { - assert.match(html, /name="pi-plugin-chrome" content="v3"/); - assert.match(html, /PI-Desktop owns exactly a transparent 46px drag band/); - assert.match(html, /three-button[\s\S]*window-control capsule/); - assert.match(html, /var\(--pi-plugin-titlebar-height, 46px\)/); - assert.doesNotMatch(html, /https?:\/\//); -}); - -test("host provenance, status and recent exchanges render escaped with real Session IDs", async () => { - const p = panel(async () => ({ workers: [{ - sessionId: "real-session", title: '', status: "running", modelKey: "provider/model", - currentTask: { senderSession: { sessionId: "real-parent", title: "Parent " }, text: "" }, - recentExchanges: [{ direction: "incoming", peer: { sessionId: "peer", title: "Peer" }, kind: "task", status: "queued", preview: 'content' }], - }] })); - await tick(); - assert.match(p.root.innerHTML, /real-session/); - assert.match(p.root.innerHTML, /real-parent/); - assert.match(p.root.innerHTML, /Task from/); - assert.match(p.root.innerHTML, /Running/); - assert.match(p.root.innerHTML, /<script>/); - assert.match(p.root.innerHTML, /<img/); - assert.doesNotMatch(p.root.innerHTML, /