agentgui 1.0.1061 → 1.0.1062
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,3 +3106,24 @@
|
|
|
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-search-hit-hash-anchor
|
|
3113
|
+
subject: Search-hit event anchor (focusEventI/focusEventTs) not carried in the hash - reload/Back loses highlighted line
|
|
3114
|
+
status: pending
|
|
3115
|
+
- id: gc-settings-section-scrollspy
|
|
3116
|
+
subject: Settings section anchor is deep-link-in only - scrolling never updates state.settingsSection or the URL
|
|
3117
|
+
status: pending
|
|
3118
|
+
- id: gc-context-budget-affordance
|
|
3119
|
+
subject: No context-size/token-budget affordance - only turn count and dollar cost shown, never remaining context headroom
|
|
3120
|
+
status: pending
|
|
3121
|
+
- id: gc-expanded-body-highlight
|
|
3122
|
+
subject: Search-result highlighting inside expanded event bodies never highlights the matched query term
|
|
3123
|
+
status: pending
|
|
3124
|
+
- id: gc-search-results-export
|
|
3125
|
+
subject: No per-session or global export of History search results themselves
|
|
3126
|
+
status: pending
|
|
3127
|
+
- id: gc-settings-storage-estimate
|
|
3128
|
+
subject: Settings has no cache/data breakdown beyond agentgui's own localStorage keys - no navigator.storage.estimate()
|
|
3129
|
+
status: pending
|
package/package.json
CHANGED
package/site/app/js/app.js
CHANGED
|
@@ -1012,6 +1012,15 @@ function fileMutationCopy(e) {
|
|
|
1012
1012
|
if (e.status === 413) return 'Too large (50MB upload cap).';
|
|
1013
1013
|
return e.message || 'The operation failed.';
|
|
1014
1014
|
}
|
|
1015
|
+
// On a 409 name-collision, suggest 'name (2)' / 'name (3)' etc instead of
|
|
1016
|
+
// leaving the user to retype the identical blocked string from scratch -
|
|
1017
|
+
// mirrors the numbering convention most desktop file managers use.
|
|
1018
|
+
function suggestAlternateName(name) {
|
|
1019
|
+
const m = /^(.*) \((\d+)\)(\.[^.]*)?$/.exec(name);
|
|
1020
|
+
if (m) return m[1] + ' (' + (parseInt(m[2], 10) + 1) + ')' + (m[3] || '');
|
|
1021
|
+
const dot = name.lastIndexOf('.');
|
|
1022
|
+
return dot > 0 ? name.slice(0, dot) + ' (2)' + name.slice(dot) : name + ' (2)';
|
|
1023
|
+
}
|
|
1015
1024
|
function openFileDialog(kind, file) {
|
|
1016
1025
|
const trigger = typeof document !== 'undefined' ? document.activeElement : null;
|
|
1017
1026
|
state.files.dialog = { kind, file: file || null, error: null, busy: false, _trigger: trigger || null };
|
|
@@ -1285,7 +1294,7 @@ function fileDialog() {
|
|
|
1285
1294
|
// flow, so a sibling alert was invisible and outside the focus trap).
|
|
1286
1295
|
if (d.kind === 'rename') {
|
|
1287
1296
|
return PromptDialog({
|
|
1288
|
-
title: 'Rename ' + d.file.name, value: d.file.name, placeholder: 'new name',
|
|
1297
|
+
title: 'Rename ' + d.file.name, value: d.suggestedValue ?? d.file.name, placeholder: 'new name',
|
|
1289
1298
|
error: d.error || null, busy: !!d.busy,
|
|
1290
1299
|
confirmLabel: d.busy ? 'renaming…' : 'rename', cancelLabel: 'cancel',
|
|
1291
1300
|
onCancel: closeFileDialog,
|
|
@@ -1295,7 +1304,17 @@ function fileDialog() {
|
|
|
1295
1304
|
runFileMutation(() => B.renameEntry(state.backend, d.file.path, v), 'renamed to ' + v,
|
|
1296
1305
|
(entries) => entries.map((e) => (e.path || e.name) === (d.file.path || d.file.name)
|
|
1297
1306
|
? { ...e, name: v, path: e.path ? e.path.slice(0, e.path.length - d.file.name.length) + v : v }
|
|
1298
|
-
: e))
|
|
1307
|
+
: e))
|
|
1308
|
+
.then(() => {
|
|
1309
|
+
// On a 409 name-collision, prefill the retry input with a
|
|
1310
|
+
// suggested alternate instead of leaving the identical blocked
|
|
1311
|
+
// string - matching the pattern uploadFiles' 409-retry rows
|
|
1312
|
+
// already establish for the same class of conflict.
|
|
1313
|
+
if (state.files.dialog === d && d.error && /already exists/i.test(d.error)) {
|
|
1314
|
+
d.suggestedValue = suggestAlternateName(v);
|
|
1315
|
+
render();
|
|
1316
|
+
}
|
|
1317
|
+
});
|
|
1299
1318
|
},
|
|
1300
1319
|
});
|
|
1301
1320
|
}
|
|
@@ -1348,11 +1367,18 @@ function fileDialog() {
|
|
|
1348
1367
|
}
|
|
1349
1368
|
return PromptDialog({
|
|
1350
1369
|
title: 'Move ' + n + ' selected ' + (n === 1 ? 'entry' : 'entries'),
|
|
1351
|
-
value: state.files.path || '', placeholder: 'destination folder path',
|
|
1370
|
+
value: d._draft ?? (state.files.path || ''), placeholder: 'destination folder path',
|
|
1352
1371
|
error: d.error || null, busy: !!d.busy,
|
|
1353
1372
|
confirmLabel: d.busy ? 'moving…' : 'move ' + n, cancelLabel: 'cancel',
|
|
1373
|
+
// A second/third allowed root has no discoverable path other than
|
|
1374
|
+
// typing it from memory - one-click chips for every accessible root,
|
|
1375
|
+
// matching the same practicality upgrade the cwd editor got.
|
|
1376
|
+
roots: (Array.isArray(state.files.roots) && state.files.roots.length > 1)
|
|
1377
|
+
? state.files.roots.map((r) => ({ path: r, label: truncate(projectLabel(r) || r, 14, 24) }))
|
|
1378
|
+
: undefined,
|
|
1354
1379
|
onCancel: closeFileDialog,
|
|
1355
|
-
onInput: (v) => { d.error = null; d._validateDest(v); },
|
|
1380
|
+
onInput: (v) => { d._draft = v; d.error = null; d._validateDest(v); },
|
|
1381
|
+
onPickRoot: (v) => { d._draft = v; d.error = null; d._validateDest(v); render(); },
|
|
1356
1382
|
onConfirm: (v) => {
|
|
1357
1383
|
if (!v) { d.error = 'enter a destination folder'; render(); return; }
|
|
1358
1384
|
if (v === state.files.path) { d.error = 'already in that folder - enter a different destination'; render(); return; }
|
|
@@ -1362,7 +1388,7 @@ function fileDialog() {
|
|
|
1362
1388
|
}
|
|
1363
1389
|
if (d.kind === 'mkdir') {
|
|
1364
1390
|
return PromptDialog({
|
|
1365
|
-
title: 'New folder', value: '', placeholder: 'folder name',
|
|
1391
|
+
title: 'New folder', value: d.suggestedValue ?? '', placeholder: 'folder name',
|
|
1366
1392
|
error: d.error || null, busy: !!d.busy,
|
|
1367
1393
|
confirmLabel: d.busy ? 'creating…' : 'create', cancelLabel: 'cancel',
|
|
1368
1394
|
onCancel: closeFileDialog,
|
|
@@ -1371,7 +1397,13 @@ function fileDialog() {
|
|
|
1371
1397
|
runFileMutation(() => B.makeDir(state.backend, state.files.path, v), 'created ' + v,
|
|
1372
1398
|
(entries) => entries.some((e) => e.name === v)
|
|
1373
1399
|
? entries
|
|
1374
|
-
: [...entries, { name: v, path: (state.files.path ? state.files.path.replace(/\/$/, '') + '/' : '') + v, type: 'dir' }])
|
|
1400
|
+
: [...entries, { name: v, path: (state.files.path ? state.files.path.replace(/\/$/, '') + '/' : '') + v, type: 'dir' }])
|
|
1401
|
+
.then(() => {
|
|
1402
|
+
if (state.files.dialog === d && d.error && /already exists/i.test(d.error)) {
|
|
1403
|
+
d.suggestedValue = suggestAlternateName(v);
|
|
1404
|
+
render();
|
|
1405
|
+
}
|
|
1406
|
+
});
|
|
1375
1407
|
},
|
|
1376
1408
|
});
|
|
1377
1409
|
}
|
|
@@ -2104,6 +2136,7 @@ function applyToolResult(parts, block) {
|
|
|
2104
2136
|
const ERR_COPY = [
|
|
2105
2137
|
[/^ws closed$/, 'Lost connection to the server.'],
|
|
2106
2138
|
[/connection lost during stream/, 'Lost connection while the agent was responding - use retry.'],
|
|
2139
|
+
[/connection dropped mid-turn/, 'Connection dropped mid-turn - the response above may be incomplete (events weren\'t replayed). Retry to try again.'],
|
|
2107
2140
|
[/no sessionId from server/, 'The server could not start the agent - check it is installed.'],
|
|
2108
2141
|
[/^sessions: \d+/, 'History is still indexing - try again in a moment.'],
|
|
2109
2142
|
];
|
|
@@ -2166,11 +2199,17 @@ function chatMain() {
|
|
|
2166
2199
|
banners.push(Alert({ key: 'cwderr', kind: 'warn', title: 'Invalid working directory', children: state.cwdError }));
|
|
2167
2200
|
}
|
|
2168
2201
|
if (state.chat.externalUpdate) {
|
|
2202
|
+
const hasDraft = !!(state.chat.draft && state.chat.draft.trim());
|
|
2169
2203
|
banners.push(Alert({ key: 'xupd', kind: 'info', title: 'This chat was updated in another tab',
|
|
2170
2204
|
children: [
|
|
2171
|
-
h('span', { key: 'xutxt' }, 'Reload it to see the latest turns, or dismiss to keep this tab\'s view. '
|
|
2205
|
+
h('span', { key: 'xutxt' }, 'Reload it to see the latest turns, or dismiss to keep this tab\'s view. '
|
|
2206
|
+
// "reload it" re-pulls the transcript from localStorage, which
|
|
2207
|
+
// silently discarded whatever was typed but not yet sent in THIS
|
|
2208
|
+
// tab - warn before that data-loss, not just after.
|
|
2209
|
+
+ (hasDraft ? 'This tab has an unsent draft that reloading will discard. ' : '')),
|
|
2172
2210
|
Btn({ key: 'xureload', disabled: state.chat.busy, onClick: () => {
|
|
2173
2211
|
if (state.chat.busy) return;
|
|
2212
|
+
if (hasDraft && !window.confirm('Reloading will discard your unsent draft in this tab. Continue?')) return;
|
|
2174
2213
|
state.chat.externalUpdate = false;
|
|
2175
2214
|
state.chat.messages = []; state.chat.resumeSid = null; state.chat.totalCost = 0;
|
|
2176
2215
|
restoreChat(); render();
|
|
@@ -2189,9 +2228,15 @@ function chatMain() {
|
|
|
2189
2228
|
Btn({ key: 'ptruncdismiss', onClick: () => { state.chat.persistTruncated = false; render(); }, children: 'dismiss' })] }));
|
|
2190
2229
|
}
|
|
2191
2230
|
if (state.agentsError) {
|
|
2231
|
+
// A failed agents.list call (server/network issue) is a distinct case
|
|
2232
|
+
// from "the list loaded but nothing is installed" (installHint below,
|
|
2233
|
+
// which only fires on a genuinely non-empty, all-unavailable list) -
|
|
2234
|
+
// recommending npx-install commands here would be actively wrong, since
|
|
2235
|
+
// we don't actually know whether anything is installed. Just clarify the
|
|
2236
|
+
// failure is transient/server-side, not a "nothing is installed" state.
|
|
2192
2237
|
banners.push(Alert({ key: 'agerr', kind: 'error', title: 'Could not load agents from the server',
|
|
2193
2238
|
children: [
|
|
2194
|
-
h('span', { key: 'agtxt', title: state.agentsError }, 'The agent list failed to load. '),
|
|
2239
|
+
h('span', { key: 'agtxt', title: state.agentsError }, 'The agent list failed to load - this is a connection issue, not necessarily a sign nothing is installed. '),
|
|
2195
2240
|
Btn({ key: 'agretry', onClick: () => loadAgents(), children: 'retry' })] }));
|
|
2196
2241
|
}
|
|
2197
2242
|
if (state.chat.loadingTranscript) {
|
|
@@ -2200,9 +2245,19 @@ function chatMain() {
|
|
|
2200
2245
|
}
|
|
2201
2246
|
if (state.chat.confirmingEdit) {
|
|
2202
2247
|
const isRetry = state.chat.confirmingEdit.kind === 'retry';
|
|
2248
|
+
// Name the actual count/cost about to be discarded instead of the vague
|
|
2249
|
+
// "the later turns" - a user retrying/editing message 2 of 40 needs to
|
|
2250
|
+
// know whether that's 1 turn or 38 before confirming a destructive undo.
|
|
2251
|
+
const truncIdx = state.chat.confirmingEdit.idx;
|
|
2252
|
+
const discarded = state.chat.messages.slice(truncIdx + 1);
|
|
2253
|
+
const discardedTurns = discarded.filter((m) => m.role === 'user').length;
|
|
2254
|
+
const discardedCost = discarded.reduce((s, m) => s + (typeof m.costUsd === 'number' ? m.costUsd : 0), 0);
|
|
2255
|
+
const discardSummary = discardedTurns
|
|
2256
|
+
? plural(discardedTurns, 'turn') + (discardedCost > 0 ? ' ($' + discardedCost.toFixed(4) + ')' : '')
|
|
2257
|
+
: null;
|
|
2203
2258
|
banners.push(Alert({ key: 'confedit', kind: 'warn', title: isRetry ? 'Retry this turn?' : 'Edit this message?',
|
|
2204
2259
|
children: [
|
|
2205
|
-
h('span', { key: 'cetext' }, (isRetry ? 'Retrying' : 'Editing') + ' will remove the later turns - continue? '),
|
|
2260
|
+
h('span', { key: 'cetext' }, (isRetry ? 'Retrying' : 'Editing') + ' will remove ' + (discardSummary ? discardSummary + ' after this point' : 'the later turns') + ' - continue? '),
|
|
2206
2261
|
Btn({ key: 'ceno', onClick: cancelEditAndResend, children: 'cancel' }),
|
|
2207
2262
|
Btn({ key: 'ceyes', danger: true, onClick: confirmEditAndResend, children: isRetry ? 'retry' : 'continue' })] }));
|
|
2208
2263
|
}
|
|
@@ -2466,9 +2521,25 @@ function cwdBrowsePick(path) {
|
|
|
2466
2521
|
}
|
|
2467
2522
|
|
|
2468
2523
|
function offlineBanner() {
|
|
2524
|
+
// A session-expired 401 is a distinct, more actionable case than a plain
|
|
2525
|
+
// network-down offline state (the fix is "reload", not "wait for
|
|
2526
|
+
// reconnect") - surfaced first since it takes priority as an explanation.
|
|
2527
|
+
if (state.sessionExpired) {
|
|
2528
|
+
return Alert({ key: 'sessionexpired', kind: 'error', title: 'Session expired',
|
|
2529
|
+
children: [
|
|
2530
|
+
h('span', { key: 'setxt' }, 'Your access token is no longer valid (it may have rotated or expired). Reload the page to sign in again. '),
|
|
2531
|
+
Btn({ key: 'sereload', onClick: () => window.location.reload(), children: 'reload now' }),
|
|
2532
|
+
] });
|
|
2533
|
+
}
|
|
2469
2534
|
if (state.health.status === 'ok' || state.health.status === 'unknown') return null;
|
|
2470
2535
|
return Alert({ key: 'offline', kind: 'error', title: 'Backend unreachable',
|
|
2471
|
-
children:
|
|
2536
|
+
children: [
|
|
2537
|
+
h('span', { key: 'otxt' }, 'agentgui can\'t reach the server (' + (state.health.error || state.health.status) + '). Chat and history actions will fail until it reconnects. '),
|
|
2538
|
+
// Previously the only recovery was waiting for the WS backoff timer or
|
|
2539
|
+
// a manual page reload - a direct retry button re-probes /health
|
|
2540
|
+
// immediately instead of making the user guess how long to wait.
|
|
2541
|
+
Btn({ key: 'oretry', disabled: !!state.healthChecking, onClick: () => recheckHealth(), children: state.healthChecking ? 'checking…' : 'retry now' }),
|
|
2542
|
+
] });
|
|
2472
2543
|
}
|
|
2473
2544
|
|
|
2474
2545
|
// (The working-directory bar now lives in the AgentChat kit; agentgui wires its
|
|
@@ -2972,10 +3043,10 @@ function eventMatchesFilter(e, f) {
|
|
|
2972
3043
|
|
|
2973
3044
|
// Scroll to + flash the first error event, widening the render window (and
|
|
2974
3045
|
// clearing the type filter) so the row is actually rendered.
|
|
2975
|
-
function
|
|
2976
|
-
const idx = state.events.findIndex(e => e.isError);
|
|
3046
|
+
function jumpToEvent(idx) {
|
|
2977
3047
|
if (idx < 0) return;
|
|
2978
3048
|
state.eventFilter = 'all';
|
|
3049
|
+
state._errorNavIdx = idx;
|
|
2979
3050
|
const fromEnd = state.events.length - idx;
|
|
2980
3051
|
if (fromEnd > state.eventsLimit) state.eventsLimit = Math.ceil(fromEnd / 300) * 300;
|
|
2981
3052
|
render();
|
|
@@ -2987,6 +3058,21 @@ function jumpToFirstError() {
|
|
|
2987
3058
|
if (row) { row.scrollIntoView({ block: 'center' }); row.classList.add('event-flash'); setTimeout(() => row.classList.remove('event-flash'), 2000); }
|
|
2988
3059
|
});
|
|
2989
3060
|
}
|
|
3061
|
+
function jumpToFirstError() {
|
|
3062
|
+
const idx = state.events.findIndex(e => e.isError);
|
|
3063
|
+
jumpToEvent(idx);
|
|
3064
|
+
}
|
|
3065
|
+
// Persistent next/prev navigation between error events - jumpToFirstError
|
|
3066
|
+
// alone only reaches the FIRST error once; a session with multiple errors had
|
|
3067
|
+
// no way to step through them without manually scanning the event list.
|
|
3068
|
+
function jumpToNextError(dir) {
|
|
3069
|
+
const errIdxs = state.events.reduce((acc, e, i) => { if (e.isError) acc.push(i); return acc; }, []);
|
|
3070
|
+
if (!errIdxs.length) return;
|
|
3071
|
+
const cur = state._errorNavIdx;
|
|
3072
|
+
let pos = errIdxs.indexOf(cur);
|
|
3073
|
+
pos = pos < 0 ? (dir > 0 ? 0 : errIdxs.length - 1) : (pos + dir + errIdxs.length) % errIdxs.length;
|
|
3074
|
+
jumpToEvent(errIdxs[pos]);
|
|
3075
|
+
}
|
|
2990
3076
|
|
|
2991
3077
|
function historyMain() {
|
|
2992
3078
|
if (!state.selectedSid) {
|
|
@@ -3012,8 +3098,14 @@ function historyMain() {
|
|
|
3012
3098
|
}
|
|
3013
3099
|
|
|
3014
3100
|
const sess = (Array.isArray(state.sessions) ? state.sessions : []).find(s => s.sid === state.selectedSid);
|
|
3101
|
+
// sess.model is the raw ccsniff-sourced field (41st-run fix); ccsniff only
|
|
3102
|
+
// reads Claude Code's own JSONL so the agent is always constant. The
|
|
3103
|
+
// History detail header previously never surfaced either, reading
|
|
3104
|
+
// identity-thin next to the same session's Running-panel/Live-dashboard
|
|
3105
|
+
// rows which both show an agent+model badge.
|
|
3106
|
+
const agentModelBit = sess?.model ? ((agentById('claude-code')?.name || 'Claude Code') + ' · ' + sess.model) : null;
|
|
3015
3107
|
const lede = sess
|
|
3016
|
-
? (projectLabel(sess.project) || pathBasename(sess.cwd) || 'unknown location') + ' · ' + plural(sess.events || 0, 'event') + ' · ' + plural(sess.userTurns || 0, 'turn') + ' · ' + fmtRelTime(sess.last)
|
|
3108
|
+
? (projectLabel(sess.project) || pathBasename(sess.cwd) || 'unknown location') + (agentModelBit ? ' · ' + agentModelBit : '') + ' · ' + plural(sess.events || 0, 'event') + ' · ' + plural(sess.userTurns || 0, 'turn') + ' · ' + fmtRelTime(sess.last)
|
|
3017
3109
|
: UNTITLED_CONVERSATION;
|
|
3018
3110
|
|
|
3019
3111
|
const head = PageHeader({
|
|
@@ -3031,6 +3123,11 @@ function historyMain() {
|
|
|
3031
3123
|
onClick: () => downloadBlob(JSON.stringify(state.events, null, 2), (projectLabel(sess?.project) || 'session') + '-' + state.selectedSid + '.json', 'application/json'),
|
|
3032
3124
|
children: 'export' }),
|
|
3033
3125
|
hasErrors ? Btn({ key: 'jumperr', onClick: jumpToFirstError, children: 'jump to first error' }) : null,
|
|
3126
|
+
// Persistent next/prev stepping between errors - jump-to-first alone only
|
|
3127
|
+
// ever reaches the FIRST one; a session with several errors had no way to
|
|
3128
|
+
// step through the rest without manually scrolling/scanning.
|
|
3129
|
+
hasErrors ? Btn({ key: 'errprev', title: 'previous error', 'aria-label': 'previous error', onClick: () => jumpToNextError(-1), children: 'prev error' }) : null,
|
|
3130
|
+
hasErrors ? Btn({ key: 'errnext', title: 'next error', 'aria-label': 'next error', onClick: () => jumpToNextError(1), children: 'next error' }) : null,
|
|
3034
3131
|
].filter(Boolean));
|
|
3035
3132
|
|
|
3036
3133
|
if (state.events.length === 0) {
|
|
@@ -4047,6 +4144,13 @@ function registerWsStatusOnce() {
|
|
|
4047
4144
|
if (state.health.ws) { delete state.health.ws; render(); }
|
|
4048
4145
|
}
|
|
4049
4146
|
});
|
|
4147
|
+
// backend.js already detects a mid-session 401 (a token that stopped being
|
|
4148
|
+
// valid - e.g. PASSWORD rotated, cookie expired) and exports this hook, but
|
|
4149
|
+
// nothing ever subscribed to it: every subsequent fetch just failed
|
|
4150
|
+
// silently with no on-screen indication of WHY. Surface it as a persistent
|
|
4151
|
+
// banner instructing a reload (the only real recovery - the token lives in
|
|
4152
|
+
// window.__WS_TOKEN, injected server-side at page load).
|
|
4153
|
+
B.onSessionExpired?.(() => { state.sessionExpired = true; render(); });
|
|
4050
4154
|
}
|
|
4051
4155
|
|
|
4052
4156
|
hydratePrefs();
|
|
@@ -2686,6 +2686,17 @@
|
|
|
2686
2686
|
@media (hover: none), (pointer: coarse) {
|
|
2687
2687
|
.ds-247420 .ds-modal-input { font-size: 16px; min-height: 44px; }
|
|
2688
2688
|
}
|
|
2689
|
+
/* PromptDialog's optional roots-chip row (destination-path prompts with more
|
|
2690
|
+
than one allowed root) - same chip idiom the cwd editor's roots row uses. */
|
|
2691
|
+
.ds-247420 .ds-prompt-roots { display: flex; align-items: center; gap: 6px; flex-wrap: wrap; margin-top: 8px; }
|
|
2692
|
+
.ds-247420 .ds-prompt-root-chip {
|
|
2693
|
+
background: var(--bg-2); border: var(--bw-hair) solid var(--rule); color: var(--fg-2);
|
|
2694
|
+
border-radius: var(--r-pill); padding: 3px 10px; font: inherit; font-size: var(--fs-tiny);
|
|
2695
|
+
cursor: pointer;
|
|
2696
|
+
}
|
|
2697
|
+
.ds-247420 .ds-prompt-root-chip:hover { border-color: var(--accent); color: var(--fg); }
|
|
2698
|
+
.ds-247420 .ds-prompt-root-chip:focus-visible { outline: var(--focus-w) solid var(--focus-color); outline-offset: var(--focus-offset); }
|
|
2699
|
+
@media (pointer: coarse) { .ds-247420 .ds-prompt-root-chip { min-height: 44px; } }
|
|
2689
2700
|
/* In-body modal error (role=alert): mutation failures (409/403) surface INSIDE
|
|
2690
2701
|
the dialog, not behind the fixed overlay. */
|
|
2691
2702
|
.ds-247420 .ds-modal-error {
|