@oddessentials/agent-guild 0.21.0 → 0.23.0
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 +5 -0
- package/package.json +1 -1
- package/web/app.js +294 -5
- package/web/index.html +21 -0
- package/web/skins/gnomeland/skin.css +2 -2
- package/web/skins/grove/skin.css +2 -2
- package/web/skins/guild/skin.css +2 -2
- package/web/skins/orbital/skin.css +2 -2
- package/web/styles.css +38 -4
package/README.md
CHANGED
|
@@ -124,6 +124,11 @@ vX.Y.Z** switches over when you are ready. The version badge opens
|
|
|
124
124
|
|
|
125
125
|
<img src="docs/images/whats-new.webp" alt="The What's new panel listing the release notes of each version">
|
|
126
126
|
|
|
127
|
+
**Keep notes at hand.** **Notes** in the top bar opens a notepad for prompts,
|
|
128
|
+
commands and to-dos, even while a terminal is open. It saves up to 100,000
|
|
129
|
+
characters as you type and keeps them in this browser, never on the manager;
|
|
130
|
+
every tab of the page shows the same notes.
|
|
131
|
+
|
|
127
132
|
**Appearance.** The Appearance menu in the top bar picks a skin and light or
|
|
128
133
|
dark mode, without a reload. **Guild** is the default fantasy look;
|
|
129
134
|
**Professional** is a plain business look with no characters; **Orbital**
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@oddessentials/agent-guild",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.23.0",
|
|
4
4
|
"description": "Launch and watch AI coding-assistant terminal sessions from one local web page. A bundled session manager owns the terminals so the page can close and reconnect.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"repository": {
|
package/web/app.js
CHANGED
|
@@ -16,6 +16,8 @@ const GITHUB_ACCOUNT_KEY = 'agentGuild.githubAccount';
|
|
|
16
16
|
const CLONE_PARENT_KEY = 'agentGuild.cloneParent';
|
|
17
17
|
const SESSION_ORDER_KEY = 'agentGuild.sessionOrder';
|
|
18
18
|
const SOUND_KEY = 'agentGuild.sound';
|
|
19
|
+
const VOICE_KEY = 'agentGuild.voice';
|
|
20
|
+
const NOTES_KEY = 'agentGuild.notes';
|
|
19
21
|
const RELEASES_URL = 'https://github.com/oddessentials/agent-guild/releases';
|
|
20
22
|
const isMac = /Mac|iPhone|iPad/.test(navigator.platform || navigator.userAgent);
|
|
21
23
|
const reducedMotion = window.matchMedia('(prefers-reduced-motion: reduce)');
|
|
@@ -63,7 +65,7 @@ const state = {
|
|
|
63
65
|
// ---- storage (may be unavailable, e.g. blocked site data) -----------------
|
|
64
66
|
|
|
65
67
|
function load(key) { try { return localStorage.getItem(key); } catch { return null; } }
|
|
66
|
-
function save(key, value) { try { value === null ? localStorage.removeItem(key) : localStorage.setItem(key, value); } catch {
|
|
68
|
+
function save(key, value) { try { value === null ? localStorage.removeItem(key) : localStorage.setItem(key, value); return true; } catch { return false; } }
|
|
67
69
|
|
|
68
70
|
// ---- helpers --------------------------------------------------------------
|
|
69
71
|
|
|
@@ -87,6 +89,12 @@ function toast(message, ms = 5000, action = null) {
|
|
|
87
89
|
toastTimer = setTimeout(() => { el.hidden = true; }, ms);
|
|
88
90
|
}
|
|
89
91
|
|
|
92
|
+
/**
|
|
93
|
+
* Handles a click, but not the second click of a double-click, which lands on whatever the first one
|
|
94
|
+
* opened or uncovered: Close over Notes, or Stop manager under a panel's Close.
|
|
95
|
+
*/
|
|
96
|
+
const firstClick = (run) => (e) => { if (e.detail < 2) run(); };
|
|
97
|
+
|
|
90
98
|
function setConnection(kind, label) {
|
|
91
99
|
const el = $('connection');
|
|
92
100
|
el.className = `connection ${kind}`;
|
|
@@ -290,8 +298,108 @@ function placeAppearanceMenu() {
|
|
|
290
298
|
const menu = $('appearance-menu');
|
|
291
299
|
if (!menu.matches(':popover-open')) return;
|
|
292
300
|
const box = $('appearance').getBoundingClientRect();
|
|
293
|
-
|
|
301
|
+
const top = Math.round(box.bottom + 6);
|
|
302
|
+
menu.style.top = `${top}px`;
|
|
303
|
+
menu.style.maxHeight = `${innerHeight - top - 8}px`;
|
|
304
|
+
menu.style.left = 'auto';
|
|
294
305
|
menu.style.right = `${Math.max(8, Math.round(innerWidth - box.right))}px`;
|
|
306
|
+
if (menu.getBoundingClientRect().left < 8) {
|
|
307
|
+
menu.style.right = 'auto';
|
|
308
|
+
menu.style.left = `${Math.max(8, Math.round(box.left))}px`;
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
// ---- notes ----------------------------------------------------------------
|
|
313
|
+
|
|
314
|
+
/** Notes share the page's storage with every other setting, so they stay far below the browser's limit for it. */
|
|
315
|
+
const NOTES_LIMIT = 100000;
|
|
316
|
+
const NOTES_STATUS = {
|
|
317
|
+
saved: 'Saved in this browser as you type',
|
|
318
|
+
long: `Not saved: notes hold up to ${NOTES_LIMIT.toLocaleString('en-US')} characters. Shorten them to save.`,
|
|
319
|
+
refused: 'Not saved: the browser’s storage for this page is full or turned off. Copy what you need before you close the page.',
|
|
320
|
+
};
|
|
321
|
+
/**
|
|
322
|
+
* `saved`: the notes as this page last read or wrote them in storage. `status`: whether the text in the
|
|
323
|
+
* panel is saved, too `long` or `refused`; `shown`: the status the line under the title shows.
|
|
324
|
+
* `pressedOutside`: the last press on the panel was on its backdrop.
|
|
325
|
+
*/
|
|
326
|
+
const notesView = { saved: '', status: 'saved', shown: 'saved', pressedOutside: false };
|
|
327
|
+
|
|
328
|
+
/** Shows notes another tab saved since this page last read or wrote them. Saved notes win over text this page could not save. */
|
|
329
|
+
function refreshNotes() {
|
|
330
|
+
let text;
|
|
331
|
+
// Unlike load(), a read that fails is not taken for empty notes.
|
|
332
|
+
try { text = localStorage.getItem(NOTES_KEY) ?? ''; } catch { return; }
|
|
333
|
+
if (text === notesView.saved) return;
|
|
334
|
+
notesView.saved = text;
|
|
335
|
+
notesView.status = 'saved';
|
|
336
|
+
$('notes-text').value = text;
|
|
337
|
+
renderNotesStatus();
|
|
338
|
+
guardLeaving();
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
/** Saves the notes on every change; emptying them removes the saved copy. */
|
|
342
|
+
function saveNotes() {
|
|
343
|
+
const text = $('notes-text').value;
|
|
344
|
+
if (text.length > NOTES_LIMIT) notesView.status = 'long';
|
|
345
|
+
else if (!save(NOTES_KEY, text || null)) notesView.status = 'refused';
|
|
346
|
+
else {
|
|
347
|
+
notesView.status = 'saved';
|
|
348
|
+
notesView.saved = text;
|
|
349
|
+
}
|
|
350
|
+
renderNotesStatus();
|
|
351
|
+
guardLeaving();
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
/** The line under the title is a live region, so it is rewritten only when the status changes. */
|
|
355
|
+
function renderNotesStatus() {
|
|
356
|
+
if (notesView.shown === notesView.status) return;
|
|
357
|
+
notesView.shown = notesView.status;
|
|
358
|
+
const sub = $('notes-sub');
|
|
359
|
+
sub.textContent = NOTES_STATUS[notesView.status];
|
|
360
|
+
sub.classList.toggle('warn', notesView.status !== 'saved');
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
/** Another tab saved the notes, or cleared this page's storage. */
|
|
364
|
+
function notesStored(e) {
|
|
365
|
+
if (e.key === NOTES_KEY || e.key === null) refreshNotes();
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
function openNotes() {
|
|
369
|
+
hideTip();
|
|
370
|
+
refreshNotes();
|
|
371
|
+
// Saving an empty note changes nothing, but shows at once when the browser keeps nothing.
|
|
372
|
+
if (notesView.status === 'saved' && !$('notes-text').value) saveNotes();
|
|
373
|
+
const dialog = $('notes');
|
|
374
|
+
if (!dialog.open) dialog.showModal();
|
|
375
|
+
$('notes-text').focus();
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
function closeNotes() {
|
|
379
|
+
if ($('notes').open) $('notes').close();
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
/** Focus goes back to the Notes button in every browser, never to the page itself. */
|
|
383
|
+
function notesClosed() {
|
|
384
|
+
$('notes-open').focus();
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
/** The second press of a double-click on Notes lands in the panel, where it must not take the focus from the text. */
|
|
388
|
+
const keepFocus = (e) => { if (e.detail > 1 && e.target !== $('notes-text')) e.preventDefault(); };
|
|
389
|
+
|
|
390
|
+
/** Whether a press or click on the panel was on its backdrop, outside the sheet. */
|
|
391
|
+
function isOutsideNotes(e) {
|
|
392
|
+
const box = e.currentTarget.getBoundingClientRect();
|
|
393
|
+
return e.target === e.currentTarget && (e.clientX < box.left || e.clientX >= box.right || e.clientY < box.top || e.clientY >= box.bottom);
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
function notesPressed(e) {
|
|
397
|
+
notesView.pressedOutside = isOutsideNotes(e);
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
/** Only a press and a release both outside close the notes: a drag into or out of them selects text. */
|
|
401
|
+
function notesClicked(e) {
|
|
402
|
+
if (notesView.pressedOutside && isOutsideNotes(e)) closeNotes();
|
|
295
403
|
}
|
|
296
404
|
|
|
297
405
|
// ---- upgrading the manager ------------------------------------------------
|
|
@@ -2858,7 +2966,9 @@ function confirmLeaving(event) {
|
|
|
2858
2966
|
|
|
2859
2967
|
function guardLeaving() {
|
|
2860
2968
|
const running = state.connected && [...state.sessions.values()].some((s) => s.status === 'running');
|
|
2861
|
-
|
|
2969
|
+
// Notes that could not be saved are lost with the page.
|
|
2970
|
+
const unsaved = notesView.status !== 'saved' && $('notes-text').value !== '';
|
|
2971
|
+
if (running || unsaved) addEventListener('beforeunload', confirmLeaving);
|
|
2862
2972
|
else removeEventListener('beforeunload', confirmLeaving);
|
|
2863
2973
|
}
|
|
2864
2974
|
|
|
@@ -3264,6 +3374,7 @@ class TerminalView {
|
|
|
3264
3374
|
this.term.write(msg.data);
|
|
3265
3375
|
break;
|
|
3266
3376
|
case 'exit':
|
|
3377
|
+
if (dictation?.id === this.id) stopDictation();
|
|
3267
3378
|
this.term.write(`\r\n\x1b[2m${exitLine(state.sessions.get(this.id), msg)}\x1b[0m\r\n`);
|
|
3268
3379
|
break;
|
|
3269
3380
|
default:
|
|
@@ -3358,6 +3469,7 @@ class TerminalView {
|
|
|
3358
3469
|
|
|
3359
3470
|
function openPanel(id) {
|
|
3360
3471
|
if (!state.sessions.has(id)) return;
|
|
3472
|
+
if (dictation && dictation.id !== id) stopDictation();
|
|
3361
3473
|
if (state.activeId && state.activeId !== id) state.views.get(state.activeId)?.unmount();
|
|
3362
3474
|
state.activeId = id;
|
|
3363
3475
|
let view = state.views.get(id);
|
|
@@ -3368,12 +3480,15 @@ function openPanel(id) {
|
|
|
3368
3480
|
}
|
|
3369
3481
|
|
|
3370
3482
|
function closePanel() {
|
|
3483
|
+
stopDictation();
|
|
3371
3484
|
if (state.activeId) state.views.get(state.activeId)?.unmount();
|
|
3372
3485
|
state.activeId = null;
|
|
3373
3486
|
$('terminal-panel').hidden = true;
|
|
3487
|
+
renderVoice();
|
|
3374
3488
|
}
|
|
3375
3489
|
|
|
3376
3490
|
function updatePanel() {
|
|
3491
|
+
renderVoice();
|
|
3377
3492
|
const s = state.sessions.get(state.activeId);
|
|
3378
3493
|
if (!s) return;
|
|
3379
3494
|
paintProviderIcon($('panel-icon'), s.provider);
|
|
@@ -3386,6 +3501,157 @@ function updatePanel() {
|
|
|
3386
3501
|
stop.textContent = s.status !== 'running' ? 'Remove' : s.multiplexer ? 'Detach' : 'Stop';
|
|
3387
3502
|
}
|
|
3388
3503
|
|
|
3504
|
+
// ---- voice input ----------------------------------------------------------
|
|
3505
|
+
|
|
3506
|
+
const Recognition = window.SpeechRecognition || window.webkitSpeechRecognition;
|
|
3507
|
+
const VOICE_RETRY_DELAYS = [250, 500, 1000];
|
|
3508
|
+
let dictation = null;
|
|
3509
|
+
|
|
3510
|
+
function voiceOn() {
|
|
3511
|
+
return Boolean(Recognition) && load(VOICE_KEY) === 'on';
|
|
3512
|
+
}
|
|
3513
|
+
|
|
3514
|
+
function renderVoice() {
|
|
3515
|
+
if (dictation) keepDictating(dictation);
|
|
3516
|
+
$('voice-choice').hidden = !Recognition;
|
|
3517
|
+
$('voice').checked = voiceOn();
|
|
3518
|
+
$('panel-voice').hidden = !voiceOn() || state.sessions.get(state.activeId)?.status !== 'running';
|
|
3519
|
+
$('panel-voice').setAttribute('aria-pressed', String(Boolean(dictation)));
|
|
3520
|
+
}
|
|
3521
|
+
|
|
3522
|
+
function changeVoice(input) {
|
|
3523
|
+
save(VOICE_KEY, input.checked ? 'on' : null);
|
|
3524
|
+
renderVoice();
|
|
3525
|
+
}
|
|
3526
|
+
|
|
3527
|
+
/** Dictated words as terminal input: one line of text, no control characters, so nothing is ever submitted. */
|
|
3528
|
+
function dictatedText(text, first) {
|
|
3529
|
+
const clean = text.replace(/[\x00-\x1f\x7f-\x9f]+/g, ' ').replace(/\s+/g, ' ').trim();
|
|
3530
|
+
return clean && (first ? clean : ` ${clean}`);
|
|
3531
|
+
}
|
|
3532
|
+
|
|
3533
|
+
function canDictate(current) {
|
|
3534
|
+
const session = state.sessions.get(current.id);
|
|
3535
|
+
return voiceOn() && state.activeId === current.id && session?.status === 'running'
|
|
3536
|
+
&& session.startedAt === current.startedAt && state.views.has(current.id)
|
|
3537
|
+
&& !state.pageAway && !state.stopping && document.visibilityState === 'visible'
|
|
3538
|
+
&& !$('app').hidden && !$('terminal-panel').hidden;
|
|
3539
|
+
}
|
|
3540
|
+
|
|
3541
|
+
/** Every async continuation belongs to one explicit click and one run of the session. */
|
|
3542
|
+
function keepDictating(current) {
|
|
3543
|
+
if (dictation !== current) return false;
|
|
3544
|
+
if (canDictate(current)) return true;
|
|
3545
|
+
stopDictation();
|
|
3546
|
+
return false;
|
|
3547
|
+
}
|
|
3548
|
+
|
|
3549
|
+
async function startDictation() {
|
|
3550
|
+
const id = state.activeId;
|
|
3551
|
+
const session = state.sessions.get(id);
|
|
3552
|
+
if (!session || dictation) return;
|
|
3553
|
+
const current = {
|
|
3554
|
+
id, startedAt: session.startedAt, lang: navigator.language || 'en-US',
|
|
3555
|
+
android: /Android/i.test(navigator.userAgent || ''), processLocally: false,
|
|
3556
|
+
rec: null, timer: null, first: true, emptyRetries: 0,
|
|
3557
|
+
};
|
|
3558
|
+
if (!canDictate(current)) return;
|
|
3559
|
+
// Register before awaiting availability so another click or leaving the terminal cancels startup too.
|
|
3560
|
+
dictation = current;
|
|
3561
|
+
renderVoice();
|
|
3562
|
+
state.views.get(id)?.term.focus();
|
|
3563
|
+
try {
|
|
3564
|
+
// Browsers without this API start in the click handler, without an unnecessary await.
|
|
3565
|
+
if (typeof Recognition.available === 'function') {
|
|
3566
|
+
current.processLocally = await Recognition.available({ langs: [current.lang], processLocally: true }) === 'available';
|
|
3567
|
+
}
|
|
3568
|
+
} catch { /* cloud recognition */ }
|
|
3569
|
+
listenForDictation(current);
|
|
3570
|
+
}
|
|
3571
|
+
|
|
3572
|
+
function listenForDictation(current) {
|
|
3573
|
+
if (!keepDictating(current)) return;
|
|
3574
|
+
let rec;
|
|
3575
|
+
try {
|
|
3576
|
+
rec = new Recognition();
|
|
3577
|
+
current.rec = rec;
|
|
3578
|
+
rec.lang = current.lang;
|
|
3579
|
+
rec.continuous = !current.android;
|
|
3580
|
+
rec.interimResults = true;
|
|
3581
|
+
if (current.processLocally) rec.processLocally = true;
|
|
3582
|
+
} catch {
|
|
3583
|
+
stopDictation();
|
|
3584
|
+
toast('Voice input could not start.');
|
|
3585
|
+
return;
|
|
3586
|
+
}
|
|
3587
|
+
// A new recognizer per Android phrase avoids cumulative transcripts from continuous mode.
|
|
3588
|
+
let committed = 0;
|
|
3589
|
+
let heard = false;
|
|
3590
|
+
const preview = $('voice-preview');
|
|
3591
|
+
rec.onresult = (e) => {
|
|
3592
|
+
if (current.rec !== rec || !keepDictating(current)) return;
|
|
3593
|
+
let interim = '';
|
|
3594
|
+
for (let i = Math.max(e.resultIndex, committed); i < e.results.length; i += 1) {
|
|
3595
|
+
const result = e.results[i];
|
|
3596
|
+
if (!result.isFinal) { interim += result[0].transcript; continue; }
|
|
3597
|
+
committed = i + 1;
|
|
3598
|
+
const text = dictatedText(result[0].transcript, current.first);
|
|
3599
|
+
if (!text) continue;
|
|
3600
|
+
state.views.get(current.id)?.term.paste(text);
|
|
3601
|
+
current.first = false;
|
|
3602
|
+
current.emptyRetries = 0;
|
|
3603
|
+
heard = true;
|
|
3604
|
+
}
|
|
3605
|
+
preview.textContent = interim.trim();
|
|
3606
|
+
preview.hidden = !preview.textContent;
|
|
3607
|
+
};
|
|
3608
|
+
rec.onerror = (e) => {
|
|
3609
|
+
if (current.rec !== rec || !keepDictating(current)) return;
|
|
3610
|
+
if (current.android && e.error === 'no-speech') return; // bounded retry after end
|
|
3611
|
+
stopDictation();
|
|
3612
|
+
if (e.error === 'not-allowed' || e.error === 'service-not-allowed') toast('Voice input needs permission to use the microphone.');
|
|
3613
|
+
else if (e.error === 'network') toast('Voice input could not reach the speech service. Check your connection.');
|
|
3614
|
+
else if (e.error === 'audio-capture') toast('No microphone was found.');
|
|
3615
|
+
else if (e.error === 'language-not-supported') toast(`Voice input does not support ${current.lang}.`);
|
|
3616
|
+
else if (e.error !== 'aborted' && e.error !== 'no-speech') toast('Voice input stopped. Press Dictate to try again.');
|
|
3617
|
+
};
|
|
3618
|
+
rec.onend = () => {
|
|
3619
|
+
if (current.rec !== rec || !keepDictating(current)) return;
|
|
3620
|
+
current.rec = null;
|
|
3621
|
+
preview.hidden = true;
|
|
3622
|
+
preview.textContent = '';
|
|
3623
|
+
if (!current.android) return stopDictation();
|
|
3624
|
+
if (!heard && current.emptyRetries >= VOICE_RETRY_DELAYS.length) {
|
|
3625
|
+
stopDictation();
|
|
3626
|
+
toast('Voice input stopped after repeated silence. Press Dictate to try again.');
|
|
3627
|
+
return;
|
|
3628
|
+
}
|
|
3629
|
+
const delay = heard ? VOICE_RETRY_DELAYS[0] : VOICE_RETRY_DELAYS[current.emptyRetries++];
|
|
3630
|
+
current.timer = setTimeout(() => {
|
|
3631
|
+
current.timer = null;
|
|
3632
|
+
listenForDictation(current);
|
|
3633
|
+
}, delay);
|
|
3634
|
+
};
|
|
3635
|
+
try {
|
|
3636
|
+
rec.start();
|
|
3637
|
+
} catch {
|
|
3638
|
+
if (current.rec !== rec || !keepDictating(current)) return;
|
|
3639
|
+
stopDictation();
|
|
3640
|
+
toast('Voice input could not start.');
|
|
3641
|
+
}
|
|
3642
|
+
}
|
|
3643
|
+
|
|
3644
|
+
function stopDictation() {
|
|
3645
|
+
const current = dictation;
|
|
3646
|
+
if (!current) return;
|
|
3647
|
+
dictation = null;
|
|
3648
|
+
clearTimeout(current.timer);
|
|
3649
|
+
try { current.rec?.abort(); } catch { /* already ended */ }
|
|
3650
|
+
$('voice-preview').hidden = true;
|
|
3651
|
+
$('voice-preview').textContent = '';
|
|
3652
|
+
$('panel-voice').setAttribute('aria-pressed', 'false');
|
|
3653
|
+
}
|
|
3654
|
+
|
|
3389
3655
|
// ---- stopping the manager -------------------------------------------------
|
|
3390
3656
|
|
|
3391
3657
|
/**
|
|
@@ -3641,6 +3907,7 @@ let newsTimer;
|
|
|
3641
3907
|
|
|
3642
3908
|
function showAuth(message = '') {
|
|
3643
3909
|
managerLoss.cancel();
|
|
3910
|
+
closePanel();
|
|
3644
3911
|
closeModels();
|
|
3645
3912
|
closeNews();
|
|
3646
3913
|
closeChangelog();
|
|
@@ -3692,6 +3959,13 @@ $('auth-form').addEventListener('submit', (e) => {
|
|
|
3692
3959
|
boot();
|
|
3693
3960
|
});
|
|
3694
3961
|
$('panel-close').addEventListener('click', closePanel);
|
|
3962
|
+
$('panel-voice').addEventListener('click', () => {
|
|
3963
|
+
if (dictation) {
|
|
3964
|
+
const { id } = dictation;
|
|
3965
|
+
stopDictation();
|
|
3966
|
+
state.views.get(id)?.term.focus();
|
|
3967
|
+
} else startDictation();
|
|
3968
|
+
});
|
|
3695
3969
|
$('models-close').addEventListener('click', closeModels);
|
|
3696
3970
|
$('history-close').addEventListener('click', closeHistory);
|
|
3697
3971
|
$('history').addEventListener('click', (e) => { if (e.target === $('history')) closeHistory(); });
|
|
@@ -3761,6 +4035,16 @@ document.addEventListener('keydown', (e) => {
|
|
|
3761
4035
|
}, true);
|
|
3762
4036
|
addEventListener('scroll', () => { if (tipFor) hideTip(); }, true);
|
|
3763
4037
|
addEventListener('resize', () => { if (tipFor) hideTip(); });
|
|
4038
|
+
$('notes-open').addEventListener('click', firstClick(openNotes));
|
|
4039
|
+
$('notes-close').addEventListener('click', firstClick(closeNotes));
|
|
4040
|
+
$('notes').addEventListener('mousedown', keepFocus);
|
|
4041
|
+
$('notes').addEventListener('pointerdown', notesPressed);
|
|
4042
|
+
$('notes').addEventListener('click', notesClicked);
|
|
4043
|
+
$('notes').addEventListener('close', notesClosed);
|
|
4044
|
+
$('notes-text').addEventListener('input', saveNotes);
|
|
4045
|
+
addEventListener('storage', notesStored);
|
|
4046
|
+
// On load, and again for a page back from the back/forward cache, which may have missed another tab's notes.
|
|
4047
|
+
addEventListener('pageshow', refreshNotes);
|
|
3764
4048
|
$('news-all').addEventListener('click', openNews);
|
|
3765
4049
|
$('news-close').addEventListener('click', closeNews);
|
|
3766
4050
|
$('news-fresh').addEventListener('click', showFreshNews);
|
|
@@ -3774,10 +4058,12 @@ $('news').addEventListener('close', () => {
|
|
|
3774
4058
|
newsView.opener = null;
|
|
3775
4059
|
});
|
|
3776
4060
|
document.addEventListener('visibilitychange', () => {
|
|
4061
|
+
if (document.visibilityState !== 'visible') stopDictation();
|
|
3777
4062
|
if (document.visibilityState === 'visible' && state.connected && Date.now() - newsLoadedAt > 60000) loadNews();
|
|
3778
4063
|
});
|
|
3779
4064
|
addEventListener('pagehide', () => {
|
|
3780
4065
|
state.pageAway = true;
|
|
4066
|
+
stopDictation();
|
|
3781
4067
|
managerLoss.cancel();
|
|
3782
4068
|
state.eventsSocket?.close();
|
|
3783
4069
|
});
|
|
@@ -3805,6 +4091,7 @@ $('changelog-restart').addEventListener('click', () => {
|
|
|
3805
4091
|
addEventListener('storage', (e) => {
|
|
3806
4092
|
if (e.key === CHANGELOG_SEEN_KEY) renderVersion();
|
|
3807
4093
|
else if (e.key === SOUND_KEY) { $('sound').checked = soundOn(); prepareSounds(); }
|
|
4094
|
+
else if (e.key === VOICE_KEY) renderVoice();
|
|
3808
4095
|
// Another tab reordered the cards.
|
|
3809
4096
|
else if (e.key === SESSION_ORDER_KEY && !drag) arrange(parseOrder(e.newValue));
|
|
3810
4097
|
});
|
|
@@ -3843,14 +4130,15 @@ $('models-more').addEventListener('click', () => {
|
|
|
3843
4130
|
renderModels();
|
|
3844
4131
|
$('models-list').children[before]?.querySelector('.model-toggle')?.focus();
|
|
3845
4132
|
});
|
|
3846
|
-
$('stop-manager').addEventListener('click', () => stopManager());
|
|
3847
|
-
$('restart-manager').addEventListener('click', () => stopManager({ restart: true }));
|
|
4133
|
+
$('stop-manager').addEventListener('click', firstClick(() => stopManager()));
|
|
4134
|
+
$('restart-manager').addEventListener('click', firstClick(() => stopManager({ restart: true })));
|
|
3848
4135
|
$('copy-command').addEventListener('click', copyCommand);
|
|
3849
4136
|
$('upgrade').addEventListener('click', upgradeManager);
|
|
3850
4137
|
$('appearance-menu').addEventListener('change', (e) => {
|
|
3851
4138
|
if (e.target.name === 'skin') changeSkin(e.target);
|
|
3852
4139
|
else if (e.target.name === 'theme') changeTheme(e.target);
|
|
3853
4140
|
else if (e.target.name === 'sound') changeSound(e.target);
|
|
4141
|
+
else if (e.target.name === 'voice') changeVoice(e.target);
|
|
3854
4142
|
});
|
|
3855
4143
|
$('appearance-menu').addEventListener('toggle', (e) => {
|
|
3856
4144
|
if (e.newState !== 'open') return;
|
|
@@ -3885,6 +4173,7 @@ $('providers').addEventListener('pointerout', (e) => {
|
|
|
3885
4173
|
applyTheme(currentTheme());
|
|
3886
4174
|
renderSkinChoices();
|
|
3887
4175
|
$('sound').checked = soundOn();
|
|
4176
|
+
renderVoice();
|
|
3888
4177
|
// Follow the system setting until the user picks a theme.
|
|
3889
4178
|
window.matchMedia('(prefers-color-scheme: dark)').addEventListener('change', (e) => {
|
|
3890
4179
|
if (!load(THEME_KEY)) applyTheme(e.matches ? 'dark' : 'light');
|
package/web/index.html
CHANGED
|
@@ -40,7 +40,13 @@
|
|
|
40
40
|
<legend>Sound</legend>
|
|
41
41
|
<label class="choice" title="Plays when the session manager stops, remains unavailable after a brief recovery check, or a new Agent Guild update is discovered. Click or type on this page to allow playback."><input id="sound" type="checkbox" name="sound"><span class="sound-alerts">Alert sounds</span></label>
|
|
42
42
|
</fieldset>
|
|
43
|
+
<fieldset id="voice-choice" hidden>
|
|
44
|
+
<legend>Voice</legend>
|
|
45
|
+
<label class="choice"><input id="voice" type="checkbox" name="voice"><span class="voice-input">Voice input</span></label>
|
|
46
|
+
<p class="voice-note">Adds a Dictate button to terminals. Your browser may send the audio to its maker, such as Google or Microsoft, to turn it into text. Nothing is heard until you press Dictate, and Enter is never pressed for you.</p>
|
|
47
|
+
</fieldset>
|
|
43
48
|
</div>
|
|
49
|
+
<button id="notes-open" class="btn notes-open" type="button" aria-haspopup="dialog">Notes</button>
|
|
44
50
|
<button id="restart-manager" class="btn restart-manager" type="button" hidden>Restart manager</button>
|
|
45
51
|
<button id="stop-manager" class="btn danger stop-manager" type="button" hidden title="Stop the session manager. This ends every session.">Stop manager</button>
|
|
46
52
|
</div>
|
|
@@ -118,11 +124,13 @@
|
|
|
118
124
|
<div id="panel-agents" class="agents"></div>
|
|
119
125
|
</div>
|
|
120
126
|
<div class="panel-actions">
|
|
127
|
+
<button id="panel-voice" class="btn voice" type="button" hidden aria-pressed="false" title="Speak into this terminal. Review the text, then press Enter yourself.">Dictate</button>
|
|
121
128
|
<button id="panel-stop" class="btn danger" type="button">Stop</button>
|
|
122
129
|
<button id="panel-close" class="btn" type="button" title="Hide the terminal. The session keeps running.">Hide</button>
|
|
123
130
|
</div>
|
|
124
131
|
</div>
|
|
125
132
|
<div id="terminal-host" class="terminal-host"></div>
|
|
133
|
+
<div id="voice-preview" class="voice-preview" hidden aria-live="polite"></div>
|
|
126
134
|
</div>
|
|
127
135
|
|
|
128
136
|
<dialog id="models" class="models" aria-labelledby="models-title">
|
|
@@ -179,6 +187,19 @@
|
|
|
179
187
|
<p id="changelog-status" class="news-status" hidden></p>
|
|
180
188
|
</dialog>
|
|
181
189
|
|
|
190
|
+
<dialog id="notes" class="news-panel notes-panel" aria-labelledby="notes-title">
|
|
191
|
+
<div class="news-bar">
|
|
192
|
+
<div class="news-heading">
|
|
193
|
+
<h2 id="notes-title">Notes</h2>
|
|
194
|
+
<p id="notes-sub" class="sub" aria-live="polite">Saved in this browser as you type</p>
|
|
195
|
+
</div>
|
|
196
|
+
<button id="notes-close" class="btn" type="button">Close</button>
|
|
197
|
+
</div>
|
|
198
|
+
<div class="news-body">
|
|
199
|
+
<textarea id="notes-text" class="notes-text" aria-labelledby="notes-title" aria-describedby="notes-sub" autocomplete="off" placeholder="Prompts, commands, to-dos: anything you want to keep at hand"></textarea>
|
|
200
|
+
</div>
|
|
201
|
+
</dialog>
|
|
202
|
+
|
|
182
203
|
<dialog id="history" class="models history" aria-labelledby="history-title">
|
|
183
204
|
<div class="models-bar">
|
|
184
205
|
<span id="history-icon" class="provider-icon small"></span>
|
|
@@ -243,7 +243,7 @@
|
|
|
243
243
|
}
|
|
244
244
|
.btn.primary:hover { filter: brightness(1.08); box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.25), inset 0 0 0 1px rgba(233, 196, 122, 0.6), 0 5px 16px rgba(40, 70, 30, 0.35); }
|
|
245
245
|
.btn.danger:hover { border-color: var(--danger); box-shadow: 0 0 0 3px color-mix(in srgb, var(--danger) 14%, transparent); }
|
|
246
|
-
.cwd input, .auth input, .history-tools input[type="search"], .history-form input, .github-fields input, .github-fields select { background: var(--panel-strong); }
|
|
246
|
+
.cwd input, .auth input, .history-tools input[type="search"], .history-form input, .github-fields input, .github-fields select, .notes-text { background: var(--panel-strong); }
|
|
247
247
|
|
|
248
248
|
.models, .news-panel, .auth, .news-latest, .appearance-menu { background: var(--panel-strong); backdrop-filter: blur(16px); }
|
|
249
249
|
.models, .news-panel { box-shadow: var(--dialog-shadow); }
|
|
@@ -362,7 +362,7 @@
|
|
|
362
362
|
.btn.primary { border-color: #6a8f3a; background: linear-gradient(#55924a, #35662f); box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.2), inset 0 0 0 1px rgba(233, 196, 122, 0.3), 0 0 16px rgba(110, 170, 80, 0.3); }
|
|
363
363
|
.btn.danger { color: #f59a8c; }
|
|
364
364
|
.btn.danger:hover { border-color: #b8402f; box-shadow: 0 0 12px rgba(240, 123, 106, 0.3); }
|
|
365
|
-
.cwd input, .auth input, .history-tools input[type="search"], .history-form input, .github-fields input, .github-fields select { border-color: #4a3824; background: rgba(10, 7, 4, 0.7); }
|
|
365
|
+
.cwd input, .auth input, .history-tools input[type="search"], .history-form input, .github-fields input, .github-fields select, .notes-text { border-color: #4a3824; background: rgba(10, 7, 4, 0.7); }
|
|
366
366
|
|
|
367
367
|
.models::backdrop, .news-panel::backdrop { background: rgba(6, 4, 2, 0.66); }
|
|
368
368
|
.terminal-panel { border-color: #5a4226; box-shadow: 0 0 0 1px rgba(233, 196, 122, 0.06), 0 30px 80px rgba(0, 0, 0, 0.7); }
|
package/web/skins/grove/skin.css
CHANGED
|
@@ -244,7 +244,7 @@
|
|
|
244
244
|
}
|
|
245
245
|
.btn.primary:hover { filter: brightness(1.07); box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.22), 0 6px 20px rgba(45, 107, 69, 0.36); }
|
|
246
246
|
.btn.danger:hover { border-color: var(--danger); box-shadow: 0 0 0 3px color-mix(in srgb, var(--danger) 14%, transparent); }
|
|
247
|
-
.cwd input, .auth input, .history-tools input[type="search"], .history-form input, .github-fields input, .github-fields select { background: var(--paper-strong); }
|
|
247
|
+
.cwd input, .auth input, .history-tools input[type="search"], .history-form input, .github-fields input, .github-fields select, .notes-text { background: var(--paper-strong); }
|
|
248
248
|
|
|
249
249
|
.models, .news-panel, .auth, .news-latest, .appearance-menu { background: var(--paper-strong); backdrop-filter: blur(16px); }
|
|
250
250
|
.models, .news-panel { box-shadow: var(--dialog-shadow); }
|
|
@@ -371,7 +371,7 @@
|
|
|
371
371
|
.btn.primary { background: linear-gradient(135deg, #4a8c5e, #2a6340); box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.18), 0 0 18px rgba(80, 160, 105, 0.35); }
|
|
372
372
|
.btn.danger { color: #ff9a8e; }
|
|
373
373
|
.btn.danger:hover { border-color: #b8443a; box-shadow: 0 0 12px rgba(240, 122, 108, 0.3); }
|
|
374
|
-
.cwd input, .auth input, .history-tools input[type="search"], .history-form input, .github-fields input, .github-fields select { border-color: #2e4a3d; background: rgba(4, 10, 8, 0.7); }
|
|
374
|
+
.cwd input, .auth input, .history-tools input[type="search"], .history-form input, .github-fields input, .github-fields select, .notes-text { border-color: #2e4a3d; background: rgba(4, 10, 8, 0.7); }
|
|
375
375
|
|
|
376
376
|
.models::backdrop, .news-panel::backdrop { background: rgba(2, 6, 5, 0.66); }
|
|
377
377
|
.terminal-panel { border-color: #2e4a3d; box-shadow: 0 0 0 1px rgba(140, 210, 160, 0.06), 0 30px 80px rgba(0, 0, 0, 0.7); }
|
package/web/skins/guild/skin.css
CHANGED
|
@@ -376,7 +376,7 @@
|
|
|
376
376
|
.btn.primary:hover { border-color: var(--gold); filter: brightness(1.08); }
|
|
377
377
|
.btn.danger { color: #ff9a94; }
|
|
378
378
|
.btn.danger:hover { border-color: #b8524d; box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.06), 0 0 12px rgba(239, 107, 102, 0.3); }
|
|
379
|
-
.cwd input, .auth input { border-color: #4a3d25; background: rgba(12, 13, 17, 0.8); }
|
|
379
|
+
.cwd input, .auth input, .notes-text { border-color: #4a3d25; background: rgba(12, 13, 17, 0.8); }
|
|
380
380
|
.auth {
|
|
381
381
|
background: var(--card-bg);
|
|
382
382
|
border: 1px solid #4a3d25; box-shadow: inset 0 0 0 1px rgba(232, 196, 124, 0.06), 0 8px 24px rgba(0, 0, 0, 0.35);
|
|
@@ -480,7 +480,7 @@
|
|
|
480
480
|
}
|
|
481
481
|
.btn.primary:hover { border-color: #b8892f; filter: brightness(1.08); }
|
|
482
482
|
.btn.danger:hover { border-color: #c4625d; }
|
|
483
|
-
.cwd input, .auth input, .history-tools input[type="search"], .history-form input {
|
|
483
|
+
.cwd input, .auth input, .history-tools input[type="search"], .history-form input, .notes-text {
|
|
484
484
|
border-color: #dccba5; background: #fffdf8; box-shadow: inset 0 1px 2px rgba(70, 48, 12, 0.06);
|
|
485
485
|
}
|
|
486
486
|
.auth { border-color: #e4d6b6; box-shadow: var(--card-shadow); }
|
|
@@ -246,7 +246,7 @@
|
|
|
246
246
|
}
|
|
247
247
|
.btn.primary:hover { filter: brightness(1.08); box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.25), 0 6px 20px rgba(122, 79, 232, 0.4); }
|
|
248
248
|
.btn.danger:hover { border-color: var(--danger); box-shadow: 0 0 0 3px color-mix(in srgb, var(--danger) 14%, transparent); }
|
|
249
|
-
.cwd input, .auth input, .history-tools input[type="search"], .history-form input, .github-fields input, .github-fields select { background: var(--glass-strong); }
|
|
249
|
+
.cwd input, .auth input, .history-tools input[type="search"], .history-form input, .github-fields input, .github-fields select, .notes-text { background: var(--glass-strong); }
|
|
250
250
|
|
|
251
251
|
.models, .news-panel, .auth, .news-latest, .appearance-menu { background: var(--glass-strong); backdrop-filter: blur(16px); }
|
|
252
252
|
.models, .news-panel { box-shadow: var(--dialog-shadow); }
|
|
@@ -367,7 +367,7 @@
|
|
|
367
367
|
.btn.primary { box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.22), 0 0 18px rgba(106, 92, 240, 0.45); }
|
|
368
368
|
.btn.danger { color: #ff8c9c; }
|
|
369
369
|
.btn.danger:hover { border-color: #c23a52; box-shadow: 0 0 12px rgba(255, 107, 129, 0.3); }
|
|
370
|
-
.cwd input, .auth input, .history-tools input[type="search"], .history-form input, .github-fields input, .github-fields select { border-color: #26355c; background: rgba(4, 7, 16, 0.7); }
|
|
370
|
+
.cwd input, .auth input, .history-tools input[type="search"], .history-form input, .github-fields input, .github-fields select, .notes-text { border-color: #26355c; background: rgba(4, 7, 16, 0.7); }
|
|
371
371
|
|
|
372
372
|
.models::backdrop, .news-panel::backdrop { background: rgba(2, 4, 10, 0.66); }
|
|
373
373
|
.terminal-panel { border-color: #2a3a66; box-shadow: 0 0 0 1px rgba(120, 170, 255, 0.06), 0 30px 80px rgba(0, 0, 0, 0.7); }
|
package/web/styles.css
CHANGED
|
@@ -22,6 +22,7 @@
|
|
|
22
22
|
--font-mono: ui-monospace, "Cascadia Code", "SF Mono", Menlo, Consolas, monospace;
|
|
23
23
|
--icon-sun: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='black' stroke-width='2' stroke-linecap='round'%3E%3Ccircle cx='12' cy='12' r='4.2'/%3E%3Cpath d='M12 2.5v2.2M12 19.3v2.2M2.5 12h2.2M19.3 12h2.2M5.3 5.3l1.6 1.6M17.1 17.1l1.6 1.6M5.3 18.7l1.6-1.6M17.1 6.9l1.6-1.6'/%3E%3C/svg%3E");
|
|
24
24
|
--icon-moon: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='black' stroke-width='2' stroke-linejoin='round'%3E%3Cpath d='M19.5 14.6A8 8 0 0 1 9.4 4.5a8 8 0 1 0 10.1 10.1z'/%3E%3C/svg%3E");
|
|
25
|
+
--icon-mic: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='black' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Crect x='9' y='3' width='6' height='11' rx='3'/%3E%3Cpath d='M5.5 11a6.5 6.5 0 0 0 13 0M12 17.5V21'/%3E%3C/svg%3E");
|
|
25
26
|
--icon-sound: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='black' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M4 9.5h3.5L12 5.5v13l-4.5-4H4z'/%3E%3Cpath d='M15.5 9a4.2 4.2 0 0 1 0 6M18.3 6.5a8 8 0 0 1 0 11'/%3E%3C/svg%3E");
|
|
26
27
|
--icon-external:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='black' stroke-width='2.2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M7 17L17 7M9 7h8v8'/%3E%3C/svg%3E");
|
|
27
28
|
--icon-cloud: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='black' stroke-width='2' stroke-linejoin='round'%3E%3Cpath d='M7 18.5h10.5a4 4 0 0 0 .6-7.96A6 6 0 0 0 6.5 9.6 4.5 4.5 0 0 0 7 18.5z'/%3E%3C/svg%3E");
|
|
@@ -29,6 +30,7 @@
|
|
|
29
30
|
--icon-github: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16'%3E%3Cpath d='M8 0c4.42 0 8 3.58 8 8a8.013 8.013 0 0 1-5.45 7.59c-.4.08-.55-.17-.55-.38 0-.27.01-1.13.01-2.2 0-.75-.25-1.23-.54-1.48 1.78-.2 3.65-.88 3.65-3.95 0-.88-.31-1.59-.82-2.15.08-.2.36-1.02-.08-2.12 0 0-.67-.22-2.2.82-.64-.18-1.32-.27-2-.27-.68 0-1.36.09-2 .27-1.53-1.03-2.2-.82-2.2-.82-.44 1.1-.16 1.92-.08 2.12-.51.56-.82 1.28-.82 2.15 0 3.06 1.86 3.75 3.64 3.95-.23.2-.44.55-.51 1.07-.46.21-1.61.55-2.33-.66-.15-.24-.6-.83-1.23-.82-.67.01-.27.38.01.53.34.19.73.9.82 1.13.16.45.68 1.31 2.69.94 0 .67.01 1.3.01 1.49 0 .21-.15.45-.55.38A7.995 7.995 0 0 1 0 8c0-4.42 3.58-8 8-8Z'/%3E%3C/svg%3E");
|
|
30
31
|
--icon-info: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='black' stroke-width='1.8' stroke-linecap='round'%3E%3Ccircle cx='12' cy='12' r='10'/%3E%3Cpath d='M12 10.8v6.2' stroke-width='2.4'/%3E%3Ccircle cx='12' cy='7.3' r='1.2' fill='black' stroke='none'/%3E%3C/svg%3E");
|
|
31
32
|
--icon-grip: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='black'%3E%3Ccircle cx='9' cy='5.5' r='1.9'/%3E%3Ccircle cx='15' cy='5.5' r='1.9'/%3E%3Ccircle cx='9' cy='12' r='1.9'/%3E%3Ccircle cx='15' cy='12' r='1.9'/%3E%3Ccircle cx='9' cy='18.5' r='1.9'/%3E%3Ccircle cx='15' cy='18.5' r='1.9'/%3E%3C/svg%3E");
|
|
33
|
+
--icon-notes: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='black' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M5 4.5h14v9.5l-5.5 5.5H5z'/%3E%3Cpath d='M13.5 19.5V14H19M8.5 9h7M8.5 12.5h4'/%3E%3C/svg%3E");
|
|
32
34
|
font-family: system-ui, -apple-system, "Segoe UI", Roboto, sans-serif;
|
|
33
35
|
font-size: 15px;
|
|
34
36
|
color-scheme: light;
|
|
@@ -336,19 +338,22 @@ main { max-width: 1200px; margin: 0 auto; padding: 26px 16px 56px; display: grid
|
|
|
336
338
|
.btn.danger { color: var(--danger); }
|
|
337
339
|
.btn.danger:hover { border-color: var(--danger); }
|
|
338
340
|
.btn:disabled { opacity: 0.6; cursor: progress; }
|
|
339
|
-
.stop-manager, .restart-manager, .appearance, .upgrade { white-space: nowrap; }
|
|
340
|
-
.appearance::before, .appearance-menu .choice > span::before {
|
|
341
|
+
.stop-manager, .restart-manager, .appearance, .notes-open, .upgrade { white-space: nowrap; }
|
|
342
|
+
.appearance::before, .notes-open::before, .appearance-menu .choice > span::before {
|
|
341
343
|
content: ""; display: inline-block; width: 1em; height: 1em; margin-right: 0.4em; vertical-align: -0.15em;
|
|
342
344
|
background: currentColor; mask: var(--icon-sun) center / contain no-repeat;
|
|
343
345
|
}
|
|
344
346
|
:root[data-theme="dark"] .appearance::before, .appearance-menu .choice > .mode-dark::before { mask-image: var(--icon-moon); }
|
|
345
347
|
.appearance-menu .choice > .sound-alerts::before { mask-image: var(--icon-sound); }
|
|
348
|
+
.appearance-menu .choice > .voice-input::before { mask-image: var(--icon-mic); }
|
|
349
|
+
.voice-note { margin: 0 8px 6px; max-width: 26ch; color: var(--muted); font-size: 0.75rem; line-height: 1.35; }
|
|
350
|
+
.notes-open::before { mask-image: var(--icon-notes); }
|
|
346
351
|
.appearance::after { margin-left: 0.45em; transform: rotate(90deg); transition: transform 0.15s var(--ease); }
|
|
347
352
|
.appearance:has(+ :popover-open)::after { transform: rotate(-90deg); }
|
|
348
353
|
.appearance-menu {
|
|
349
354
|
position: fixed; inset: auto; margin: 0; min-width: 190px; padding: 6px; display: none;
|
|
350
355
|
border: 1px solid var(--border); border-radius: var(--radius); background: var(--surface); color: var(--text);
|
|
351
|
-
box-shadow: 0 12px 32px rgba(0, 0, 0, 0.22); font-size: 0.85rem;
|
|
356
|
+
box-shadow: 0 12px 32px rgba(0, 0, 0, 0.22); font-size: 0.85rem; overflow-y: auto;
|
|
352
357
|
}
|
|
353
358
|
.appearance-menu:popover-open { display: grid; gap: 4px; animation: fade-in 0.12s var(--ease); }
|
|
354
359
|
.appearance-menu fieldset { margin: 0; padding: 0; border: 0; display: grid; }
|
|
@@ -393,10 +398,24 @@ body:has(#app[hidden]):has(#auth[hidden]):has(#stopped[hidden])::after {
|
|
|
393
398
|
display: flex; align-items: center; justify-content: space-between; gap: 12px; padding: 10px 12px;
|
|
394
399
|
background: var(--surface); border-bottom: 1px solid var(--border); flex-wrap: wrap;
|
|
395
400
|
}
|
|
396
|
-
.panel-title { display: flex; align-items: center; gap: 10px; min-width: 0; flex: 1; }
|
|
401
|
+
.panel-title { display: flex; align-items: center; gap: 10px; min-width: 0; flex: 1 1 16rem; }
|
|
397
402
|
.panel-title .title { font-weight: 600; }
|
|
398
403
|
.panel-title .sub { color: var(--muted); font-size: 0.78rem; }
|
|
399
404
|
.panel-actions { display: flex; gap: 8px; }
|
|
405
|
+
.btn.voice { white-space: nowrap; }
|
|
406
|
+
.btn.voice::before {
|
|
407
|
+
content: ""; display: inline-block; width: 1em; height: 1em; margin-right: 0.35em; vertical-align: -0.15em;
|
|
408
|
+
background: currentColor; mask: var(--icon-mic) center / contain no-repeat;
|
|
409
|
+
}
|
|
410
|
+
.btn.voice[aria-pressed="true"] { color: var(--danger); border-color: var(--danger); }
|
|
411
|
+
.btn.voice[aria-pressed="true"]::before { animation: voice-pulse 1.2s ease-in-out infinite; }
|
|
412
|
+
@keyframes voice-pulse { 50% { opacity: 0.35; } }
|
|
413
|
+
.voice-preview {
|
|
414
|
+
position: absolute; left: 12px; right: 12px; bottom: 12px; max-height: 30%; overflow-y: auto; z-index: 1;
|
|
415
|
+
padding: 8px 12px; border: 1px solid var(--border); border-radius: calc(var(--radius) - 2px);
|
|
416
|
+
background: var(--surface); color: var(--muted); font-size: clamp(0.8rem, 1.6vw, 0.95rem); font-style: italic;
|
|
417
|
+
overflow-wrap: anywhere; box-shadow: 0 8px 24px rgba(0, 0, 0, 0.3); pointer-events: none;
|
|
418
|
+
}
|
|
400
419
|
.terminal-host { flex: 1; min-height: 0; padding: 8px 4px 4px 10px; }
|
|
401
420
|
.terminal-host > div { height: 100%; }
|
|
402
421
|
@media (pointer: coarse) {
|
|
@@ -547,6 +566,20 @@ a.news-link:visited { color: var(--muted); }
|
|
|
547
566
|
.changelog-changes li::marker { color: var(--muted); }
|
|
548
567
|
.changelog-changes code { font-size: 0.8rem; padding: 0 4px; border-radius: 4px; background: var(--surface-2); }
|
|
549
568
|
|
|
569
|
+
/* Notes: the News side sheet, holding one text field that skins style with their other fields. */
|
|
570
|
+
.notes-panel .news-body { display: flex; flex-direction: column; padding-top: 12px; }
|
|
571
|
+
.notes-text {
|
|
572
|
+
flex: 1; min-height: 8em; margin: 0; padding: 10px 12px; resize: none;
|
|
573
|
+
border: 1px solid var(--border); border-radius: var(--radius); background: var(--surface); color: var(--text);
|
|
574
|
+
font: inherit; font-size: 0.9rem; line-height: 1.55;
|
|
575
|
+
}
|
|
576
|
+
.notes-text:focus-visible { outline: 2px solid var(--focus); outline-offset: 1px; }
|
|
577
|
+
.notes-panel .sub.warn { color: var(--warn); }
|
|
578
|
+
@media (pointer: coarse) {
|
|
579
|
+
/* iOS zooms in on a field with smaller text when it takes focus. */
|
|
580
|
+
.notes-text { font-size: 16px; }
|
|
581
|
+
}
|
|
582
|
+
|
|
550
583
|
.history-tools { display: flex; flex-wrap: wrap; align-items: center; gap: 10px; padding: 10px 14px 0; }
|
|
551
584
|
.history-tools input[type="search"] {
|
|
552
585
|
flex: 1 1 220px; min-width: 0; padding: 7px 10px; border-radius: var(--radius); border: 1px solid var(--border);
|
|
@@ -674,6 +707,7 @@ a.news-link:visited { color: var(--muted); }
|
|
|
674
707
|
.model-name::before, .provider .copies summary::before, .appearance::after, .btn, .models, .news-panel, .models::backdrop, .news-panel::backdrop { transition: none; }
|
|
675
708
|
.github-waiting::before, body:has(#app[hidden]):has(#auth[hidden]):has(#stopped[hidden])::after { animation: none; }
|
|
676
709
|
.btn:active:not(:disabled) { transform: none; }
|
|
710
|
+
.btn.voice[aria-pressed="true"]::before { animation: none; }
|
|
677
711
|
.drag-handle, .session-grid::before, .session-grid.reordering::before,
|
|
678
712
|
.session-grid > .session-card.dragging, .session-grid > .session-card.settling { transition: none; }
|
|
679
713
|
}
|