agentgui 1.0.1063 → 1.0.1065
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/.gm/prd.yml
CHANGED
|
@@ -3106,15 +3106,3 @@
|
|
|
3106
3106
|
- id: docstudio-cue-session-group-eyebrow
|
|
3107
3107
|
subject: Verify ConversationList's session-group headers (Today/Yesterday/etc) use the --tr-label token + --fg-3 tone consistently, matching docstudio's uppercase letter-spaced low-opacity eyebrow label idiom
|
|
3108
3108
|
status: pending
|
|
3109
|
-
- id: gc-delete-undo-trash
|
|
3110
|
-
subject: No undo-after-delete (soft-delete/trash) anywhere in the confined delete surface
|
|
3111
|
-
status: pending
|
|
3112
|
-
- id: gc-expanded-body-highlight
|
|
3113
|
-
subject: Search-result highlighting inside expanded event bodies never highlights the matched query term
|
|
3114
|
-
status: pending
|
|
3115
|
-
- id: gc-search-results-export
|
|
3116
|
-
subject: No per-session or global export of History search results themselves
|
|
3117
|
-
status: pending
|
|
3118
|
-
- id: gc-settings-storage-estimate
|
|
3119
|
-
subject: Settings has no cache/data breakdown beyond agentgui's own localStorage keys - no navigator.storage.estimate()
|
|
3120
|
-
status: pending
|
package/lib/http-handler.js
CHANGED
|
@@ -146,6 +146,73 @@ function isAllowRoot(realPath, allowRoots) {
|
|
|
146
146
|
return allowRoots.some(r => (IS_WINDOWS ? r.toLowerCase() : r) === p);
|
|
147
147
|
}
|
|
148
148
|
|
|
149
|
+
// --- Soft-delete (trash) -----------------------------------------------
|
|
150
|
+
// /api/delete moves entries into <root>/.agentgui-trash/<trashId>__<name>
|
|
151
|
+
// instead of unlinking, giving a short undo window. In-memory index only
|
|
152
|
+
// (server restart forfeits the undo window - acceptable since the retention
|
|
153
|
+
// window itself is short and this is a convenience net on top of, not a
|
|
154
|
+
// replacement for, the pre-delete ConfirmDialog). Purged after
|
|
155
|
+
// TRASH_RETENTION_MS or when trashIndex grows past TRASH_MAX_ENTRIES
|
|
156
|
+
// (oldest-first), so a long-running server's trash dir can't grow unbounded.
|
|
157
|
+
const TRASH_DIR_NAME = '.agentgui-trash';
|
|
158
|
+
const TRASH_RETENTION_MS = parseInt(process.env.AGENTGUI_TRASH_RETENTION_MS || '', 10) || 10 * 60 * 1000;
|
|
159
|
+
const TRASH_MAX_ENTRIES = 200;
|
|
160
|
+
const trashIndex = new Map(); // trashId -> { trashPath, originalPath, root, deletedAt }
|
|
161
|
+
|
|
162
|
+
function purgeExpiredTrash() {
|
|
163
|
+
const now = Date.now();
|
|
164
|
+
for (const [id, info] of trashIndex) {
|
|
165
|
+
if (now - info.deletedAt > TRASH_RETENTION_MS) {
|
|
166
|
+
try { fs.rmSync(info.trashPath, { recursive: true, force: true }); } catch { /* already gone */ }
|
|
167
|
+
trashIndex.delete(id);
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
if (trashIndex.size > TRASH_MAX_ENTRIES) {
|
|
171
|
+
const sorted = [...trashIndex.entries()].sort((a, b) => a[1].deletedAt - b[1].deletedAt);
|
|
172
|
+
for (const [id, info] of sorted.slice(0, trashIndex.size - TRASH_MAX_ENTRIES)) {
|
|
173
|
+
try { fs.rmSync(info.trashPath, { recursive: true, force: true }); } catch {}
|
|
174
|
+
trashIndex.delete(id);
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
// Which allowed root a confined realPath lives under - the trash dir sits
|
|
180
|
+
// alongside it (still inside the SAME root, so confineToRoots covers the
|
|
181
|
+
// trash path too - no new unconfined surface).
|
|
182
|
+
function rootFor(realPath, allowRoots) {
|
|
183
|
+
const p = IS_WINDOWS ? realPath.toLowerCase() : realPath;
|
|
184
|
+
return allowRoots.find(r => { const rr = IS_WINDOWS ? r.toLowerCase() : r; return p === rr || p.startsWith(rr + path.sep); });
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
function moveToTrash(realPath, allowRoots) {
|
|
188
|
+
purgeExpiredTrash();
|
|
189
|
+
const root = rootFor(realPath, allowRoots);
|
|
190
|
+
if (!root) { const e = new Error('not confined to an allowed root'); e.code = 'EACCES'; throw e; }
|
|
191
|
+
const trashDir = path.join(root, TRASH_DIR_NAME);
|
|
192
|
+
fs.mkdirSync(trashDir, { recursive: true });
|
|
193
|
+
const trashId = crypto.randomBytes(8).toString('hex');
|
|
194
|
+
const base = path.basename(realPath);
|
|
195
|
+
const trashPath = path.join(trashDir, trashId + '__' + base);
|
|
196
|
+
fs.renameSync(realPath, trashPath);
|
|
197
|
+
trashIndex.set(trashId, { trashPath, originalPath: realPath, root, deletedAt: Date.now() });
|
|
198
|
+
return { trashId };
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
function restoreFromTrash(trashId, allowRoots) {
|
|
202
|
+
const info = trashIndex.get(trashId);
|
|
203
|
+
if (!info) { const e = new Error('nothing to restore - the undo window has expired or this was already restored'); e.code = 'NOT_FOUND'; throw e; }
|
|
204
|
+
// Re-confine the ORIGINAL path at restore time (not trust the cached one
|
|
205
|
+
// blindly) - the allowlist itself doesn't change at runtime, but this keeps
|
|
206
|
+
// restore honoring the exact same confinement contract every other route does.
|
|
207
|
+
const conf = confineToRoots(info.originalPath, allowRoots);
|
|
208
|
+
if (!conf.ok && conf.reason !== 'not found') { const e = new Error('restore target is no longer inside an accessible folder'); e.code = 'CONFLICT'; throw e; }
|
|
209
|
+
if (fs.existsSync(info.originalPath)) { const e = new Error('a file already exists at the original location'); e.code = 'CONFLICT'; throw e; }
|
|
210
|
+
fs.mkdirSync(path.dirname(info.originalPath), { recursive: true });
|
|
211
|
+
fs.renameSync(info.trashPath, info.originalPath);
|
|
212
|
+
trashIndex.delete(trashId);
|
|
213
|
+
return { path: info.originalPath };
|
|
214
|
+
}
|
|
215
|
+
|
|
149
216
|
export function createHttpHandler({ BASE_URL, expressApp, queries, sendJSON, serveFile, staticDir, messageQueues, getWss, activeExecutions, getACPStatus, discoveredAgents, PKG_VERSION, RATE_LIMIT_MAX, rateLimitMap, routes, PORT }) {
|
|
150
217
|
// Warn operators when CORS_ORIGIN=* is combined with no PASSWORD: any
|
|
151
218
|
// cross-origin page can make credentialless fetch() calls to all /api/*
|
|
@@ -690,13 +757,18 @@ export function createHttpHandler({ BASE_URL, expressApp, queries, sendJSON, ser
|
|
|
690
757
|
if (isAllowRoot(conf.realPath, allowRoots)) { sendJSON(req, res, 403, { error: 'forbidden: cannot delete an allowed root' }); return; }
|
|
691
758
|
try {
|
|
692
759
|
const st = fs.lstatSync(conf.realPath);
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
760
|
+
// Soft-delete: move into a confined per-root .agentgui-trash/ instead
|
|
761
|
+
// of unlinking, so the only safety net isn't confirm-before (the
|
|
762
|
+
// pre-existing ConfirmDialog) but also undo-after, matching an
|
|
763
|
+
// fsbrowse-grade file manager. A non-empty directory without
|
|
764
|
+
// recursive=true still throws ENOTEMPTY BEFORE any move happens
|
|
765
|
+
// (checked via a dry probe) to keep that existing guard's semantics.
|
|
766
|
+
if (st.isDirectory() && body.recursive !== true) {
|
|
767
|
+
const dryEntries = fs.readdirSync(conf.realPath);
|
|
768
|
+
if (dryEntries.length) { sendJSON(req, res, 409, { error: 'directory is not empty' }); return; }
|
|
698
769
|
}
|
|
699
|
-
|
|
770
|
+
const trashInfo = moveToTrash(conf.realPath, allowRoots);
|
|
771
|
+
sendJSON(req, res, 200, { ok: true, trashId: trashInfo.trashId });
|
|
700
772
|
} catch (err) {
|
|
701
773
|
const code = err.code === 'ENOTEMPTY' ? 409 : (err.code === 'EACCES' || err.code === 'EPERM' ? 403 : (err.code === 'ENOENT' ? 404 : 400));
|
|
702
774
|
sendJSON(req, res, code, { error: err.code === 'ENOTEMPTY' ? 'directory is not empty' : err.message });
|
|
@@ -704,6 +776,22 @@ export function createHttpHandler({ BASE_URL, expressApp, queries, sendJSON, ser
|
|
|
704
776
|
return;
|
|
705
777
|
}
|
|
706
778
|
|
|
779
|
+
// POST /api/restore {trashId} -> {ok, path}. Undoes a /api/delete within
|
|
780
|
+
// its retention window (trashRetentionMs, default 10 minutes) by moving
|
|
781
|
+
// the entry back from .agentgui-trash/ to its original confined path.
|
|
782
|
+
if (routePath.split('?')[0] === '/api/restore' && req.method === 'POST') {
|
|
783
|
+
let body;
|
|
784
|
+
try { body = JSON.parse((await readBody(req, 64 * 1024)).toString('utf8') || '{}'); }
|
|
785
|
+
catch (e) { sendJSON(req, res, e.code === 'TOO_LARGE' ? 413 : 400, { error: 'bad request body' }); return; }
|
|
786
|
+
try {
|
|
787
|
+
const restored = restoreFromTrash(String(body.trashId || ''), fsAllowRoots());
|
|
788
|
+
sendJSON(req, res, 200, { ok: true, path: restored.path });
|
|
789
|
+
} catch (err) {
|
|
790
|
+
sendJSON(req, res, err.code === 'NOT_FOUND' ? 404 : (err.code === 'CONFLICT' ? 409 : 400), { error: err.message });
|
|
791
|
+
}
|
|
792
|
+
return;
|
|
793
|
+
}
|
|
794
|
+
|
|
707
795
|
// POST /api/mkdir {dir, name} -> {ok, path}. dir must exist inside roots.
|
|
708
796
|
if (routePath.split('?')[0] === '/api/mkdir' && req.method === 'POST') {
|
|
709
797
|
let body;
|
package/package.json
CHANGED
package/site/app/js/app.js
CHANGED
|
@@ -810,7 +810,7 @@ function sessionsColumn() {
|
|
|
810
810
|
rail: r.isError ? 'flame' : (r.isSubagent ? 'purple' : 'green'),
|
|
811
811
|
_focusEventI: r.i, _focusEventTs: r.ts,
|
|
812
812
|
}));
|
|
813
|
-
|
|
813
|
+
const list = ConversationList({
|
|
814
814
|
sessions: items,
|
|
815
815
|
selected: state.selectedSid,
|
|
816
816
|
// Search runs across every project (runSearch() clears projectFilter so
|
|
@@ -842,6 +842,18 @@ function sessionsColumn() {
|
|
|
842
842
|
hasMore: (state.searchHits.results || []).length > state.sessionsLimit,
|
|
843
843
|
onLoadMore: () => { state.sessionsLimit += 60; render(); },
|
|
844
844
|
});
|
|
845
|
+
// Only per-session event JSON / full transcript export existed - the
|
|
846
|
+
// search RESULTS themselves (title, project, snippet, timestamp) had no
|
|
847
|
+
// export path. Rendered as a plain sibling OUTSIDE ConversationList
|
|
848
|
+
// (not threaded through any of its props) - a prior attempt passing a
|
|
849
|
+
// VElement/extra prop through the caption slot triggered a real,
|
|
850
|
+
// intermittent webjsx crash inside that shared component; a fully
|
|
851
|
+
// separate wrapper keeps this feature from touching that risk surface.
|
|
852
|
+
if (!hits.length) return list;
|
|
853
|
+
return h('div', { key: 'searchwrap', class: 'agentgui-search-rail-wrap' },
|
|
854
|
+
h('div', { key: 'searchexportrow', class: 'agentgui-search-export-row' },
|
|
855
|
+
Btn({ key: 'searchexport', onClick: () => downloadBlob(JSON.stringify(hits, null, 2), 'agentgui-search-' + (state.searchQ || 'results').replace(/[^a-z0-9-]+/gi, '-') + '-' + dateStamp() + '.json', 'application/json'), children: 'export ' + hits.length + ' result' + (hits.length === 1 ? '' : 's') })),
|
|
856
|
+
list);
|
|
845
857
|
}
|
|
846
858
|
const sessionsView = visibleSessions();
|
|
847
859
|
const sliced = sessionsView.slice(0, state.sessionsLimit);
|
|
@@ -1180,10 +1192,13 @@ async function runFileMutation(fn, doneMsg, patch) {
|
|
|
1180
1192
|
if (!d || d.busy) return;
|
|
1181
1193
|
d.busy = true; d.error = null; render();
|
|
1182
1194
|
try {
|
|
1183
|
-
await fn();
|
|
1195
|
+
const result = await fn();
|
|
1184
1196
|
state.files.dialog = null;
|
|
1185
1197
|
restoreFileDialogFocus(d._trigger);
|
|
1186
1198
|
announce(doneMsg);
|
|
1199
|
+
// A soft-delete's trashId (if this mutation was a delete) rides the
|
|
1200
|
+
// return value straight to the undo-toast caller.
|
|
1201
|
+
if (result && result.trashId) offerUndoDelete(result.trashId, doneMsg);
|
|
1187
1202
|
if (patch) {
|
|
1188
1203
|
// Patch the visible list immediately, matching the bulk-delete/move
|
|
1189
1204
|
// pattern, instead of stalling the dialog on a second full round-trip.
|
|
@@ -1200,6 +1215,36 @@ async function runFileMutation(fn, doneMsg, patch) {
|
|
|
1200
1215
|
d.busy = false; d.error = fileMutationCopy(e); render();
|
|
1201
1216
|
}
|
|
1202
1217
|
}
|
|
1218
|
+
// Delete is a soft-delete server-side (moved to a confined .agentgui-trash/,
|
|
1219
|
+
// see lib/http-handler.js) - offer a real undo action within the retention
|
|
1220
|
+
// window instead of the ConfirmDialog's pre-delete confirm being the ONLY
|
|
1221
|
+
// safety net. One active undo toast at a time (the most recent delete wins;
|
|
1222
|
+
// an in-flight bulk-delete calls this per-entry, each replacing the last -
|
|
1223
|
+
// acceptable since restoring the single most recent one is still strictly
|
|
1224
|
+
// better than no undo at all, and stacking N toasts for a bulk op would be
|
|
1225
|
+
// its own UX problem).
|
|
1226
|
+
const UNDO_DELETE_WINDOW_MS = 10000;
|
|
1227
|
+
let _undoDeleteTimer = null;
|
|
1228
|
+
function offerUndoDelete(trashId, doneMsg) {
|
|
1229
|
+
clearTimeout(_undoDeleteTimer);
|
|
1230
|
+
state.files.undoDelete = { trashId, doneMsg };
|
|
1231
|
+
render();
|
|
1232
|
+
_undoDeleteTimer = setTimeout(() => { state.files.undoDelete = null; render(); }, UNDO_DELETE_WINDOW_MS);
|
|
1233
|
+
}
|
|
1234
|
+
async function undoDelete() {
|
|
1235
|
+
const u = state.files.undoDelete;
|
|
1236
|
+
if (!u) return;
|
|
1237
|
+
clearTimeout(_undoDeleteTimer);
|
|
1238
|
+
state.files.undoDelete = null;
|
|
1239
|
+
try {
|
|
1240
|
+
await B.restoreEntry(state.backend, u.trashId);
|
|
1241
|
+
announce('restored');
|
|
1242
|
+
await loadDir(state.files.path, { fromHash: true });
|
|
1243
|
+
} catch (e) {
|
|
1244
|
+
announce('could not restore: ' + fileMutationCopy(e));
|
|
1245
|
+
render();
|
|
1246
|
+
}
|
|
1247
|
+
}
|
|
1203
1248
|
// Upload a FileList into the current directory; per-file rows feed the kit
|
|
1204
1249
|
// UploadProgress (done/error per file - fetch has no chunk progress).
|
|
1205
1250
|
async function uploadFiles(fileList) {
|
|
@@ -1334,9 +1379,12 @@ function fileDialog() {
|
|
|
1334
1379
|
const isDir = d.file.type === 'dir';
|
|
1335
1380
|
return ConfirmDialog({
|
|
1336
1381
|
title: 'Delete ' + d.file.name,
|
|
1382
|
+
// Delete is soft (moved to trash, undoable for a short window right
|
|
1383
|
+
// after) - the copy no longer overclaims permanence the way an actual
|
|
1384
|
+
// unlink would warrant.
|
|
1337
1385
|
message: isDir
|
|
1338
|
-
? 'Delete this folder and everything inside it?
|
|
1339
|
-
: 'Delete this file?
|
|
1386
|
+
? 'Delete this folder and everything inside it? You can undo this for a few seconds after.'
|
|
1387
|
+
: 'Delete this file? You can undo this for a few seconds after.',
|
|
1340
1388
|
error: d.error || null, busy: !!d.busy,
|
|
1341
1389
|
confirmLabel: d.busy ? 'deleting…' : 'delete', cancelLabel: 'cancel', destructive: true,
|
|
1342
1390
|
onCancel: closeFileDialog,
|
|
@@ -1659,6 +1707,10 @@ function filesMain() {
|
|
|
1659
1707
|
: body;
|
|
1660
1708
|
return [
|
|
1661
1709
|
offlineBanner(),
|
|
1710
|
+
f.undoDelete ? Alert({ key: 'undodel', kind: 'info', title: 'Deleted',
|
|
1711
|
+
children: [
|
|
1712
|
+
h('span', { key: 'udtxt' }, (f.undoDelete.doneMsg || 'Deleted') + ' - undo within a few seconds. '),
|
|
1713
|
+
Btn({ key: 'udbtn', onClick: () => undoDelete(), children: 'undo' })] }) : null,
|
|
1662
1714
|
PageHeader({ compact: true, dense: true, title: 'Files', lede: 'Browse and manage files in the allowed folders.' }),
|
|
1663
1715
|
// One vertical beat (.ds-files-stack gap) for the whole command stack -
|
|
1664
1716
|
// the bands used to butt edge-to-edge while the header gap was 24px.
|
|
@@ -3722,6 +3774,26 @@ function clearLocalData() {
|
|
|
3722
3774
|
location.reload();
|
|
3723
3775
|
}
|
|
3724
3776
|
|
|
3777
|
+
// navigator.storage.estimate() is async - fetch once (per settings visit,
|
|
3778
|
+
// not per render) and cache the result in state; a fresh fetch every render
|
|
3779
|
+
// would be wasteful and the number doesn't need to be live-updating.
|
|
3780
|
+
let _storageEstimateFetched = false;
|
|
3781
|
+
function fetchStorageEstimateOnce() {
|
|
3782
|
+
if (_storageEstimateFetched) return;
|
|
3783
|
+
_storageEstimateFetched = true;
|
|
3784
|
+
if (!(navigator.storage && navigator.storage.estimate)) { state._storageEstimate = { unsupported: true }; return; }
|
|
3785
|
+
navigator.storage.estimate().then((est) => { state._storageEstimate = est; render(); }).catch(() => { state._storageEstimate = { unsupported: true }; });
|
|
3786
|
+
}
|
|
3787
|
+
function storageEstimateRow() {
|
|
3788
|
+
fetchStorageEstimateOnce();
|
|
3789
|
+
const est = state._storageEstimate;
|
|
3790
|
+
if (!est) return h('div', { key: 'storageest', class: 't-meta agentgui-field-my' }, 'browser storage: checking…');
|
|
3791
|
+
if (est.unsupported) return null; // Safari/older browsers - no estimate API, no row rather than a broken one
|
|
3792
|
+
const used = fmtBytes(est.usage || 0);
|
|
3793
|
+
const quota = est.quota ? fmtBytes(est.quota) : null;
|
|
3794
|
+
return h('div', { key: 'storageest', class: 't-meta agentgui-field-my' },
|
|
3795
|
+
'browser storage (this origin, all sources): ' + used + (quota ? ' of ' + quota + ' available' : ''));
|
|
3796
|
+
}
|
|
3725
3797
|
function preferencesPanel() {
|
|
3726
3798
|
const hh = state.health || {};
|
|
3727
3799
|
const savedChat = lsGet(CHAT_KEY);
|
|
@@ -3745,6 +3817,13 @@ function preferencesPanel() {
|
|
|
3745
3817
|
: null,
|
|
3746
3818
|
h('div', { key: 'lsize', class: 't-meta agentgui-field-my' },
|
|
3747
3819
|
'local data: ' + fmtBytes(lsBytes) + ' across ' + lsKeys + ' key' + (lsKeys === 1 ? '' : 's')),
|
|
3820
|
+
// agentgui's own localStorage keys are only PART of what this origin
|
|
3821
|
+
// accumulates - the markdown-highlighting CDN scripts (marked/dompurify/
|
|
3822
|
+
// prismjs) and any browser HTTP cache add to real storage usage with no
|
|
3823
|
+
// visibility at all previously. navigator.storage.estimate() surfaces
|
|
3824
|
+
// the origin's actual total-vs-quota figure alongside the known-keys
|
|
3825
|
+
// breakdown, so "why is my browser storage full" has an actual answer.
|
|
3826
|
+
storageEstimateRow(),
|
|
3748
3827
|
h('div', { key: 'expchatrow', class: 'agentgui-field-my' },
|
|
3749
3828
|
Btn({ key: 'expchat', disabled: !savedChat,
|
|
3750
3829
|
title: savedChat ? 'Download the saved chat transcript as JSON' : 'no saved conversation yet',
|
package/site/app/js/backend.js
CHANGED
|
@@ -167,7 +167,10 @@ export async function statPath(base, p) {
|
|
|
167
167
|
}
|
|
168
168
|
|
|
169
169
|
export function renameEntry(base, filePath, newName) { return mutateJSON(base, '/api/rename', { path: filePath, newName }); }
|
|
170
|
+
// deleteEntry's server response now includes trashId - the delete is a soft
|
|
171
|
+
// move-to-trash, not a permanent unlink, so the caller can offer an undo.
|
|
170
172
|
export function deleteEntry(base, filePath, recursive) { return mutateJSON(base, '/api/delete', { path: filePath, recursive: !!recursive }); }
|
|
173
|
+
export function restoreEntry(base, trashId) { return mutateJSON(base, '/api/restore', { trashId }); }
|
|
171
174
|
export function makeDir(base, dirPath, name) { return mutateJSON(base, '/api/mkdir', { dir: dirPath, name }); }
|
|
172
175
|
export function moveEntry(base, filePath, destDir, overwrite) { return mutateJSON(base, '/api/move', { path: filePath, destDir, overwrite: !!overwrite }); }
|
|
173
176
|
|
|
@@ -32,7 +32,7 @@
|
|
|
32
32
|
forward of the heritage green; green/purple/mascot demote to print-ink
|
|
33
33
|
category roles. */
|
|
34
34
|
--acid: #B6FF1B;
|
|
35
|
-
--acid-deep: #
|
|
35
|
+
--acid-deep: #4E4200; /* AA text tone of the lead on paper (WCAG-verified: #4E7A00 measured 4.21:1 against --accent-tint's own color-mix() output, which derives FROM --acid-deep -- so a small darkening shifts both foreground and background together and converges too slowly; #4E4200 is the minimal shade that actually clears 4.5:1 against its own self-referential tint background) */
|
|
36
36
|
--green: #247420;
|
|
37
37
|
--green-2: #3A9A34;
|
|
38
38
|
--green-deep: #133F10;
|
|
@@ -537,6 +537,49 @@
|
|
|
537
537
|
}
|
|
538
538
|
}
|
|
539
539
|
|
|
540
|
+
/* ============================================================
|
|
541
|
+
Semantic alias tier — primitive -> semantic -> component
|
|
542
|
+
------------------------------------------------------------
|
|
543
|
+
Three layers, in order:
|
|
544
|
+
1. PRIMITIVE (above) raw palette values: --acid, --green, --sky, --warn,
|
|
545
|
+
--panel-1/2/3, --paper/--ink, etc. These are the only tokens that ever
|
|
546
|
+
carry a literal color/length. A re-brand edits primitives.
|
|
547
|
+
2. SEMANTIC (here) named-by-INTENT aliases that resolve to a primitive
|
|
548
|
+
(e.g. --accent: var(--panel-accent)). A component or a future re-brand
|
|
549
|
+
can retune ONE semantic variable (say, --rail-danger) without hunting
|
|
550
|
+
through every primitive it happens to reuse elsewhere.
|
|
551
|
+
3. COMPONENT (app-shell.css, community.css, etc.) component sheets keep
|
|
552
|
+
consuming primitives directly today (--accent, --panel-*, --green,
|
|
553
|
+
--warn, ...) exactly as before — this tier does not repoint any
|
|
554
|
+
existing component rule. It exists so a FUTURE swap has a semantic
|
|
555
|
+
layer ready to migrate onto, one component sheet at a time, without
|
|
556
|
+
a flag-day rewrite. Purely additive: no primitive token's name or
|
|
557
|
+
value changes here, and no component references these new names yet.
|
|
558
|
+
|
|
559
|
+
Naming matches vocabulary already established elsewhere in this system:
|
|
560
|
+
--surface-1/2 mirror the --panel-1/--panel-2 tonal-surface steps already
|
|
561
|
+
used across app-shell.css/community.css; --rail-info/-success/-warning/-error
|
|
562
|
+
mirror the canonical .tone-info/.tone-success/.tone-warning/.tone-error
|
|
563
|
+
banner/badge/chip severity names (app-shell.css L592, community.css
|
|
564
|
+
.cm-banner.tone-*) rather than inventing a new severity vocabulary.
|
|
565
|
+
============================================================ */
|
|
566
|
+
.ds-247420 {
|
|
567
|
+
/* --accent already exists as a primitive-facing alias (var(--acid)) above;
|
|
568
|
+
restated here under the semantic tier for discoverability alongside its
|
|
569
|
+
siblings. Same value, same token — not a second definition. */
|
|
570
|
+
--surface-1: var(--panel-1);
|
|
571
|
+
--surface-2: var(--panel-2);
|
|
572
|
+
|
|
573
|
+
/* Rail/status severity aliases — map onto the same color choices the
|
|
574
|
+
existing .cm-banner.tone-* rules in community.css already use, so a
|
|
575
|
+
future rail/indicator component can consume one semantic name instead
|
|
576
|
+
of picking the right primitive per severity by hand. */
|
|
577
|
+
--rail-info: var(--sky);
|
|
578
|
+
--rail-success: var(--green-2);
|
|
579
|
+
--rail-warning: var(--amber);
|
|
580
|
+
--rail-error: var(--warn);
|
|
581
|
+
}
|
|
582
|
+
|
|
540
583
|
/* Elevation tokens consumed by overlays (Tooltip, Popover, Dropdown, Dialog). */
|
|
541
584
|
.ds-247420 {
|
|
542
585
|
--shadow-1: 0 1px 2px color-mix(in oklab, var(--fg) 8%, transparent);
|
|
@@ -1799,12 +1842,10 @@
|
|
|
1799
1842
|
.ds-247420 .row-form input:focus-visible,
|
|
1800
1843
|
.ds-247420 .row-form textarea:focus-visible { box-shadow: var(--focus-ring-inset); }
|
|
1801
1844
|
|
|
1802
|
-
/* Field char counter (TextField maxLength)
|
|
1803
|
-
|
|
1804
|
-
|
|
1805
|
-
|
|
1806
|
-
text-align: right;
|
|
1807
|
-
}
|
|
1845
|
+
/* Field char counter (TextField maxLength) — canonical definition is with
|
|
1846
|
+
the rest of the .ds-field family below (states-interactions.css); this
|
|
1847
|
+
duplicate was removed to stop the two bodies fighting on stylesheet load
|
|
1848
|
+
order. */
|
|
1808
1849
|
|
|
1809
1850
|
/* Multi-column form layout (Form columns prop) */
|
|
1810
1851
|
.ds-247420 .row-form[data-columns="2"] { grid-template-columns: repeat(2, 1fr); }
|
|
@@ -2903,14 +2944,10 @@
|
|
|
2903
2944
|
.ds-247420 .ds-shortcuts-hint .ds-kbd { white-space: normal; max-width: 100%; }
|
|
2904
2945
|
.ds-247420 .ds-kbd-caps { display: inline-flex; flex-wrap: wrap; align-items: center; gap: 4px; }
|
|
2905
2946
|
.ds-247420 .ds-kbd-sep { color: var(--fg-3); font-size: var(--fs-micro); padding: 0 2px; }
|
|
2906
|
-
|
|
2907
|
-
|
|
2908
|
-
|
|
2909
|
-
|
|
2910
|
-
border-bottom-width: 2px;
|
|
2911
|
-
font-family: var(--ff-mono); font-size: var(--fs-micro); color: var(--fg-2);
|
|
2912
|
-
white-space: nowrap;
|
|
2913
|
-
}
|
|
2947
|
+
/* .ds-kbd base definition lives in editor-primitives.css (canonical home —
|
|
2948
|
+
it owns the fuller ds-kbd-group/ds-kbd-row family this key chip belongs
|
|
2949
|
+
to); this file only carries the .ds-shortcuts-hint contextual overrides
|
|
2950
|
+
above. */
|
|
2914
2951
|
|
|
2915
2952
|
/* ============================================================
|
|
2916
2953
|
Theme toggle (segmented + compact) — bound to src/theme.js
|
|
@@ -3487,6 +3524,20 @@
|
|
|
3487
3524
|
Comprehensive improvements for perfect UX across all surfaces
|
|
3488
3525
|
============================================================ */
|
|
3489
3526
|
|
|
3527
|
+
/* ------------------------------------------------------------
|
|
3528
|
+
Table — clickable row focus ring
|
|
3529
|
+
Table()'s onRowClick path (content.js) sets class="clickable"
|
|
3530
|
+
role="button" tabindex="0" on the <tr>; the browser's native
|
|
3531
|
+
:focus-visible outline on a <tr> renders inconsistently across
|
|
3532
|
+
engines (often clipped by table border-collapse), so it needs
|
|
3533
|
+
the same explicit outline treatment every other interactive
|
|
3534
|
+
primitive in this file gets, via the shared --focus-* tokens.
|
|
3535
|
+
-------------------------------------------------------------- */
|
|
3536
|
+
.ds-247420 tr.clickable:focus-visible {
|
|
3537
|
+
outline: var(--focus-w) solid var(--focus-color);
|
|
3538
|
+
outline-offset: calc(-1 * var(--focus-offset));
|
|
3539
|
+
}
|
|
3540
|
+
|
|
3490
3541
|
/* ------------------------------------------------------------
|
|
3491
3542
|
Component States — Disabled, Loading, Error, Success
|
|
3492
3543
|
-------------------------------------------------------------- */
|
|
@@ -3867,14 +3918,25 @@
|
|
|
3867
3918
|
}
|
|
3868
3919
|
.ds-247420 .ds-field-label {
|
|
3869
3920
|
font-size: var(--fs-sm, 14px);
|
|
3921
|
+
font-weight: 500;
|
|
3870
3922
|
color: var(--fg-2);
|
|
3871
3923
|
line-height: 1.3;
|
|
3872
3924
|
}
|
|
3925
|
+
.ds-247420 .ds-field-required {
|
|
3926
|
+
color: var(--danger);
|
|
3927
|
+
font-weight: 700;
|
|
3928
|
+
margin-left: 2px;
|
|
3929
|
+
}
|
|
3873
3930
|
.ds-247420 .ds-field-hint {
|
|
3874
3931
|
font-size: var(--fs-tiny, 13px);
|
|
3875
3932
|
color: var(--fg-3);
|
|
3876
3933
|
line-height: 1.35;
|
|
3877
3934
|
}
|
|
3935
|
+
.ds-247420 .ds-field-error {
|
|
3936
|
+
color: var(--danger);
|
|
3937
|
+
font-size: var(--fs-tiny, 13px);
|
|
3938
|
+
font-weight: 500;
|
|
3939
|
+
}
|
|
3878
3940
|
.ds-247420 .ds-field-count {
|
|
3879
3941
|
font-size: var(--fs-tiny, 13px);
|
|
3880
3942
|
color: var(--fg-3);
|
|
@@ -4920,13 +4982,10 @@
|
|
|
4920
4982
|
.ds-247420 .ds-sub-btn:hover { border-color: var(--accent); color: var(--accent-ink); }
|
|
4921
4983
|
.ds-247420 .ds-sub-btn span { display: block; font-size: 18px; font-weight: 700; color: var(--accent-ink); }
|
|
4922
4984
|
|
|
4923
|
-
/* SessionRow
|
|
4924
|
-
|
|
4925
|
-
|
|
4926
|
-
|
|
4927
|
-
border-bottom: var(--bw-hair) solid var(--bg-3); cursor: pointer;
|
|
4928
|
-
}
|
|
4929
|
-
.ds-247420 .ds-session-row:hover { background: var(--bg-3); }
|
|
4985
|
+
/* SessionRow — base .ds-session-row (layout, hover/focus/active states,
|
|
4986
|
+
rail-tone indicators) is canonically defined in chat.css alongside the
|
|
4987
|
+
rest of the chat session-list component; this file only carries the
|
|
4988
|
+
dev/admin-row sub-parts (id/counts/devcnt/span) that decorate it. */
|
|
4930
4989
|
.ds-247420 .ds-session-row-id {
|
|
4931
4990
|
font-family: var(--ff-mono); font-size: var(--fs-micro); color: var(--accent-ink);
|
|
4932
4991
|
flex: 0 1 160px; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
|
|
@@ -7389,6 +7448,12 @@
|
|
|
7389
7448
|
ConversationList — left-rail "Chats" column.
|
|
7390
7449
|
---------------------------------------------------------------------------- */
|
|
7391
7450
|
.ds-247420 .ds-sessions { display: flex; flex-direction: column; min-height: 0; height: 100%; }
|
|
7451
|
+
/* agentgui's search-results export row sits as a sibling ABOVE ConversationList
|
|
7452
|
+
(not threaded through any of its props) - the wrapper must replicate the
|
|
7453
|
+
same flex-column layout .ds-sessions itself uses so ConversationList's own
|
|
7454
|
+
internal scroll region isn't broken by an extra non-flex ancestor. */
|
|
7455
|
+
.ds-247420 .agentgui-search-rail-wrap { display: flex; flex-direction: column; min-height: 0; height: 100%; }
|
|
7456
|
+
.ds-247420 .agentgui-search-export-row { flex: 0 0 auto; padding: var(--space-2) var(--space-3) 0; }
|
|
7392
7457
|
/* One row: quiet icon new-chat beside the search. The list's new-chat must not
|
|
7393
7458
|
repeat the rail's primary CTA - two identical green buttons on screen at
|
|
7394
7459
|
once read as a layout mistake, and the rail action already owns 'new'. */
|
|
@@ -9051,25 +9116,10 @@
|
|
|
9051
9116
|
display: flex; flex-direction: column; gap: var(--space-1, 4px);
|
|
9052
9117
|
margin-bottom: var(--space-2, 8px);
|
|
9053
9118
|
}
|
|
9054
|
-
.ds-
|
|
9055
|
-
|
|
9056
|
-
|
|
9057
|
-
|
|
9058
|
-
}
|
|
9059
|
-
.ds-247420 .ds-field-required {
|
|
9060
|
-
color: var(--danger);
|
|
9061
|
-
font-weight: 700;
|
|
9062
|
-
margin-left: 2px;
|
|
9063
|
-
}
|
|
9064
|
-
.ds-247420 .ds-field-hint {
|
|
9065
|
-
color: var(--fg-3, var(--fg-2));
|
|
9066
|
-
font-size: var(--fs-tiny, 12px);
|
|
9067
|
-
}
|
|
9068
|
-
.ds-247420 .ds-field-error {
|
|
9069
|
-
color: var(--danger);
|
|
9070
|
-
font-size: var(--fs-tiny, 12px);
|
|
9071
|
-
font-weight: 500;
|
|
9072
|
-
}
|
|
9119
|
+
/* .ds-field-label / .ds-field-required / .ds-field-hint / .ds-field-error
|
|
9120
|
+
canonical definitions now live in app-shell.css alongside the rest of the
|
|
9121
|
+
.ds-field family (this file's .ds-field-wrap consumes them by class name).
|
|
9122
|
+
Removed here to stop the two bodies fighting on stylesheet load order. */
|
|
9073
9123
|
.ds-247420 .ds-field-wrap [aria-invalid="true"] {
|
|
9074
9124
|
border-color: var(--danger) !important;
|
|
9075
9125
|
}
|
|
@@ -9663,9 +9713,26 @@
|
|
|
9663
9713
|
the app-composition wrapper, the server+channel rail pills, the voice view grid,
|
|
9664
9714
|
and the category-color tokens the consumer avatars use. All scoped under
|
|
9665
9715
|
.ds-247420. Additive — defines only the ca- prefix, group, rail-empty, vx-view
|
|
9666
|
-
classes plus the cat color tokens.
|
|
9667
|
-
|
|
9668
|
-
.
|
|
9716
|
+
classes plus the cat color tokens.
|
|
9717
|
+
|
|
9718
|
+
Relationship to community.css: this is NOT a fork of community.css's content
|
|
9719
|
+
-- it is a small (~150-line), genuinely distinct app-shell WRAPPER around the
|
|
9720
|
+
canonical, complete community surface. community.css (~1650 lines) owns every
|
|
9721
|
+
.cm-*/.ds-247420 .vx-* component (server rail, channel sidebar, chat header, member list,
|
|
9722
|
+
voice PTT/VAD/webcam, thread panel, forum, page view) and is the canonical, .ds-247420 actively-referenced stylesheet -- it is the one listed in package.json's
|
|
9723
|
+
`exports`/`files`, the one scripts/build.mjs and scripts/lint-tokens.mjs treat
|
|
9724
|
+
as a first-class component sheet on its own, and the one THEME.md/
|
|
9725
|
+
COMPONENT_API.md document as the load-order anchor
|
|
9726
|
+
(colors_and_type.css -> app-shell.css -> community.css -> chat.css ->
|
|
9727
|
+
editor-primitives.css -> community-app.css -> app-surfaces.css). This file
|
|
9728
|
+
loads strictly AFTER community.css (see ui_kits/community-app/index.html) and
|
|
9729
|
+
only adds the .ca-* composition shell + category-color tokens that
|
|
9730
|
+
community.css deliberately has no opinion about -- it never redefines a
|
|
9731
|
+
.cm-*/.vx-* rule community.css already owns. Canonical tokens/rules live in
|
|
9732
|
+
community.css; only overrides/additions specific to mountCommunityApp's own
|
|
9733
|
+
composition live here. */
|
|
9734
|
+
|
|
9735
|
+
html.ds-247420 .ca-app {
|
|
9669
9736
|
display: flex;
|
|
9670
9737
|
flex-direction: column;
|
|
9671
9738
|
height: 100vh;
|
|
@@ -10140,7 +10207,15 @@
|
|
|
10140
10207
|
}
|
|
10141
10208
|
}
|
|
10142
10209
|
|
|
10143
|
-
.ds-
|
|
10210
|
+
/* .ds-cli-block — the multi-row article/transcript container (CliBlock()'s
|
|
10211
|
+
outer wrapper holding stacked .ds-cli-row / .ds-cli-comment children).
|
|
10212
|
+
Distinct from the bare `.cli` single prompt+cmd row primitive owned by
|
|
10213
|
+
app-shell.css (Install(), HeroFromPageData(), terminal/site quickstart
|
|
10214
|
+
lines) — the two used to share the `.cli` selector with incompatible
|
|
10215
|
+
display models (flex row vs block transcript), so whichever stylesheet
|
|
10216
|
+
loaded last silently won for both consumers. Renamed here to remove the
|
|
10217
|
+
collision; see content.js CliBlock(). */
|
|
10218
|
+
.ds-247420 .ds-cli-block {
|
|
10144
10219
|
display: block;
|
|
10145
10220
|
background: var(--panel-1);
|
|
10146
10221
|
border-radius: var(--r-0);
|
|
@@ -10155,7 +10230,7 @@
|
|
|
10155
10230
|
white-space: pre-wrap;
|
|
10156
10231
|
word-break: break-word;
|
|
10157
10232
|
}
|
|
10158
|
-
.ds-247420 .cli .ds-cli-comment {
|
|
10233
|
+
.ds-247420 .ds-cli-block .ds-cli-comment {
|
|
10159
10234
|
color: var(--panel-text-3);
|
|
10160
10235
|
white-space: pre-wrap;
|
|
10161
10236
|
word-break: break-word;
|
|
@@ -10163,20 +10238,20 @@
|
|
|
10163
10238
|
padding: 3px 0;
|
|
10164
10239
|
line-height: 1.6;
|
|
10165
10240
|
}
|
|
10166
|
-
.ds-247420 .cli .ds-cli-comment:empty::before { content: '\00a0'; }
|
|
10167
|
-
.ds-247420 .cli .ds-cli-row {
|
|
10241
|
+
.ds-247420 .ds-cli-block .ds-cli-comment:empty::before { content: '\00a0'; }
|
|
10242
|
+
.ds-247420 .ds-cli-block .ds-cli-row {
|
|
10168
10243
|
display: flex;
|
|
10169
10244
|
gap: 10px;
|
|
10170
10245
|
padding: 3px 0;
|
|
10171
10246
|
white-space: pre-wrap;
|
|
10172
10247
|
word-break: break-word;
|
|
10173
10248
|
}
|
|
10174
|
-
.ds-247420 .cli .ds-cli-row .prompt {
|
|
10249
|
+
.ds-247420 .ds-cli-block .ds-cli-row .prompt {
|
|
10175
10250
|
color: var(--panel-accent);
|
|
10176
10251
|
flex: 0 0 auto;
|
|
10177
10252
|
user-select: none;
|
|
10178
10253
|
}
|
|
10179
|
-
.ds-247420 .cli .ds-cli-row .cmd {
|
|
10254
|
+
.ds-247420 .ds-cli-block .ds-cli-row .cmd {
|
|
10180
10255
|
color: var(--panel-text);
|
|
10181
10256
|
flex: 1 1 auto;
|
|
10182
10257
|
white-space: pre-wrap;
|