@dev-mainsequence/codex-plugin 0.7.9 → 0.7.10

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.10",
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/build-info.json CHANGED
@@ -1,5 +1,5 @@
1
1
  {
2
- "version": "0.7.9",
2
+ "version": "0.7.10",
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);
@@ -419,8 +419,8 @@ without any image-fetch fallback. The legacy `view: assets` request remains a
419
419
  network-free no-op for old panel HTML; reopen the current panel to render icons.
420
420
 
421
421
  The shared devtools transport still coalesces the combined response across chat
422
- processes. There is no periodic agent/icon refresh. Explicit Refresh requests a
423
- fresh directory. No change is made to the shared login or Claude.
422
+ processes. There is no periodic agent/icon refresh (until 0.7.10, below). Explicit Refresh
423
+ requests a fresh directory. No change is made to the shared login or Claude.
424
424
 
425
425
  ## Direct project tools, timeouts and distinct refusals — 0.7.8
426
426
 
@@ -520,3 +520,55 @@ with bounded paging and filters.`). `tools/call ms_crm_contacts_list_68ee6e71` w
520
520
  the same release's tools from the same shared file under the `127.0.0.1:8000` names. The record
521
521
  is the local `artifacts/live-acceptance-0.7.9.json`. The pre-backend listing of every scope is a
522
522
  devtools follow-up: a fresh server's first list could be scoped to the configured backend.
523
+
524
+ Published on 2026-10-23 (UTC) from tag `v0.7.9` on main `9cddf65`, the merge of the acceptance
525
+ record above, by the release workflow (run 40032861384: `npm ci` from the registry devtools
526
+ archive, `release:check`, `npm publish --provenance` through trusted publishing). The registry
527
+ serves `@dev-mainsequence/codex-plugin@0.7.9` as `latest`, 70 files, integrity
528
+ `sha512-5EmHyWdb3V+DB8EJmKsfK/P8bvMP+2DO04ntwuuO7EqDwbIVo5DNCOJctJJAiQlHqdVKNgbFSoEMUKjHKKL0iA==`,
529
+ read from the registry after publishing (a local `npm pack` of the same files has another
530
+ integrity). The devtools marketplace entry was advanced from 0.4.2 to 0.7.9
531
+ (mainsequence-sdk/mainsequence-devtools#20, main `18cbfc8`). In an isolated `CODEX_HOME` with
532
+ the Codex CLI 0.163.0, `codex plugin marketplace add
533
+ https://github.com/mainsequence-sdk/mainsequence-devtools.git`, `codex plugin marketplace upgrade
534
+ mainsequence` and `codex plugin add mainsequence@mainsequence` installed `mainsequence@mainsequence`
535
+ 0.7.9 from the npm package into `plugins/cache/mainsequence/mainsequence/0.7.9`, with the
536
+ manifest, `dist/panel-server.js`, `dist/runner.js`, hooks, skills and `.mcp.json` in place and
537
+ `build-info.json` naming devtools 0.3.12 by its registry integrity; `codex plugin list` reported
538
+ it installed and enabled. The owner's own Codex home (local `mainsequence-local` marketplace,
539
+ 0.7.8 installed) was not touched; upgrading it is a user step: `codex plugin marketplace upgrade
540
+ mainsequence`, `codex plugin add mainsequence@mainsequence`, then review the hook and start a new
541
+ chat.
542
+
543
+ ## Agent icons follow Command Center — 0.7.10
544
+
545
+ An icon changed in Command Center reached an open panel only after Refresh or a new app card:
546
+ the Agents list drew an agent's inline icon when it listed agents and nothing re-read the
547
+ directory while the list stayed on screen. (The Claude Desktop plugin had the same gap and fixed
548
+ it in its 0.4.20 with the per-agent icon route and entity tags; the Codex panel takes icons
549
+ inline from the directory, so it re-reads that instead.)
550
+
551
+ - While the Agents list or an agent's details stay on screen, the panel re-reads what it shows
552
+ once a minute: the directory page (`mainsequence_discover`, same search and offset) for the
553
+ list, the agent view for the details. The read carries no `refresh` flag: the shared
554
+ transport keeps a directory read for 30 s, so a read a minute later is fresh without
555
+ invalidating other panels' caches, unlike the Refresh button.
556
+ - Only a changed icon is redrawn, in place: the list, drafts and scroll position stay. A
557
+ removed icon shows the placeholder; a failed re-read keeps the drawn icons and tries again at
558
+ the next minute.
559
+ - The timer ends with the screen (another section, a detail or list change, environment
560
+ change, teardown) and pauses after 240 re-reads until the screen is opened again. Nothing is
561
+ stored; the inline icons live in the panel's memory as before.
562
+
563
+ Verification on 2026-10-24: `npm run release:check` from the registry devtools 0.3.12 archive,
564
+ 56 tests (new: the panel UI test drives the minute timer through the vm harness and checks an
565
+ unchanged, changed, removed, restored and failed re-read, the detail view's re-read, the timer
566
+ ending on another section and on teardown, and that the Refresh button alone sends `refresh`),
567
+ release metadata, the standalone and npm archives, the npm archive exercised as Codex installs
568
+ it. Live acceptance with the built server over stdio and the server-stamped panel page driven
569
+ in a minimal DOM (Production, seven agents, the shared machine session): the list drew seven
570
+ icons; two minute re-reads each read the directory page without `refresh` (14–17 s per read
571
+ against the local backend, in the background), found no change and redrew nothing; the timer
572
+ ended when Tools was shown, came back on Agents and ended on teardown. No icon was changed in
573
+ Command Center during the run; the changed, removed and failed paths are the UI test's.
574
+ Publication records for 0.7.10 follow below once released.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@dev-mainsequence/codex-plugin",
3
- "version": "0.7.9",
3
+ "version": "0.7.10",
4
4
  "description": "MainSequence for Codex: durable agent messaging in native chat, Agents and Tools discovery, developer commands and conversational MCPs",
5
5
  "license": "MIT",
6
6
  "engines": {