@oddessentials/agent-guild 0.20.0 → 0.22.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 +19 -1
- package/package.json +1 -1
- package/src/manager/command-resolver.mjs +9 -6
- package/src/manager/config.mjs +21 -0
- package/src/manager/herdr.mjs +156 -0
- package/src/manager/main.mjs +6 -1
- package/src/manager/providers.mjs +1 -1
- package/src/manager/server.mjs +7 -3
- package/src/manager/session-manager.mjs +229 -16
- package/src/manager/session.mjs +53 -15
- package/src/manager/shells.mjs +63 -7
- package/web/app.js +190 -15
- package/web/index.html +14 -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 +18 -2
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 NOTES_KEY = 'agentGuild.notes';
|
|
19
20
|
const RELEASES_URL = 'https://github.com/oddessentials/agent-guild/releases';
|
|
20
21
|
const isMac = /Mac|iPhone|iPad/.test(navigator.platform || navigator.userAgent);
|
|
21
22
|
const reducedMotion = window.matchMedia('(prefers-reduced-motion: reduce)');
|
|
@@ -63,7 +64,7 @@ const state = {
|
|
|
63
64
|
// ---- storage (may be unavailable, e.g. blocked site data) -----------------
|
|
64
65
|
|
|
65
66
|
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 {
|
|
67
|
+
function save(key, value) { try { value === null ? localStorage.removeItem(key) : localStorage.setItem(key, value); return true; } catch { return false; } }
|
|
67
68
|
|
|
68
69
|
// ---- helpers --------------------------------------------------------------
|
|
69
70
|
|
|
@@ -87,6 +88,12 @@ function toast(message, ms = 5000, action = null) {
|
|
|
87
88
|
toastTimer = setTimeout(() => { el.hidden = true; }, ms);
|
|
88
89
|
}
|
|
89
90
|
|
|
91
|
+
/**
|
|
92
|
+
* Handles a click, but not the second click of a double-click, which lands on whatever the first one
|
|
93
|
+
* opened or uncovered: Close over Notes, or Stop manager under a panel's Close.
|
|
94
|
+
*/
|
|
95
|
+
const firstClick = (run) => (e) => { if (e.detail < 2) run(); };
|
|
96
|
+
|
|
90
97
|
function setConnection(kind, label) {
|
|
91
98
|
const el = $('connection');
|
|
92
99
|
el.className = `connection ${kind}`;
|
|
@@ -294,6 +301,99 @@ function placeAppearanceMenu() {
|
|
|
294
301
|
menu.style.right = `${Math.max(8, Math.round(innerWidth - box.right))}px`;
|
|
295
302
|
}
|
|
296
303
|
|
|
304
|
+
// ---- notes ----------------------------------------------------------------
|
|
305
|
+
|
|
306
|
+
/** Notes share the page's storage with every other setting, so they stay far below the browser's limit for it. */
|
|
307
|
+
const NOTES_LIMIT = 100000;
|
|
308
|
+
const NOTES_STATUS = {
|
|
309
|
+
saved: 'Saved in this browser as you type',
|
|
310
|
+
long: `Not saved: notes hold up to ${NOTES_LIMIT.toLocaleString('en-US')} characters. Shorten them to save.`,
|
|
311
|
+
refused: 'Not saved: the browser’s storage for this page is full or turned off. Copy what you need before you close the page.',
|
|
312
|
+
};
|
|
313
|
+
/**
|
|
314
|
+
* `saved`: the notes as this page last read or wrote them in storage. `status`: whether the text in the
|
|
315
|
+
* panel is saved, too `long` or `refused`; `shown`: the status the line under the title shows.
|
|
316
|
+
* `pressedOutside`: the last press on the panel was on its backdrop.
|
|
317
|
+
*/
|
|
318
|
+
const notesView = { saved: '', status: 'saved', shown: 'saved', pressedOutside: false };
|
|
319
|
+
|
|
320
|
+
/** Shows notes another tab saved since this page last read or wrote them. Saved notes win over text this page could not save. */
|
|
321
|
+
function refreshNotes() {
|
|
322
|
+
let text;
|
|
323
|
+
// Unlike load(), a read that fails is not taken for empty notes.
|
|
324
|
+
try { text = localStorage.getItem(NOTES_KEY) ?? ''; } catch { return; }
|
|
325
|
+
if (text === notesView.saved) return;
|
|
326
|
+
notesView.saved = text;
|
|
327
|
+
notesView.status = 'saved';
|
|
328
|
+
$('notes-text').value = text;
|
|
329
|
+
renderNotesStatus();
|
|
330
|
+
guardLeaving();
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
/** Saves the notes on every change; emptying them removes the saved copy. */
|
|
334
|
+
function saveNotes() {
|
|
335
|
+
const text = $('notes-text').value;
|
|
336
|
+
if (text.length > NOTES_LIMIT) notesView.status = 'long';
|
|
337
|
+
else if (!save(NOTES_KEY, text || null)) notesView.status = 'refused';
|
|
338
|
+
else {
|
|
339
|
+
notesView.status = 'saved';
|
|
340
|
+
notesView.saved = text;
|
|
341
|
+
}
|
|
342
|
+
renderNotesStatus();
|
|
343
|
+
guardLeaving();
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
/** The line under the title is a live region, so it is rewritten only when the status changes. */
|
|
347
|
+
function renderNotesStatus() {
|
|
348
|
+
if (notesView.shown === notesView.status) return;
|
|
349
|
+
notesView.shown = notesView.status;
|
|
350
|
+
const sub = $('notes-sub');
|
|
351
|
+
sub.textContent = NOTES_STATUS[notesView.status];
|
|
352
|
+
sub.classList.toggle('warn', notesView.status !== 'saved');
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
/** Another tab saved the notes, or cleared this page's storage. */
|
|
356
|
+
function notesStored(e) {
|
|
357
|
+
if (e.key === NOTES_KEY || e.key === null) refreshNotes();
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
function openNotes() {
|
|
361
|
+
hideTip();
|
|
362
|
+
refreshNotes();
|
|
363
|
+
// Saving an empty note changes nothing, but shows at once when the browser keeps nothing.
|
|
364
|
+
if (notesView.status === 'saved' && !$('notes-text').value) saveNotes();
|
|
365
|
+
const dialog = $('notes');
|
|
366
|
+
if (!dialog.open) dialog.showModal();
|
|
367
|
+
$('notes-text').focus();
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
function closeNotes() {
|
|
371
|
+
if ($('notes').open) $('notes').close();
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
/** Focus goes back to the Notes button in every browser, never to the page itself. */
|
|
375
|
+
function notesClosed() {
|
|
376
|
+
$('notes-open').focus();
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
/** The second press of a double-click on Notes lands in the panel, where it must not take the focus from the text. */
|
|
380
|
+
const keepFocus = (e) => { if (e.detail > 1 && e.target !== $('notes-text')) e.preventDefault(); };
|
|
381
|
+
|
|
382
|
+
/** Whether a press or click on the panel was on its backdrop, outside the sheet. */
|
|
383
|
+
function isOutsideNotes(e) {
|
|
384
|
+
const box = e.currentTarget.getBoundingClientRect();
|
|
385
|
+
return e.target === e.currentTarget && (e.clientX < box.left || e.clientX >= box.right || e.clientY < box.top || e.clientY >= box.bottom);
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
function notesPressed(e) {
|
|
389
|
+
notesView.pressedOutside = isOutsideNotes(e);
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
/** Only a press and a release both outside close the notes: a drag into or out of them selects text. */
|
|
393
|
+
function notesClicked(e) {
|
|
394
|
+
if (notesView.pressedOutside && isOutsideNotes(e)) closeNotes();
|
|
395
|
+
}
|
|
396
|
+
|
|
297
397
|
// ---- upgrading the manager ------------------------------------------------
|
|
298
398
|
|
|
299
399
|
function renderUpgrade() {
|
|
@@ -580,7 +680,9 @@ function renderShells(card, provider) {
|
|
|
580
680
|
const chip = host.children[i];
|
|
581
681
|
chip.setAttribute('aria-selected', String(shell.id === selected));
|
|
582
682
|
chip.textContent = shell.label;
|
|
583
|
-
chip.title =
|
|
683
|
+
chip.title = shell.multiplexer
|
|
684
|
+
? `Start new sessions inside ${shell.label}. Stopping or closing one detaches it; it keeps running in ${shell.label}.\n${shell.path}`
|
|
685
|
+
: `Start new sessions in ${shell.label}${shell.id === provider.defaultShell ? ', used unless you pick another' : ''}\n${shell.path}`;
|
|
584
686
|
});
|
|
585
687
|
}
|
|
586
688
|
|
|
@@ -2689,6 +2791,8 @@ function accountLabel(s) {
|
|
|
2689
2791
|
|
|
2690
2792
|
function statusText(s) {
|
|
2691
2793
|
if (s.status === 'exited') {
|
|
2794
|
+
// A multiplexer's client exits 1 when detached by Stop: its exit code tells the user nothing.
|
|
2795
|
+
if (s.multiplexer) return 'Closed';
|
|
2692
2796
|
if (s.signal) return `Exited (${s.signal})`;
|
|
2693
2797
|
return s.exitCode === 0 || s.exitCode === null ? 'Exited' : `Exited (${s.exitCode})`;
|
|
2694
2798
|
}
|
|
@@ -2732,8 +2836,24 @@ function resumable(s) {
|
|
|
2732
2836
|
&& !runningOn(s.provider.id, s.account?.id ?? 'default', id));
|
|
2733
2837
|
}
|
|
2734
2838
|
|
|
2839
|
+
/** A stopped tmux or herdr card whose multiplexer still has its session. */
|
|
2840
|
+
function reattachable(s) {
|
|
2841
|
+
return Boolean(s.status === 'exited' && s.multiplexer?.reattachable);
|
|
2842
|
+
}
|
|
2843
|
+
|
|
2844
|
+
async function reattachSession(id) {
|
|
2845
|
+
try {
|
|
2846
|
+
upsertSession((await api('POST', `/sessions/${id}/reattach`)).session);
|
|
2847
|
+
openPanel(id);
|
|
2848
|
+
} catch (err) {
|
|
2849
|
+
if (err instanceof AuthError) return showAuth(err.message);
|
|
2850
|
+
toast(err.message, 8000);
|
|
2851
|
+
}
|
|
2852
|
+
}
|
|
2853
|
+
|
|
2735
2854
|
function resumeCard(id) {
|
|
2736
2855
|
const s = state.sessions.get(id);
|
|
2856
|
+
if (s && reattachable(s)) return reattachSession(id);
|
|
2737
2857
|
if (!s || !resumable(s)) return;
|
|
2738
2858
|
const provider = state.providers.find((p) => p.id === s.provider.id);
|
|
2739
2859
|
const account = provider.accounts?.find((a) => a.id === s.account?.id)?.id;
|
|
@@ -2778,14 +2898,20 @@ function updateCard(node, s) {
|
|
|
2778
2898
|
renderAgents(node.querySelector('.agents'), s.agents, s.shells || []);
|
|
2779
2899
|
paintReporting(node.querySelector('.agents-row'), s);
|
|
2780
2900
|
node.classList.toggle('exited', s.status === 'exited');
|
|
2781
|
-
node.querySelector('.stop')
|
|
2901
|
+
const stop = node.querySelector('.stop');
|
|
2902
|
+
stop.hidden = s.status !== 'running';
|
|
2903
|
+
stop.textContent = s.multiplexer ? 'Detach' : 'Stop';
|
|
2904
|
+
stop.title = s.multiplexer ? `Detach from ${s.multiplexer.label}; it keeps running. Reattach with: ${s.multiplexer.attach}` : '';
|
|
2782
2905
|
node.querySelector('.remove').hidden = s.status === 'running';
|
|
2783
2906
|
const useButton = node.querySelector('.use-folder');
|
|
2784
2907
|
useButton.hidden = !clonedPath(s);
|
|
2785
2908
|
useButton.title = clonedPath(s) ? `Make ${s.clone.path} the working folder, so new sessions start there` : '';
|
|
2786
2909
|
const resume = node.querySelector('.resume');
|
|
2787
|
-
resume.hidden = !resumable(s);
|
|
2788
|
-
resume.
|
|
2910
|
+
resume.hidden = !resumable(s) && !reattachable(s);
|
|
2911
|
+
resume.textContent = s.multiplexer ? 'Reattach' : 'Resume';
|
|
2912
|
+
resume.title = s.multiplexer
|
|
2913
|
+
? `Attach this card to its ${s.multiplexer.label} session again, as ${s.multiplexer.attach} would`
|
|
2914
|
+
: `Start ${s.provider.tool} again on this session${id ? ` (${id})` : ''} in ${s.cwd}`;
|
|
2789
2915
|
const modelLabel = s.model ? `, model ${modelText(s)}` : '';
|
|
2790
2916
|
const accountName = accountLabel(s) ? `, ${accountLabel(s)} account` : '';
|
|
2791
2917
|
const shellCount = (s.shells || []).length;
|
|
@@ -2832,11 +2958,18 @@ function confirmLeaving(event) {
|
|
|
2832
2958
|
|
|
2833
2959
|
function guardLeaving() {
|
|
2834
2960
|
const running = state.connected && [...state.sessions.values()].some((s) => s.status === 'running');
|
|
2835
|
-
|
|
2961
|
+
// Notes that could not be saved are lost with the page.
|
|
2962
|
+
const unsaved = notesView.status !== 'saved' && $('notes-text').value !== '';
|
|
2963
|
+
if (running || unsaved) addEventListener('beforeunload', confirmLeaving);
|
|
2836
2964
|
else removeEventListener('beforeunload', confirmLeaving);
|
|
2837
2965
|
}
|
|
2838
2966
|
|
|
2839
2967
|
function upsertSession(session) {
|
|
2968
|
+
// An exit is final for the process it ended. A reply sent before it, such as Stop's, can
|
|
2969
|
+
// arrive after the exit was announced and must not show the session running again; a
|
|
2970
|
+
// reattached tmux or herdr session runs a newer process, started after that exit.
|
|
2971
|
+
const known = state.sessions.get(session.id);
|
|
2972
|
+
if (known?.status === 'exited' && session.status !== 'exited' && !(Date.parse(session.startedAt) > Date.parse(known.exitedAt))) return;
|
|
2840
2973
|
state.sessions.set(session.id, session);
|
|
2841
2974
|
renderSessions();
|
|
2842
2975
|
noticeClone(session);
|
|
@@ -2850,17 +2983,25 @@ function dropSession(id) {
|
|
|
2850
2983
|
renderSessions();
|
|
2851
2984
|
}
|
|
2852
2985
|
|
|
2986
|
+
/** What happens to a running session when it is stopped, for the confirmation. */
|
|
2987
|
+
function stopNote(s) {
|
|
2988
|
+
const mux = s.multiplexer;
|
|
2989
|
+
return mux ? `${mux.label} keeps running it. Reattach it from its card, or in a terminal with: ${mux.attach}` : `The ${s.provider.tool} process will be ended.`;
|
|
2990
|
+
}
|
|
2991
|
+
|
|
2853
2992
|
async function stopSession(id) {
|
|
2854
2993
|
const s = state.sessions.get(id);
|
|
2855
2994
|
if (!s || s.status !== 'running') return;
|
|
2856
|
-
if (!confirm(
|
|
2995
|
+
if (!confirm(`${s.multiplexer ? 'Detach' : 'Stop'} "${s.name}"? ${stopNote(s)}`)) return;
|
|
2857
2996
|
try { upsertSession((await api('POST', `/sessions/${id}/stop`)).session); } catch (err) { toast(err.message); }
|
|
2858
2997
|
}
|
|
2859
2998
|
|
|
2860
2999
|
async function removeSession(id) {
|
|
2861
3000
|
const s = state.sessions.get(id);
|
|
2862
3001
|
if (!s) return;
|
|
2863
|
-
|
|
3002
|
+
// The card goes, so only the terminal can reattach it.
|
|
3003
|
+
const question = s.multiplexer ? `Detach it and remove it? ${s.multiplexer.label} keeps running it. Reattach it in a terminal with: ${s.multiplexer.attach}` : 'End it and remove it?';
|
|
3004
|
+
if (s.status === 'running' && !confirm(`"${s.name}" is still running. ${question}`)) return;
|
|
2864
3005
|
try { await api('DELETE', `/sessions/${id}`); dropSession(id); } catch (err) { toast(err.message); }
|
|
2865
3006
|
}
|
|
2866
3007
|
|
|
@@ -3131,6 +3272,23 @@ function suppressQueryReplies(term) {
|
|
|
3131
3272
|
}
|
|
3132
3273
|
}
|
|
3133
3274
|
|
|
3275
|
+
/** The text an OSC 52 sequence ("c;<base64>") asks to copy, or null for a clipboard query or anything unreadable. */
|
|
3276
|
+
function osc52Text(data) {
|
|
3277
|
+
const payload = data.slice(data.indexOf(';') + 1);
|
|
3278
|
+
if (!data.includes(';') || !payload || payload === '?' || payload.length > 4 * 1024 * 1024) return null;
|
|
3279
|
+
try {
|
|
3280
|
+
return new TextDecoder().decode(Uint8Array.from(atob(payload), (c) => c.charCodeAt(0)));
|
|
3281
|
+
} catch {
|
|
3282
|
+
return null;
|
|
3283
|
+
}
|
|
3284
|
+
}
|
|
3285
|
+
|
|
3286
|
+
/** The line a terminal shows when its process ends. A tmux or herdr client's exit code says nothing about the session it showed. */
|
|
3287
|
+
function exitLine(s, { exitCode, signal }) {
|
|
3288
|
+
if (s?.multiplexer) return '[closed]';
|
|
3289
|
+
return `[process exited with ${signal ? `signal ${signal}` : `code ${exitCode ?? 0}`}]`;
|
|
3290
|
+
}
|
|
3291
|
+
|
|
3134
3292
|
class TerminalView {
|
|
3135
3293
|
constructor(sessionId) {
|
|
3136
3294
|
this.id = sessionId;
|
|
@@ -3141,6 +3299,8 @@ class TerminalView {
|
|
|
3141
3299
|
fontSize: coarsePointer.matches ? 14 : 13,
|
|
3142
3300
|
scrollback: 5000,
|
|
3143
3301
|
macOptionIsMeta: true,
|
|
3302
|
+
// Option-drag selects text even while a program reads the mouse, as Shift-drag does elsewhere.
|
|
3303
|
+
macOptionClickForcesSelection: true,
|
|
3144
3304
|
theme: TERMINAL_THEME,
|
|
3145
3305
|
});
|
|
3146
3306
|
this.fit = new window.FitAddon.FitAddon();
|
|
@@ -3148,6 +3308,13 @@ class TerminalView {
|
|
|
3148
3308
|
this.term.loadAddon(new window.WebLinksAddon.WebLinksAddon((_e, uri) => window.open(uri, '_blank', 'noopener,noreferrer')));
|
|
3149
3309
|
this.term.attachCustomKeyEventHandler((e) => this.handleKey(e));
|
|
3150
3310
|
suppressQueryReplies(this.term);
|
|
3311
|
+
// Programs copy with OSC 52: tmux's copy mode, and herdr where it cannot reach the
|
|
3312
|
+
// system clipboard itself. Only the open terminal may write it; nothing reads it.
|
|
3313
|
+
this.term.parser.registerOscHandler(52, (data) => {
|
|
3314
|
+
const text = osc52Text(data);
|
|
3315
|
+
if (text !== null && state.activeId === this.id) navigator.clipboard?.writeText(text).catch(() => {});
|
|
3316
|
+
return true;
|
|
3317
|
+
});
|
|
3151
3318
|
this.term.onData((data) => {
|
|
3152
3319
|
this.send({ type: 'input', data });
|
|
3153
3320
|
});
|
|
@@ -3198,11 +3365,9 @@ class TerminalView {
|
|
|
3198
3365
|
case 'data':
|
|
3199
3366
|
this.term.write(msg.data);
|
|
3200
3367
|
break;
|
|
3201
|
-
case 'exit':
|
|
3202
|
-
|
|
3203
|
-
this.term.write(`\r\n\x1b[2m[process exited with ${how}]\x1b[0m\r\n`);
|
|
3368
|
+
case 'exit':
|
|
3369
|
+
this.term.write(`\r\n\x1b[2m${exitLine(state.sessions.get(this.id), msg)}\x1b[0m\r\n`);
|
|
3204
3370
|
break;
|
|
3205
|
-
}
|
|
3206
3371
|
default:
|
|
3207
3372
|
break;
|
|
3208
3373
|
}
|
|
@@ -3320,7 +3485,7 @@ function updatePanel() {
|
|
|
3320
3485
|
$('panel-sub').title = modelTitle(s);
|
|
3321
3486
|
renderAgents($('panel-agents'), s.agents, s.shells || []);
|
|
3322
3487
|
const stop = $('panel-stop');
|
|
3323
|
-
stop.textContent = s.status
|
|
3488
|
+
stop.textContent = s.status !== 'running' ? 'Remove' : s.multiplexer ? 'Detach' : 'Stop';
|
|
3324
3489
|
}
|
|
3325
3490
|
|
|
3326
3491
|
// ---- stopping the manager -------------------------------------------------
|
|
@@ -3698,6 +3863,16 @@ document.addEventListener('keydown', (e) => {
|
|
|
3698
3863
|
}, true);
|
|
3699
3864
|
addEventListener('scroll', () => { if (tipFor) hideTip(); }, true);
|
|
3700
3865
|
addEventListener('resize', () => { if (tipFor) hideTip(); });
|
|
3866
|
+
$('notes-open').addEventListener('click', firstClick(openNotes));
|
|
3867
|
+
$('notes-close').addEventListener('click', firstClick(closeNotes));
|
|
3868
|
+
$('notes').addEventListener('mousedown', keepFocus);
|
|
3869
|
+
$('notes').addEventListener('pointerdown', notesPressed);
|
|
3870
|
+
$('notes').addEventListener('click', notesClicked);
|
|
3871
|
+
$('notes').addEventListener('close', notesClosed);
|
|
3872
|
+
$('notes-text').addEventListener('input', saveNotes);
|
|
3873
|
+
addEventListener('storage', notesStored);
|
|
3874
|
+
// On load, and again for a page back from the back/forward cache, which may have missed another tab's notes.
|
|
3875
|
+
addEventListener('pageshow', refreshNotes);
|
|
3701
3876
|
$('news-all').addEventListener('click', openNews);
|
|
3702
3877
|
$('news-close').addEventListener('click', closeNews);
|
|
3703
3878
|
$('news-fresh').addEventListener('click', showFreshNews);
|
|
@@ -3780,8 +3955,8 @@ $('models-more').addEventListener('click', () => {
|
|
|
3780
3955
|
renderModels();
|
|
3781
3956
|
$('models-list').children[before]?.querySelector('.model-toggle')?.focus();
|
|
3782
3957
|
});
|
|
3783
|
-
$('stop-manager').addEventListener('click', () => stopManager());
|
|
3784
|
-
$('restart-manager').addEventListener('click', () => stopManager({ restart: true }));
|
|
3958
|
+
$('stop-manager').addEventListener('click', firstClick(() => stopManager()));
|
|
3959
|
+
$('restart-manager').addEventListener('click', firstClick(() => stopManager({ restart: true })));
|
|
3785
3960
|
$('copy-command').addEventListener('click', copyCommand);
|
|
3786
3961
|
$('upgrade').addEventListener('click', upgradeManager);
|
|
3787
3962
|
$('appearance-menu').addEventListener('change', (e) => {
|
package/web/index.html
CHANGED
|
@@ -41,6 +41,7 @@
|
|
|
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
43
|
</div>
|
|
44
|
+
<button id="notes-open" class="btn notes-open" type="button" aria-haspopup="dialog">Notes</button>
|
|
44
45
|
<button id="restart-manager" class="btn restart-manager" type="button" hidden>Restart manager</button>
|
|
45
46
|
<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
47
|
</div>
|
|
@@ -179,6 +180,19 @@
|
|
|
179
180
|
<p id="changelog-status" class="news-status" hidden></p>
|
|
180
181
|
</dialog>
|
|
181
182
|
|
|
183
|
+
<dialog id="notes" class="news-panel notes-panel" aria-labelledby="notes-title">
|
|
184
|
+
<div class="news-bar">
|
|
185
|
+
<div class="news-heading">
|
|
186
|
+
<h2 id="notes-title">Notes</h2>
|
|
187
|
+
<p id="notes-sub" class="sub" aria-live="polite">Saved in this browser as you type</p>
|
|
188
|
+
</div>
|
|
189
|
+
<button id="notes-close" class="btn" type="button">Close</button>
|
|
190
|
+
</div>
|
|
191
|
+
<div class="news-body">
|
|
192
|
+
<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>
|
|
193
|
+
</div>
|
|
194
|
+
</dialog>
|
|
195
|
+
|
|
182
196
|
<dialog id="history" class="models history" aria-labelledby="history-title">
|
|
183
197
|
<div class="models-bar">
|
|
184
198
|
<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
|
@@ -29,6 +29,7 @@
|
|
|
29
29
|
--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
30
|
--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
31
|
--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");
|
|
32
|
+
--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
33
|
font-family: system-ui, -apple-system, "Segoe UI", Roboto, sans-serif;
|
|
33
34
|
font-size: 15px;
|
|
34
35
|
color-scheme: light;
|
|
@@ -336,13 +337,14 @@ main { max-width: 1200px; margin: 0 auto; padding: 26px 16px 56px; display: grid
|
|
|
336
337
|
.btn.danger { color: var(--danger); }
|
|
337
338
|
.btn.danger:hover { border-color: var(--danger); }
|
|
338
339
|
.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 {
|
|
340
|
+
.stop-manager, .restart-manager, .appearance, .notes-open, .upgrade { white-space: nowrap; }
|
|
341
|
+
.appearance::before, .notes-open::before, .appearance-menu .choice > span::before {
|
|
341
342
|
content: ""; display: inline-block; width: 1em; height: 1em; margin-right: 0.4em; vertical-align: -0.15em;
|
|
342
343
|
background: currentColor; mask: var(--icon-sun) center / contain no-repeat;
|
|
343
344
|
}
|
|
344
345
|
:root[data-theme="dark"] .appearance::before, .appearance-menu .choice > .mode-dark::before { mask-image: var(--icon-moon); }
|
|
345
346
|
.appearance-menu .choice > .sound-alerts::before { mask-image: var(--icon-sound); }
|
|
347
|
+
.notes-open::before { mask-image: var(--icon-notes); }
|
|
346
348
|
.appearance::after { margin-left: 0.45em; transform: rotate(90deg); transition: transform 0.15s var(--ease); }
|
|
347
349
|
.appearance:has(+ :popover-open)::after { transform: rotate(-90deg); }
|
|
348
350
|
.appearance-menu {
|
|
@@ -547,6 +549,20 @@ a.news-link:visited { color: var(--muted); }
|
|
|
547
549
|
.changelog-changes li::marker { color: var(--muted); }
|
|
548
550
|
.changelog-changes code { font-size: 0.8rem; padding: 0 4px; border-radius: 4px; background: var(--surface-2); }
|
|
549
551
|
|
|
552
|
+
/* Notes: the News side sheet, holding one text field that skins style with their other fields. */
|
|
553
|
+
.notes-panel .news-body { display: flex; flex-direction: column; padding-top: 12px; }
|
|
554
|
+
.notes-text {
|
|
555
|
+
flex: 1; min-height: 8em; margin: 0; padding: 10px 12px; resize: none;
|
|
556
|
+
border: 1px solid var(--border); border-radius: var(--radius); background: var(--surface); color: var(--text);
|
|
557
|
+
font: inherit; font-size: 0.9rem; line-height: 1.55;
|
|
558
|
+
}
|
|
559
|
+
.notes-text:focus-visible { outline: 2px solid var(--focus); outline-offset: 1px; }
|
|
560
|
+
.notes-panel .sub.warn { color: var(--warn); }
|
|
561
|
+
@media (pointer: coarse) {
|
|
562
|
+
/* iOS zooms in on a field with smaller text when it takes focus. */
|
|
563
|
+
.notes-text { font-size: 16px; }
|
|
564
|
+
}
|
|
565
|
+
|
|
550
566
|
.history-tools { display: flex; flex-wrap: wrap; align-items: center; gap: 10px; padding: 10px 14px 0; }
|
|
551
567
|
.history-tools input[type="search"] {
|
|
552
568
|
flex: 1 1 220px; min-width: 0; padding: 7px 10px; border-radius: var(--radius); border: 1px solid var(--border);
|