@oddessentials/agent-guild 0.22.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/package.json +1 -1
- package/web/app.js +178 -1
- package/web/index.html +7 -0
- package/web/styles.css +20 -2
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,7 @@ 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';
|
|
19
20
|
const NOTES_KEY = 'agentGuild.notes';
|
|
20
21
|
const RELEASES_URL = 'https://github.com/oddessentials/agent-guild/releases';
|
|
21
22
|
const isMac = /Mac|iPhone|iPad/.test(navigator.platform || navigator.userAgent);
|
|
@@ -297,8 +298,15 @@ function placeAppearanceMenu() {
|
|
|
297
298
|
const menu = $('appearance-menu');
|
|
298
299
|
if (!menu.matches(':popover-open')) return;
|
|
299
300
|
const box = $('appearance').getBoundingClientRect();
|
|
300
|
-
|
|
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';
|
|
301
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
|
+
}
|
|
302
310
|
}
|
|
303
311
|
|
|
304
312
|
// ---- notes ----------------------------------------------------------------
|
|
@@ -3366,6 +3374,7 @@ class TerminalView {
|
|
|
3366
3374
|
this.term.write(msg.data);
|
|
3367
3375
|
break;
|
|
3368
3376
|
case 'exit':
|
|
3377
|
+
if (dictation?.id === this.id) stopDictation();
|
|
3369
3378
|
this.term.write(`\r\n\x1b[2m${exitLine(state.sessions.get(this.id), msg)}\x1b[0m\r\n`);
|
|
3370
3379
|
break;
|
|
3371
3380
|
default:
|
|
@@ -3460,6 +3469,7 @@ class TerminalView {
|
|
|
3460
3469
|
|
|
3461
3470
|
function openPanel(id) {
|
|
3462
3471
|
if (!state.sessions.has(id)) return;
|
|
3472
|
+
if (dictation && dictation.id !== id) stopDictation();
|
|
3463
3473
|
if (state.activeId && state.activeId !== id) state.views.get(state.activeId)?.unmount();
|
|
3464
3474
|
state.activeId = id;
|
|
3465
3475
|
let view = state.views.get(id);
|
|
@@ -3470,12 +3480,15 @@ function openPanel(id) {
|
|
|
3470
3480
|
}
|
|
3471
3481
|
|
|
3472
3482
|
function closePanel() {
|
|
3483
|
+
stopDictation();
|
|
3473
3484
|
if (state.activeId) state.views.get(state.activeId)?.unmount();
|
|
3474
3485
|
state.activeId = null;
|
|
3475
3486
|
$('terminal-panel').hidden = true;
|
|
3487
|
+
renderVoice();
|
|
3476
3488
|
}
|
|
3477
3489
|
|
|
3478
3490
|
function updatePanel() {
|
|
3491
|
+
renderVoice();
|
|
3479
3492
|
const s = state.sessions.get(state.activeId);
|
|
3480
3493
|
if (!s) return;
|
|
3481
3494
|
paintProviderIcon($('panel-icon'), s.provider);
|
|
@@ -3488,6 +3501,157 @@ function updatePanel() {
|
|
|
3488
3501
|
stop.textContent = s.status !== 'running' ? 'Remove' : s.multiplexer ? 'Detach' : 'Stop';
|
|
3489
3502
|
}
|
|
3490
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
|
+
|
|
3491
3655
|
// ---- stopping the manager -------------------------------------------------
|
|
3492
3656
|
|
|
3493
3657
|
/**
|
|
@@ -3743,6 +3907,7 @@ let newsTimer;
|
|
|
3743
3907
|
|
|
3744
3908
|
function showAuth(message = '') {
|
|
3745
3909
|
managerLoss.cancel();
|
|
3910
|
+
closePanel();
|
|
3746
3911
|
closeModels();
|
|
3747
3912
|
closeNews();
|
|
3748
3913
|
closeChangelog();
|
|
@@ -3794,6 +3959,13 @@ $('auth-form').addEventListener('submit', (e) => {
|
|
|
3794
3959
|
boot();
|
|
3795
3960
|
});
|
|
3796
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
|
+
});
|
|
3797
3969
|
$('models-close').addEventListener('click', closeModels);
|
|
3798
3970
|
$('history-close').addEventListener('click', closeHistory);
|
|
3799
3971
|
$('history').addEventListener('click', (e) => { if (e.target === $('history')) closeHistory(); });
|
|
@@ -3886,10 +4058,12 @@ $('news').addEventListener('close', () => {
|
|
|
3886
4058
|
newsView.opener = null;
|
|
3887
4059
|
});
|
|
3888
4060
|
document.addEventListener('visibilitychange', () => {
|
|
4061
|
+
if (document.visibilityState !== 'visible') stopDictation();
|
|
3889
4062
|
if (document.visibilityState === 'visible' && state.connected && Date.now() - newsLoadedAt > 60000) loadNews();
|
|
3890
4063
|
});
|
|
3891
4064
|
addEventListener('pagehide', () => {
|
|
3892
4065
|
state.pageAway = true;
|
|
4066
|
+
stopDictation();
|
|
3893
4067
|
managerLoss.cancel();
|
|
3894
4068
|
state.eventsSocket?.close();
|
|
3895
4069
|
});
|
|
@@ -3917,6 +4091,7 @@ $('changelog-restart').addEventListener('click', () => {
|
|
|
3917
4091
|
addEventListener('storage', (e) => {
|
|
3918
4092
|
if (e.key === CHANGELOG_SEEN_KEY) renderVersion();
|
|
3919
4093
|
else if (e.key === SOUND_KEY) { $('sound').checked = soundOn(); prepareSounds(); }
|
|
4094
|
+
else if (e.key === VOICE_KEY) renderVoice();
|
|
3920
4095
|
// Another tab reordered the cards.
|
|
3921
4096
|
else if (e.key === SESSION_ORDER_KEY && !drag) arrange(parseOrder(e.newValue));
|
|
3922
4097
|
});
|
|
@@ -3963,6 +4138,7 @@ $('appearance-menu').addEventListener('change', (e) => {
|
|
|
3963
4138
|
if (e.target.name === 'skin') changeSkin(e.target);
|
|
3964
4139
|
else if (e.target.name === 'theme') changeTheme(e.target);
|
|
3965
4140
|
else if (e.target.name === 'sound') changeSound(e.target);
|
|
4141
|
+
else if (e.target.name === 'voice') changeVoice(e.target);
|
|
3966
4142
|
});
|
|
3967
4143
|
$('appearance-menu').addEventListener('toggle', (e) => {
|
|
3968
4144
|
if (e.newState !== 'open') return;
|
|
@@ -3997,6 +4173,7 @@ $('providers').addEventListener('pointerout', (e) => {
|
|
|
3997
4173
|
applyTheme(currentTheme());
|
|
3998
4174
|
renderSkinChoices();
|
|
3999
4175
|
$('sound').checked = soundOn();
|
|
4176
|
+
renderVoice();
|
|
4000
4177
|
// Follow the system setting until the user picks a theme.
|
|
4001
4178
|
window.matchMedia('(prefers-color-scheme: dark)').addEventListener('change', (e) => {
|
|
4002
4179
|
if (!load(THEME_KEY)) applyTheme(e.matches ? 'dark' : 'light');
|
package/web/index.html
CHANGED
|
@@ -40,6 +40,11 @@
|
|
|
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>
|
|
44
49
|
<button id="notes-open" class="btn notes-open" type="button" aria-haspopup="dialog">Notes</button>
|
|
45
50
|
<button id="restart-manager" class="btn restart-manager" type="button" hidden>Restart manager</button>
|
|
@@ -119,11 +124,13 @@
|
|
|
119
124
|
<div id="panel-agents" class="agents"></div>
|
|
120
125
|
</div>
|
|
121
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>
|
|
122
128
|
<button id="panel-stop" class="btn danger" type="button">Stop</button>
|
|
123
129
|
<button id="panel-close" class="btn" type="button" title="Hide the terminal. The session keeps running.">Hide</button>
|
|
124
130
|
</div>
|
|
125
131
|
</div>
|
|
126
132
|
<div id="terminal-host" class="terminal-host"></div>
|
|
133
|
+
<div id="voice-preview" class="voice-preview" hidden aria-live="polite"></div>
|
|
127
134
|
</div>
|
|
128
135
|
|
|
129
136
|
<dialog id="models" class="models" aria-labelledby="models-title">
|
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");
|
|
@@ -344,13 +345,15 @@ main { max-width: 1200px; margin: 0 auto; padding: 26px 16px 56px; display: grid
|
|
|
344
345
|
}
|
|
345
346
|
:root[data-theme="dark"] .appearance::before, .appearance-menu .choice > .mode-dark::before { mask-image: var(--icon-moon); }
|
|
346
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; }
|
|
347
350
|
.notes-open::before { mask-image: var(--icon-notes); }
|
|
348
351
|
.appearance::after { margin-left: 0.45em; transform: rotate(90deg); transition: transform 0.15s var(--ease); }
|
|
349
352
|
.appearance:has(+ :popover-open)::after { transform: rotate(-90deg); }
|
|
350
353
|
.appearance-menu {
|
|
351
354
|
position: fixed; inset: auto; margin: 0; min-width: 190px; padding: 6px; display: none;
|
|
352
355
|
border: 1px solid var(--border); border-radius: var(--radius); background: var(--surface); color: var(--text);
|
|
353
|
-
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;
|
|
354
357
|
}
|
|
355
358
|
.appearance-menu:popover-open { display: grid; gap: 4px; animation: fade-in 0.12s var(--ease); }
|
|
356
359
|
.appearance-menu fieldset { margin: 0; padding: 0; border: 0; display: grid; }
|
|
@@ -395,10 +398,24 @@ body:has(#app[hidden]):has(#auth[hidden]):has(#stopped[hidden])::after {
|
|
|
395
398
|
display: flex; align-items: center; justify-content: space-between; gap: 12px; padding: 10px 12px;
|
|
396
399
|
background: var(--surface); border-bottom: 1px solid var(--border); flex-wrap: wrap;
|
|
397
400
|
}
|
|
398
|
-
.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; }
|
|
399
402
|
.panel-title .title { font-weight: 600; }
|
|
400
403
|
.panel-title .sub { color: var(--muted); font-size: 0.78rem; }
|
|
401
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
|
+
}
|
|
402
419
|
.terminal-host { flex: 1; min-height: 0; padding: 8px 4px 4px 10px; }
|
|
403
420
|
.terminal-host > div { height: 100%; }
|
|
404
421
|
@media (pointer: coarse) {
|
|
@@ -690,6 +707,7 @@ a.news-link:visited { color: var(--muted); }
|
|
|
690
707
|
.model-name::before, .provider .copies summary::before, .appearance::after, .btn, .models, .news-panel, .models::backdrop, .news-panel::backdrop { transition: none; }
|
|
691
708
|
.github-waiting::before, body:has(#app[hidden]):has(#auth[hidden]):has(#stopped[hidden])::after { animation: none; }
|
|
692
709
|
.btn:active:not(:disabled) { transform: none; }
|
|
710
|
+
.btn.voice[aria-pressed="true"]::before { animation: none; }
|
|
693
711
|
.drag-handle, .session-grid::before, .session-grid.reordering::before,
|
|
694
712
|
.session-grid > .session-card.dragging, .session-grid > .session-card.settling { transition: none; }
|
|
695
713
|
}
|