@oooooooooo/ch0de 1.9.56 → 1.9.58
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 +141 -0
- package/package.json +2 -2
- package/serve-frontend.html +340 -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,50 @@ function cmdServe(argv) {
|
|
|
7296
7298
|
return 'ses_' + crypto.randomBytes(8).toString('hex');
|
|
7297
7299
|
}
|
|
7298
7300
|
|
|
7301
|
+
function verifyProviderKey(baseUrl, key) {
|
|
7302
|
+
return new Promise(function(resolve){
|
|
7303
|
+
try {
|
|
7304
|
+
var u = new URL(baseUrl + '/models');
|
|
7305
|
+
var req = http.request(u, {
|
|
7306
|
+
method: 'GET',
|
|
7307
|
+
headers: {'Authorization': 'Bearer ' + key, 'Accept': 'application/json'},
|
|
7308
|
+
timeout: 8000
|
|
7309
|
+
}, function(resp){
|
|
7310
|
+
var working = resp.statusCode >= 200 && resp.statusCode < 400;
|
|
7311
|
+
resp.resume();
|
|
7312
|
+
resolve(working);
|
|
7313
|
+
});
|
|
7314
|
+
req.on('error', function(){ resolve(false); });
|
|
7315
|
+
req.on('timeout', function(){ req.destroy(); resolve(false); });
|
|
7316
|
+
req.end();
|
|
7317
|
+
} catch(e) { resolve(false); }
|
|
7318
|
+
});
|
|
7319
|
+
}
|
|
7320
|
+
|
|
7321
|
+
function providerInfo(pid) {
|
|
7322
|
+
var info = null;
|
|
7323
|
+
try {
|
|
7324
|
+
for (var id in PROVIDERS) { if (id === pid) { info = PROVIDERS[id]; break; } }
|
|
7325
|
+
} catch(e) {}
|
|
7326
|
+
if (info) return info;
|
|
7327
|
+
try {
|
|
7328
|
+
var dir = require('./provider-directory');
|
|
7329
|
+
var listed = dir.list();
|
|
7330
|
+
for (var i = 0; i < listed.length; i++) { if (listed[i].id === pid) { info = listed[i]; break; } }
|
|
7331
|
+
} catch(e) {}
|
|
7332
|
+
return info;
|
|
7333
|
+
}
|
|
7334
|
+
|
|
7335
|
+
function loadServeConfig() {
|
|
7336
|
+
try { return JSON.parse(fs.readFileSync(CONFIG_PATH, 'utf8')); } catch(e) { return { freeOnly: true, defaultProvider: 'auto', providers: {}, connections: {} }; }
|
|
7337
|
+
}
|
|
7338
|
+
|
|
7339
|
+
function saveServeConfig(cfg) {
|
|
7340
|
+
try { fs.writeFileSync(CONFIG_PATH, JSON.stringify(cfg, null, 2), 'utf8'); } catch(e) {}
|
|
7341
|
+
}
|
|
7342
|
+
|
|
7343
|
+
var serveState = { provider: 'auto', mode: 'execute', theme: 'dark', details: false, timeoutSeconds: null, acceptanceTest: null, yolo: false };
|
|
7344
|
+
|
|
7299
7345
|
function json(res, code, obj) {
|
|
7300
7346
|
res.writeHead(code, {'Content-Type': 'application/json', 'Access-Control-Allow-Origin': '*'});
|
|
7301
7347
|
res.end(JSON.stringify(obj));
|
|
@@ -7393,6 +7439,101 @@ function cmdServe(argv) {
|
|
|
7393
7439
|
}
|
|
7394
7440
|
}
|
|
7395
7441
|
|
|
7442
|
+
if (pathname === '/api/settings' && req.method === 'GET') {
|
|
7443
|
+
var cfg = loadServeConfig();
|
|
7444
|
+
json(res, 200, {ok: true, config: cfg, state: serveState});
|
|
7445
|
+
return;
|
|
7446
|
+
}
|
|
7447
|
+
|
|
7448
|
+
if (pathname === '/api/settings' && req.method === 'POST') {
|
|
7449
|
+
readBody(req).then(function(body){
|
|
7450
|
+
var parsed;
|
|
7451
|
+
try { parsed = JSON.parse(body || '{}'); } catch(e) { json(res, 400, {ok:false, error:'invalid json'}); return; }
|
|
7452
|
+
var cfg = loadServeConfig();
|
|
7453
|
+
if (parsed.freeOnly !== undefined) cfg.freeOnly = !!parsed.freeOnly;
|
|
7454
|
+
if (parsed.defaultProvider !== undefined) cfg.defaultProvider = parsed.defaultProvider;
|
|
7455
|
+
if (parsed.mode !== undefined) serveState.mode = parsed.mode;
|
|
7456
|
+
if (parsed.theme !== undefined) serveState.theme = parsed.theme;
|
|
7457
|
+
if (parsed.details !== undefined) serveState.details = !!parsed.details;
|
|
7458
|
+
if (parsed.timeoutSeconds !== undefined) serveState.timeoutSeconds = parsed.timeoutSeconds ? Number(parsed.timeoutSeconds) : null;
|
|
7459
|
+
if (parsed.acceptanceTest !== undefined) serveState.acceptanceTest = parsed.acceptanceTest || null;
|
|
7460
|
+
if (parsed.yolo !== undefined) serveState.yolo = !!parsed.yolo;
|
|
7461
|
+
if (parsed.provider !== undefined) serveState.provider = parsed.provider;
|
|
7462
|
+
saveServeConfig(cfg);
|
|
7463
|
+
json(res, 200, {ok: true, config: cfg, state: serveState});
|
|
7464
|
+
}).catch(function(e){ json(res, 500, {ok:false, error: e.message}); });
|
|
7465
|
+
return;
|
|
7466
|
+
}
|
|
7467
|
+
|
|
7468
|
+
if (pathname === '/api/providers' && req.method === 'GET') {
|
|
7469
|
+
var out = [];
|
|
7470
|
+
try {
|
|
7471
|
+
var cfg = loadServeConfig();
|
|
7472
|
+
var keys = cfg.providerKeys || {};
|
|
7473
|
+
for (var id in PROVIDERS) {
|
|
7474
|
+
var p = PROVIDERS[id];
|
|
7475
|
+
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, hasKey: !!keys[id]});
|
|
7476
|
+
}
|
|
7477
|
+
var dir = require('./provider-directory');
|
|
7478
|
+
var listed = dir.list();
|
|
7479
|
+
var ids = {};
|
|
7480
|
+
out.forEach(function(x){ids[x.id]=true;});
|
|
7481
|
+
listed.forEach(function(p){
|
|
7482
|
+
if (!ids[p.id]) {
|
|
7483
|
+
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, hasKey: !!keys[p.id]});
|
|
7484
|
+
}
|
|
7485
|
+
});
|
|
7486
|
+
} catch(e) {}
|
|
7487
|
+
json(res, 200, {ok: true, providers: out});
|
|
7488
|
+
}
|
|
7489
|
+
|
|
7490
|
+
var keyM = pathname.match(/^\/api\/provider\/([^\/]+)\/key(\/test)?$/);
|
|
7491
|
+
if (keyM) {
|
|
7492
|
+
var pid = keyM[1];
|
|
7493
|
+
var isTest = !!keyM[2];
|
|
7494
|
+
if (req.method === 'GET' && !isTest) {
|
|
7495
|
+
var cfg2 = loadServeConfig();
|
|
7496
|
+
var keys2 = cfg2.providerKeys || {};
|
|
7497
|
+
json(res, 200, {ok: true, hasKey: !!keys2[pid]});
|
|
7498
|
+
return;
|
|
7499
|
+
}
|
|
7500
|
+
if (req.method === 'POST' && !isTest) {
|
|
7501
|
+
readBody(req).then(function(body){
|
|
7502
|
+
var parsed;
|
|
7503
|
+
try { parsed = JSON.parse(body || '{}'); } catch(e) { json(res, 400, {ok:false, error:'invalid json'}); return; }
|
|
7504
|
+
var key = String(parsed.key || '');
|
|
7505
|
+
var cfg3 = loadServeConfig();
|
|
7506
|
+
if (!cfg3.providerKeys) cfg3.providerKeys = {};
|
|
7507
|
+
cfg3.providerKeys[pid] = key;
|
|
7508
|
+
saveServeConfig(cfg3);
|
|
7509
|
+
var pinfo = providerInfo(pid);
|
|
7510
|
+
if (pinfo && pinfo.baseUrl) {
|
|
7511
|
+
verifyProviderKey(pinfo.baseUrl, key).then(function(working){
|
|
7512
|
+
json(res, 200, {ok: true, working: working});
|
|
7513
|
+
}).catch(function(){ json(res, 200, {ok: true, working: false}); });
|
|
7514
|
+
} else {
|
|
7515
|
+
json(res, 200, {ok: true, working: false});
|
|
7516
|
+
}
|
|
7517
|
+
}).catch(function(e){ json(res, 500, {ok:false, error: e.message}); });
|
|
7518
|
+
return;
|
|
7519
|
+
}
|
|
7520
|
+
if (req.method === 'GET' && isTest) {
|
|
7521
|
+
var cfg4 = loadServeConfig();
|
|
7522
|
+
var keys4 = cfg4.providerKeys || {};
|
|
7523
|
+
var key = String(keys4[pid] || '');
|
|
7524
|
+
if (!key) { json(res, 200, {ok: true, working: false, hasKey: false}); return; }
|
|
7525
|
+
var pinfo2 = providerInfo(pid);
|
|
7526
|
+
if (pinfo2 && pinfo2.baseUrl) {
|
|
7527
|
+
verifyProviderKey(pinfo2.baseUrl, key).then(function(working){
|
|
7528
|
+
json(res, 200, {ok: true, working: working, hasKey: true});
|
|
7529
|
+
}).catch(function(){ json(res, 200, {ok: true, working: false, hasKey: true}); });
|
|
7530
|
+
} else {
|
|
7531
|
+
json(res, 200, {ok: true, working: false, hasKey: true});
|
|
7532
|
+
}
|
|
7533
|
+
return;
|
|
7534
|
+
}
|
|
7535
|
+
}
|
|
7536
|
+
|
|
7396
7537
|
res.writeHead(404, {'Content-Type': 'text/plain'});
|
|
7397
7538
|
res.end('Not found');
|
|
7398
7539
|
});
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@oooooooooo/ch0de",
|
|
3
|
-
"version": "1.9.
|
|
3
|
+
"version": "1.9.58",
|
|
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": {
|
|
@@ -151,7 +151,7 @@
|
|
|
151
151
|
},
|
|
152
152
|
"dependencies": {
|
|
153
153
|
"@modelcontextprotocol/sdk": "1.32.2",
|
|
154
|
-
"@oooooooooo/ch0de": "^1.9.
|
|
154
|
+
"@oooooooooo/ch0de": "^1.9.56",
|
|
155
155
|
"lean": "^0.1.1"
|
|
156
156
|
},
|
|
157
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,59 @@ 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; cursor: pointer; }
|
|
62
|
+
.provider-row:last-child { border-bottom: none; }
|
|
63
|
+
.provider-row:hover { background: #141414; }
|
|
64
|
+
#providersList { max-height: 340px; overflow-y: auto; border: 1px solid #141414; border-radius: 6px; padding: 10px; background: #000; margin-top: 8px; }
|
|
65
|
+
.provider-info { display: flex; align-items: center; gap: 10px; flex: 1; }
|
|
66
|
+
.provider-name { font-weight: bold; color: #e0e0e0; }
|
|
67
|
+
.provider-id { color: #666; font-size: 11px; }
|
|
68
|
+
.provider-status { font-size: 11px; padding: 2px 8px; border-radius: 4px; }
|
|
69
|
+
.provider-status.free { background: #1a1a1a; color: #22dd55; border: 1px solid #22dd5533; }
|
|
70
|
+
.provider-status.keyed { background: #1a1a1a; color: #ff9900; border: 1px solid #ff990033; }
|
|
71
|
+
.provider-status.haskey { background: #1a1a1a; color: #22dd55; border: 1px solid #22dd5533; }
|
|
72
|
+
.provider-status.working { background: #1a1a1a; color: #22dd55; border: 1px solid #22dd55; }
|
|
73
|
+
.provider-status.badkey { background: #1a1a1a; color: #ff4444; border: 1px solid #ff444433; }
|
|
74
|
+
.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; }
|
|
75
|
+
.btn-save:hover { background: #3dffa0; }
|
|
76
|
+
.btn-save:disabled { background: #1a1a1a; color: #666; cursor: not-allowed; }
|
|
77
|
+
|
|
78
|
+
/* Key input modal */
|
|
79
|
+
.key-modal-overlay { position: fixed; inset: 0; background: rgba(0,0,0,0.7); z-index: 100; display: none; }
|
|
80
|
+
.key-modal { position: fixed; top: 20%; left: 25%; right: 25%; background: #0a0a0a; border: 1px solid #22dd5533; border-radius: 12px; z-index: 110; padding: 24px; display: none; box-shadow: 0 4px 20px rgba(0,0,0,0.4); }
|
|
81
|
+
.key-modal.open, .key-modal-overlay.open { display: block; }
|
|
82
|
+
.key-modal-title { color: #22dd55; font-weight: bold; font-size: 1.1rem; margin-bottom: 16px; }
|
|
83
|
+
.key-modal-field { margin-bottom: 16px; }
|
|
84
|
+
.key-modal-label { display: block; color: #888; font-size: 11px; text-transform: uppercase; letter-spacing: 1px; margin-bottom: 8px; }
|
|
85
|
+
.key-modal-input { width: 100%; background: #000; border: 1px solid #22dd5533; color: #e0e0e0; padding: 10px 14px; border-radius: 6px; font-family: inherit; font-size: 13px; outline: none; }
|
|
86
|
+
.key-modal-input:focus { border-color: #22dd55; }
|
|
87
|
+
.key-modal-actions { display: flex; gap: 10px; margin-top: 20px; }
|
|
88
|
+
.key-status { margin-top: 12px; padding: 8px 12px; border-radius: 6px; font-size: 12px; display: none; }
|
|
89
|
+
.key-status.ok { background: #141414; color: #22dd55; border: 1px solid #22dd5533; display: block; }
|
|
90
|
+
.key-status.err { background: #141414; color: #ff4444; border: 1px solid #ff444433; display: block; }
|
|
91
|
+
.key-status.saving { background: #141414; color: #ff9900; border: 1px solid #ff990033; display: block; }
|
|
36
92
|
</style>
|
|
37
93
|
</head>
|
|
38
94
|
<body>
|
|
@@ -40,6 +96,7 @@ body { background: #000; color: #e0e0e0; font-family: "IBM Plex Mono", monospace
|
|
|
40
96
|
<div class="sidebar">
|
|
41
97
|
<div class="header">
|
|
42
98
|
<div class="logo">chode <span class="status-badge">serve</span></div>
|
|
99
|
+
<button class="settings-btn" id="settingsBtn">Settings</button>
|
|
43
100
|
</div>
|
|
44
101
|
<button class="new-session" id="newSessionBtn">New Session</button>
|
|
45
102
|
<div class="sessions" id="sessionsList">
|
|
@@ -66,9 +123,94 @@ body { background: #000; color: #e0e0e0; font-family: "IBM Plex Mono", monospace
|
|
|
66
123
|
</div>
|
|
67
124
|
</div>
|
|
68
125
|
</div>
|
|
126
|
+
<div class="settings-overlay" id="settingsOverlay"></div>
|
|
127
|
+
<div class="settings-panel" id="settingsPanel">
|
|
128
|
+
<div class="settings-header">
|
|
129
|
+
<div class="settings-title">chode settings</div>
|
|
130
|
+
<button class="settings-close" id="settingsClose">Close</button>
|
|
131
|
+
</div>
|
|
132
|
+
<div class="settings-tabs">
|
|
133
|
+
<button class="settings-tab active" data-tab="general">General</button>
|
|
134
|
+
<button class="settings-tab" data-tab="providers">Providers</button>
|
|
135
|
+
<button class="settings-tab" data-tab="session">Session</button>
|
|
136
|
+
</div>
|
|
137
|
+
<div class="settings-content">
|
|
138
|
+
<div class="settings-section" id="tab-general">
|
|
139
|
+
<div class="setting-row">
|
|
140
|
+
<span class="setting-label">Free mode</span>
|
|
141
|
+
<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>
|
|
142
|
+
</div>
|
|
143
|
+
<div class="setting-row">
|
|
144
|
+
<span class="setting-label">Default provider</span>
|
|
145
|
+
<select id="settingDefaultProvider" class="setting-select"></select>
|
|
146
|
+
</div>
|
|
147
|
+
<div class="setting-row">
|
|
148
|
+
<span class="setting-label">Mode</span>
|
|
149
|
+
<select id="settingMode" class="setting-select">
|
|
150
|
+
<option value="execute">Execute</option>
|
|
151
|
+
<option value="plan">Plan</option>
|
|
152
|
+
</select>
|
|
153
|
+
</div>
|
|
154
|
+
<div class="setting-row">
|
|
155
|
+
<span class="setting-label">Theme</span>
|
|
156
|
+
<select id="settingTheme" class="setting-select">
|
|
157
|
+
<option value="dark">Dark</option>
|
|
158
|
+
<option value="light">Light</option>
|
|
159
|
+
<option value="system">System</option>
|
|
160
|
+
</select>
|
|
161
|
+
</div>
|
|
162
|
+
<div class="setting-row">
|
|
163
|
+
<span class="setting-label">Details</span>
|
|
164
|
+
<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>
|
|
165
|
+
</div>
|
|
166
|
+
<div class="setting-row">
|
|
167
|
+
<span class="setting-label">Timeout (seconds)</span>
|
|
168
|
+
<input type="number" id="settingTimeoutSeconds" class="setting-input" placeholder="e.g. 600">
|
|
169
|
+
</div>
|
|
170
|
+
<div class="setting-row">
|
|
171
|
+
<span class="setting-label">Acceptance test</span>
|
|
172
|
+
<input type="text" id="settingAcceptanceTest" class="setting-input" placeholder="path/to/test.cjs">
|
|
173
|
+
</div>
|
|
174
|
+
<div class="setting-row">
|
|
175
|
+
<span class="setting-label">Yolo mode</span>
|
|
176
|
+
<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>
|
|
177
|
+
</div>
|
|
178
|
+
<button class="btn-save" id="settingsSave">Save settings</button>
|
|
179
|
+
</div>
|
|
180
|
+
<div class="settings-section" id="tab-providers" style="display:none;">
|
|
181
|
+
<div class="setting-section-title">Available providers</div>
|
|
182
|
+
<div id="providersList"></div>
|
|
183
|
+
</div>
|
|
184
|
+
<div class="settings-section" id="tab-session" style="display:none;">
|
|
185
|
+
<div class="setting-section-title">Current session</div>
|
|
186
|
+
<div class="setting-row">
|
|
187
|
+
<span class="setting-label">Session ID</span>
|
|
188
|
+
<span class="setting-value" id="settingSessionId">-</span>
|
|
189
|
+
</div>
|
|
190
|
+
<div class="setting-row">
|
|
191
|
+
<span class="setting-label">Messages</span>
|
|
192
|
+
<span class="setting-value" id="settingSessionMessages">0</span>
|
|
193
|
+
</div>
|
|
194
|
+
</div>
|
|
195
|
+
</div>
|
|
196
|
+
</div>
|
|
197
|
+
<div class="key-modal-overlay" id="keyModalOverlay"></div>
|
|
198
|
+
<div class="key-modal" id="keyModal">
|
|
199
|
+
<div class="key-modal-title" id="keyModalTitle">API Key</div>
|
|
200
|
+
<div class="key-modal-field">
|
|
201
|
+
<span class="key-modal-label" id="keyModalLabel">API Key</span>
|
|
202
|
+
<input type="password" id="keyModalInput" class="key-modal-input" placeholder="Enter API key">
|
|
203
|
+
</div>
|
|
204
|
+
<div class="key-modal-actions">
|
|
205
|
+
<button class="btn-save" id="keyCancelBtn" style="background:#141414;color:#e0e0e0;border:1px solid #22dd5533;">Cancel</button>
|
|
206
|
+
<button class="btn-save" id="keySubmitBtn">Save & Test</button>
|
|
207
|
+
</div>
|
|
208
|
+
<div class="key-status" id="keyStatus"></div>
|
|
209
|
+
</div>
|
|
69
210
|
<script>
|
|
70
211
|
const API = "http://127.0.0.1:" + (window.location.port || "4096");
|
|
71
212
|
let currentSessionId = null;
|
|
213
|
+
let settingsOpen = false;
|
|
72
214
|
|
|
73
215
|
async function loadSessions() {
|
|
74
216
|
const res = await fetch(API + "/api/sessions");
|
|
@@ -82,6 +224,7 @@ async function loadSessions() {
|
|
|
82
224
|
div.onclick = function() { selectSession(s.id); };
|
|
83
225
|
list.appendChild(div);
|
|
84
226
|
});
|
|
227
|
+
updateSessionTab();
|
|
85
228
|
}
|
|
86
229
|
|
|
87
230
|
async function createSession() {
|
|
@@ -140,6 +283,95 @@ async function sendMessage() {
|
|
|
140
283
|
}
|
|
141
284
|
}
|
|
142
285
|
|
|
286
|
+
async function loadSettings() {
|
|
287
|
+
try {
|
|
288
|
+
const res = await fetch(API + "/api/settings");
|
|
289
|
+
const data = await res.json();
|
|
290
|
+
if (!data.ok) return;
|
|
291
|
+
var cfg = data.config || {};
|
|
292
|
+
var st = data.state || {};
|
|
293
|
+
document.getElementById("settingFreeOnly").checked = !!cfg.freeOnly;
|
|
294
|
+
document.getElementById("settingDefaultProvider").value = cfg.defaultProvider || "auto";
|
|
295
|
+
document.getElementById("settingMode").value = st.mode || "execute";
|
|
296
|
+
document.getElementById("settingTheme").value = st.theme || "dark";
|
|
297
|
+
document.getElementById("settingDetails").checked = !!st.details;
|
|
298
|
+
document.getElementById("settingTimeoutSeconds").value = st.timeoutSeconds || "";
|
|
299
|
+
document.getElementById("settingAcceptanceTest").value = st.acceptanceTest || "";
|
|
300
|
+
document.getElementById("settingYolo").checked = !!st.yolo;
|
|
301
|
+
} catch(e) {}
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
async function saveSettings() {
|
|
305
|
+
var body = {
|
|
306
|
+
freeOnly: document.getElementById("settingFreeOnly").checked,
|
|
307
|
+
defaultProvider: document.getElementById("settingDefaultProvider").value,
|
|
308
|
+
mode: document.getElementById("settingMode").value,
|
|
309
|
+
theme: document.getElementById("settingTheme").value,
|
|
310
|
+
details: document.getElementById("settingDetails").checked,
|
|
311
|
+
timeoutSeconds: document.getElementById("settingTimeoutSeconds").value || null,
|
|
312
|
+
acceptanceTest: document.getElementById("settingAcceptanceTest").value || null,
|
|
313
|
+
yolo: document.getElementById("settingYolo").checked,
|
|
314
|
+
};
|
|
315
|
+
try {
|
|
316
|
+
const res = await fetch(API + "/api/settings", {
|
|
317
|
+
method: "POST",
|
|
318
|
+
headers: { "Content-Type": "application/json" },
|
|
319
|
+
body: JSON.stringify(body)
|
|
320
|
+
});
|
|
321
|
+
const data = await res.json();
|
|
322
|
+
if (data.ok) {
|
|
323
|
+
// Refresh provider list for default provider select
|
|
324
|
+
await loadProviders();
|
|
325
|
+
}
|
|
326
|
+
} catch(e) {}
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
async function loadProviders() {
|
|
330
|
+
try {
|
|
331
|
+
const res = await fetch(API + "/api/providers");
|
|
332
|
+
const data = await res.json();
|
|
333
|
+
var select = document.getElementById("settingDefaultProvider");
|
|
334
|
+
var list = document.getElementById("providersList");
|
|
335
|
+
select.innerHTML = '<option value="auto">auto</option>';
|
|
336
|
+
list.innerHTML = '';
|
|
337
|
+
(data.providers || []).forEach(function(p) {
|
|
338
|
+
var opt = document.createElement("option");
|
|
339
|
+
opt.value = p.id;
|
|
340
|
+
opt.textContent = p.name || p.id;
|
|
341
|
+
select.appendChild(opt);
|
|
342
|
+
var row = document.createElement("div");
|
|
343
|
+
row.className = "provider-row";
|
|
344
|
+
var statusCls, statusText;
|
|
345
|
+
if (!p.requiresKey) { statusCls = "free"; statusText = "free"; }
|
|
346
|
+
else if (p.hasKey) { statusCls = "working"; statusText = "key saved"; }
|
|
347
|
+
else { statusCls = "keyed"; statusText = "needs API key"; }
|
|
348
|
+
var extra = p.builtIn ? "" : ' <span style="font-size:10px;color:#888;margin-left:6px;">(' + (p.models||0) + ' models)</span>';
|
|
349
|
+
row.innerHTML =
|
|
350
|
+
'<div class="provider-info">' +
|
|
351
|
+
'<div class="provider-name">' + escapeHtml(p.name || p.id) + extra + '</div>' +
|
|
352
|
+
'<div class="provider-id">' + escapeHtml(p.id) + (p.baseUrl ? ' · ' + escapeHtml(String(p.baseUrl).slice(0,40)) : '') + '</div>' +
|
|
353
|
+
'</div>' +
|
|
354
|
+
'<div class="provider-status ' + statusCls + '">' + statusText + '</div>';
|
|
355
|
+
row.onclick = function() { openKeyModal(p); };
|
|
356
|
+
list.appendChild(row);
|
|
357
|
+
});
|
|
358
|
+
} catch(e) {}
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
function updateSessionTab() {
|
|
362
|
+
var sidEl = document.getElementById("settingSessionId");
|
|
363
|
+
var msgEl = document.getElementById("settingSessionMessages");
|
|
364
|
+
if (currentSessionId) {
|
|
365
|
+
sidEl.textContent = currentSessionId;
|
|
366
|
+
// count messages from the session list item
|
|
367
|
+
var item = Array.from(document.querySelectorAll(".session-item")).find(function(el){ return el.textContent && el.textContent.includes(currentSessionId); });
|
|
368
|
+
msgEl.textContent = "loaded";
|
|
369
|
+
} else {
|
|
370
|
+
sidEl.textContent = "-";
|
|
371
|
+
msgEl.textContent = "0";
|
|
372
|
+
}
|
|
373
|
+
}
|
|
374
|
+
|
|
143
375
|
function addMessage(role, content, scroll) {
|
|
144
376
|
var messagesEl = document.getElementById("messages");
|
|
145
377
|
var div = document.createElement("div");
|
|
@@ -155,11 +387,118 @@ function escapeHtml(text) {
|
|
|
155
387
|
return div.innerHTML;
|
|
156
388
|
}
|
|
157
389
|
|
|
390
|
+
function openSettings() {
|
|
391
|
+
document.getElementById("settingsOverlay").classList.add("open");
|
|
392
|
+
document.getElementById("settingsPanel").classList.add("open");
|
|
393
|
+
settingsOpen = true;
|
|
394
|
+
loadSettings();
|
|
395
|
+
loadProviders();
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
function closeSettings() {
|
|
399
|
+
document.getElementById("settingsOverlay").classList.remove("open");
|
|
400
|
+
document.getElementById("settingsPanel").classList.remove("open");
|
|
401
|
+
settingsOpen = false;
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
var keyModalProvider = null;
|
|
405
|
+
function openKeyModal(p) {
|
|
406
|
+
keyModalProvider = p;
|
|
407
|
+
document.getElementById("keyModalTitle").textContent = "API Key · " + (p.name || p.id);
|
|
408
|
+
document.getElementById("keyModalLabel").textContent = p.env ? ("API Key (" + p.env + ")") : "API Key";
|
|
409
|
+
document.getElementById("keyModalInput").value = "";
|
|
410
|
+
document.getElementById("keyStatus").style.display = "none";
|
|
411
|
+
document.getElementById("keyModalOverlay").classList.add("open");
|
|
412
|
+
document.getElementById("keyModal").classList.add("open");
|
|
413
|
+
document.getElementById("keyModalInput").focus();
|
|
414
|
+
}
|
|
415
|
+
function closeKeyModal() {
|
|
416
|
+
document.getElementById("keyModalOverlay").classList.remove("open");
|
|
417
|
+
document.getElementById("keyModal").classList.remove("open");
|
|
418
|
+
keyModalProvider = null;
|
|
419
|
+
}
|
|
420
|
+
async function submitKey() {
|
|
421
|
+
if (!keyModalProvider) return;
|
|
422
|
+
var input = document.getElementById("keyModalInput");
|
|
423
|
+
var key = input.value.trim();
|
|
424
|
+
if (!key) return;
|
|
425
|
+
var status = document.getElementById("keyStatus");
|
|
426
|
+
var btn = document.getElementById("keySubmitBtn");
|
|
427
|
+
status.textContent = "Saving and testing key…";
|
|
428
|
+
status.className = "key-status saving";
|
|
429
|
+
status.style.display = "block";
|
|
430
|
+
btn.disabled = true;
|
|
431
|
+
try {
|
|
432
|
+
const res = await fetch(API + "/api/provider/" + encodeURIComponent(keyModalProvider.id) + "/key", {
|
|
433
|
+
method: "POST",
|
|
434
|
+
headers: { "Content-Type": "application/json" },
|
|
435
|
+
body: JSON.stringify({ key: key })
|
|
436
|
+
});
|
|
437
|
+
const data = await res.json();
|
|
438
|
+
if (data.ok && data.working) {
|
|
439
|
+
status.textContent = "Key is working ✓";
|
|
440
|
+
status.className = "key-status ok";
|
|
441
|
+
// Refresh provider list to show updated status
|
|
442
|
+
await loadProviders();
|
|
443
|
+
setTimeout(closeKeyModal, 800);
|
|
444
|
+
} else {
|
|
445
|
+
status.textContent = "Key saved but could not be verified";
|
|
446
|
+
status.className = "key-status err";
|
|
447
|
+
}
|
|
448
|
+
} catch(e) {
|
|
449
|
+
status.textContent = "Error: " + e.message;
|
|
450
|
+
status.className = "key-status err";
|
|
451
|
+
} finally {
|
|
452
|
+
btn.disabled = false;
|
|
453
|
+
}
|
|
454
|
+
}
|
|
455
|
+
async function testKey() {
|
|
456
|
+
if (!keyModalProvider) return;
|
|
457
|
+
var status = document.getElementById("keyStatus");
|
|
458
|
+
status.textContent = "Testing key…";
|
|
459
|
+
status.className = "key-status saving";
|
|
460
|
+
status.style.display = "block";
|
|
461
|
+
try {
|
|
462
|
+
const res = await fetch(API + "/api/provider/" + encodeURIComponent(keyModalProvider.id) + "/key/test");
|
|
463
|
+
const data = await res.json();
|
|
464
|
+
if (data.ok && data.working) {
|
|
465
|
+
status.textContent = "Key is working ✓";
|
|
466
|
+
status.className = "key-status ok";
|
|
467
|
+
await loadProviders();
|
|
468
|
+
} else {
|
|
469
|
+
status.textContent = "Key verification failed";
|
|
470
|
+
status.className = "key-status err";
|
|
471
|
+
}
|
|
472
|
+
} catch(e) {
|
|
473
|
+
status.textContent = "Error: " + e.message;
|
|
474
|
+
status.className = "key-status err";
|
|
475
|
+
}
|
|
476
|
+
}
|
|
477
|
+
|
|
158
478
|
document.getElementById("newSessionBtn").onclick = createSession;
|
|
159
479
|
document.getElementById("sendBtn").onclick = sendMessage;
|
|
160
480
|
document.getElementById("messageInput").addEventListener("keypress", function(e) {
|
|
161
481
|
if (e.key === "Enter") sendMessage();
|
|
162
482
|
});
|
|
483
|
+
document.getElementById("settingsBtn").onclick = openSettings;
|
|
484
|
+
document.getElementById("settingsClose").onclick = closeSettings;
|
|
485
|
+
document.getElementById("settingsOverlay").onclick = function(e) { if (e.target === this) closeSettings(); };
|
|
486
|
+
document.getElementById("settingsSave").onclick = function() { saveSettings(); closeSettings(); };
|
|
487
|
+
document.getElementById("keyCancelBtn").onclick = closeKeyModal;
|
|
488
|
+
document.getElementById("keySubmitBtn").onclick = submitKey;
|
|
489
|
+
document.getElementById("keyModalOverlay").onclick = function(e) { if (e.target === this) closeKeyModal(); };
|
|
490
|
+
document.getElementById("keyModalInput").addEventListener("keypress", function(e) {
|
|
491
|
+
if (e.key === "Enter") submitKey();
|
|
492
|
+
});
|
|
493
|
+
|
|
494
|
+
document.querySelectorAll(".settings-tab").forEach(function(tab) {
|
|
495
|
+
tab.onclick = function() {
|
|
496
|
+
document.querySelectorAll(".settings-tab").forEach(function(t){ t.classList.remove("active"); });
|
|
497
|
+
tab.classList.add("active");
|
|
498
|
+
document.querySelectorAll(".settings-section").forEach(function(s){ s.style.display = "none"; });
|
|
499
|
+
document.getElementById("tab-" + tab.getAttribute("data-tab")).style.display = "block";
|
|
500
|
+
};
|
|
501
|
+
});
|
|
163
502
|
|
|
164
503
|
loadSessions();
|
|
165
504
|
setTimeout(async function() {
|
|
@@ -169,4 +508,4 @@ setTimeout(async function() {
|
|
|
169
508
|
}, 500);
|
|
170
509
|
</script>
|
|
171
510
|
</body>
|
|
172
|
-
</html>
|
|
511
|
+
</html>
|