@dev-mainsequence/codex-plugin 0.7.9 → 0.7.11

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "mainsequence",
3
- "version": "0.7.9",
3
+ "version": "0.7.11",
4
4
  "description": "Message MainSequence agents in Codex chat, recover saved replies, discover project MCPs and run developer commands using your shared session.",
5
5
  "author": {
6
6
  "name": "MainSequence"
package/README.md CHANGED
@@ -1,182 +1,176 @@
1
1
  # MainSequence for Codex
2
2
 
3
- This repository is the **MainSequence Codex plugin**. Its production code lives in
4
- `src/`, its Codex manifest in `manifests/codex/`, and its installable bundle in `codex/`.
5
-
6
- The **0.7.1 development build** follows Claude's MainSequence panel layout using
7
- Codex theme tokens: Home, Code, Projects, Agents, Tools, Activity and Settings.
8
- It includes repository setup and maintenance, project setup and links, deployment
9
- history/stages/logs, shared account/settings controls, direct messages and agent tasks.
10
- Replies and task results use native MCP tools and remain in Codex conversation context.
11
-
12
- Ask Codex to **open MainSequence**. Shared devtools **0.3.5** owns authentication,
13
- account binding, domain APIs, local project operations, authenticated assets and durable
14
- delivery. Claude's repository and dependency remain unchanged. See the
15
- [capability matrix and verification](docs/agents-tools-panel.md), including the stock
16
- Codex host differences. No probe or custom Codex build is shipped.
17
-
18
- Choose **Talk**, select **Message** or **Task** when the agent supports both, write a
19
- message, then **Send in Codex**. The reply returns through a native
20
- MCP tool and stays in chat context. After Stop, use **Messages and recovery → Recover in
21
- Codex** on the original request. Recovery reuses saved work; an unknown remote outcome
22
- blocks resend. Browsing or selecting an example never sends a message automatically.
23
-
24
- The plugin also registers the `ms` slash-menu entry for explicit developer commands and
25
- the `mainsequence-mcp` skill for conversational project MCP access. The independent
26
- Claude application is unchanged.
27
-
28
- Version 0.3.1 added saved MCP names (see [the release notes](docs/codex-verification.md)). Version 0.3.0 was the first
29
- release from this repository. Until devtools 0.2.8 the plugin was built and published from
30
- [mainsequence-devtools](https://github.com/mainsequence-sdk/mainsequence-devtools) under the
31
- name `mainsequence-devtools` ([ADR 0003 there](https://github.com/mainsequence-sdk/mainsequence-devtools/blob/main/docs/adr/0003-codex-plugin-extraction.md)).
32
- The shared implementation is still that package; this repository owns the hook adapter, the
33
- presentation layer, the manifests, skills and icons, the host checks and the releases.
3
+ MainSequence for Codex puts your MainSequence account inside Codex: sign in once, set up
4
+ and maintain your repositories and projects, message platform agents from chat, connect
5
+ project MCPs and call their tools, and follow deployments. It is the Codex plugin
6
+ `mainsequence@mainsequence`, published as the npm package `@dev-mainsequence/codex-plugin`.
7
+
8
+ It is built on the shared [MainSequence developer tooling](https://github.com/mainsequence-sdk/mainsequence-devtools),
9
+ which also powers the [VS Code extension](https://github.com/mainsequence-sdk/ms-vscode-extension)
10
+ and the [Claude Desktop plugin](https://github.com/mainsequence-sdk/mainsequence-claude-desktop).
11
+ One sign-in on your machine serves all three and the `mainsequence` and
12
+ `command-center-sdk` command lines.
34
13
 
35
14
  ## Install
36
15
 
37
- The marketplace stays where people added it, in the devtools repository:
16
+ You need the Codex desktop app or the Codex CLI, Node 22 or newer on your `PATH`, and a
17
+ MainSequence account. Repository setup needs Git and SSH; Python repositories need `uv`,
18
+ Vite + React repositories need npm; local image builds need Docker with buildx. The plugin
19
+ runs on macOS and Linux. On Windows the shared sign-in cannot be kept between sessions, so
20
+ the plugin is not supported there yet.
38
21
 
39
22
  ```sh
40
- codex plugin marketplace add mainsequence-sdk/mainsequence-devtools
23
+ codex plugin marketplace add mainsequence-sdk/mainsequence-codex-plugin
41
24
  codex plugin add mainsequence@mainsequence
42
25
  ```
43
26
 
44
- Codex installs the published npm package `@dev-mainsequence/codex-plugin` without running
45
- lifecycle scripts; the archive already contains the compiled runner and the pinned devtools
46
- package. Node 22 or newer is required. Review and trust the plugin's hook before use
47
- (`/hooks` in the CLI, or the hook prompt in the desktop app): installation does not trust
48
- hooks, and an untrusted hook cannot intercept `/ms`. Then start a new chat, or toggle
49
- MainSequence off and on in the desktop Plugins settings to hot-reload an existing chat.
27
+ Codex installs the published npm package without running lifecycle scripts. The archive
28
+ contains the compiled plugin and the exact MainSequence developer tooling it was built
29
+ with, so nothing else is downloaded or built. Then:
50
30
 
51
- ### Upgrading from `mainsequence-devtools`
31
+ 1. Review and trust the plugin's hook: `/hooks` in the CLI, or the hook prompt in the
32
+ desktop app. Installation does not trust hooks, and `/ms` commands do not run until
33
+ the hook is trusted.
34
+ 2. Start a new chat, or toggle MainSequence off and on in the desktop Plugins settings to
35
+ reload an existing chat.
52
36
 
53
- The plugin was renamed, and Codex identifies a plugin by its marketplace entry name, so the
54
- installed `mainsequence-devtools@mainsequence` is not updated in place. Refresh the
55
- marketplace, install the new entry, trust its hook, then remove the old plugin so that only
56
- one hook answers `/ms`:
37
+ To update, refresh the marketplace and reinstall the entry, then start a new chat:
57
38
 
58
39
  ```sh
59
40
  codex plugin marketplace upgrade mainsequence
60
41
  codex plugin add mainsequence@mainsequence
61
- codex plugin remove mainsequence-devtools@mainsequence
62
42
  ```
63
43
 
64
- Your MainSequence login (the shared machine session) and saved MCP connections
65
- (`mcp-connections.json` beside the CLI configuration) are not stored by the plugin, so
66
- nothing needs migrating. The exact behaviour of the installed host during this change is
67
- recorded in [the verification document](docs/codex-verification.md).
44
+ If you added the marketplace from the devtools repository (before 2026-10-24), replace it
45
+ once. The plugin keeps its name, and your sign-in and saved MCP connections are not stored
46
+ by the plugin, so nothing else changes:
47
+
48
+ ```sh
49
+ codex plugin marketplace remove mainsequence
50
+ codex plugin marketplace add mainsequence-sdk/mainsequence-codex-plugin
51
+ codex plugin add mainsequence@mainsequence
52
+ ```
68
53
 
69
- ### A local build
54
+ ## Sign in and choose an environment
55
+
56
+ Ask Codex to **open MainSequence**. The panel has **Home, Code, Projects, Agents, Tools,
57
+ Activity** and **Settings**.
58
+
59
+ 1. In **Settings**, choose **Sign in with browser**. The hosted MainSequence login opens in
60
+ your browser; account creation is part of that flow. The session is saved in your
61
+ operating system's credential store (the login Keychain on macOS, Secret Service on
62
+ Linux), where the VS Code extension, the Claude Desktop plugin and the MainSequence
63
+ command lines find it too. **Check connection**, **Renew session** and **Sign out** are
64
+ in the same place.
65
+ 2. The backend defaults to `https://api.main-sequence.app`. If your organization runs its
66
+ own MainSequence deployment, set its API URL as the backend in Settings before signing
67
+ in. Set the **Command Center URL** there as well, so project and repository links open
68
+ in Command Center.
69
+ 3. Pick an **organization environment** in the panel header. Agents, MCPs, projects and
70
+ deployments are shown for that environment, and MainSequence tools Codex calls are
71
+ bound to it.
72
+
73
+ From chat, `/ms login --backend https://api.main-sequence.app` does the same sign-in, and
74
+ `/ms auth-status --check` verifies it.
75
+
76
+ ## What you can do
77
+
78
+ - **Agents.** Browse the environment's agents with their descriptions, skills and example
79
+ questions. **Talk** prepares a conversation without sending anything. Choose **Message**
80
+ or **Task** when the agent supports both, write your message and **Send in Codex**: the
81
+ agent's reply comes back through a MainSequence tool and stays in the chat's context.
82
+ If you stop Codex mid-send, **Messages and recovery → Recover in Codex** delivers the
83
+ same request once more without sending it twice; a request with an unknown outcome is
84
+ blocked rather than resent. Nothing is ever sent automatically.
85
+ - **Tools.** Connected project MCPs of the environment come first; **Add MCP** searches
86
+ the environment's releases. Every tool of a connected MCP is registered in Codex as
87
+ `ms_<tool>_<hash>` with the project's own schema, so "list my CRM contacts" is one
88
+ ordinary tool call. You can also just ask: "Which internal MCPs are available?", "Add
89
+ metatable-mcp", "Use it to list my tables." See
90
+ [conversational project MCP connections](docs/conversational-mcp.md). A project MCP that
91
+ has scaled to zero can take over a minute to answer its first call; the plugin waits.
92
+ - **Code.** Your account's repositories and the local copies under your repositories
93
+ folder. **Set up** clones a repository into `<folder>/<organization>/projects/<name>-<uid>`
94
+ with its deploy key and writes an endpoint-only `.env`, optionally installing
95
+ dependencies. A local repository offers status, dependency sync and install, SDK update,
96
+ instructions and skills updates, legacy credential cleanup, a Docker build preview, a
97
+ devcontainer preview, and a confirmed delete.
98
+ - **Projects.** The environment's MainSequence projects with their repository branches.
99
+ **Set up project locally** prepares every member branch in its own checkout and hands
100
+ the workspace to Codex, with a project `AGENTS.md`.
101
+ - **Deployments.** The latest run of a repository branch, its stages and logs, and the
102
+ environment's deployment history. Read-only.
103
+ - **Activity.** Running and finished operations with progress, results and cancellation.
104
+ - **`/ms` commands.** In Codex desktop, type `/ms`, select **ms** from the slash menu,
105
+ append the arguments and submit, for example `/ms --help`, `/ms status`, `/ms sync`. The
106
+ plugin's hook runs the command before the model sees it and shows the result; `--json`
107
+ returns the structured result. Ordinary prompts pass through untouched. The Codex CLI
108
+ rejects custom `/ms` input, so use the bundled command line there:
109
+ `npx -p @dev-mainsequence/codex-plugin ms-devtools --help`.
110
+
111
+ [Capabilities and host differences](docs/agents-tools-panel.md) lists every surface,
112
+ what stock Codex can and cannot expose, and which MainSequence operation each plugin
113
+ tool uses.
114
+
115
+ ## Troubleshooting
116
+
117
+ - **`/ms` reaches the model or answers "the hook must be enabled and trusted".** Trust the
118
+ hook (`/hooks`), then start a new chat.
119
+ - **The panel says you are not signed in.** Use **Settings → Sign in with browser**, or
120
+ `/ms login --backend <url>`. One sign-in per backend is kept on the machine.
121
+ - **A newly connected MCP's tools are missing.** Tools connected in the current chat are
122
+ registered for the next chat; until then Codex uses them through the plugin's own
123
+ `mainsequence_tools` / `mainsequence_call_tool`.
124
+ - **A project tool times out.** Cold project MCPs are slow on their first call; the plugin
125
+ allows up to five minutes. Check the project's deployment in **Deployments**.
126
+ - **Codex cannot start the plugin.** Node 22 or newer must be on the `PATH` Codex uses.
127
+ **Settings** shows runtime diagnostics.
128
+
129
+ ## Development
130
+
131
+ - `src/` is the plugin: `runner.ts` (the `/ms` hook and the `ms-devtools` command line),
132
+ `hosts/codex.ts` and `hosts/codexPresentation.ts` (command parsing and output), and the
133
+ `panel-server` with the MainSequence panel and the `mainsequence_*` tools.
134
+ - `manifests/codex/` holds the Codex manifest, hooks, skills and icons;
135
+ `scripts/build-codex.cjs` compiles `src/` and writes the installable artifact to `codex/`,
136
+ with every needed devtools module vendored, and mirrors it to the package root for
137
+ `npm pack`. An installed plugin needs neither this checkout nor an npm download.
138
+ - `.agents/plugins/marketplace.json` is the public marketplace Codex reads from this
139
+ repository. It names the last published npm version.
140
+ - `tests/dependency-boundary.test.cjs` forbids private devtools imports; shared behaviour
141
+ comes only from the exact `@dev-mainsequence/devtools` package API.
70
142
 
71
143
  ```sh
144
+ npm ci --ignore-scripts
72
145
  npm run build
73
146
  codex plugin marketplace add /absolute/path/to/mainsequence-codex-plugin/artifacts/local-marketplace
74
147
  codex plugin add mainsequence@mainsequence-local
75
148
  ```
76
149
 
77
- The local catalog loads the built artifact in `codex/`; the installed plugin runs from
78
- Codex's cache. `npm run pack:codex` also writes a standalone archive of that artifact.
79
-
80
- ## Use
81
-
82
- In Codex desktop, type `/ms` and **select `ms` from the slash menu**, then append the
83
- arguments and submit:
84
-
85
- ```text
86
- /ms --help
87
- /ms login --backend https://api.main-sequence.app
88
- /ms status
89
- /ms sync
90
- ```
91
-
92
- Selection inserts a native skill mention. The hook accepts that mention only when it
93
- references this plugin's own `skills/ms/SKILL.md`, parses the single-line arguments as data
94
- without any shell, and blocks the turn with a readable result on success and failure, so a
95
- handled command never reaches the model. `/ms --help --all` lists every command and
96
- `/ms COMMAND --help` its options; `--json` exports the structured result. Ordinary prompts
97
- pass through. Codex CLI rejects custom `/ms` input before submission; use `ms-devtools` in
98
- a terminal there (see below).
99
-
100
- For project MCPs, ask in conversation: "Which internal MCPs are available?", "Add
101
- metatable-mcp", "Use it to list my tables." See
102
- [conversational project MCP connections](docs/conversational-mcp.md) and
103
- [ADR 0002](docs/adr/0002-conversational-project-mcp-client.md).
104
-
105
- Every saved project MCP's tools are also registered directly, as the Claude Desktop
106
- plugin registers them: `ms_<tool>_<hash>` (for example `ms_crm_contacts_list_…`), each
107
- with the project's own schema and bound to one saved connection and environment. Codex
108
- calls them like any other tool, with no `contextId` or environment selection. The list
109
- comes from devtools' shared project-tool catalog (`project-tool-catalog.json` beside the
110
- shared CLI state, the same names, schemas and results as the Claude Desktop plugin; devtools
111
- ADR 0004) and is refreshed in the background; a connection saved in the current chat appears
112
- in the next chat, and is usable at once through `mainsequence_tools` /
113
- `mainsequence_call_tool`. A cold project MCP can take over a minute to answer: devtools gives
114
- project requests 120 s and honours a tool's advertised timeout up to 300 s, and the plugin's
115
- server allows Codex tool calls 300 seconds (`tool_timeout_sec` in `.mcp.json`) instead of
116
- the host default of 60.
117
-
118
- ## Architecture and development
119
-
120
- Shared functionality comes from the exact `@dev-mainsequence/devtools` package API. The
121
- build (`scripts/build-codex.cjs`) compiles `src/` to `build/`, then writes the installable
122
- artifact to `codex/`: the manifest, hooks, skills and icons from `manifests/codex`, the
123
- runner and hook adapter with every reachable devtools `dist` module vendored into
124
- `dist/vendor/devtools`, and `build-info.json` with the pinned devtools version and
125
- registry integrity. Codex's npm plugin source reads the manifest from the package root, so
126
- the build mirrors `.codex-plugin`, `hooks`, `skills`, `assets`, `build-info.json` and the
127
- bundled `dist` there for `npm pack`; those mirrors are ignored by Git. An installed plugin
128
- needs neither this checkout, nor `node_modules`, nor an npm download at execution time.
129
-
130
- `src/runner.ts` is the plugin's entry point: `dist/runner.js --codex-hook` serves the
131
- hook, and any other invocation is the plain `ms-devtools` terminal CLI, so this package's
132
- `bin` keeps working in a shell (`npx -p @dev-mainsequence/codex-plugin ms-devtools --help`).
133
- `src/hosts/codex.ts` recognises `/ms` and the linked skill mention and dispatches through
134
- devtools' exported parser and tokenizer; `src/hosts/codexPresentation.ts` renders help and
135
- results. `tests/dependency-boundary.test.cjs` forbids private devtools imports.
136
-
137
- ### Dependencies
138
-
139
150
  `npm ci --ignore-scripts` installs the pinned devtools from the registry with the integrity
140
- recorded in `package-lock.json`. While that version is not published yet, run
141
-
142
- ```sh
143
- node scripts/install-devtools.cjs /path/to/dev-mainsequence-devtools-0.3.2.tgz
144
- ```
145
-
146
- with the archive from `npm pack` of the devtools release branch, or without an argument to
147
- build it from the devtools repository at `DEVTOOLS_SOURCE_REF` (default `v<pinned version>`).
148
- Either way the archive must match the lockfile integrity, and the registry pin is not
149
- changed (`scripts/use-local-devtools.cjs`). CI does the same until the version is
150
- published. After publication, confirm that `npm view @dev-mainsequence/devtools@0.3.2
151
- dist.integrity` equals the lockfile entry before tagging a release; if the registry archive
152
- differs, update the lockfile and rebuild so `build-info.json` records the published
153
- integrity.
154
-
155
- For a discovery upgrade, use the exact devtools archive while it is unpublished;
156
- with build dependencies already installed,
157
- `node scripts/use-local-devtools.cjs /path/to/dev-mainsequence-devtools-0.3.2.tgz` verifies
158
- its integrity and stages it without changing the registry pin. Publish devtools first,
159
- verify its registry integrity, then release the Codex plugin. No desktop activation
160
- or authenticated live-service acceptance is implied by a successful local build.
161
-
162
- ### Verification and release
163
-
164
- ```sh
165
- npm run release:check
166
- ```
167
-
168
- type-checks, builds, runs the Node tests (hook, presentation, packaging and dependency
169
- boundary), checks release metadata, packs the standalone artifact and the npm archive, and
170
- verifies the npm archive as Codex installs it. With a built plugin installed and trusted in
171
- a Codex host, `npm run verify:codex` runs real app-server turns through a counted offline
172
- model fixture, and `npm run verify:codex:reload` checks the existing-chat reload behaviour
173
- in a temporary Codex home. Set `MAINSEQUENCE_CODEX_BINARY` when the Codex CLI is not on
174
- `PATH`, `CODEX_HOME` to check an isolated home, and `MAINSEQUENCE_CODEX_PLUGIN_ID` for a
175
- plugin installed from another marketplace. Results are recorded in
176
- [docs/codex-verification.md](docs/codex-verification.md).
177
-
178
- A `v<version>` tag runs the same checks from the published devtools archive and publishes
179
- the verified npm archive with provenance through npm trusted publishing (repository
180
- `mainsequence-sdk/mainsequence-codex-plugin`, workflow `release.yml`, environment `npm`).
181
- Published versions are immutable. After publishing, point the marketplace entry in the
182
- devtools repository at the new version.
151
+ in `package-lock.json`. For a devtools version that is not published yet,
152
+ `node scripts/install-devtools.cjs /path/to/dev-mainsequence-devtools-<version>.tgz` stages
153
+ that archive without changing the registry pin; CI does the same. Before tagging a release,
154
+ confirm `npm view @dev-mainsequence/devtools@<pinned version> dist.integrity` equals the
155
+ lockfile entry.
156
+
157
+ `npm run release:check` type-checks, builds, runs the tests, checks the release metadata,
158
+ the marketplace and the documentation, packs the standalone artifact and the npm archive,
159
+ and verifies the npm archive as Codex installs it. With the built plugin installed and
160
+ trusted in a Codex host, `npm run verify:codex` runs real app-server turns against a
161
+ counted offline model fixture and `npm run verify:codex:reload` checks reloading in an
162
+ existing chat; set `MAINSEQUENCE_CODEX_BINARY` when the Codex CLI is not on `PATH`,
163
+ `CODEX_HOME` for an isolated home and `MAINSEQUENCE_CODEX_PLUGIN_ID` for a plugin installed
164
+ from another marketplace. `docs/local-development-backend.md` describes pointing a
165
+ workstation at a development backend.
166
+
167
+ To release: set the new version in `package.json`, `package-lock.json` and
168
+ `manifests/codex/.codex-plugin/plugin.json`, run `npm run release:check`, merge, and push
169
+ the tag `v<version>`. The release workflow repeats the checks from the published devtools
170
+ archive and publishes the verified npm archive with provenance through npm trusted
171
+ publishing. Published versions are immutable. Write the release notes on the GitHub
172
+ release of that tag. After publishing, read the archive's integrity from the registry
173
+ (`npm view @dev-mainsequence/codex-plugin@<version> dist.integrity`; a local `npm pack` has
174
+ another), then advance `.agents/plugins/marketplace.json` to the published version. The
175
+ marketplace stays on the last published version while a release is being prepared;
176
+ `node scripts/check-marketplace.cjs` enforces that it never advertises an unpublished one.
package/build-info.json CHANGED
@@ -1,5 +1,5 @@
1
1
  {
2
- "version": "0.7.9",
2
+ "version": "0.7.11",
3
3
  "node": ">=22",
4
4
  "host": "codex",
5
5
  "devtools": {
@@ -38,6 +38,30 @@ let preparationGeneration=0, recipientKinds=[];
38
38
  let recipient = null, draftRequest = null, messageBusy = false, deliveryGeneration = 0, savedRequests = [];
39
39
  let sequence = 0, generation = 0, connected = false, busy = false, snapshot = null, catalog = 'agents', offset = 0, currentPage = null, addingTools = false;
40
40
  const assetCache = new Map(), toolCache = new Map(), activity = [];
41
+ // Agent icons change in Command Center without any event reaching the panel. While the Agents list or an
42
+ // agent's details stay on screen, the shown icons are re-read once a minute (the shared transport keeps a
43
+ // directory read for 30 s, so this is a fresh read without invalidating other panels' caches) and only a
44
+ // changed icon is redrawn, in place. The timer ends with the screen and pauses after 240 re-checks.
45
+ const ICON_RECHECK_MS = 60000, ICON_RECHECK_LIMIT = 240, drawnIcons = new Map();
46
+ let iconWatch = null, iconChecks = 0;
47
+ function watchIcons(run, load) {
48
+ clearTimeout(iconWatch);
49
+ iconWatch = setTimeout(async () => {
50
+ if (!connected || run !== generation || iconChecks >= ICON_RECHECK_LIMIT) return;
51
+ iconChecks++;
52
+ try {
53
+ const current = await load();
54
+ if (run !== generation) return;
55
+ for (const { uid, icon: next } of current) {
56
+ if (JSON.stringify(next ?? null) === JSON.stringify(assetCache.get(uid) ?? null)) continue;
57
+ if (next) assetCache.set(uid, next); else assetCache.delete(uid);
58
+ const drawn = drawnIcons.get(uid) || new Set();
59
+ for (const entry of [...drawn]) { drawn.delete(entry); entry.el.replaceWith(icon(uid, entry.large)); }
60
+ }
61
+ } catch { /* A failed re-check keeps the drawn icons; the next one tries again. */ }
62
+ if (run === generation) watchIcons(run, load);
63
+ }, ICON_RECHECK_MS);
64
+ }
41
65
  const environmentKey = state => state?.account?.checked ? 'mainsequence.environment.' + JSON.stringify([state.account.backend,state.account.username]) : null;
42
66
  function rememberedEnvironment(state) {try {const key=environmentKey(state);return key ? localStorage.getItem(key) : null;} catch {return null;}}
43
67
  function rememberEnvironment() {try {const key=environmentKey(snapshot),uid=$('environment').value;if(key && snapshot.environments.some(e=>e.uid===uid))localStorage.setItem(key,uid);} catch {}}
@@ -64,7 +88,7 @@ function empty(title, detail) { return group('empty', node('h2', title), caption
64
88
  function facts(values) { const dl = node('dl', undefined, 'facts'); for (const [k, v] of values) if (v) dl.append(node('dt', k), node('dd', v)); return dl; }
65
89
  function details(label, content) { const d = node('details'); d.append(node('summary', label), content); return d; }
66
90
  function controls() { const ready = connected && snapshot?.contextId && $('environment').value; $('refresh').disabled = !connected || busy; $('environment').disabled = !snapshot?.contextId; $('search').disabled = !ready; $('search-button').disabled = !ready || busy; $('previous').disabled = busy || offset === 0; $('next').disabled = busy || !currentPage || currentPage.semantic || offset + currentPage.results.length >= currentPage.count || !currentPage.results.length; $('directory').setAttribute('aria-busy', String(busy)); for (const id of ['send-message', 'draft-message']) $(id).disabled = messageBusy || !recipient || !ready; $('message').disabled = messageBusy; $('response-kind').disabled = messageBusy || !recipient || recipientKinds.length < 2 || Boolean(recipient.taskRequestId); $('refresh-deliveries').disabled = !connected || !snapshot?.messagingAvailable; }
67
- function clearResults() { $('code-filters').hidden=catalog!=='code';$('search-button').hidden=catalog==='code'; $('results').replaceChildren(); $('pager').hidden = true; currentPage = null; $('confirmation').replaceChildren(); }
91
+ function clearResults() { clearTimeout(iconWatch); drawnIcons.clear(); $('code-filters').hidden=catalog!=='code';$('search-button').hidden=catalog==='code'; $('results').replaceChildren(); $('pager').hidden = true; currentPage = null; $('confirmation').replaceChildren(); }
68
92
  function clearComposer() { ++preparationGeneration; recipient = null; recipientKinds=[]; draftRequest = null; $('response-kind-field').hidden=true; $('composer').hidden = true; $('message').value = ''; }
69
93
  function errorText(error) { return [error.result && error.code ? error.code + ': ' + error.message : error.message, error.effects?.message, error.result ? JSON.stringify(error.result.structuredContent || error.result.content || error.result, null, 2) : ''].filter(Boolean).join('\n'); }
70
94
  function failure(error) { $('notice').textContent = errorText(error); if (['CONTEXT_CHANGED', 'AUTH_REJECTED', 'NOT_AUTHENTICATED'].includes(error.code)) { snapshot = null; workspaceContext=null;operationCache=[];clearComposer(); ++deliveryGeneration; savedRequests = []; assetCache.clear(); $('deliveries').hidden = true; $('delivery-results').replaceChildren(); $('environment').value = ''; clearResults(); accountIndicator('Session needs refreshing','error'); } }
@@ -75,7 +99,8 @@ async function view(kind, args = {}, target = selection()) { return call('mainse
75
99
  function stateLabel(request) { if(request.task) return 'Task · '+(request.task.status || (request.task.terminal ? 'finished' : 'accepted')); return { pending: 'Waiting for Codex', preparing: 'Preparing agent', sending: 'Waiting for reply', delivered: 'Reply received', failed: 'Could not send', uncertain: 'Delivery unknown · resend blocked', cancelled: 'Cancelled', expired: 'Expired' }[request.state] || request.state; }
76
100
  function latestAgent(uid, env) { return [...savedRequests].reverse().find(r => r.agentUid === uid && r.environmentUid === env); }
77
101
  function defaults(agent) { return [agent.defaults?.provider, agent.defaults?.model, agent.defaults?.thinking].filter(Boolean).join(' · '); }
78
- function icon(uid, large = false) { const asset = assetCache.get(uid), reduced = typeof matchMedia === 'function' && matchMedia('(prefers-reduced-motion: reduce)').matches; const source = asset && (reduced ? asset.stillSource : asset.source); if (source && /^data:image\/(png|webp|svg\+xml);base64,[A-Za-z0-9+/=]+$/.test(source)) { const e = node(asset.rendering === 'mask' ? 'span' : 'img', undefined, 'agent-icon' + (large ? ' large' : '') + (asset.rendering === 'mask' ? ' mask' : '')); if (asset.rendering === 'mask') e.style.setProperty('--agent-image', 'url("' + source + '")'); else { e.src = source; e.alt = ''; } e.style.imageRendering = asset.imageRendering; e.setAttribute('aria-hidden', 'true'); return e; } const e = node('span', '◇', 'agent-icon agent-placeholder' + (large ? ' large' : '')); e.setAttribute('aria-hidden', 'true'); return e; }
102
+ function icon(uid, large = false) { const e = drawIcon(uid, large); if (!drawnIcons.has(uid)) drawnIcons.set(uid, new Set()); drawnIcons.get(uid).add({ el: e, large }); return e; }
103
+ function drawIcon(uid, large = false) { const asset = assetCache.get(uid), reduced = typeof matchMedia === 'function' && matchMedia('(prefers-reduced-motion: reduce)').matches; const source = asset && (reduced ? asset.stillSource : asset.source); if (source && /^data:image\/(png|webp|svg\+xml);base64,[A-Za-z0-9+/=]+$/.test(source)) { const e = node(asset.rendering === 'mask' ? 'span' : 'img', undefined, 'agent-icon' + (large ? ' large' : '') + (asset.rendering === 'mask' ? ' mask' : '')); if (asset.rendering === 'mask') e.style.setProperty('--agent-image', 'url("' + source + '")'); else { e.src = source; e.alt = ''; } e.style.imageRendering = asset.imageRendering; e.setAttribute('aria-hidden', 'true'); return e; } const e = node('span', '◇', 'agent-icon agent-placeholder' + (large ? ' large' : '')); e.setAttribute('aria-hidden', 'true'); return e; }
79
104
  // A small Markdown subset; every content node is text. Remote HTML and links never execute.
80
105
  function markdown(value) { const root = node('div', undefined, 'markdown'); let list; for (const line of String(value || '').split('\n')) { if (/^[-*] /.test(line)) { if (!list) { list = node('ul'); root.append(list); } list.append(node('li', line.slice(2))); } else { list = null; root.append(node(/^#{1,6} /.test(line) ? 'h3' : 'p', line.replace(/^#{1,6} /, ''))); } } return root; }
81
106
  function responseKinds(kinds, selected) {
@@ -125,14 +150,14 @@ async function refresh() {
125
150
  function pageControls(page) { currentPage = page; $('pager').hidden = page.count <= 20; $('page').textContent = page.semantic ? `${page.results.length} ranked results` : `${page.results.length ? offset + 1 : 0}–${offset + page.results.length} of ${page.count}`; }
126
151
  function back(label, fn) { return button('← ' + label, fn, 'plain muted'); }
127
152
  async function detail(load, draw) { const run = ++generation; busy = true; clearResults(); $('list-toolbar').hidden = true; $('notice').textContent = 'Loading…'; controls(); try { const data = await load(); if (run !== generation) return; $('notice').textContent = ''; draw(data, run); } catch (error) { if (run === generation) { failure(error); append($('results'),back(catalog[0].toUpperCase() + catalog.slice(1), () => discover(offset))); } } finally { if (run === generation) { busy = false; controls(); } } }
128
- async function agentDetail(agent, target, run) { if (run !== generation) return; return detail(() => view('agent', { agentUid: agent.uid }, target), ({ agent: full }, detailRun) => { if(full.icon)assetCache.set(full.uid,full.icon);else assetCache.delete(full.uid); const output = $('results'); append(output,back('Agents', () => discover(offset)), group('detail-heading row', icon(full.uid, true), group('grow', node('h2', full.name), caption(defaults(full))), talkButton(full, target)), facts([['Environment', environmentName()], ['Branch', full.repositoryBranch], ['Deployment', full.runtimeState === 'unknown' ? 'Status unknown' : full.runtimeState]]), markdown(full.description), button('New conversation…',()=>review('New conversation with '+full.name,'Start a fresh agent session. Existing replies and saved requests remain in this Codex chat.',generation,async()=>{conversationKeys.set(full.uid,crypto.randomUUID());draftRequest=null;talk(full,target);})), node('h3','Skills')); for (const skill of full.skills || []) { const section = group('detail-section', node('h3', skill.name), markdown(skill.description)); for (const example of skill.examples || []) { section.append(button(example, () => { if (detailRun !== generation) return; talk(full, target); $('message').value = example; $('message').focus(); }, 'example')); } append(output,section); } append(output,details('Technical details', facts([['Agent ID', full.uid], ['Response types', (full.responseKinds || []).join(', ')], ['Runtime release', full.runtimeReleaseUid]]))); }); }
153
+ async function agentDetail(agent, target, run) { if (run !== generation) return; return detail(() => view('agent', { agentUid: agent.uid }, target), ({ agent: full }, detailRun) => { if(full.icon)assetCache.set(full.uid,full.icon);else assetCache.delete(full.uid); iconChecks = 0; watchIcons(detailRun, async () => { const again = (await view('agent', { agentUid: full.uid }, target)).agent; return [{ uid: again.uid, icon: again.icon ?? null }]; }); const output = $('results'); append(output,back('Agents', () => discover(offset)), group('detail-heading row', icon(full.uid, true), group('grow', node('h2', full.name), caption(defaults(full))), talkButton(full, target)), facts([['Environment', environmentName()], ['Branch', full.repositoryBranch], ['Deployment', full.runtimeState === 'unknown' ? 'Status unknown' : full.runtimeState]]), markdown(full.description), button('New conversation…',()=>review('New conversation with '+full.name,'Start a fresh agent session. Existing replies and saved requests remain in this Codex chat.',generation,async()=>{conversationKeys.set(full.uid,crypto.randomUUID());draftRequest=null;talk(full,target);})), node('h3','Skills')); for (const skill of full.skills || []) { const section = group('detail-section', node('h3', skill.name), markdown(skill.description)); for (const example of skill.examples || []) { section.append(button(example, () => { if (detailRun !== generation) return; talk(full, target); $('message').value = example; $('message').focus(); }, 'example')); } append(output,section); } append(output,details('Technical details', facts([['Agent ID', full.uid], ['Response types', (full.responseKinds || []).join(', ')], ['Runtime release', full.runtimeReleaseUid]]))); }); }
129
154
  function review(title, description, run, action) { const box = group('review', node('h3', title), node('p', description), group('row')); const controls = box.children[2], cancel = button('Cancel', () => box.replaceChildren()); const confirm = button('Confirm', async () => { if (run !== generation) return; confirm.disabled = true; cancel.disabled = true; try { await action(); if (run === generation) $('confirmation').replaceChildren(); } catch (error) { if (run === generation) { box.append(node('pre', errorText(error))); cancel.disabled = false; } } }, 'primary'); controls.append(confirm, cancel); $('confirmation').replaceChildren(box); }
130
155
  async function connection(item, target, run, connect) { if (run !== generation) return; review((connect ? 'Connect ' : 'Disconnect ') + item.name, (connect ? 'Make this MCP available to this account in ' : 'Remove the saved connection from ') + environmentName() + '. This updates the shared devtools connection list.', run, async () => { const data = await call('mainsequence_connection', { ...target, releaseUid: item.uid, action: connect ? 'connect' : 'disconnect' }); activity.push({ title: (connect ? 'Connected ' : 'Disconnected ') + item.name, summary: data.summary }); await discover(offset); }); }
131
156
  function toolRow(item, target, run) { return group('item row', group('grow', button(item.name, () => item.saved ? toolDetail(item, target, run) : connection(item, target, run, true), 'name'), caption(item.saved ? (toolCache.has(target.contextId+item.uid) ? toolCache.get(target.contextId+item.uid).tools.length+' tools' : 'Connected') : item.repository || 'Available MCP')), button(item.saved ? 'Disconnect' : 'Connect', () => connection(item, target, run, !item.saved))); }
132
157
  async function toolDetail(item, target, run) { if (run !== generation) return; return detail(async()=>{try{const data=await call('mainsequence_tools',{...target,releaseUid:item.uid});toolCache.set(target.contextId+item.uid,data);return data;}catch(error){const cached=toolCache.get(target.contextId+item.uid);if(!cached)throw error;return {...cached,catalogError:error.message};}}, (data, detailRun) => { const output = $('results'); append(output,back('Tools', () => { addingTools = false; return discover(0); }), group('detail-heading row between', group('grow', node('h2', item.name), caption(`Connected · ${data.tools.length} tools`)), button('Reload tools', () => toolDetail(item, target, generation)), button('Disconnect', () => connection(item, target, generation, false)))); if(data.catalogError) append(output,caption('Showing the previous catalog. Reload failed: '+data.catalogError)); if (!data.tools.length) append(output,empty('No tools exposed', 'Reload after the MCP publishes its catalog.')); for (const tool of data.tools) { const row = group('item', node('h3', tool.name), markdown(tool.description)); const input = node('textarea'); input.setAttribute('aria-label', 'JSON arguments for ' + tool.name); input.value = '{}'; const result = node('div'); const advanced = details('Schema and execution', group('stack', node('pre', JSON.stringify(tool.inputSchema, null, 2)), caption('Arguments (JSON)'), input, button('Review call', () => { let args; try { args = JSON.parse(input.value); if (!args || typeof args !== 'object' || Array.isArray(args)) throw Error(); } catch { result.replaceChildren(caption('Enter a JSON object matching the schema.')); return; } review('Run ' + tool.name + '?', item.name + ' · ' + environmentName() + '\n' + JSON.stringify(args, null, 2), detailRun, async () => { const response = await call('mainsequence_call_tool', { ...target, releaseUid: item.uid, tool: tool.name, arguments: args }); activity.push({ title: tool.name, summary: 'Tool call completed' }); if (detailRun === generation) result.replaceChildren(node('pre', JSON.stringify(response.result, null, 2))); }); }), result)); row.append(advanced); append(output,row); } }); }
133
158
  function renderHome() { const a = snapshot?.account; const output = $('results'); if (!a?.authenticated || !$('environment').value) append(output,card('Next steps', node('h3', a?.authenticated ? 'Choose an environment' : 'Sign in to MainSequence'), caption(a?.authenticated ? 'Use the environment selector above to browse your projects, agents and tools.' : 'Use /ms login in Codex to restore the shared session.'))); append(output,card('This session', facts([['Account', a?.username], ['Environment', environmentName()], ['Backend', a?.backend]]), caption('Your MainSequence agent replies are saved in this native Codex conversation.'), group('row wrap', button('Talk to an agent', () => tab('agents')), button('Connected tools', () => tab('tools'))))); if(snapshot?.contextId && $('environment').value) {const session=card('Local workspace',caption('Open a folder to inspect its branch, SDK and repository status.'),button('Open in Code',()=>tab('code')),button('All deployments',()=>deployments()));append(output,session);} const recent = savedRequests.slice(-3).reverse(); append(output,card('Recent', ...(recent.length ? recent.map(r => group('item', node('h3', r.agentName), caption(stateLabel(r)))) : [caption('No agent messages in this chat yet.')]), button('See all activity', () => tab('activity')))); }
134
159
  function renderActivity() { const output = $('results'); append(output,group('row between', node('h2', 'Activity'), button('Refresh messages', async () => { await loadDeliveries(); if (catalog === 'activity') { $('results').replaceChildren(); renderActivity(); } }))); for (const request of [...savedRequests].reverse()) append(output,group('item', group('row between', node('h3', request.agentName), node('small', stateLabel(request))), caption(snapshot?.environments.find(e => e.uid === request.environmentUid)?.name || ''), request.receipt && node('p', request.receipt.message, 'receipt'), request.task && group('row wrap',button('Continue task',()=>continueTask(request)),button('Refresh task',async()=>{await call('mainsequence_agent_task',{action:'get',requestId:request.requestId});await loadDeliveries();discover();}),!request.task.terminal && button('Cancel task',()=>review('Cancel agent task','Request cancellation of this accepted task. Its saved request will not be sent again.',generation,async()=>{await call('mainsequence_agent_task',{action:'cancel',requestId:request.requestId});await loadDeliveries();discover();}))), request.reason && caption(request.reason), details('Message details', group('stack', node('p', request.message), caption('Request ' + request.requestId + ' · Dispatches ' + request.sendAttempts))), button(request.state === 'delivered' ? 'Show reply in Codex' : 'View recovery', () => { $('deliveries').open = true; $('deliveries').scrollIntoView?.({ block: 'nearest' }); }))); for(const op of [...operationCache].reverse()) append(output,operationRow(op)); for (const item of [...activity].reverse()) append(output,group('item', node('h3', item.title), caption(item.summary))); if (!savedRequests.length && !activity.length) append(output,empty('No activity yet', 'Agent messages and MCP connection changes appear here.')); }
135
- async function discover(nextOffset = 0, fresh = false) { const run = ++generation; offset = nextOffset; clearResults(); busy = false; $('list-toolbar').hidden = !['agents', 'projects', 'code'].includes(catalog) && !(catalog === 'tools' && addingTools); $('notice').textContent = ''; controls(); if (catalog === 'home') { renderHome(); return; } if (catalog === 'activity') { try { await operations(); } catch(e) { failure(e); } renderActivity(); return; } if (catalog === 'settings' && !$('environment').value) { await renderSettings(null, run); return; } if (!snapshot?.contextId || !$('environment').value) { $('notice').textContent = snapshot?.contextId ? 'Choose an environment to continue.' : 'Refresh or sign in to continue.'; return; } const target = selection(); busy = true; controls(); $('notice').textContent = 'Loading…'; try { if (catalog === 'settings') await renderSettings(target, run); else if (catalog === 'code') { await renderCode(target,run); } else if (catalog === 'projects') { const page = await view('projects', { search: $('search').value, offset }, target); if (run !== generation) return; pageControls(page); if (!page.results.length) append($('results'),empty('No projects found', 'Try another search or environment.')); for (const project of page.results) append($('results'),group('item row', group('grow', button(project.name, () => projectDetail(project, target, run), 'name'), caption(project.description?.split('\n')[0])), caption((project.code_repository_branch_uids || []).length + ' branches'))); } else if (catalog === 'tools' && !addingTools) { const page = await view('connections', {}, target); if (run !== generation) return; append($('results'),group('row between', node('h2', 'Connected in ' + environmentName()), button('Add MCP', () => { addingTools = true; return discover(0); }))); if (!page.results.length) append($('results'),empty('No connected MCPs', 'Add an MCP to make its tools available in Codex.')); for (const item of page.results) append($('results'),toolRow(item, target, run)); } else { const page = await call('mainsequence_discover', { ...target, catalog, search: $('search').value, offset, ...(fresh ? {refresh:true} : {}) }); if (run !== generation) return; pageControls(page); if (catalog === 'tools') append($('results'),back('Tools', () => { addingTools = false; return discover(0); })); if (!page.results.length) append($('results'),empty(catalog === 'agents' ? 'No agents found' : 'No MCPs found', 'Try another search or environment.')); if(catalog==='agents') append($('results'),caption(page.semantic?page.results.length+' matching agents':page.count+' agents · showing '+(page.results.length?offset+1:0)+'–'+(offset+page.results.length))); const rows = group('stack'); rows.style.gap = '0'; append($('results'),rows); for (const item of page.results) rows.append(catalog === 'agents' ? agentRow(item, target, run) : toolRow(item, target, run)); } if (run === generation) $('notice').textContent = ''; } catch (error) { if (run === generation) failure(error); } finally { if (run === generation) { busy = false; controls(); } } }
160
+ async function discover(nextOffset = 0, fresh = false) { const run = ++generation; offset = nextOffset; clearResults(); busy = false; $('list-toolbar').hidden = !['agents', 'projects', 'code'].includes(catalog) && !(catalog === 'tools' && addingTools); $('notice').textContent = ''; controls(); if (catalog === 'home') { renderHome(); return; } if (catalog === 'activity') { try { await operations(); } catch(e) { failure(e); } renderActivity(); return; } if (catalog === 'settings' && !$('environment').value) { await renderSettings(null, run); return; } if (!snapshot?.contextId || !$('environment').value) { $('notice').textContent = snapshot?.contextId ? 'Choose an environment to continue.' : 'Refresh or sign in to continue.'; return; } const target = selection(); busy = true; controls(); $('notice').textContent = 'Loading…'; try { if (catalog === 'settings') await renderSettings(target, run); else if (catalog === 'code') { await renderCode(target,run); } else if (catalog === 'projects') { const page = await view('projects', { search: $('search').value, offset }, target); if (run !== generation) return; pageControls(page); if (!page.results.length) append($('results'),empty('No projects found', 'Try another search or environment.')); for (const project of page.results) append($('results'),group('item row', group('grow', button(project.name, () => projectDetail(project, target, run), 'name'), caption(project.description?.split('\n')[0])), caption((project.code_repository_branch_uids || []).length + ' branches'))); } else if (catalog === 'tools' && !addingTools) { const page = await view('connections', {}, target); if (run !== generation) return; append($('results'),group('row between', node('h2', 'Connected in ' + environmentName()), button('Add MCP', () => { addingTools = true; return discover(0); }))); if (!page.results.length) append($('results'),empty('No connected MCPs', 'Add an MCP to make its tools available in Codex.')); for (const item of page.results) append($('results'),toolRow(item, target, run)); } else { const page = await call('mainsequence_discover', { ...target, catalog, search: $('search').value, offset, ...(fresh ? {refresh:true} : {}) }); if (run !== generation) return; pageControls(page); if (catalog === 'tools') append($('results'),back('Tools', () => { addingTools = false; return discover(0); })); if (!page.results.length) append($('results'),empty(catalog === 'agents' ? 'No agents found' : 'No MCPs found', 'Try another search or environment.')); if(catalog==='agents') append($('results'),caption(page.semantic?page.results.length+' matching agents':page.count+' agents · showing '+(page.results.length?offset+1:0)+'–'+(offset+page.results.length))); const rows = group('stack'); rows.style.gap = '0'; append($('results'),rows); for (const item of page.results) rows.append(catalog === 'agents' ? agentRow(item, target, run) : toolRow(item, target, run)); if (catalog === 'agents') { iconChecks = 0; watchIcons(run, async () => (await call('mainsequence_discover', { ...target, catalog: 'agents', search: $('search').value, offset })).results.map(a => ({ uid: a.uid, icon: a.icon ?? null }))); } } if (run === generation) $('notice').textContent = ''; } catch (error) { if (run === generation) failure(error); } finally { if (run === generation) { busy = false; controls(); } } }
136
161
  function tab(next, discoverView=true) { catalog = next; addingTools = false; $('search').value = ''; $('search').placeholder = 'Search ' + ({ code: 'repositories', tools: 'MCPs' }[next] || next); $('search').setAttribute('aria-label', $('search').placeholder); for (const key of sections) { $(key + '-tab').setAttribute('aria-selected', String(key === next)); $(key + '-tab').setAttribute('tabindex', ['activity','settings'].includes(key) || key === next ? '0' : '-1'); } $('directory').setAttribute('aria-labelledby', next + '-tab'); return discoverView ? discover(0) : undefined; }
137
162
  $('refresh').onclick = () => snapshot?.contextId ? discover(offset, true) : refresh(); $('environment').onchange = () => { rememberEnvironment();clearComposer(); workspaceContext=null; assetCache.clear(); addingTools = false; $('search').value = ''; return discover(0); }; $('search-form').onsubmit = e => { e.preventDefault(); return discover(0); }; $('previous').onclick = () => discover(Math.max(0, offset - 20)); $('next').onclick = () => discover(offset + 20); $('switch-agent').onclick = () => tab('agents');
138
163
  for (const key of sections) { $(key + '-tab').onclick = () => tab(key); $(key + '-tab').onkeydown = e => { if (['ArrowLeft', 'ArrowRight', 'Home', 'End'].includes(e.key)) { e.preventDefault(); const next = e.key === 'Home' ? sections[0] : e.key === 'End' ? sections.at(-1) : sections[(sections.indexOf(key) + (e.key === 'ArrowRight' ? 1 : sections.length - 1)) % sections.length]; $(next + '-tab').focus(); void tab(next); } }; }
@@ -246,7 +271,7 @@ $('send-message').onclick = () => sendMessage(true); $('draft-message').onclick
246
271
  $('close-composer').onclick = () => { clearComposer(); controls(); };
247
272
  $('refresh-deliveries').onclick = loadDeliveries;
248
273
 
249
- function teardown() { ++deliveryGeneration; ++generation; connected = false; for (const p of pending.values()) { clearTimeout(p.timeout); p.reject(new Error('The panel closed.')); } pending.clear(); controls(); }
274
+ function teardown() { ++deliveryGeneration; ++generation; connected = false; clearTimeout(iconWatch); for (const p of pending.values()) { clearTimeout(p.timeout); p.reject(new Error('The panel closed.')); } pending.clear(); controls(); }
250
275
  addEventListener('message', event => {
251
276
  if (event.source !== parent || event.data?.jsonrpc !== '2.0') return;
252
277
  const message = event.data, p = pending.get(message.id);