@oddessentials/agent-guild 0.7.1 → 0.8.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.
Files changed (92) hide show
  1. package/README.md +5 -2
  2. package/package.json +1 -1
  3. package/web/app.js +69 -17
  4. package/web/index.html +12 -3
  5. package/web/skins/guild/skin.css +502 -0
  6. package/web/skins/professional/banners/anthropic/idle.svg +1 -0
  7. package/web/skins/professional/banners/anthropic/locked.svg +1 -0
  8. package/web/skins/professional/banners/anthropic/working.svg +1 -0
  9. package/web/skins/professional/banners/google/idle.svg +1 -0
  10. package/web/skins/professional/banners/google/locked.svg +1 -0
  11. package/web/skins/professional/banners/google/working.svg +1 -0
  12. package/web/skins/professional/banners/openai/idle.svg +1 -0
  13. package/web/skins/professional/banners/openai/locked.svg +1 -0
  14. package/web/skins/professional/banners/openai/working.svg +1 -0
  15. package/web/skins/professional/banners/shell/idle.svg +1 -0
  16. package/web/skins/professional/banners/shell/locked.svg +1 -0
  17. package/web/skins/professional/banners/shell/working.svg +1 -0
  18. package/web/skins/professional/banners/xai/idle.svg +1 -0
  19. package/web/skins/professional/banners/xai/locked.svg +1 -0
  20. package/web/skins/professional/banners/xai/working.svg +1 -0
  21. package/web/skins/professional/icons/anthropic.svg +1 -0
  22. package/web/skins/professional/icons/google.svg +1 -0
  23. package/web/skins/professional/icons/openai.svg +1 -0
  24. package/web/skins/professional/icons/shell.svg +1 -0
  25. package/web/skins/professional/icons/xai.svg +1 -0
  26. package/web/skins/professional/page-light.svg +1 -0
  27. package/web/skins/professional/page.svg +1 -0
  28. package/web/skins/professional/skin.css +253 -0
  29. package/web/skins/professional/ui/empty-state-light.svg +1 -0
  30. package/web/skins/professional/ui/empty-state.svg +1 -0
  31. package/web/skins/professional/ui/lock.svg +1 -0
  32. package/web/styles.css +43 -44
  33. package/web/theme.js +18 -3
  34. package/web/art/art.css +0 -467
  35. /package/web/{art/ui/guild-crest.png → brand/crest.png} +0 -0
  36. /package/web/{art/ui → brand}/favicon.png +0 -0
  37. /package/web/{art → skins/guild}/characters/anthropic/idle.avif +0 -0
  38. /package/web/{art → skins/guild}/characters/anthropic/idle.webp +0 -0
  39. /package/web/{art → skins/guild}/characters/anthropic/locked.avif +0 -0
  40. /package/web/{art → skins/guild}/characters/anthropic/locked.webp +0 -0
  41. /package/web/{art → skins/guild}/characters/anthropic/working.avif +0 -0
  42. /package/web/{art → skins/guild}/characters/anthropic/working.webp +0 -0
  43. /package/web/{art → skins/guild}/characters/google/idle.avif +0 -0
  44. /package/web/{art → skins/guild}/characters/google/idle.webp +0 -0
  45. /package/web/{art → skins/guild}/characters/google/locked.avif +0 -0
  46. /package/web/{art → skins/guild}/characters/google/locked.webp +0 -0
  47. /package/web/{art → skins/guild}/characters/google/working.avif +0 -0
  48. /package/web/{art → skins/guild}/characters/google/working.webp +0 -0
  49. /package/web/{art → skins/guild}/characters/openai/idle.avif +0 -0
  50. /package/web/{art → skins/guild}/characters/openai/idle.webp +0 -0
  51. /package/web/{art → skins/guild}/characters/openai/locked.avif +0 -0
  52. /package/web/{art → skins/guild}/characters/openai/locked.webp +0 -0
  53. /package/web/{art → skins/guild}/characters/openai/working.avif +0 -0
  54. /package/web/{art → skins/guild}/characters/openai/working.webp +0 -0
  55. /package/web/{art → skins/guild}/characters/shell/idle.avif +0 -0
  56. /package/web/{art → skins/guild}/characters/shell/idle.webp +0 -0
  57. /package/web/{art → skins/guild}/characters/shell/locked.avif +0 -0
  58. /package/web/{art → skins/guild}/characters/shell/locked.webp +0 -0
  59. /package/web/{art → skins/guild}/characters/shell/working.avif +0 -0
  60. /package/web/{art → skins/guild}/characters/shell/working.webp +0 -0
  61. /package/web/{art → skins/guild}/characters/xai/idle.avif +0 -0
  62. /package/web/{art → skins/guild}/characters/xai/idle.webp +0 -0
  63. /package/web/{art → skins/guild}/characters/xai/locked.avif +0 -0
  64. /package/web/{art → skins/guild}/characters/xai/locked.webp +0 -0
  65. /package/web/{art → skins/guild}/characters/xai/working.avif +0 -0
  66. /package/web/{art → skins/guild}/characters/xai/working.webp +0 -0
  67. /package/web/{art → skins/guild}/familiars/aether.png +0 -0
  68. /package/web/{art → skins/guild}/familiars/flame.png +0 -0
  69. /package/web/{art → skins/guild}/familiars/leaf.png +0 -0
  70. /package/web/{art → skins/guild}/familiars/night.png +0 -0
  71. /package/web/{art → skins/guild}/icons/anthropic.avif +0 -0
  72. /package/web/{art → skins/guild}/icons/anthropic.webp +0 -0
  73. /package/web/{art → skins/guild}/icons/google.avif +0 -0
  74. /package/web/{art → skins/guild}/icons/google.webp +0 -0
  75. /package/web/{art → skins/guild}/icons/openai.avif +0 -0
  76. /package/web/{art → skins/guild}/icons/openai.webp +0 -0
  77. /package/web/{art → skins/guild}/icons/shell.avif +0 -0
  78. /package/web/{art → skins/guild}/icons/shell.webp +0 -0
  79. /package/web/{art → skins/guild}/icons/xai.avif +0 -0
  80. /package/web/{art → skins/guild}/icons/xai.webp +0 -0
  81. /package/web/{art → skins/guild}/page-light.avif +0 -0
  82. /package/web/{art → skins/guild}/page-light.webp +0 -0
  83. /package/web/{art → skins/guild}/page.avif +0 -0
  84. /package/web/{art → skins/guild}/page.webp +0 -0
  85. /package/web/{art → skins/guild}/ui/empty-state.avif +0 -0
  86. /package/web/{art → skins/guild}/ui/empty-state.webp +0 -0
  87. /package/web/{art → skins/guild}/ui/frame-ornate.png +0 -0
  88. /package/web/{art → skins/guild}/ui/frame.png +0 -0
  89. /package/web/{art → skins/guild}/ui/gem-mana.png +0 -0
  90. /package/web/{art → skins/guild}/ui/gem-vitality.png +0 -0
  91. /package/web/{art → skins/guild}/ui/level-medallion.png +0 -0
  92. /package/web/{art → skins/guild}/ui/padlock.png +0 -0
package/README.md CHANGED
@@ -99,8 +99,11 @@ vX.Y.Z** switches over when you are ready. The version badge opens
99
99
 
100
100
  <img src="docs/images/whats-new.webp" alt="The What's new panel listing the release notes of each version">
101
101
 
102
- **Light or dark.** The page follows your system theme, and the top-bar
103
- toggle switches it.
102
+ **Appearance.** The Appearance menu in the top bar picks a skin and light or
103
+ dark mode, without a reload. **Guild** is the default fantasy look;
104
+ **Professional** is a plain business look with no characters. The page
105
+ follows your system's light or dark setting until you pick one. See
106
+ [docs/SKINS.md](docs/SKINS.md) to make another.
104
107
 
105
108
  ## Commands
106
109
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@oddessentials/agent-guild",
3
- "version": "0.7.1",
3
+ "version": "0.8.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
@@ -5,6 +5,7 @@ const TOKEN_KEY = 'agentGuild.token';
5
5
  const CWD_KEY = 'agentGuild.cwd';
6
6
  const ACCOUNTS_KEY = 'agentGuild.accounts';
7
7
  const THEME_KEY = 'agentGuild.theme';
8
+ const SKIN_KEY = 'agentGuild.skin';
8
9
  const NEWS_SEEN_KEY = 'agentGuild.newsSeen';
9
10
  const NEWS_FILTER_KEY = 'agentGuild.newsFilter';
10
11
  const CHANGELOG_SEEN_KEY = 'agentGuild.changelogSeen';
@@ -142,33 +143,72 @@ function unreadRelease() {
142
143
 
143
144
  // ---- theme ----------------------------------------------------------------
144
145
 
145
- /** theme.js applied the saved or system theme before the first paint; this keeps the button in step. */
146
+ /** theme.js applied the saved or system theme before the first paint; this keeps the menu in step. */
146
147
  function applyTheme(theme) {
147
148
  document.documentElement.dataset.theme = theme;
148
- const button = $('theme-toggle');
149
- const other = theme === 'dark' ? 'light' : 'dark';
150
- button.textContent = theme === 'dark' ? 'Light' : 'Dark';
151
- button.dataset.next = other;
152
- button.title = `Switch to the ${other} theme`;
153
- button.setAttribute('aria-label', `Switch to the ${other} theme`);
149
+ document.querySelector(`#appearance-menu input[name="theme"][value="${theme}"]`).checked = true;
154
150
  }
155
151
 
156
152
  function currentTheme() {
157
153
  return document.documentElement.dataset.theme === 'dark' ? 'dark' : 'light';
158
154
  }
159
155
 
160
- function toggleTheme(event) {
161
- const theme = currentTheme() === 'dark' ? 'light' : 'dark';
162
- save(THEME_KEY, theme);
163
- if (!document.startViewTransition || reducedMotion.matches) return applyTheme(theme);
164
- const box = event.currentTarget.getBoundingClientRect();
156
+ /** Repaints the page with `change`, revealed in a circle growing from the control that asked for it. */
157
+ function revealChange(control, change) {
158
+ if (!document.startViewTransition || reducedMotion.matches) return change();
159
+ const box = control.getBoundingClientRect();
165
160
  const x = box.left + box.width / 2;
166
161
  const y = box.top + box.height / 2;
167
162
  const root = document.documentElement.style;
168
163
  root.setProperty('--reveal-x', `${Math.round(x)}px`);
169
164
  root.setProperty('--reveal-y', `${Math.round(y)}px`);
170
165
  root.setProperty('--reveal-r', `${Math.ceil(Math.hypot(Math.max(x, innerWidth - x), Math.max(y, innerHeight - y)))}px`);
171
- document.startViewTransition(() => applyTheme(theme));
166
+ // The browser skips the animation (still applying the change) when the page is hidden or another transition starts.
167
+ document.startViewTransition(change).ready.catch(() => {});
168
+ }
169
+
170
+ function changeTheme(input) {
171
+ const theme = input.value === 'dark' ? 'dark' : 'light';
172
+ save(THEME_KEY, theme);
173
+ revealChange(input.closest('label'), () => applyTheme(theme));
174
+ }
175
+
176
+ // ---- skin -----------------------------------------------------------------
177
+
178
+ /** The skins theme.js offers; it applied the saved one before the first paint. */
179
+ const SKINS = window.agentGuildSkins ?? [{ id: 'guild', name: 'Guild' }];
180
+
181
+ function renderSkinChoices() {
182
+ const choices = SKINS.map((skin) => {
183
+ const label = document.createElement('label');
184
+ label.className = 'choice';
185
+ const input = Object.assign(document.createElement('input'), { type: 'radio', name: 'skin', value: skin.id });
186
+ input.checked = skin.id === document.documentElement.dataset.skin;
187
+ label.append(input, Object.assign(document.createElement('span'), { textContent: skin.name }));
188
+ return label;
189
+ });
190
+ $('skin-choices').append(...choices);
191
+ }
192
+
193
+ /**
194
+ * Switches skin in place. Entrance and level-up animations are cleared first
195
+ * so the new skin does not replay them on every card.
196
+ */
197
+ function changeSkin(input) {
198
+ const skin = input.value;
199
+ if (!SKINS.some((s) => s.id === skin) || skin === document.documentElement.dataset.skin) return;
200
+ save(SKIN_KEY, skin);
201
+ for (const el of document.querySelectorAll('.deal, .enter, .level-up, .summon')) el.classList.remove('deal', 'enter', 'level-up', 'summon');
202
+ revealChange(input.closest('label'), () => { document.documentElement.dataset.skin = skin; });
203
+ }
204
+
205
+ /** Places the open menu under its button, right-aligned with it and kept on screen. */
206
+ function placeAppearanceMenu() {
207
+ const menu = $('appearance-menu');
208
+ if (!menu.matches(':popover-open')) return;
209
+ const box = $('appearance').getBoundingClientRect();
210
+ menu.style.top = `${Math.round(box.bottom + 6)}px`;
211
+ menu.style.right = `${Math.max(8, Math.round(innerWidth - box.right))}px`;
172
212
  }
173
213
 
174
214
  // ---- upgrading the manager ------------------------------------------------
@@ -2443,9 +2483,11 @@ function buildCard(session) {
2443
2483
  if (id) copyId(id);
2444
2484
  });
2445
2485
  node.querySelector('.model-pill').addEventListener('click', () => openSessionModel(session.id));
2486
+ // Skins name their own keyframes, so the one-shot classes clear on any animation they run.
2446
2487
  node.addEventListener('animationend', (e) => {
2447
- if (e.animationName === 'level-up') e.target.classList.remove('level-up');
2448
- else if (e.animationName === 'card-enter' && e.target === node) node.classList.remove('enter');
2488
+ if (e.target.classList.contains('level-up')) e.target.classList.remove('level-up');
2489
+ else if (e.target.classList.contains('summon')) e.target.classList.remove('summon');
2490
+ else if (e.target === node) node.classList.remove('enter');
2449
2491
  });
2450
2492
  return node;
2451
2493
  }
@@ -3170,9 +3212,18 @@ $('stop-manager').addEventListener('click', () => stopManager());
3170
3212
  $('restart-manager').addEventListener('click', () => stopManager({ restart: true }));
3171
3213
  $('copy-command').addEventListener('click', copyCommand);
3172
3214
  $('upgrade').addEventListener('click', upgradeManager);
3173
- $('theme-toggle').addEventListener('click', toggleTheme);
3215
+ $('appearance-menu').addEventListener('change', (e) => {
3216
+ if (e.target.name === 'skin') changeSkin(e.target);
3217
+ else if (e.target.name === 'theme') changeTheme(e.target);
3218
+ });
3219
+ $('appearance-menu').addEventListener('toggle', (e) => {
3220
+ if (e.newState !== 'open') return;
3221
+ placeAppearanceMenu();
3222
+ e.currentTarget.querySelector('input:checked')?.focus();
3223
+ });
3224
+ window.addEventListener('resize', placeAppearanceMenu);
3174
3225
  $('providers').addEventListener('animationend', (e) => {
3175
- if (e.animationName === 'deal' && e.target === e.currentTarget.lastElementChild) e.currentTarget.classList.remove('deal');
3226
+ if (e.target === e.currentTarget.lastElementChild) e.currentTarget.classList.remove('deal');
3176
3227
  });
3177
3228
  let tiltFrame = 0;
3178
3229
  $('providers').addEventListener('pointermove', (e) => {
@@ -3196,6 +3247,7 @@ $('providers').addEventListener('pointerout', (e) => {
3196
3247
  }
3197
3248
  });
3198
3249
  applyTheme(currentTheme());
3250
+ renderSkinChoices();
3199
3251
  // Follow the system setting until the user picks a theme.
3200
3252
  window.matchMedia('(prefers-color-scheme: dark)').addEventListener('change', (e) => {
3201
3253
  if (!load(THEME_KEY)) applyTheme(e.matches ? 'dark' : 'light');
package/web/index.html CHANGED
@@ -6,11 +6,12 @@
6
6
  <title>Agent Guild</title>
7
7
  <meta name="color-scheme" content="light dark">
8
8
  <script src="/theme.js"></script>
9
- <link rel="icon" href="/art/ui/favicon.png" type="image/png">
9
+ <link rel="icon" href="/brand/favicon.png" type="image/png">
10
10
  <link rel="preload" href="/fonts/cinzel.woff2" as="font" type="font/woff2" crossorigin>
11
11
  <link rel="stylesheet" href="/vendor/xterm/xterm.css">
12
12
  <link rel="stylesheet" href="/styles.css">
13
- <link rel="stylesheet" href="/art/art.css">
13
+ <link rel="stylesheet" href="/skins/guild/skin.css">
14
+ <link rel="stylesheet" href="/skins/professional/skin.css">
14
15
  </head>
15
16
  <body>
16
17
  <header class="topbar">
@@ -24,7 +25,15 @@
24
25
  </div>
25
26
  <span id="upgrade-note" class="upgrade-note" hidden></span>
26
27
  <button id="upgrade" class="btn upgrade" type="button" hidden></button>
27
- <button id="theme-toggle" class="btn theme-toggle" type="button"></button>
28
+ <button id="appearance" class="btn appearance" type="button" popovertarget="appearance-menu" aria-haspopup="dialog">Appearance</button>
29
+ <div id="appearance-menu" class="appearance-menu" popover role="dialog" aria-label="Appearance">
30
+ <fieldset id="skin-choices"><legend>Skin</legend></fieldset>
31
+ <fieldset>
32
+ <legend>Mode</legend>
33
+ <label class="choice"><input type="radio" name="theme" value="light"><span class="mode-light">Light</span></label>
34
+ <label class="choice"><input type="radio" name="theme" value="dark"><span class="mode-dark">Dark</span></label>
35
+ </fieldset>
36
+ </div>
28
37
  <button id="restart-manager" class="btn restart-manager" type="button" hidden>Restart manager</button>
29
38
  <button id="stop-manager" class="btn danger stop-manager" type="button" hidden title="Stop the session manager. This ends every session.">Stop manager</button>
30
39
  </div>