@oooooooooo/ch0de 1.9.55 → 1.9.57
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.
- package/chode.js +58 -0
- package/install.cjs +2 -2
- package/package.json +3 -1
- package/serve-frontend.html +225 -1
package/chode.js
CHANGED
|
@@ -7278,8 +7278,10 @@ function cmdServe(argv) {
|
|
|
7278
7278
|
if (!portVal) portVal = '4097'; // default unlikely to conflict with OpenCode (4096)
|
|
7279
7279
|
var PORT = parseInt(portVal, 10);
|
|
7280
7280
|
var SESSIONS_DIR = path.join(ROOT, '.chode', 'sessions');
|
|
7281
|
+
var CONFIG_PATH = path.join(ROOT, '.chode', 'config.json');
|
|
7281
7282
|
|
|
7282
7283
|
if (!fs.existsSync(SESSIONS_DIR)) fs.mkdirSync(SESSIONS_DIR, {recursive:true});
|
|
7284
|
+
if (!fs.existsSync(path.dirname(CONFIG_PATH))) fs.mkdirSync(path.dirname(CONFIG_PATH), {recursive:true});
|
|
7283
7285
|
|
|
7284
7286
|
var sessions = {};
|
|
7285
7287
|
try {
|
|
@@ -7296,6 +7298,16 @@ function cmdServe(argv) {
|
|
|
7296
7298
|
return 'ses_' + crypto.randomBytes(8).toString('hex');
|
|
7297
7299
|
}
|
|
7298
7300
|
|
|
7301
|
+
function loadServeConfig() {
|
|
7302
|
+
try { return JSON.parse(fs.readFileSync(CONFIG_PATH, 'utf8')); } catch(e) { return { freeOnly: true, defaultProvider: 'auto', providers: {}, connections: {} }; }
|
|
7303
|
+
}
|
|
7304
|
+
|
|
7305
|
+
function saveServeConfig(cfg) {
|
|
7306
|
+
try { fs.writeFileSync(CONFIG_PATH, JSON.stringify(cfg, null, 2), 'utf8'); } catch(e) {}
|
|
7307
|
+
}
|
|
7308
|
+
|
|
7309
|
+
var serveState = { provider: 'auto', mode: 'execute', theme: 'dark', details: false, timeoutSeconds: null, acceptanceTest: null, yolo: false };
|
|
7310
|
+
|
|
7299
7311
|
function json(res, code, obj) {
|
|
7300
7312
|
res.writeHead(code, {'Content-Type': 'application/json', 'Access-Control-Allow-Origin': '*'});
|
|
7301
7313
|
res.end(JSON.stringify(obj));
|
|
@@ -7393,6 +7405,52 @@ function cmdServe(argv) {
|
|
|
7393
7405
|
}
|
|
7394
7406
|
}
|
|
7395
7407
|
|
|
7408
|
+
if (pathname === '/api/settings' && req.method === 'GET') {
|
|
7409
|
+
var cfg = loadServeConfig();
|
|
7410
|
+
json(res, 200, {ok: true, config: cfg, state: serveState});
|
|
7411
|
+
return;
|
|
7412
|
+
}
|
|
7413
|
+
|
|
7414
|
+
if (pathname === '/api/settings' && req.method === 'POST') {
|
|
7415
|
+
readBody(req).then(function(body){
|
|
7416
|
+
var parsed;
|
|
7417
|
+
try { parsed = JSON.parse(body || '{}'); } catch(e) { json(res, 400, {ok:false, error:'invalid json'}); return; }
|
|
7418
|
+
var cfg = loadServeConfig();
|
|
7419
|
+
if (parsed.freeOnly !== undefined) cfg.freeOnly = !!parsed.freeOnly;
|
|
7420
|
+
if (parsed.defaultProvider !== undefined) cfg.defaultProvider = parsed.defaultProvider;
|
|
7421
|
+
if (parsed.mode !== undefined) serveState.mode = parsed.mode;
|
|
7422
|
+
if (parsed.theme !== undefined) serveState.theme = parsed.theme;
|
|
7423
|
+
if (parsed.details !== undefined) serveState.details = !!parsed.details;
|
|
7424
|
+
if (parsed.timeoutSeconds !== undefined) serveState.timeoutSeconds = parsed.timeoutSeconds ? Number(parsed.timeoutSeconds) : null;
|
|
7425
|
+
if (parsed.acceptanceTest !== undefined) serveState.acceptanceTest = parsed.acceptanceTest || null;
|
|
7426
|
+
if (parsed.yolo !== undefined) serveState.yolo = !!parsed.yolo;
|
|
7427
|
+
if (parsed.provider !== undefined) serveState.provider = parsed.provider;
|
|
7428
|
+
saveServeConfig(cfg);
|
|
7429
|
+
json(res, 200, {ok: true, config: cfg, state: serveState});
|
|
7430
|
+
}).catch(function(e){ json(res, 500, {ok:false, error: e.message}); });
|
|
7431
|
+
return;
|
|
7432
|
+
}
|
|
7433
|
+
|
|
7434
|
+
if (pathname === '/api/providers' && req.method === 'GET') {
|
|
7435
|
+
var out = [];
|
|
7436
|
+
try {
|
|
7437
|
+
for (var id in PROVIDERS) {
|
|
7438
|
+
var p = PROVIDERS[id];
|
|
7439
|
+
out.push({id: id, name: p.name || id, requiresKey: !!p.requiresKey, free: require('./provider-capacity').free ? require('./provider-capacity').free(p) : true, category: p.category || '', builtIn: true});
|
|
7440
|
+
}
|
|
7441
|
+
var dir = require('./provider-directory');
|
|
7442
|
+
var listed = dir.list();
|
|
7443
|
+
var ids = {};
|
|
7444
|
+
out.forEach(function(x){ids[x.id]=true;});
|
|
7445
|
+
listed.forEach(function(p){
|
|
7446
|
+
if (!ids[p.id]) {
|
|
7447
|
+
out.push({id: p.id, name: p.name, requiresKey: !!p.env, free: !p.env, category: 'directory', builtIn: false, baseUrl: p.baseUrl, models: p.models.length, doc: p.doc, verification: p.verification});
|
|
7448
|
+
}
|
|
7449
|
+
});
|
|
7450
|
+
} catch(e) {}
|
|
7451
|
+
json(res, 200, {ok: true, providers: out});
|
|
7452
|
+
}
|
|
7453
|
+
|
|
7396
7454
|
res.writeHead(404, {'Content-Type': 'text/plain'});
|
|
7397
7455
|
res.end('Not found');
|
|
7398
7456
|
});
|
package/install.cjs
CHANGED
|
@@ -4,10 +4,10 @@ const fs=require('fs'),os=require('os'),path=require('path'),crypto=require('cry
|
|
|
4
4
|
const REPOSITORY='https://github.com/fogennnnn/chode.git';
|
|
5
5
|
function execute(command,args,options={}){const r=spawnSync(command,args,{encoding:'utf8',windowsHide:true,timeout:180000,maxBuffer:4*1024*1024,...options});if(r.error||r.status!==0)throw Error(String(r.stderr||r.stdout||r.error||'Command failed').trim().slice(-2000));return r.stdout.trim();}
|
|
6
6
|
function npmRunner(){const directories=[path.dirname(process.execPath),...String(process.env.PATH||'').split(path.delimiter)];const candidates=[process.env.npm_execpath,path.join(path.dirname(process.execPath),'..','lib','node_modules','npm','bin','npm-cli.js'),'/usr/share/nodejs/npm/bin/npm-cli.js'];for(const directory of directories){candidates.push(path.join(directory,'node_modules','npm','bin','npm-cli.js'));try{candidates.push(fs.realpathSync(path.join(directory,'npm')));}catch(_){}}const cli=candidates.find(p=>p&&/npm-cli\.js$/i.test(p)&&fs.existsSync(p));if(!cli)throw Error('npm was not found. Install Node.js 18+ with npm, then retry.');return args=>execute(process.execPath,[cli,...args]);}
|
|
7
|
-
async function install(options={}){if(Number(process.versions.node.split('.')[0])<18)throw Error('Node.js 18+ is required.');const release=await fetchRelease();execute('git',['--version']);const npm=npmRunner(),temporaryRoot=fs.realpathSync(os.tmpdir()),staging=fs.mkdtempSync(path.join(temporaryRoot,'chode-install-'));const prefix=options.prefix?['--prefix',path.resolve(options.prefix)]:[];try{const checkout=path.join(staging,'source');console.log('Downloading the latest Chode from master…');execute('git',['clone','--depth','1','--single-branch','--no-tags','--branch','master',REPOSITORY,checkout]);const project=path.join(checkout,'chode'),manifest=JSON.parse(fs.readFileSync(path.join(project,'package.json'),'utf8'));if(manifest.name!=='chode'||!fs.existsSync(path.join(project,'chode.js')))throw Error('Downloaded source is not the expected Chode project.');verifyRelease(project,release);const commit=execute('git',['-C',checkout,'rev-parse','HEAD']);const packed=JSON.parse(npm(['pack',project,'--ignore-scripts','--json','--pack-destination',staging]));if(!Array.isArray(packed)||packed.length!==1||path.basename(packed[0].filename)!==packed[0].filename)throw Error('Unexpected package archive.');const archive=path.join(staging,packed[0].filename);console.log('Installing Chode '+manifest.version+'…');npm(['install','--global','--ignore-scripts','--no-audit','--no-fund',...prefix,archive]);const modules=npm(['root','--global',...prefix]),
|
|
7
|
+
async function install(options={}){if(Number(process.versions.node.split('.')[0])<18)throw Error('Node.js 18+ is required.');const release=await fetchRelease();execute('git',['--version']);const npm=npmRunner(),temporaryRoot=fs.realpathSync(os.tmpdir()),staging=fs.mkdtempSync(path.join(temporaryRoot,'chode-install-'));const prefix=options.prefix?['--prefix',path.resolve(options.prefix)]:[];try{const checkout=path.join(staging,'source');console.log('Downloading the latest Chode from master…');execute('git',['clone','--depth','1','--single-branch','--no-tags','--branch','master',REPOSITORY,checkout]);const project=path.join(checkout,'chode'),manifest=JSON.parse(fs.readFileSync(path.join(project,'package.json'),'utf8'));if((manifest.name!=='chode'&&manifest.name!=='@oooooooooo/ch0de')||!fs.existsSync(path.join(project,'chode.js')))throw Error('Downloaded source is not the expected Chode project.');verifyRelease(project,release);const commit=execute('git',['-C',checkout,'rev-parse','HEAD']);const packed=JSON.parse(npm(['pack',project,'--ignore-scripts','--json','--pack-destination',staging]));if(!Array.isArray(packed)||packed.length!==1||path.basename(packed[0].filename)!==packed[0].filename)throw Error('Unexpected package archive.');const archive=path.join(staging,packed[0].filename);console.log('Installing Chode '+manifest.version+'…');npm(['install','--global','--force','--ignore-scripts','--no-audit','--no-fund',...prefix,archive]);const modules=npm(['root','--global',...prefix]),pkgRoot=manifest.name.startsWith('@')?path.join(modules,manifest.name.split('/')[0],manifest.name.split('/')[1]):path.join(modules,manifest.name),installed=path.join(pkgRoot,'chode.js');verifyRelease(path.dirname(installed),release);const version=execute(process.execPath,[installed,'--version']);if(version!==manifest.version)throw Error('Installed version mismatch: expected '+manifest.version+', received '+version);console.log('Installed Chode '+version+' · '+commit.slice(0,12));console.log('Verified executable: '+installed);warnShadowed(modules);console.log('Start coding: chode');if(options.prefix)console.log('Command directory: '+(process.platform==='win32'?path.resolve(options.prefix):path.join(path.resolve(options.prefix),'bin')));console.log(options.prefix?'If chode is not found, add the command directory above to PATH and reopen your terminal.':'If chode is not found, add npm\'s global command directory to PATH and reopen your terminal.');return {version,commit,installed};}finally{const resolved=path.resolve(staging);if(path.dirname(resolved)!==temporaryRoot||!path.basename(resolved).startsWith('chode-install-'))throw Error('Refusing cleanup outside the installer staging directory.');fs.rmSync(resolved,{recursive:true,force:true});}}
|
|
8
8
|
if(require.main===module){try{const args=process.argv.slice(2);if(args.length&&!(args.length===2&&args[0]==='--prefix'))throw Error('Usage: node install.cjs [--prefix directory]');install({prefix:args[1]||process.env.CHODE_INSTALL_PREFIX}).catch(error=>{console.error('Chode installation failed: '+error.message);process.exitCode=1;});}catch(error){console.error('Chode installation failed: '+error.message);process.exitCode=1;}}
|
|
9
9
|
function digestFile(root,file){let content=fs.readFileSync(path.join(root,file),'utf8').replace(/\r\n/g,'\n');if(file==='package.json'){const p=JSON.parse(content);content=JSON.stringify({name:p.name,version:p.version,main:p.main,bin:p.bin,engines:p.engines,files:p.files,dependencies:p.dependencies||{}});}return crypto.createHash('sha256').update(content).digest('hex');}
|
|
10
|
-
function verifyRelease(root,release){if(!release||release.channel!=='master'||!/^\d+\.\d+\.\d+$/.test(release.version)||!release.files)throw Error('Verified release metadata is invalid.');const parts=release.version.split('.').map(Number);if(parts[0]<1||parts[0]===1&&(parts[1]<7||parts[1]===7&&parts[2]<2))throw Error('Release is below the supported security floor.');const p=JSON.parse(fs.readFileSync(path.join(root,'package.json'),'utf8'));if(p.version!==release.version)throw Error('Downloaded version does not match the verified release. No stale version will be installed; retry after publication completes.');if(p.name!=='chode'||p.bin
|
|
10
|
+
function verifyRelease(root,release){if(!release||release.channel!=='master'||!/^\d+\.\d+\.\d+$/.test(release.version)||!release.files)throw Error('Verified release metadata is invalid.');const parts=release.version.split('.').map(Number);if(parts[0]<1||parts[0]===1&&(parts[1]<7||parts[1]===7&&parts[2]<2))throw Error('Release is below the supported security floor.');const p=JSON.parse(fs.readFileSync(path.join(root,'package.json'),'utf8'));if(p.version!==release.version)throw Error('Downloaded version does not match the verified release. No stale version will be installed; retry after publication completes.');if((p.name!=='chode'&&p.name!=='@oooooooooo/ch0de')||p.bin&&!(p.bin.chode==='chode.js'||p.bin.ch0de==='chode.js'))throw Error('Unexpected package identity.');for(const required of ['package.json','chode.js','provider-policy.js'])if(!release.files[required])throw Error('Release integrity list is incomplete.');for(const file of Object.keys(release.files)){if(path.isAbsolute(file)||file.split(/[\\/]/).includes('..')||!/^[a-zA-Z0-9_./-]+$/.test(file))throw Error('Unsafe release file path.');if(digestFile(root,file)!==release.files[file])throw Error('Release integrity mismatch: '+file);}for(const file of p.files||[]){if(/\.(?:js|cjs)$/.test(file)){if(!release.files[file])throw Error('Unverified runtime file: '+file);const content=fs.readFileSync(path.join(root,file),'utf8');if(new RegExp('poll'+'inat','i').test(content))throw Error('Forbidden retired fallback detected in '+file);}}return true;}
|
|
11
11
|
async function fetchRelease(options={}){
|
|
12
12
|
const request=options.fetch||fetch,pause=options.sleep||(ms=>new Promise(resolve=>setTimeout(resolve,ms)));let last;
|
|
13
13
|
for(let attempt=0;attempt<3;attempt++){
|
package/package.json
CHANGED
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@oooooooooo/ch0de",
|
|
3
|
-
"version": "1.9.
|
|
3
|
+
"version": "1.9.57",
|
|
4
4
|
"description": "The self-healing AI coding harness. Free tiers. HEMO auto-provisioning. Zero lock-in. Canonical brand: ch0de.",
|
|
5
5
|
"main": "chode.js",
|
|
6
6
|
"bin": {
|
|
7
|
+
"chode": "chode.js",
|
|
7
8
|
"ch0de": "chode.js",
|
|
8
9
|
"chode-council": "expert-panel.cjs"
|
|
9
10
|
},
|
|
@@ -150,6 +151,7 @@
|
|
|
150
151
|
},
|
|
151
152
|
"dependencies": {
|
|
152
153
|
"@modelcontextprotocol/sdk": "1.32.2",
|
|
154
|
+
"@oooooooooo/ch0de": "^1.9.56",
|
|
153
155
|
"lean": "^0.1.1"
|
|
154
156
|
},
|
|
155
157
|
"devDependencies": {
|
package/serve-frontend.html
CHANGED
|
@@ -11,6 +11,7 @@ body { background: #000; color: #e0e0e0; font-family: "IBM Plex Mono", monospace
|
|
|
11
11
|
.sidebar { width: 280px; background: #0a0a0a; border-right: 1px solid #1a1a1a; display: flex; flex-direction: column; }
|
|
12
12
|
.main { flex: 1; display: flex; flex-direction: column; background: #000; }
|
|
13
13
|
.header { height: 60px; background: #0a0a0a; border-bottom: 1px solid #1a1a1a; display: flex; align-items: center; justify-content: space-between; padding: 0 20px; }
|
|
14
|
+
.header-right { display: flex; align-items: center; gap: 12px; }
|
|
14
15
|
.logo { color: #22dd55; font-weight: bold; font-size: 1.25rem; letter-spacing: -0.5px; }
|
|
15
16
|
.sessions { flex: 1; overflow-y: auto; padding: 12px; }
|
|
16
17
|
.session-item { padding: 10px 12px; border-radius: 8px; cursor: pointer; margin-bottom: 6px; border: 1px solid transparent; transition: all 0.2s; }
|
|
@@ -18,6 +19,8 @@ body { background: #000; color: #e0e0e0; font-family: "IBM Plex Mono", monospace
|
|
|
18
19
|
.session-item.active { background: #141414; border-color: #22dd55; }
|
|
19
20
|
.new-session { padding: 10px 12px; background: #22dd55; color: #000; border: none; border-radius: 8px; font-family: inherit; font-weight: bold; cursor: pointer; width: 100%; margin-bottom: 12px; }
|
|
20
21
|
.new-session:hover { background: #3dffa0; }
|
|
22
|
+
.settings-btn { padding: 8px 16px; background: #141414; color: #e0e0e0; border: 1px solid #22dd5533; border-radius: 8px; font-family: inherit; font-size: 12px; cursor: pointer; }
|
|
23
|
+
.settings-btn:hover { border-color: #22dd55; background: #1a1a1a; }
|
|
21
24
|
.chat { flex: 1; display: flex; flex-direction: column; overflow: hidden; }
|
|
22
25
|
.messages { flex: 1; overflow-y: auto; padding: 24px; display: flex; flex-direction: column; gap: 16px; }
|
|
23
26
|
.message { max-width: 70%; padding: 12px 16px; border-radius: 12px; line-height: 1.5; }
|
|
@@ -33,6 +36,40 @@ body { background: #000; color: #e0e0e0; font-family: "IBM Plex Mono", monospace
|
|
|
33
36
|
#sendBtn:disabled { background: #1a1a1a; color: #666; cursor: not-allowed; }
|
|
34
37
|
.empty-state { display: flex; flex-direction: column; align-items: center; justify-content: center; flex: 1; color: #666; }
|
|
35
38
|
.status-badge { display: inline-block; padding: 2px 8px; border-radius: 4px; background: #1a1a1a; border: 1px solid #22dd5533; color: #22dd55; font-size: 11px; margin-left: 8px; }
|
|
39
|
+
/* Settings panel */
|
|
40
|
+
.settings-overlay { position: fixed; inset: 0; background: rgba(0,0,0,0.6); z-index: 50; display: none; }
|
|
41
|
+
.settings-panel { position: fixed; top: 10%; left: 10%; right: 10%; bottom: 10%; background: #0a0a0a; border: 1px solid #22dd5533; border-radius: 12px; z-index: 60; display: none; flex-direction: column; overflow: hidden; }
|
|
42
|
+
.settings-panel.open, .settings-overlay.open { display: block; }
|
|
43
|
+
.settings-header { height: 50px; background: #0a0a0a; border-bottom: 1px solid #1a1a1a; display: flex; align-items: center; justify-content: space-between; padding: 0 20px; }
|
|
44
|
+
.settings-title { color: #22dd55; font-weight: bold; font-size: 1.1rem; }
|
|
45
|
+
.settings-close { background: transparent; border: 1px solid #22dd5533; color: #e0e0e0; border-radius: 6px; padding: 4px 12px; font-family: inherit; font-size: 12px; cursor: pointer; }
|
|
46
|
+
.settings-close:hover { border-color: #22dd55; }
|
|
47
|
+
.settings-tabs { display: flex; border-bottom: 1px solid #1a1a1a; }
|
|
48
|
+
.settings-tab { padding: 10px 16px; background: transparent; border: none; border-bottom: 2px solid transparent; color: #888; font-family: inherit; font-size: 12px; cursor: pointer; }
|
|
49
|
+
.settings-tab:hover { color: #e0e0e0; background: #141414; }
|
|
50
|
+
.settings-tab.active { color: #22dd55; border-bottom-color: #22dd55; }
|
|
51
|
+
.settings-content { flex: 1; overflow-y: auto; padding: 24px; }
|
|
52
|
+
.settings-section { margin-bottom: 30px; }
|
|
53
|
+
.settings-section-title { color: #888; font-size: 11px; text-transform: uppercase; letter-spacing: 1px; margin-bottom: 14px; }
|
|
54
|
+
.setting-row { display: flex; align-items: center; justify-content: space-between; padding: 10px 0; border-bottom: 1px solid #141414; }
|
|
55
|
+
.setting-row:last-child { border-bottom: none; }
|
|
56
|
+
.setting-label { color: #e0e0e0; font-size: 13px; }
|
|
57
|
+
.setting-value { color: #888; font-size: 12px; }
|
|
58
|
+
.setting-select, .setting-input { background: #000; border: 1px solid #22dd5533; color: #e0e0e0; padding: 6px 10px; border-radius: 6px; font-family: inherit; font-size: 12px; outline: none; }
|
|
59
|
+
.setting-select:focus, .setting-input:focus { border-color: #22dd55; }
|
|
60
|
+
.setting-checkbox { width: 18px; height: 18px; accent-color: #22dd55; background: #000; border: 1px solid #22dd5533; cursor: pointer; }
|
|
61
|
+
.provider-row { display: flex; align-items: center; justify-content: space-between; padding: 8px 0; border-bottom: 1px solid #141414; font-size: 12px; }
|
|
62
|
+
.provider-row:last-child { border-bottom: none; }
|
|
63
|
+
#providersList { max-height: 340px; overflow-y: auto; border: 1px solid #141414; border-radius: 6px; padding: 10px; background: #000; margin-top: 8px; }
|
|
64
|
+
.provider-info { display: flex; align-items: center; gap: 10px; flex: 1; }
|
|
65
|
+
.provider-name { font-weight: bold; color: #e0e0e0; }
|
|
66
|
+
.provider-id { color: #666; font-size: 11px; }
|
|
67
|
+
.provider-status { font-size: 11px; padding: 2px 8px; border-radius: 4px; }
|
|
68
|
+
.provider-status.free { background: #1a1a1a; color: #22dd55; border: 1px solid #22dd5533; }
|
|
69
|
+
.provider-status.keyed { background: #1a1a1a; color: #ff9900; border: 1px solid #ff990033; }
|
|
70
|
+
.btn-save { background: #22dd55; color: #000; border: none; border-radius: 6px; padding: 8px 16px; font-family: inherit; font-weight: bold; cursor: pointer; margin-top: 20px; }
|
|
71
|
+
.btn-save:hover { background: #3dffa0; }
|
|
72
|
+
.btn-save:disabled { background: #1a1a1a; color: #666; cursor: not-allowed; }
|
|
36
73
|
</style>
|
|
37
74
|
</head>
|
|
38
75
|
<body>
|
|
@@ -40,6 +77,7 @@ body { background: #000; color: #e0e0e0; font-family: "IBM Plex Mono", monospace
|
|
|
40
77
|
<div class="sidebar">
|
|
41
78
|
<div class="header">
|
|
42
79
|
<div class="logo">chode <span class="status-badge">serve</span></div>
|
|
80
|
+
<button class="settings-btn" id="settingsBtn">Settings</button>
|
|
43
81
|
</div>
|
|
44
82
|
<button class="new-session" id="newSessionBtn">New Session</button>
|
|
45
83
|
<div class="sessions" id="sessionsList">
|
|
@@ -66,9 +104,81 @@ body { background: #000; color: #e0e0e0; font-family: "IBM Plex Mono", monospace
|
|
|
66
104
|
</div>
|
|
67
105
|
</div>
|
|
68
106
|
</div>
|
|
107
|
+
<div class="settings-overlay" id="settingsOverlay"></div>
|
|
108
|
+
<div class="settings-panel" id="settingsPanel">
|
|
109
|
+
<div class="settings-header">
|
|
110
|
+
<div class="settings-title">chode settings</div>
|
|
111
|
+
<button class="settings-close" id="settingsClose">Close</button>
|
|
112
|
+
</div>
|
|
113
|
+
<div class="settings-tabs">
|
|
114
|
+
<button class="settings-tab active" data-tab="general">General</button>
|
|
115
|
+
<button class="settings-tab" data-tab="providers">Providers</button>
|
|
116
|
+
<button class="settings-tab" data-tab="session">Session</button>
|
|
117
|
+
</div>
|
|
118
|
+
<div class="settings-content">
|
|
119
|
+
<div class="settings-section" id="tab-general">
|
|
120
|
+
<div class="setting-row">
|
|
121
|
+
<span class="setting-label">Free mode</span>
|
|
122
|
+
<label style="display:flex;align-items:center;gap:8px;cursor:pointer;"><input type="checkbox" id="settingFreeOnly" class="setting-checkbox"><span style="color:#e0e0e0;font-size:12px;">Enable free (keyless) providers only</span></label>
|
|
123
|
+
</div>
|
|
124
|
+
<div class="setting-row">
|
|
125
|
+
<span class="setting-label">Default provider</span>
|
|
126
|
+
<select id="settingDefaultProvider" class="setting-select"></select>
|
|
127
|
+
</div>
|
|
128
|
+
<div class="setting-row">
|
|
129
|
+
<span class="setting-label">Mode</span>
|
|
130
|
+
<select id="settingMode" class="setting-select">
|
|
131
|
+
<option value="execute">Execute</option>
|
|
132
|
+
<option value="plan">Plan</option>
|
|
133
|
+
</select>
|
|
134
|
+
</div>
|
|
135
|
+
<div class="setting-row">
|
|
136
|
+
<span class="setting-label">Theme</span>
|
|
137
|
+
<select id="settingTheme" class="setting-select">
|
|
138
|
+
<option value="dark">Dark</option>
|
|
139
|
+
<option value="light">Light</option>
|
|
140
|
+
<option value="system">System</option>
|
|
141
|
+
</select>
|
|
142
|
+
</div>
|
|
143
|
+
<div class="setting-row">
|
|
144
|
+
<span class="setting-label">Details</span>
|
|
145
|
+
<label style="display:flex;align-items:center;gap:8px;cursor:pointer;"><input type="checkbox" id="settingDetails" class="setting-checkbox"><span style="color:#e0e0e0;font-size:12px;">Show tool and provider diagnostics</span></label>
|
|
146
|
+
</div>
|
|
147
|
+
<div class="setting-row">
|
|
148
|
+
<span class="setting-label">Timeout (seconds)</span>
|
|
149
|
+
<input type="number" id="settingTimeoutSeconds" class="setting-input" placeholder="e.g. 600">
|
|
150
|
+
</div>
|
|
151
|
+
<div class="setting-row">
|
|
152
|
+
<span class="setting-label">Acceptance test</span>
|
|
153
|
+
<input type="text" id="settingAcceptanceTest" class="setting-input" placeholder="path/to/test.cjs">
|
|
154
|
+
</div>
|
|
155
|
+
<div class="setting-row">
|
|
156
|
+
<span class="setting-label">Yolo mode</span>
|
|
157
|
+
<label style="display:flex;align-items:center;gap:8px;cursor:pointer;"><input type="checkbox" id="settingYolo" class="setting-checkbox"><span style="color:#e0e0e0;font-size:12px;">Skip confirmation prompts</span></label>
|
|
158
|
+
</div>
|
|
159
|
+
<button class="btn-save" id="settingsSave">Save settings</button>
|
|
160
|
+
</div>
|
|
161
|
+
<div class="settings-section" id="tab-providers" style="display:none;">
|
|
162
|
+
<div class="setting-section-title">Available providers</div>
|
|
163
|
+
<div id="providersList"></div>
|
|
164
|
+
</div>
|
|
165
|
+
<div class="settings-section" id="tab-session" style="display:none;">
|
|
166
|
+
<div class="setting-section-title">Current session</div>
|
|
167
|
+
<div class="setting-row">
|
|
168
|
+
<span class="setting-label">Session ID</span>
|
|
169
|
+
<span class="setting-value" id="settingSessionId">-</span>
|
|
170
|
+
</div>
|
|
171
|
+
<div class="setting-row">
|
|
172
|
+
<span class="setting-label">Messages</span>
|
|
173
|
+
<span class="setting-value" id="settingSessionMessages">0</span>
|
|
174
|
+
</div>
|
|
175
|
+
</div>
|
|
176
|
+
</div>
|
|
177
|
+
</div>
|
|
69
178
|
<script>
|
|
70
179
|
const API = "http://127.0.0.1:" + (window.location.port || "4096");
|
|
71
180
|
let currentSessionId = null;
|
|
181
|
+
let settingsOpen = false;
|
|
72
182
|
|
|
73
183
|
async function loadSessions() {
|
|
74
184
|
const res = await fetch(API + "/api/sessions");
|
|
@@ -82,6 +192,7 @@ async function loadSessions() {
|
|
|
82
192
|
div.onclick = function() { selectSession(s.id); };
|
|
83
193
|
list.appendChild(div);
|
|
84
194
|
});
|
|
195
|
+
updateSessionTab();
|
|
85
196
|
}
|
|
86
197
|
|
|
87
198
|
async function createSession() {
|
|
@@ -140,6 +251,92 @@ async function sendMessage() {
|
|
|
140
251
|
}
|
|
141
252
|
}
|
|
142
253
|
|
|
254
|
+
async function loadSettings() {
|
|
255
|
+
try {
|
|
256
|
+
const res = await fetch(API + "/api/settings");
|
|
257
|
+
const data = await res.json();
|
|
258
|
+
if (!data.ok) return;
|
|
259
|
+
var cfg = data.config || {};
|
|
260
|
+
var st = data.state || {};
|
|
261
|
+
document.getElementById("settingFreeOnly").checked = !!cfg.freeOnly;
|
|
262
|
+
document.getElementById("settingDefaultProvider").value = cfg.defaultProvider || "auto";
|
|
263
|
+
document.getElementById("settingMode").value = st.mode || "execute";
|
|
264
|
+
document.getElementById("settingTheme").value = st.theme || "dark";
|
|
265
|
+
document.getElementById("settingDetails").checked = !!st.details;
|
|
266
|
+
document.getElementById("settingTimeoutSeconds").value = st.timeoutSeconds || "";
|
|
267
|
+
document.getElementById("settingAcceptanceTest").value = st.acceptanceTest || "";
|
|
268
|
+
document.getElementById("settingYolo").checked = !!st.yolo;
|
|
269
|
+
} catch(e) {}
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
async function saveSettings() {
|
|
273
|
+
var body = {
|
|
274
|
+
freeOnly: document.getElementById("settingFreeOnly").checked,
|
|
275
|
+
defaultProvider: document.getElementById("settingDefaultProvider").value,
|
|
276
|
+
mode: document.getElementById("settingMode").value,
|
|
277
|
+
theme: document.getElementById("settingTheme").value,
|
|
278
|
+
details: document.getElementById("settingDetails").checked,
|
|
279
|
+
timeoutSeconds: document.getElementById("settingTimeoutSeconds").value || null,
|
|
280
|
+
acceptanceTest: document.getElementById("settingAcceptanceTest").value || null,
|
|
281
|
+
yolo: document.getElementById("settingYolo").checked,
|
|
282
|
+
};
|
|
283
|
+
try {
|
|
284
|
+
const res = await fetch(API + "/api/settings", {
|
|
285
|
+
method: "POST",
|
|
286
|
+
headers: { "Content-Type": "application/json" },
|
|
287
|
+
body: JSON.stringify(body)
|
|
288
|
+
});
|
|
289
|
+
const data = await res.json();
|
|
290
|
+
if (data.ok) {
|
|
291
|
+
// Refresh provider list for default provider select
|
|
292
|
+
await loadProviders();
|
|
293
|
+
}
|
|
294
|
+
} catch(e) {}
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
async function loadProviders() {
|
|
298
|
+
try {
|
|
299
|
+
const res = await fetch(API + "/api/providers");
|
|
300
|
+
const data = await res.json();
|
|
301
|
+
var select = document.getElementById("settingDefaultProvider");
|
|
302
|
+
var list = document.getElementById("providersList");
|
|
303
|
+
select.innerHTML = '<option value="auto">auto</option>';
|
|
304
|
+
list.innerHTML = '';
|
|
305
|
+
(data.providers || []).forEach(function(p) {
|
|
306
|
+
var opt = document.createElement("option");
|
|
307
|
+
opt.value = p.id;
|
|
308
|
+
opt.textContent = p.name || p.id;
|
|
309
|
+
select.appendChild(opt);
|
|
310
|
+
var row = document.createElement("div");
|
|
311
|
+
row.className = "provider-row";
|
|
312
|
+
var statusCls = p.requiresKey ? "keyed" : "free";
|
|
313
|
+
var statusText = p.requiresKey ? "needs API key" : "free";
|
|
314
|
+
var extra = p.builtIn ? "" : ' <span style="font-size:10px;color:#888;margin-left:6px;">(' + (p.models||0) + ' models)</span>';
|
|
315
|
+
row.innerHTML =
|
|
316
|
+
'<div class="provider-info">' +
|
|
317
|
+
'<div class="provider-name">' + escapeHtml(p.name || p.id) + extra + '</div>' +
|
|
318
|
+
'<div class="provider-id">' + escapeHtml(p.id) + (p.baseUrl ? ' · ' + escapeHtml(String(p.baseUrl).slice(0,40)) : '') + '</div>' +
|
|
319
|
+
'</div>' +
|
|
320
|
+
'<div class="provider-status ' + statusCls + '">' + statusText + '</div>';
|
|
321
|
+
list.appendChild(row);
|
|
322
|
+
});
|
|
323
|
+
} catch(e) {}
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
function updateSessionTab() {
|
|
327
|
+
var sidEl = document.getElementById("settingSessionId");
|
|
328
|
+
var msgEl = document.getElementById("settingSessionMessages");
|
|
329
|
+
if (currentSessionId) {
|
|
330
|
+
sidEl.textContent = currentSessionId;
|
|
331
|
+
// count messages from the session list item
|
|
332
|
+
var item = Array.from(document.querySelectorAll(".session-item")).find(function(el){ return el.textContent && el.textContent.includes(currentSessionId); });
|
|
333
|
+
msgEl.textContent = "loaded";
|
|
334
|
+
} else {
|
|
335
|
+
sidEl.textContent = "-";
|
|
336
|
+
msgEl.textContent = "0";
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
|
|
143
340
|
function addMessage(role, content, scroll) {
|
|
144
341
|
var messagesEl = document.getElementById("messages");
|
|
145
342
|
var div = document.createElement("div");
|
|
@@ -155,11 +352,38 @@ function escapeHtml(text) {
|
|
|
155
352
|
return div.innerHTML;
|
|
156
353
|
}
|
|
157
354
|
|
|
355
|
+
function openSettings() {
|
|
356
|
+
document.getElementById("settingsOverlay").classList.add("open");
|
|
357
|
+
document.getElementById("settingsPanel").classList.add("open");
|
|
358
|
+
settingsOpen = true;
|
|
359
|
+
loadSettings();
|
|
360
|
+
loadProviders();
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
function closeSettings() {
|
|
364
|
+
document.getElementById("settingsOverlay").classList.remove("open");
|
|
365
|
+
document.getElementById("settingsPanel").classList.remove("open");
|
|
366
|
+
settingsOpen = false;
|
|
367
|
+
}
|
|
368
|
+
|
|
158
369
|
document.getElementById("newSessionBtn").onclick = createSession;
|
|
159
370
|
document.getElementById("sendBtn").onclick = sendMessage;
|
|
160
371
|
document.getElementById("messageInput").addEventListener("keypress", function(e) {
|
|
161
372
|
if (e.key === "Enter") sendMessage();
|
|
162
373
|
});
|
|
374
|
+
document.getElementById("settingsBtn").onclick = openSettings;
|
|
375
|
+
document.getElementById("settingsClose").onclick = closeSettings;
|
|
376
|
+
document.getElementById("settingsOverlay").onclick = function(e) { if (e.target === this) closeSettings(); };
|
|
377
|
+
document.getElementById("settingsSave").onclick = function() { saveSettings(); closeSettings(); };
|
|
378
|
+
|
|
379
|
+
document.querySelectorAll(".settings-tab").forEach(function(tab) {
|
|
380
|
+
tab.onclick = function() {
|
|
381
|
+
document.querySelectorAll(".settings-tab").forEach(function(t){ t.classList.remove("active"); });
|
|
382
|
+
tab.classList.add("active");
|
|
383
|
+
document.querySelectorAll(".settings-section").forEach(function(s){ s.style.display = "none"; });
|
|
384
|
+
document.getElementById("tab-" + tab.getAttribute("data-tab")).style.display = "block";
|
|
385
|
+
};
|
|
386
|
+
});
|
|
163
387
|
|
|
164
388
|
loadSessions();
|
|
165
389
|
setTimeout(async function() {
|
|
@@ -169,4 +393,4 @@ setTimeout(async function() {
|
|
|
169
393
|
}, 500);
|
|
170
394
|
</script>
|
|
171
395
|
</body>
|
|
172
|
-
</html>
|
|
396
|
+
</html>
|