tinywebui 0.1.4 → 0.1.5
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/README.md +20 -13
- package/bin/tinywebui.js +69 -53
- package/package.json +25 -8
- package/public/app.js +40 -34
- package/public/chat/composer.js +1 -1
- package/public/chat/sidebar.js +8 -5
- package/public/index.html +88 -53
- package/public/panels/admin.js +40 -1
- package/public/panels/model-editor.js +336 -0
- package/public/panels/panels.js +37 -12
- package/public/panels/settings-home.js +125 -0
- package/public/panels/settings.js +124 -36
- package/public/shared/attachment_note.js +7 -1
- package/public/shared/events.js +1 -0
- package/public/shared/features.js +1 -0
- package/public/shared/origins.js +1 -0
- package/public/shared/usage.js +16 -1
- package/public/styles.css +84 -21
- package/src/access/auth.js +155 -155
- package/src/access/auth_gate.js +39 -38
- package/src/access/email.js +17 -0
- package/src/access/policy.js +123 -136
- package/src/audit.js +1 -1
- package/src/automations/automation.js +135 -123
- package/src/automations/scheduler.js +138 -127
- package/src/chat/compact.js +149 -156
- package/src/chat/llm.js +380 -382
- package/src/chat/runs.js +162 -151
- package/src/chat/tool_executor.js +229 -218
- package/src/cli.js +535 -433
- package/src/config/approval.js +35 -29
- package/src/config/config.js +546 -454
- package/src/config/data_dir.js +24 -0
- package/src/config/models.js +191 -115
- package/src/config/refs.js +32 -0
- package/src/config/secrets.js +153 -0
- package/src/files/documents.js +57 -68
- package/src/files/images.js +30 -31
- package/src/harness/agent.js +60 -60
- package/src/harness/attribution.js +66 -58
- package/src/harness/checkpoint.js +67 -62
- package/src/harness/history.js +28 -23
- package/src/harness/prompt.js +40 -57
- package/src/harness/provider.js +284 -283
- package/src/harness/types.js +1 -0
- package/src/http.js +90 -72
- package/src/log.js +11 -12
- package/src/mcp.js +398 -374
- package/src/retrieval/embedding.js +228 -240
- package/src/retrieval/retrieval.js +182 -173
- package/src/routes/admin.js +178 -138
- package/src/routes/auth.js +184 -111
- package/src/routes/automations.js +99 -82
- package/src/routes/chats.js +240 -218
- package/src/routes/documents.js +130 -120
- package/src/routes/library.js +43 -46
- package/src/routes/mcp.js +49 -49
- package/src/routes/settings.js +127 -122
- package/src/routes/types.js +1 -0
- package/src/server.js +426 -415
- package/src/store/automations.js +106 -106
- package/src/store/chats.js +217 -242
- package/src/store/documents.js +181 -185
- package/src/store/embeddings.js +52 -54
- package/src/store/index.js +95 -49
- package/src/store/messages.js +55 -80
- package/src/store/schema.js +128 -107
- package/src/store/scope.js +18 -17
- package/src/store/search.js +25 -27
- package/src/store/turns.js +53 -53
- package/src/store/usage.js +343 -269
- package/src/store/users.js +162 -87
- package/src/store/wire.js +50 -41
- package/src/tools/ask_tool.js +68 -68
- package/src/tools/chat_search_tool.js +38 -44
- package/src/tools/context_tool.js +131 -138
- package/src/tools/document_tool.js +73 -78
- package/src/tools/mcp_tool.js +274 -232
- package/src/tools/task_tool.js +53 -50
- package/src/tools/types.js +1 -0
- package/public/panels/connectors.js +0 -184
- package/src/set-password.js +0 -43
|
@@ -4,7 +4,8 @@ import { addError } from '../chat/transcript.js';
|
|
|
4
4
|
import { whoami, loadAccess } from '../core/access.js';
|
|
5
5
|
import { api } from '../core/api.js';
|
|
6
6
|
import { PANEL, closePanel } from './panels.js';
|
|
7
|
-
import {
|
|
7
|
+
import { initModelEditor, renderCatalog, collectCatalog, setLocked, newConnector, newModel, modelPages, usesDefaultEndpoint } from './model-editor.js';
|
|
8
|
+
import { renderHome } from './settings-home.js';
|
|
8
9
|
|
|
9
10
|
/**
|
|
10
11
|
* The tools panel: built-ins first, then every MCP server with its own health
|
|
@@ -23,6 +24,10 @@ export const isReadOnly = () => readOnly;
|
|
|
23
24
|
// The deployment's model as last loaded; the composer's model menu starts from it.
|
|
24
25
|
let model = '';
|
|
25
26
|
export const configuredModel = () => model;
|
|
27
|
+
// An admin of an instance whose default model has no endpoint yet: the page
|
|
28
|
+
// opens settings on load, since nothing else works until one is connected.
|
|
29
|
+
let setupNeeded = false;
|
|
30
|
+
export const needsSetup = () => setupNeeded;
|
|
26
31
|
|
|
27
32
|
function renderToolPanel(data) {
|
|
28
33
|
const box = $('tools');
|
|
@@ -231,11 +236,60 @@ export async function loadMcp() {
|
|
|
231
236
|
: locked ? 'set in code (read-only)' : path;
|
|
232
237
|
}
|
|
233
238
|
|
|
239
|
+
/* ---------- pages ----------
|
|
240
|
+
* Settings is a home page (endpoints and their models) and a page per thing
|
|
241
|
+
* opened from it. Every page edits the same unsaved draft, so moving between
|
|
242
|
+
* them loses nothing and one Save writes it all. */
|
|
243
|
+
|
|
244
|
+
let view = 'home';
|
|
245
|
+
// Whether this person edits the deployment's endpoints and catalog.
|
|
246
|
+
let admin = false;
|
|
247
|
+
let defaultKey = { set: false, hint: '' };
|
|
248
|
+
let modelField = null; // the default-model field, moved between its two homes
|
|
249
|
+
|
|
250
|
+
function showView(name, page) {
|
|
251
|
+
view = name;
|
|
252
|
+
for (const v of document.querySelectorAll('#settings .set-view')) v.hidden = v.dataset.view !== name;
|
|
253
|
+
const current = document.querySelector(`#settings .set-view[data-view="${name}"]`);
|
|
254
|
+
let title = current.dataset.title;
|
|
255
|
+
if (name === 'connector' || name === 'model') {
|
|
256
|
+
$(`${name}Slot`).replaceChildren(page);
|
|
257
|
+
page.refresh?.();
|
|
258
|
+
const s = page.summary();
|
|
259
|
+
title = s.label || s.id || (name === 'model' ? 'New model' : 'New connector');
|
|
260
|
+
}
|
|
261
|
+
if (name === 'home' && admin) renderHome({ open: showView, defaultModel: $('model').value.trim(), defaultKey, defaultModelField: modelField });
|
|
262
|
+
// Off the home card (catalog not empty, or not an admin) the field lives on
|
|
263
|
+
// the defaults page; an admin with a catalog picks the default per model.
|
|
264
|
+
if (!modelField.isConnected || name !== 'home') $('defaultModelGroup').appendChild(modelField);
|
|
265
|
+
$('defaultModelGroup').hidden = admin;
|
|
266
|
+
$('setTitle').textContent = title;
|
|
267
|
+
$('setBack').hidden = name === 'home' || readOnly;
|
|
268
|
+
document.querySelector('#settings .settings-content').scrollTop = 0;
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
/** Esc and Back: one page up, or false when already at the top. */
|
|
272
|
+
export function settingsBack() {
|
|
273
|
+
if (view === 'home' || readOnly) return false;
|
|
274
|
+
showView('home');
|
|
275
|
+
return true;
|
|
276
|
+
}
|
|
277
|
+
|
|
234
278
|
export function initSettings() {
|
|
235
279
|
$('saveMcp').onclick = saveMcp;
|
|
236
280
|
$('save').onclick = saveConfig;
|
|
237
281
|
$('testDefault').onclick = testDefault;
|
|
238
|
-
|
|
282
|
+
modelField = $('defaultModelField');
|
|
283
|
+
$('setBack').onclick = settingsBack;
|
|
284
|
+
for (const b of document.querySelectorAll('#settings [data-open]')) b.onclick = () => showView(b.dataset.open);
|
|
285
|
+
$('addConnector').onclick = () => showView('connector', newConnector());
|
|
286
|
+
initModelEditor({
|
|
287
|
+
isDefault: (id) => Boolean(id) && $('model').value.trim() === id,
|
|
288
|
+
makeDefault: (id) => { $('model').value = id; },
|
|
289
|
+
renameDefault: (from, to) => { if (from && $('model').value.trim() === from) $('model').value = to; },
|
|
290
|
+
removed: () => showView('home'),
|
|
291
|
+
addModel: (cid) => showView('model', newModel(cid))
|
|
292
|
+
});
|
|
239
293
|
}
|
|
240
294
|
|
|
241
295
|
async function testDefault() {
|
|
@@ -303,7 +357,7 @@ export async function loadConfig() {
|
|
|
303
357
|
$('myInstructionsGroup').hidden = !mine.includes('instructions');
|
|
304
358
|
$('myInstructions').value = whoami().instructions || '';
|
|
305
359
|
$('themeGroup').hidden = readOnly && !mine.includes('theme');
|
|
306
|
-
$('
|
|
360
|
+
$('myPrefsLink').hidden = readOnly ? !mine.length : false;
|
|
307
361
|
for (const id of FORM_KEYS) {
|
|
308
362
|
const locked = readOnly || lockedKeys.has(id);
|
|
309
363
|
$(id).disabled = locked;
|
|
@@ -332,9 +386,11 @@ export async function loadConfig() {
|
|
|
332
386
|
$('timezone').value = cfg.timezone ?? '';
|
|
333
387
|
$('toolApproval').value = cfg.toolApproval ?? 'writes';
|
|
334
388
|
// The admin block is only sent to (and only meaningful for) the sole user or an admin.
|
|
335
|
-
|
|
336
|
-
$('
|
|
389
|
+
admin = Boolean(whoami().isAdmin) && !readOnly && cfg.baseUrl !== undefined;
|
|
390
|
+
$('adminConnection').hidden = !admin;
|
|
391
|
+
for (const n of document.querySelectorAll('#settings .admin-only')) n.hidden = !admin;
|
|
337
392
|
if (admin) {
|
|
393
|
+
defaultKey = { set: Boolean(cfg.hasApiKey), hint: cfg.keyHint || '' };
|
|
338
394
|
$('baseUrl').value = cfg.baseUrl ?? '';
|
|
339
395
|
$('apiKey').value = '';
|
|
340
396
|
$('apiKey').placeholder = cfg.hasApiKey ? `set (…${cfg.keyHint || ''}) — blank keeps it` : 'not set';
|
|
@@ -344,29 +400,25 @@ export async function loadConfig() {
|
|
|
344
400
|
$('cache').value = String(cfg.cache !== false);
|
|
345
401
|
$('reasoningReplay').value = cfg.reasoningReplay ?? 'auto';
|
|
346
402
|
$('extraBody').value = Object.keys(cfg.extraBody || {}).length ? JSON.stringify(cfg.extraBody, null, 2) : '';
|
|
347
|
-
|
|
348
|
-
|
|
403
|
+
renderCatalog(cfg);
|
|
404
|
+
setLocked(Boolean(cfg.frozen) || lockedKeys.has('models') || lockedKeys.has('connectors'));
|
|
405
|
+
$('addConnector').hidden = Boolean(cfg.frozen) || lockedKeys.has('connectors');
|
|
349
406
|
}
|
|
407
|
+
showView(readOnly ? 'prefs' : 'home');
|
|
408
|
+
saved = draftState();
|
|
350
409
|
// The chip is the model picker's button now; the tool count lives in the
|
|
351
410
|
// + menu, where the tools themselves are.
|
|
352
411
|
// Tier 3 shows your own model; the configured one is only the default.
|
|
353
412
|
const me = whoami();
|
|
354
|
-
const bits = [
|
|
355
|
-
|
|
413
|
+
const bits = [['multiuser', 'trusted-header'].includes(me.authMode) && me.model ? me.modelLabel || me.model : cfg.modelLabel || cfg.model].filter(Boolean);
|
|
414
|
+
setupNeeded = admin && !cfg.ready;
|
|
415
|
+
if (!cfg.ready) bits.push('NO MODEL CONNECTED');
|
|
356
416
|
if (cfg.mcpErrors?.length) bits.push(`${cfg.mcpErrors.length} mcp err`);
|
|
357
417
|
$('status').textContent = bits.join(' · ');
|
|
358
418
|
}
|
|
359
419
|
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
if (!$('myInstructionsGroup').hidden) {
|
|
363
|
-
try { await api.post('/api/me/prefs', { instructions: $('myInstructions').value.trim() || null }); } catch (err) {
|
|
364
|
-
$('saveMsg').textContent = `save failed (${err.message})`;
|
|
365
|
-
return;
|
|
366
|
-
}
|
|
367
|
-
await loadAccess();
|
|
368
|
-
if (readOnly) { $('saveMsg').textContent = 'saved'; closePanel(PANEL.SETTINGS); return; }
|
|
369
|
-
}
|
|
420
|
+
/** The config patch the pages currently hold. Throws on bad JSON. */
|
|
421
|
+
function buildPatch() {
|
|
370
422
|
const patch = {
|
|
371
423
|
systemPrompt: $('systemPrompt').value,
|
|
372
424
|
model: $('model').value.trim(),
|
|
@@ -385,26 +437,61 @@ async function saveConfig() {
|
|
|
385
437
|
timezone: $('timezone').value,
|
|
386
438
|
toolApproval: $('toolApproval').value
|
|
387
439
|
};
|
|
388
|
-
if (
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
440
|
+
if (admin) {
|
|
441
|
+
const apiKey = $('apiKey').value;
|
|
442
|
+
const catalog = collectCatalog();
|
|
443
|
+
// A default that is no longer an enabled entry (renamed, removed, or none
|
|
444
|
+
// picked yet) becomes the first enabled one, as the server would read it.
|
|
445
|
+
const on = catalog.models.filter((m) => m.enabled !== false);
|
|
446
|
+
if (on.length && !on.some((m) => m.id === patch.model || (m.model || m.id) === patch.model)) patch.model = on[0].id;
|
|
447
|
+
Object.assign(patch, {
|
|
448
|
+
// The top-level endpoint only while it is shown; saving it otherwise
|
|
449
|
+
// would bring the card back with nothing on it.
|
|
450
|
+
...(usesDefaultEndpoint() ? { baseUrl: $('baseUrl').value.trim(), ...(apiKey ? { apiKey } : {}) } : {}),
|
|
451
|
+
llmCompaction: $('llmCompaction').value === 'true',
|
|
452
|
+
compactionMaxTokens: Number($('compactionMaxTokens').value) || 8192,
|
|
453
|
+
expandCharBudget: Number($('expandCharBudget').value) || 0,
|
|
454
|
+
cache: $('cache').value === 'true',
|
|
455
|
+
reasoningReplay: $('reasoningReplay').value,
|
|
456
|
+
extraBody: $('extraBody').value.trim() ? JSON.parse($('extraBody').value) : {},
|
|
457
|
+
...catalog
|
|
458
|
+
});
|
|
459
|
+
}
|
|
460
|
+
for (const k of lockedKeys) delete patch[k];
|
|
461
|
+
return patch;
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
// The draft as of the last load, to tell whether closing would lose edits.
|
|
465
|
+
let saved = '';
|
|
466
|
+
function draftState() {
|
|
467
|
+
if (readOnly) return $('myInstructions').value;
|
|
468
|
+
try { return JSON.stringify([buildPatch(), $('myInstructions').value]); } catch { return null; }
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
/** Whether closing now would drop unsaved edits; asks, and on yes reloads the saved settings. */
|
|
472
|
+
export function settingsCanClose() {
|
|
473
|
+
const now = draftState();
|
|
474
|
+
if (now === saved) return true;
|
|
475
|
+
if (!window.confirm('Discard unsaved settings changes?')) return false;
|
|
476
|
+
loadConfig();
|
|
477
|
+
return true;
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
async function saveConfig() {
|
|
481
|
+
// Personal instructions go to the person's own prefs, whatever else they may edit.
|
|
482
|
+
if (!$('myInstructionsGroup').hidden) {
|
|
483
|
+
try { await api.post('/api/me/prefs', { instructions: $('myInstructions').value.trim() || null }); } catch (err) {
|
|
403
484
|
$('saveMsg').textContent = `save failed (${err.message})`;
|
|
404
485
|
return;
|
|
405
486
|
}
|
|
487
|
+
await loadAccess();
|
|
488
|
+
if (readOnly) { $('saveMsg').textContent = 'saved'; saved = draftState(); closePanel(PANEL.SETTINGS, { force: true }); return; }
|
|
489
|
+
}
|
|
490
|
+
let patch;
|
|
491
|
+
try { patch = buildPatch(); } catch (err) {
|
|
492
|
+
$('saveMsg').textContent = `save failed (${err.message})`;
|
|
493
|
+
return;
|
|
406
494
|
}
|
|
407
|
-
for (const k of lockedKeys) delete patch[k];
|
|
408
495
|
try { await api.post('/api/config', patch); } catch (err) {
|
|
409
496
|
$('saveMsg').textContent = `save failed (${err.message})`;
|
|
410
497
|
return;
|
|
@@ -413,12 +500,13 @@ async function saveConfig() {
|
|
|
413
500
|
await loadAccess(); // a changed model changes what it can take
|
|
414
501
|
await loadTools(); // the per-tool "default" labels follow the global mode
|
|
415
502
|
$('saveMsg').textContent = 'saved';
|
|
416
|
-
closePanel(PANEL.SETTINGS);
|
|
503
|
+
closePanel(PANEL.SETTINGS, { force: true });
|
|
417
504
|
}
|
|
418
505
|
|
|
419
506
|
/** The panel's onOpen: a stale "saved" would read as this visit's. */
|
|
420
507
|
export function onSettingsOpen() {
|
|
421
508
|
if (!$('save').disabled) $('saveMsg').textContent = '';
|
|
509
|
+
showView(readOnly ? 'prefs' : 'home');
|
|
422
510
|
}
|
|
423
511
|
|
|
424
512
|
/** The panel's onClose. */
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
// @ts-check
|
|
1
2
|
/**
|
|
2
3
|
* The inline note that tells the model a document's id ("read_document" needs
|
|
3
4
|
* it). It lives in the stored message text, so the page strips it back out to
|
|
@@ -5,6 +6,7 @@
|
|
|
5
6
|
* note, the transcript reads it, and they cannot drift apart.
|
|
6
7
|
*/
|
|
7
8
|
|
|
9
|
+
/** @param {{ id: string, filename: string, char_len: number }} doc */
|
|
8
10
|
export function attachmentNote(doc) {
|
|
9
11
|
return `\n\n[Attached document: "${doc.filename}" (id: ${doc.id}, ${doc.char_len.toLocaleString('en-US')} chars). Use read_document to search or read it.]`;
|
|
10
12
|
}
|
|
@@ -14,7 +16,11 @@ export function attachmentNote(doc) {
|
|
|
14
16
|
const NOTES_AT_END = /(?:\n\n\[Attached document: ".*?" \(id: ([\w.-]+), [\d,]+ chars\)\. Use read_document to search or read it\.\])+$/;
|
|
15
17
|
const ONE_NOTE = /\[Attached document: ".*?" \(id: ([\w.-]+), /g;
|
|
16
18
|
|
|
17
|
-
/**
|
|
19
|
+
/**
|
|
20
|
+
* A stored message's text without its notes, and the document ids they named.
|
|
21
|
+
* @param {unknown} content
|
|
22
|
+
* @returns {{ text: string, ids: string[] }}
|
|
23
|
+
*/
|
|
18
24
|
export function splitAttachmentNotes(content) {
|
|
19
25
|
const text = String(content ?? '');
|
|
20
26
|
const tail = NOTES_AT_END.exec(text);
|
package/public/shared/events.js
CHANGED
package/public/shared/origins.js
CHANGED
package/public/shared/usage.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
// @ts-check
|
|
1
2
|
/**
|
|
2
3
|
* Reading a provider's usage object. Shared by both sides so the transcript's
|
|
3
4
|
* per-turn line and the Statistics panel count the same tokens.
|
|
@@ -11,15 +12,24 @@
|
|
|
11
12
|
* DeepSeek : prompt_cache_hit_tokens / prompt_cache_miss_tokens
|
|
12
13
|
*/
|
|
13
14
|
|
|
15
|
+
/**
|
|
16
|
+
* A usage object as some provider sent it: any shape, every field optional.
|
|
17
|
+
* @typedef {{ [field: string]: any }} RawUsage
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
/** @param {unknown} v @returns {number | null} */
|
|
14
21
|
const num = (v) => {
|
|
15
22
|
if (v == null) return null;
|
|
16
23
|
const n = Number(v);
|
|
17
24
|
return Number.isFinite(n) && n >= 0 ? n : null;
|
|
18
25
|
};
|
|
19
26
|
|
|
27
|
+
/** @param {RawUsage | null | undefined} u */
|
|
20
28
|
export const inputTokens = (u) => num(u?.prompt_tokens ?? u?.input_tokens);
|
|
29
|
+
/** @param {RawUsage | null | undefined} u */
|
|
21
30
|
export const outputTokens = (u) => num(u?.completion_tokens ?? u?.output_tokens);
|
|
22
31
|
|
|
32
|
+
/** @param {RawUsage | null | undefined} u */
|
|
23
33
|
export const cacheReads = (u) => num(
|
|
24
34
|
u?.prompt_tokens_details?.cached_tokens
|
|
25
35
|
?? u?.input_tokens_details?.cached_tokens
|
|
@@ -28,6 +38,7 @@ export const cacheReads = (u) => num(
|
|
|
28
38
|
?? u?.cached_tokens
|
|
29
39
|
);
|
|
30
40
|
|
|
41
|
+
/** @param {RawUsage | null | undefined} u */
|
|
31
42
|
export const cacheWrites = (u) => num(
|
|
32
43
|
u?.cache_creation_input_tokens
|
|
33
44
|
?? u?.cache_write_tokens
|
|
@@ -36,8 +47,12 @@ export const cacheWrites = (u) => num(
|
|
|
36
47
|
?? u?.input_tokens_details?.cache_write_tokens
|
|
37
48
|
);
|
|
38
49
|
|
|
39
|
-
/**
|
|
50
|
+
/**
|
|
51
|
+
* Folds one or more raw usage objects into the numbers a turn displays.
|
|
52
|
+
* @param {RawUsage[]} raws
|
|
53
|
+
*/
|
|
40
54
|
export function tally(raws) {
|
|
55
|
+
/** @type {{ in: number, out: number, cached: number, written: number, discount: number, reported: boolean, raw: RawUsage[], provider: string | null, providers: string[] }} */
|
|
41
56
|
const t = { in: 0, out: 0, cached: 0, written: 0, discount: 0, reported: false, raw: raws, provider: null, providers: [] };
|
|
42
57
|
for (const u of raws) {
|
|
43
58
|
if (u.provider) {
|
package/public/styles.css
CHANGED
|
@@ -84,7 +84,6 @@ header .grow { flex: 1; }
|
|
|
84
84
|
font-size: var(--fs-meta); color: var(--muted); font-family: var(--mono);
|
|
85
85
|
min-width: 3rem; overflow: hidden; white-space: nowrap; text-overflow: ellipsis;
|
|
86
86
|
}
|
|
87
|
-
#menu { display: none; }
|
|
88
87
|
|
|
89
88
|
/* One focus ring for everything, drawn outside the element so a dense row does
|
|
90
89
|
not clip it. Keyboard only -- the mouse never sees it. */
|
|
@@ -108,6 +107,10 @@ input, textarea, select {
|
|
|
108
107
|
background: var(--raise); border: 1px solid var(--line); border-radius: var(--r-sm); resize: vertical;
|
|
109
108
|
transition: border-color .12s, box-shadow .12s;
|
|
110
109
|
}
|
|
110
|
+
input[type="checkbox"] {
|
|
111
|
+
width: 1rem; height: 1rem; min-height: 0; margin: 0; padding: 0;
|
|
112
|
+
flex: none; accent-color: var(--accent); cursor: pointer;
|
|
113
|
+
}
|
|
111
114
|
input:focus, textarea:focus, select:focus {
|
|
112
115
|
outline: none; border-color: var(--accent); box-shadow: 0 0 0 3px var(--accent-soft);
|
|
113
116
|
}
|
|
@@ -295,7 +298,10 @@ main { flex: 1; display: flex; flex-direction: column; min-width: 0; min-height:
|
|
|
295
298
|
#login[hidden] { display:none; }
|
|
296
299
|
.login-card { width:min(22rem,100%); display:flex; flex-direction:column; gap:.6rem; padding:1.5rem; border:1px solid var(--line); border-radius:var(--r-md); background:var(--raise); }
|
|
297
300
|
.login-card h2 { margin:0 0 .5rem; }
|
|
298
|
-
|
|
301
|
+
.invite-form { display:flex; flex-direction:column; gap:.5rem; padding:.75rem 0 1rem; }
|
|
302
|
+
.invite-form > div:first-of-type { display:flex; gap:.5rem; }
|
|
303
|
+
.invite-form input { flex:1 1 14rem; }
|
|
304
|
+
#loginMsg:empty { display:none; }
|
|
299
305
|
.admin-users { display:flex; flex-direction:column; gap:.4rem; }
|
|
300
306
|
.admin-row { display:flex; flex-wrap:wrap; gap:.5rem .75rem; align-items:center; padding:.5rem 0; border-top:1px solid var(--line); font-size:var(--fs-meta); }
|
|
301
307
|
.admin-row:first-child { border-top:0; }
|
|
@@ -390,7 +396,7 @@ main { flex: 1; display: flex; flex-direction: column; min-width: 0; min-height:
|
|
|
390
396
|
.automation-switch input:focus-visible + .switch-track { box-shadow:0 0 0 3px var(--accent-soft); }
|
|
391
397
|
|
|
392
398
|
/* Inline create/edit form: one column, instructions full width. */
|
|
393
|
-
.automation-editor { display:flex; flex-direction:column;
|
|
399
|
+
.automation-editor { display:flex; flex-direction:column; gap:.75rem; }
|
|
394
400
|
.automation-card .automation-editor { border-top:1px solid var(--line); padding-top:.75rem; }
|
|
395
401
|
.automation-card > h3 + .automation-editor { border-top:0; padding-top:0; }
|
|
396
402
|
.automation-row { display:flex; flex-wrap:wrap; gap:.75rem; }
|
|
@@ -530,13 +536,13 @@ details.tool.off .name, details.tool.off .desc { opacity: .6; }
|
|
|
530
536
|
}
|
|
531
537
|
.tool .params { margin-top: .5rem; }
|
|
532
538
|
.tool .param {
|
|
533
|
-
display: flex; gap: .5rem; font-family: var(--mono);
|
|
539
|
+
display: flex; flex-wrap: wrap; gap: 0 .5rem; font-family: var(--mono);
|
|
534
540
|
font-size: var(--fs-meta); padding: .125rem 0;
|
|
535
541
|
}
|
|
536
542
|
.tool .param b { color: var(--fg); font-weight: 500; }
|
|
537
543
|
.tool .param .ty { color: var(--faint); }
|
|
538
544
|
.tool .param .req { color: var(--err); }
|
|
539
|
-
.tool .param .d { color: var(--muted); flex: 1; min-width: 0; }
|
|
545
|
+
.tool .param .d { color: var(--muted); flex: 1 1 12rem; min-width: 0; }
|
|
540
546
|
.row { display: flex; gap: .75rem; flex-wrap: wrap; align-items: flex-end; }
|
|
541
547
|
.row > * { flex: 1 1 11rem; }
|
|
542
548
|
|
|
@@ -809,7 +815,7 @@ details.step > summary:hover { background: var(--panel); }
|
|
|
809
815
|
/* The composer: one raised card holding attach, input and send, rather than
|
|
810
816
|
three loose controls on a ruled strip. The page's one shadow lives here. */
|
|
811
817
|
footer { padding: .5rem .875rem 1rem; flex-shrink: 0; }
|
|
812
|
-
form {
|
|
818
|
+
#form {
|
|
813
819
|
max-width: var(--measure); margin: 0 auto;
|
|
814
820
|
display: flex; gap: .5rem; align-items: flex-end;
|
|
815
821
|
}
|
|
@@ -1181,11 +1187,14 @@ button.nav-item:active:not(:disabled) { transform: none; }
|
|
|
1181
1187
|
.chat-folder-heading { background: var(--panel); padding: .5rem; }
|
|
1182
1188
|
#side-resize { right: -.3125rem; }
|
|
1183
1189
|
|
|
1184
|
-
/*
|
|
1185
|
-
|
|
1186
|
-
|
|
1190
|
+
/* One sidebar toggle at every width: it collapses the desktop sidebar and
|
|
1191
|
+
opens the mobile drawer. It sits above the full-column panels because the
|
|
1192
|
+
sidebar is the way out of them -- they have no close buttons of their own --
|
|
1193
|
+
so every header in the column leaves room for it. */
|
|
1194
|
+
#menu { position: absolute; top: .25rem; left: .25rem; z-index: 11; display: inline-flex; }
|
|
1187
1195
|
body.side-collapsed #side { display: none; }
|
|
1188
|
-
|
|
1196
|
+
.automation-head { padding-left: 3rem; }
|
|
1197
|
+
#log { padding-top: 2.75rem; }
|
|
1189
1198
|
|
|
1190
1199
|
/* Full-column panels clip themselves to this frame. Keeping main overflow
|
|
1191
1200
|
visible lets focus rings and composer popovers extend where they need to. */
|
|
@@ -1381,7 +1390,11 @@ button.pop-item:active:not(:disabled) { transform: none; }
|
|
|
1381
1390
|
width: min(46rem, 100%); margin: 0 auto 1rem; padding: 1.125rem 1.25rem 1.25rem;
|
|
1382
1391
|
background: var(--raise); border: 1px solid var(--line); border-radius: var(--r-md);
|
|
1383
1392
|
}
|
|
1384
|
-
.set-group > header {
|
|
1393
|
+
.set-group > header {
|
|
1394
|
+
/* Not the page's top bar: plain block, no bar height, padding or blur. */
|
|
1395
|
+
display: block; height: auto; padding: 0; border: 0; background: none; backdrop-filter: none;
|
|
1396
|
+
margin-bottom: .875rem;
|
|
1397
|
+
}
|
|
1385
1398
|
.set-group h3 { margin: 0; font-size: 1rem; font-weight: 600; letter-spacing: -.005em; }
|
|
1386
1399
|
.set-group > header p { margin: .125rem 0 0; color: var(--muted); font-size: var(--fs-small); }
|
|
1387
1400
|
.set-grid { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 1rem 1rem; }
|
|
@@ -1397,15 +1410,56 @@ button.pop-item:active:not(:disabled) { transform: none; }
|
|
|
1397
1410
|
.policy-checks { display: flex; flex-wrap: wrap; gap: .25rem 1rem; margin-top: .25rem; }
|
|
1398
1411
|
.policy-checks label { display: inline-flex; gap: .3rem; align-items: center; }
|
|
1399
1412
|
/* A person without the settings feature sees only what an admin lets them customise. */
|
|
1400
|
-
#settings.prefs-only .set-group:not(.mine), #settings.prefs-only .set-
|
|
1401
|
-
|
|
1402
|
-
|
|
1403
|
-
.
|
|
1404
|
-
.
|
|
1405
|
-
|
|
1406
|
-
|
|
1407
|
-
.
|
|
1408
|
-
.
|
|
1413
|
+
#settings.prefs-only .set-group:not(.mine), #settings.prefs-only .set-link:not(.mine), #settings.prefs-only #adminConnection { display: none; }
|
|
1414
|
+
.set-divider.set-first { margin-top: 0; padding-top: 0; border-top: 0; }
|
|
1415
|
+
#settings .mine[hidden], #settings .set-view[hidden], #settings .set-group[hidden] { display: none; }
|
|
1416
|
+
.automation-head .set-back { margin-left: -.375rem; }
|
|
1417
|
+
.automation-head .set-back + h2 { margin-right: auto; }
|
|
1418
|
+
|
|
1419
|
+
/* Home: one card per endpoint with its models under it, then links to the other pages. */
|
|
1420
|
+
.ep-head { display: flex; align-items: flex-start; gap: .75rem; }
|
|
1421
|
+
.ep-head > div { flex: 1; min-width: 0; }
|
|
1422
|
+
.ep-head p { margin: .125rem 0 0; color: var(--muted); font-size: var(--fs-small); overflow-wrap: anywhere; }
|
|
1423
|
+
.model-list { margin: .25rem -.5rem .5rem; display: flex; flex-direction: column; }
|
|
1424
|
+
.model-link {
|
|
1425
|
+
display: flex; align-items: center; gap: .75rem; width: 100%; padding: .5rem; text-align: left;
|
|
1426
|
+
background: none; border: 0; border-radius: var(--r-sm); color: var(--fg);
|
|
1427
|
+
}
|
|
1428
|
+
.model-link:hover { background: var(--accent-soft); }
|
|
1429
|
+
.model-link.off { opacity: .55; }
|
|
1430
|
+
.model-text { flex: 1; min-width: 0; display: flex; flex-direction: column; }
|
|
1431
|
+
.model-name { font-weight: 500; }
|
|
1432
|
+
.model-sub { color: var(--faint); font-size: var(--fs-meta); font-family: var(--mono); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
|
1433
|
+
.model-chips { display: flex; gap: .25rem; flex-wrap: wrap; justify-content: flex-end; }
|
|
1434
|
+
.chip { padding: .0625rem .5rem; border: 1px solid var(--line); border-radius: var(--r-pill); font-size: var(--fs-meta); color: var(--muted); }
|
|
1435
|
+
.chip-default { border-color: currentColor; color: var(--fg); font-weight: 500; }
|
|
1436
|
+
.chev { color: var(--faint); font-size: 1.25rem; line-height: 1; }
|
|
1437
|
+
.add-model { margin-top: .25rem; }
|
|
1438
|
+
.ep-open { margin: .25rem 0 .75rem; }
|
|
1439
|
+
.ep-open small { display: block; }
|
|
1440
|
+
.ep-empty { margin: .25rem 0 .5rem; color: var(--muted); font-size: var(--fs-small); }
|
|
1441
|
+
/* A fresh instance: the first step of setting one up. */
|
|
1442
|
+
.ep-setup p { margin: .25rem 0 1rem; color: var(--muted); font-size: var(--fs-small); }
|
|
1443
|
+
.set-add { width: min(46rem, 100%); margin: 0 auto 1.5rem; }
|
|
1444
|
+
.set-nav { width: min(46rem, 100%); margin: 0 auto; display: flex; flex-direction: column; border: 1px solid var(--line); border-radius: var(--r-md); background: var(--raise); overflow: hidden; }
|
|
1445
|
+
.set-link {
|
|
1446
|
+
display: flex; align-items: center; width: 100%; padding: .875rem 1.25rem; text-align: left;
|
|
1447
|
+
background: none; border: 0; border-radius: 0; color: var(--fg);
|
|
1448
|
+
}
|
|
1449
|
+
.set-link + .set-link { border-top: 1px solid var(--line); }
|
|
1450
|
+
.set-link:hover { background: var(--accent-soft); }
|
|
1451
|
+
.set-link::after { content: '›'; margin-left: auto; padding-left: .75rem; color: var(--faint); font-size: 1.25rem; }
|
|
1452
|
+
.set-link b { display: block; font-weight: 600; }
|
|
1453
|
+
.set-link small { display: block; color: var(--muted); font-size: var(--fs-small); }
|
|
1454
|
+
.set-link[hidden] { display: none; }
|
|
1455
|
+
.set-foot { width: min(46rem, 100%); margin: 0 auto 1rem; display: flex; justify-content: flex-end; gap: .5rem; }
|
|
1456
|
+
.danger { color: var(--err); }
|
|
1457
|
+
.inline-check { display: inline-flex !important; gap: .375rem; align-items: center; margin: 0 !important; font-weight: 400 !important; }
|
|
1458
|
+
/* Editor fields inside the model and connector pages. */
|
|
1459
|
+
#modelSlot textarea.mono { font-family: var(--mono, monospace); font-size: var(--fs-small); }
|
|
1460
|
+
.cfg-actions { display: flex; gap: .75rem; align-items: center; flex-wrap: wrap; }
|
|
1461
|
+
.cfg-tags { display: flex; gap: 1rem; flex-wrap: wrap; align-items: center; }
|
|
1462
|
+
.cfg-tags label { display: inline-flex; gap: .3rem; align-items: center; margin: 0; font-weight: 400; }
|
|
1409
1463
|
|
|
1410
1464
|
.brand-home {
|
|
1411
1465
|
flex: 1; min-width: 0; display: flex; align-items: center; gap: .5rem;
|
|
@@ -1447,7 +1501,6 @@ button.pop-item:active:not(:disabled) { transform: none; }
|
|
|
1447
1501
|
.usage-composition-track { display: flex; height: .7rem; overflow: hidden; border-radius: var(--r-xs); margin: .5rem 0; }
|
|
1448
1502
|
#statisticsFilter { margin-left: auto; min-width: 0; }
|
|
1449
1503
|
#statisticsFilter select { max-width: 100%; }
|
|
1450
|
-
#statisticsFilter + button { margin-left: 0; }
|
|
1451
1504
|
.task-card {
|
|
1452
1505
|
max-width: var(--measure); margin: 0 auto .5rem; padding: .5rem .75rem;
|
|
1453
1506
|
border: 1px solid var(--line); border-radius: var(--r-md); background: var(--panel);
|
|
@@ -1469,3 +1522,13 @@ button.pop-item:active:not(:disabled) { transform: none; }
|
|
|
1469
1522
|
.task-completed { opacity: .6; }
|
|
1470
1523
|
.task-completed span:last-child { text-decoration: line-through; }
|
|
1471
1524
|
.no-shrink { flex: 0 0 auto; }
|
|
1525
|
+
|
|
1526
|
+
/* Text from outside -- links, paths, emails, model and tool ids -- can be one
|
|
1527
|
+
unbroken token wider than a phone. As a flex item it would set the item's
|
|
1528
|
+
minimum width and push the row past the edge (or, centred, past the left
|
|
1529
|
+
edge where nothing scrolls to it), so let it break anywhere instead. */
|
|
1530
|
+
#inviteResult, #mcpPath, .model-name, .policy-checks label, .policy-group b,
|
|
1531
|
+
.tool .name, .tool .doc, .tool .param b, .tool .param .d, .tool-group-h .name,
|
|
1532
|
+
.tool-group-h .meta, .sec-h .usage {
|
|
1533
|
+
overflow-wrap: anywhere; min-width: 0;
|
|
1534
|
+
}
|