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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 26 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -201,6 +201,31 @@ connector name. If it is missing, Codexify asks for it in a popup, saves it in
a restart. Cancelling leaves the action untouched. Quickstart saves the name
you enter during setup; no connector name is assumed automatically.

For a lighter managed ChatGPT window, use **Settings → Browser → Browser
simplicity**: **Simplified** reduces decoration and defers offscreen history;
**Bones** adds compact navigation, plain messages/code and a minimal composer.
Both preserve model/apps menus, Stop, Copy and tool approvals. **Off** restores
the full interface. Existing simplification settings remain compatible; restart
Codexify to apply a level.

**Fast setup** in Settings → General or Browser applies the recommended
configuration in one save: Steer delivery, stalled-browser recovery, automatic
retries, progress tracking and agent controls. Browser simplification
(Simplified/Bones), infinite retries and turn renewal are not part of it; enable
each separately if you want it. It also
reduces animations, sounds and recap interruptions. Restart once using the
existing button; tool refresh is queued automatically when needed. Model/effort
choices, renewal limits and explicit per-chat stops remain yours. New quickstart
setups receive these defaults automatically; reruns preserve saved choices.
For scripted setup, `codexify config fast-setup` applies the same validated preset
in one command. Use `--config <path>` to target a particular installation.

Healthy conversation switching now defaults to a one-second interval; actual
HTTP 429 and Retry-After backoffs still apply. An explicitly saved interval is
preserved: set **Conversation loading interval (seconds)** to `1` for this policy.
Long tasks should retain an active saved goal and rollout journal so automatic
continuation can resume the same objective after an interrupted turn.

**Turn renewal** is independent of the progress journal and is **off by default**.
Enable it in **Settings → Chats & goals → Turn renewal**. Its per-model/effort
limits have individual enable switches and remain editable while disabled.
Expand Down Expand Up @@ -565,6 +590,7 @@ Report reproducible bugs in [GitHub Issues](https://github.com/devnoname120/code
| Setup and usage guides | [Wiki](https://github.com/devnoname120/codexify/wiki) |
| CLI options, configuration, and tool behavior | [Technical reference](docs/REFERENCE.md) |
| Additional MCP servers and reusable workflows | [MCP bridging](docs/REFERENCE.md#bridging-other-mcp-servers) · [Skills](docs/REFERENCE.md#skills) |
| Control agents from any AI client | [Universal agent connection](docs/CODING_APP_AGENTS.md) |
| Building or contributing | [Architecture](docs/ARCHITECTURE.md) · [Development commands](docs/REFERENCE.md#dev-commands) |

## License
Expand Down
18 changes: 9 additions & 9 deletions browser-companion/protocol-fixtures/v1.json
Original file line number Diff line number Diff line change
@@ -1,27 +1,27 @@
{
"schemaHash": "2f7852fd999d639b18e829bc273b2e38021c40c839ec0feb0ff2c793cfeb9e03",
"build": "fixture+protocol.2f7852fd999d639b18e829bc273b2e38021c40c839ec0feb0ff2c793cfeb9e03",
"schemaHash": "36957a042ed31cae11d0401411999119d39c22af98278a46b25c412b0be765c6",
"build": "fixture+protocol.36957a042ed31cae11d0401411999119d39c22af98278a46b25c412b0be765c6",
"descriptor": {
"version": 1,
"kind": "codexify-browser-companion",
"pid": 1234,
"endpoint": "http://127.0.0.1:43123",
"token": "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA",
"build": "fixture+protocol.2f7852fd999d639b18e829bc273b2e38021c40c839ec0feb0ff2c793cfeb9e03",
"build": "fixture+protocol.36957a042ed31cae11d0401411999119d39c22af98278a46b25c412b0be765c6",
"createdAt": "2026-10-09T12:00:00Z"
},
"health": {
"protocolVersion": 1,
"kind": "codexify-browser-companion",
"build": "fixture+protocol.2f7852fd999d639b18e829bc273b2e38021c40c839ec0feb0ff2c793cfeb9e03",
"build": "fixture+protocol.36957a042ed31cae11d0401411999119d39c22af98278a46b25c412b0be765c6",
"status": "idle",
"authenticated": true,
"accountFingerprint": "fixture-account",
"activeActionId": null
},
"capabilities": {
"protocolVersion": 1,
"build": "fixture+protocol.2f7852fd999d639b18e829bc273b2e38021c40c839ec0feb0ff2c793cfeb9e03",
"build": "fixture+protocol.36957a042ed31cae11d0401411999119d39c22af98278a46b25c412b0be765c6",
"accountFingerprint": "fixture-account",
"catalogRevision": "fixture-catalog",
"models": [
Expand Down Expand Up @@ -50,7 +50,7 @@
},
"revealReceipt": {
"protocolVersion": 1,
"build": "fixture+protocol.2f7852fd999d639b18e829bc273b2e38021c40c839ec0feb0ff2c793cfeb9e03",
"build": "fixture+protocol.36957a042ed31cae11d0401411999119d39c22af98278a46b25c412b0be765c6",
"revealed": true,
"activeActionId": null
},
Expand Down Expand Up @@ -218,7 +218,7 @@
],
"actionSnapshot": {
"protocolVersion": 1,
"build": "fixture+protocol.2f7852fd999d639b18e829bc273b2e38021c40c839ec0feb0ff2c793cfeb9e03",
"build": "fixture+protocol.36957a042ed31cae11d0401411999119d39c22af98278a46b25c412b0be765c6",
"actionId": "fixture-action",
"state": "generating",
"sendAccepted": true,
Expand All @@ -232,7 +232,7 @@
},
"waitingSnapshot": {
"protocolVersion": 1,
"build": "fixture+protocol.2f7852fd999d639b18e829bc273b2e38021c40c839ec0feb0ff2c793cfeb9e03",
"build": "fixture+protocol.36957a042ed31cae11d0401411999119d39c22af98278a46b25c412b0be765c6",
"actionId": "fixture-waiting",
"state": "waiting",
"sendAccepted": false,
Expand All @@ -248,7 +248,7 @@
},
"cancelReceipt": {
"protocolVersion": 1,
"build": "fixture+protocol.2f7852fd999d639b18e829bc273b2e38021c40c839ec0feb0ff2c793cfeb9e03",
"build": "fixture+protocol.36957a042ed31cae11d0401411999119d39c22af98278a46b25c412b0be765c6",
"actionId": "fixture-action",
"state": "ambiguous",
"cancelled": true
Expand Down
7 changes: 7 additions & 0 deletions browser-companion/protocol-schema-v1.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,13 @@
"kind": "codexify-browser-companion",
"fieldNaming": "camelCase",
"strictObjects": true,
"parallelActionPages": {
"actionCapacity": "optional integer 1..4; absent means one legacy page",
"availableActionSlots": "optional integer 0..4; capacity is not authentication authority",
"activeActionIds": "optional list of at most four opaque action IDs; accepted and uncertain actions retain page ownership",
"readyHealth": "GET /v1/health?ready=1 awaits fresh idle-page session proof; ordinary health remains bounded and passive",
"ordering": "one action per canonical conversation; maintenance drains all page lanes; global HTTP429 and actual Send spacing remain shared"
},
"buildBinding": "<component-version>+protocol.<sha256-of-this-file>",
"endpoints": [
"GET /v1/health",
Expand Down
107 changes: 107 additions & 0 deletions browser-companion/scripts/bones-shell-smoke.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
import assert from "node:assert/strict";
import {resolve} from "node:path";
import {chromium} from "patchright";
import {privateChromeExecutable} from "../src/chrome-executable";
import {BONES_SHELL_CSS,installMinimalChatGptPage,styleRestoredChatGptPage,type BrowserSimplicity} from "../src/minimal-page";
import {activeComposer,composerText,insertExactPrompt} from "../src/composer";
import {readChatGptAccount} from "../src/account";
import {inspectConnectorSettings,beginConnectorToolRefresh} from "../src/account-settings";

// A fresh, entirely routed fixture: never uses the live managed profile/account.
const executablePath=process.env.CODEXIFY_TEST_CHROME_PATH??await privateChromeExecutable(resolve(".browsers",`${process.platform}-${process.arch}`));
const browser=await chromium.launch({executablePath,headless:true,timeout:20_000});
const fixture=`<!doctype html><html><head><style>
body{margin:0;font:16px sans-serif}aside.app-shell-left-panel{position:fixed;inset:0 auto 0 0;width:260px;background:#ddd;overflow:hidden}
#app-shell-sidebar{width:260px;height:100%;overflow:hidden}#sidebar-inner{overflow:hidden}button{min-height:28px}
main{margin-left:270px;padding:16px}.rounded{border-radius:24px;box-shadow:0 2px 8px #777}
#portal-composer{position:fixed;bottom:24px;left:80px;right:24px;background:white;padding:8px}
#prompt-textarea{min-height:30px;white-space:pre-wrap}[role=menu]{position:fixed;left:60px;top:120px;background:white;border:1px solid}
</style></head><body>
<aside class="app-shell-left-panel" id="rail"><div id="sidebar-inner"><div id="app-shell-sidebar"><nav aria-label="Main navigation" id="modern-nav">History and projects</nav><button aria-label="Open profile menu" id="profile">Profile</button><button data-testid="create-new-chat-button" id="new-chat">New</button></div></div></aside>
<main><header id="shell-header"><div role="tablist"><button>Chat</button><button>Work</button></div></header><header id="legacy-header"><button data-testid="model-switcher-dropdown-button" id="legacy-model">Model</button></header><h1 id="welcome">How can I help?</h1><div id="turns"></div></main>
<div id="portal-composer" class="rounded"><form data-chatgpt-composer class="rounded"><div id="prompt-textarea" contenteditable="plaintext-only" role="textbox" class="rounded"></div><button type="button" id="apps" aria-haspopup="menu">Apps</button><button type="button" id="effort" data-codexify-effort-control="true" aria-haspopup="menu">Effort</button><button id="send" data-testid="send-button" type="submit">Send</button><button id="stop" data-testid="stop-button" type="button">Stop</button><button id="voice" data-testid="voice-mode-button" type="button">Voice</button></form></div>
<div id="profile-menu" role="menu" hidden><button id="profile-action">Account settings</button></div><div id="apps-menu" role="menu" hidden><button id="app-choice">Codexify</button></div><div id="effort-menu" role="menu" hidden><button id="effort-choice">High</button></div>
<aside id="approval" role="dialog" style="position:fixed;right:12px;top:90px">Tool permission <button id="approve">Approve</button></aside><div role="alert" id="error" style="position:fixed;right:12px;top:150px">A recoverable tool error</div>
<script>window.clicks={};window.sent=[];document.addEventListener('click',event=>{const id=event.target.id;if(id)window.clicks[id]=(window.clicks[id]||0)+1;document.body.dataset.clicks=JSON.stringify(window.clicks);});
for(const [trigger,menu] of [['profile','profile-menu'],['apps','apps-menu'],['effort','effort-menu']])document.getElementById(trigger).onclick=()=>{document.getElementById(menu).hidden=false;};
document.querySelector('form').onsubmit=event=>{event.preventDefault();window.sent.push(document.getElementById('prompt-textarea').innerText);document.body.dataset.sent=JSON.stringify(window.sent);document.getElementById('welcome').remove();document.getElementById('turns').innerHTML='<article data-turn-key="answer"><div data-conversation-role="assistant"><header id="answer-header"><h2 id="answer-heading">Verified answer heading</h2></header><p>Fresh answer</p><button id="copy" data-testid="copy-turn-action-button">Copy</button></div></article>';document.getElementById('prompt-textarea').textContent='';};</script></body></html>`;
const settingsFixture=`<!doctype html><style>body{font:16px sans-serif}button{min-height:28px}</style><main>
<header id="settings-header"><h1 id="settings-title">Settings</h1><button id="refresh" data-action="refresh-tools">Refresh tools</button></header>
<div role="tablist" id="settings-tabs"><button>Details</button><button>Permissions</button></div>
<h2 id="connector-title">Codexify</h2><section><h2 id="manage-heading">Manage app</h2><p>Connector details</p></section>
<section role="dialog" id="settings-approval"><header id="approval-header"><h2 id="approval-heading">Tool permission</h2><button id="approve-setting">Approve</button></header></section>
</main><script>document.querySelector('#refresh').onclick=function(){document.body.dataset.refreshes=String(Number(document.body.dataset.refreshes||0)+1);this.disabled=true;setTimeout(()=>this.disabled=false,100);};</script>`;
try {
const context=await browser.newContext({viewport:{width:1280,height:800}});
await context.route("**/*",route=>new URL(route.request().url()).pathname==='/api/auth/session'
?route.fulfill({json:{user:{id:'fixture-user'},account:{id:'fixture-account',planType:'pro',structure:'personal'}}})
:route.fulfill({contentType:"text/html",body:new URL(route.request().url()).pathname.startsWith('/settings')?settingsFixture:fixture}));
await installMinimalChatGptPage(context,BONES_SHELL_CSS,1,"bones");
const page=await context.newPage();await page.goto("https://chatgpt.com/c/bones-shell-fixture");
const css=async(selector:string,key:string)=>page.locator(selector).evaluate((el,property)=>getComputedStyle(el).getPropertyValue(property),key);
const visible=async(selector:string)=>assert.equal(await page.locator(selector).isVisible(),true,`${selector} must remain usable`);
const bones=async()=>{
for(const selector of ["#modern-nav","#shell-header","#voice"])assert.equal(await page.locator(selector).isVisible(),false,`${selector} must be hidden in Bones`);
assert.equal(await css("#rail","width"),"0px");
for(const selector of ["#profile","#new-chat"]){await visible(selector);assert.equal(await css(selector,"position"),"fixed");}
assert.equal(await css("#profile","bottom"),"6px");assert.equal(await css("#new-chat","top"),"6px");
for(const selector of ["#portal-composer","#prompt-textarea"]){assert.equal(await css(selector,"border-radius"),"0px");assert.equal(await css(selector,"box-shadow"),"none");}
await visible("#legacy-header");await visible("#legacy-model");
};
await bones();assert.equal(await page.locator("#welcome").isVisible(),false);
await page.locator("#profile").click();await page.locator("#profile-action").click();await page.locator("#profile-menu").evaluate(el=>{(el as HTMLElement).hidden=true;});
await page.locator("#new-chat").click();await page.locator("#legacy-model").click();
assert.equal(await page.locator("#prompt-textarea").getAttribute("contenteditable"),"plaintext-only");
assert.equal(await page.locator("main #prompt-textarea").count(),0,"Composer must really be in an external portal");
for(const [trigger,choice,menu] of [["#apps","#app-choice","#apps-menu"],["#effort","#effort-choice","#effort-menu"]]){
await page.locator(trigger!).click();await page.locator(choice!).click();await page.locator(menu!).evaluate(el=>{(el as HTMLElement).hidden=true;});
}
const prompt="Exact Bones prompt\nwith approval and tool continuation";
const composer=await activeComposer(page);await insertExactPrompt(composer,prompt);assert.equal(await composerText(composer),prompt);
await page.locator('[data-testid="send-button"]').click();await page.locator('[data-testid="stop-button"]').click();
for(const selector of ["#answer-header","#answer-heading","#copy","#approval","#error"])await visible(selector);
await page.locator("#copy").click();await page.locator("#approve").click();
const actions=await page.evaluate(()=>({clicks:JSON.parse(document.body.dataset.clicks??"{}"),sent:JSON.parse(document.body.dataset.sent??"[]")}));
assert.deepEqual(actions.sent,[prompt]);
for(const id of ["profile","profile-action","new-chat","legacy-model","apps","app-choice","effort","effort-choice","send","stop","copy","approve"])assert.equal(actions.clicks[id],1,`${id} must handle a real click`);
// Hydration and restored-tab styling must repair the same active mode.
await page.evaluate(()=>{const replacement=document.createElement("head");document.head.replaceWith(replacement);});
await styleRestoredChatGptPage(page,BONES_SHELL_CSS,1,"bones");await bones();await visible("#answer-heading");
let revision=2;
for(const level of ["simplified","off"] as BrowserSimplicity[]){
await installMinimalChatGptPage(context,BONES_SHELL_CSS,revision,level);await styleRestoredChatGptPage(page,BONES_SHELL_CSS,revision++,level);
await page.goto(`https://chatgpt.com/c/${level}-shell-fixture`);
for(const selector of ["#rail","#modern-nav","#shell-header","#voice","#legacy-header"])await visible(selector);
assert.notEqual(await css("#rail","width"),"0px");assert.notEqual(await css("#profile","position"),"fixed");
assert.notEqual(await css("#portal-composer","border-radius"),"0px","Bones must not leak into another mode");
assert.equal(await page.locator("html").getAttribute("data-codexify-simplicity"),level);
}
await installMinimalChatGptPage(context,BONES_SHELL_CSS,revision,"bones");await styleRestoredChatGptPage(page,BONES_SHELL_CSS,revision,"bones");
await page.goto("https://chatgpt.com/c/bones-navigation-fixture");await bones();assert.equal(await page.locator("#welcome").isVisible(),false);
// A portal composer can remain mounted while SPA settings replace MAIN.
// Route ownership, rather than generic headings or composer presence, must
// decide which shell chrome may be hidden.
await page.evaluate(()=>{document.body.dataset.chatMain=document.querySelector('main')!.innerHTML;history.pushState({},'', '/settings/plugins-settings/plugin_asdk_app_fixture');document.querySelector('main')!.innerHTML='<header id="spa-settings-header"><h1 id="spa-settings-title">Codexify</h1><button id="spa-refresh">Refresh tools</button></header>';});
for(const selector of ['#spa-settings-header','#spa-settings-title','#spa-refresh'])await visible(selector);
assert.equal(await page.locator('#prompt-textarea').count(),1,'The external composer intentionally remains during SPA settings');
assert.equal(await page.locator('html').getAttribute('data-codexify-chat-page'),'false');
await page.evaluate(()=>{history.replaceState({},'', '/c/spa-chat-return');document.querySelector('main')!.innerHTML=document.body.dataset.chatMain!;});
await bones();assert.equal(await page.locator('#welcome').isVisible(),false);
await page.goto('https://chatgpt.com/g/g-p-fixture/project');await bones();assert.equal(await page.locator('#welcome').isVisible(),false);
const account=(await readChatGptAccount(page)).fingerprint;
const inspectedAt=performance.now();await inspectConnectorSettings(page,'asdk_app_fixture','Codexify',account);const settingsInspectionMs=performance.now()-inspectedAt;
const settingsVisible=async()=>{for(const selector of ['#settings-header','#settings-title','#settings-tabs','#connector-title','#manage-heading','#refresh','#settings-approval','#approval-header','#approval-heading'])await visible(selector);};
await settingsVisible();assert.equal(await page.locator('html').getAttribute('data-codexify-chat-page'),'false');
const refresh=await beginConnectorToolRefresh(page,'asdk_app_fixture','Codexify',account);
try {assert.equal(await refresh.wait(AbortSignal.timeout(2_000)),true);}finally{await refresh.dispose();}
assert.equal(await page.locator('body').getAttribute('data-refreshes'),'1','The real refresh helper must click the fixture control exactly once');
await page.evaluate(()=>document.head.replaceWith(document.createElement('head')));
await styleRestoredChatGptPage(page,BONES_SHELL_CSS,revision,'bones');await settingsVisible();
for(const path of ['/settings/billing','/settings/plugins-settings/plugin_asdk_app_fixture/permissions','/unknown-maintenance-page']){
await page.goto(`https://chatgpt.com${path}`);
if(path.startsWith('/settings'))await settingsVisible();
else {await visible('#shell-header');await visible('#welcome');}
assert.equal(await page.locator('html').getAttribute('data-codexify-chat-page'),'false');
}
console.log(JSON.stringify({ok:true,settingsInspectionMs,settingsRefreshClicks:1,checks:["modern shell hidden","profile/newchat fixed and clickable","legacy model control","portal plaintext composer","Apps/effort menus","exact Send","Stop/Copy/approval/error controls","answer headings","head replacement/restored style","Simplified/Off isolation with extra Bones CSS","navigation persistence","SPA push/replace route changes with retained composer","project chat welcome hidden","full-page plugin and billing headings preserved","actual settings inspection and Refresh helper","settings approvals preserved","unknown routes fail open"],scope:"Fresh routed Chrome fixture; no live profile/account"}));
} finally {await browser.close();}
Loading