@agentprojectcontext/apx 1.40.1 → 1.42.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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@agentprojectcontext/apx",
3
- "version": "1.40.1",
3
+ "version": "1.42.0",
4
4
  "description": "APX — unified CLI + daemon for the Agent Project Context (APC) standard.",
5
5
  "publishConfig": {
6
6
  "access": "public"
@@ -132,9 +132,8 @@
132
132
  <div class="center" id="cap-center"></div>
133
133
  <div class="cap-actions" id="cap-actions"></div>
134
134
  </div>
135
- <div id="caption-slot"></div>
136
135
  <div id="conv-slot"></div>
137
- <div id="session-slot"></div>
136
+ <div id="footer-slot"></div>
138
137
  `;
139
138
  const $cap = $root.querySelector("#cap");
140
139
  const $capBadge = $root.querySelector("#cap-badge");
@@ -142,9 +141,14 @@
142
141
  const $badgeTxt = $root.querySelector("#badge-txt");
143
142
  const $capCenter = $root.querySelector("#cap-center");
144
143
  const $capActions = $root.querySelector("#cap-actions");
145
- const $captionSlot = $root.querySelector("#caption-slot");
146
144
  const $convSlot = $root.querySelector("#conv-slot");
147
- const $sessionSlot = $root.querySelector("#session-slot");
145
+ const $footerSlot = $root.querySelector("#footer-slot");
146
+
147
+ // Footer (hint + session actions) state: the shortcut to display and a
148
+ // signature guard so render() doesn't rebuild the bar (and re-trigger the
149
+ // pill animation) on every keystroke.
150
+ let captionShortcut = "CommandOrControl+G";
151
+ let footerSig = "";
148
152
 
149
153
  $badgeMic.innerHTML = ICON.mic();
150
154
  $badgeTxt.innerHTML = ICON.text();
@@ -172,7 +176,7 @@
172
176
  }
173
177
  document.documentElement.setAttribute("data-theme", theme);
174
178
  setPosition(position);
175
- initialCaption(shortcut);
179
+ captionShortcut = shortcut || "CommandOrControl+G";
176
180
  configReady = true;
177
181
  // If render() already fired the bootstrap paint (because the IPC was
178
182
  // slow), the existing input has the stale placeholder. Patch it in
@@ -184,7 +188,7 @@
184
188
  }).catch(() => {
185
189
  document.documentElement.setAttribute("data-theme", "light");
186
190
  setPosition("right");
187
- initialCaption("CommandOrControl+G");
191
+ captionShortcut = "CommandOrControl+G";
188
192
  configReady = true;
189
193
  render();
190
194
  });
@@ -208,12 +212,36 @@
208
212
  .replace(/\+/g, "");
209
213
  }
210
214
 
211
- function initialCaption(shortcut) {
212
- const sc = formatShortcut(shortcut);
213
- $captionSlot.innerHTML = `
214
- <div class="caption">Mantené <span class="kbd">${sc}</span> para hablar
215
- <span class="kbd">⌥ /</span> para escribir</div>
215
+ function captionHtml() {
216
+ const sc = formatShortcut(captionShortcut);
217
+ return `<div class="caption">Mantené <span class="kbd">${sc}</span> para hablar
218
+ <span class="kbd">⌥ /</span> para escribir</div>`;
219
+ }
220
+
221
+ // Unified bottom bar: the "Mantené ⌘G…" hint on the LEFT, session actions on
222
+ // the RIGHT — small translucent pills in the same glass language as the hint
223
+ // (not the old big white buttons). "Cerrar ventana" is always present so the
224
+ // empty-idle window can be dismissed; "Nueva sesión" joins once a session
225
+ // exists. The conversation card sits above this bar (input → messages → bar).
226
+ function renderFooter() {
227
+ const hasSession = messages.length > 0;
228
+ // "Cerrar ventana" while it's alone (empty state); shortened to "Cerrar"
229
+ // once "Nueva sesión" joins so all three pills fit on one line.
230
+ const closeLabel = hasSession ? "Cerrar" : "Cerrar ventana";
231
+ const sig = `${hasSession}|${captionShortcut}`;
232
+ if (sig === footerSig && $footerSlot.firstChild) return; // no churn per keystroke
233
+ footerSig = sig;
234
+ $footerSlot.innerHTML = `
235
+ <div class="footer-bar">
236
+ ${captionHtml()}
237
+ <div class="footer-actions">
238
+ ${hasSession ? `<button class="cap-pill act-new" id="btn-new"><span class="ic">${ICON.plus()}</span> Nueva sesión</button>` : ""}
239
+ <button class="cap-pill act-close" id="btn-close"><span class="ic">${ICON.close()}</span> ${closeLabel}</button>
240
+ </div>
241
+ </div>
216
242
  `;
243
+ $footerSlot.querySelector("#btn-new")?.addEventListener("click", newSession);
244
+ $footerSlot.querySelector("#btn-close")?.addEventListener("click", closeWindow);
217
245
  }
218
246
 
219
247
  // ── Render: capsule center + actions vary by mode ────────────────────────
@@ -338,15 +366,9 @@
338
366
  addBtn("ghost", "Detener", ICON.stop(), () => stopSpeaking());
339
367
  }
340
368
 
341
- // caption visible only when idle AND no messages yet
342
- $captionSlot.style.display = (mode === "idle" && messages.length === 0) ? "" : "none";
343
-
344
- // session bar visible when there are messages
345
- if (messages.length > 0 || mode !== "idle") {
346
- renderSessionBar();
347
- } else {
348
- $sessionSlot.innerHTML = "";
349
- }
369
+ // Unified bottom bar (hint on the left + session actions on the right) —
370
+ // always present, so there's always a way to close the window.
371
+ renderFooter();
350
372
 
351
373
  // conv card visible when there's any content
352
374
  const wantConv = messages.length > 0 || mode === "transcribing" || mode === "thinking" || mode === "speaking";
@@ -356,18 +378,6 @@
356
378
  requestWindowResize();
357
379
  }
358
380
 
359
- function renderSessionBar() {
360
- if ($sessionSlot.querySelector(".session-bar")) return; // keep DOM stable
361
- $sessionSlot.innerHTML = `
362
- <div class="session-bar">
363
- <button class="sbtn new" id="btn-new"><span class="ic">${ICON.plus()}</span> Nueva sesión</button>
364
- <button class="sbtn close" id="btn-close"><span class="ic">${ICON.close()}</span> Cerrar</button>
365
- </div>
366
- `;
367
- $sessionSlot.querySelector("#btn-new").addEventListener("click", newSession);
368
- $sessionSlot.querySelector("#btn-close").addEventListener("click", closeWindow);
369
- }
370
-
371
381
  // ── Conversation card ────────────────────────────────────────────────────
372
382
  let $convScroll = null;
373
383
  function ensureConv() {
@@ -1115,9 +1125,8 @@
1115
1125
  toolPillsByName = {};
1116
1126
  pendingUserText = "";
1117
1127
  $convSlot.innerHTML = "";
1118
- $sessionSlot.innerHTML = "";
1119
1128
  mode = "idle";
1120
- render();
1129
+ render(); // footer rebuilds (drops "Nueva sesión" now that there's no session)
1121
1130
  }
1122
1131
  function closeWindow() { window.apx?.close?.(); }
1123
1132
 
@@ -193,6 +193,36 @@ button { font-family: inherit; }
193
193
  }
194
194
  [data-theme="dark"] .kbd { color: color-mix(in oklch, var(--accent) 60%, white); }
195
195
 
196
+ /* Bottom bar: the "Mantené ⌘G…" hint on the left, small session-action pills
197
+ on the right. The conversation card (when present) sits above this bar. */
198
+ .footer-bar {
199
+ display: flex; align-items: center; justify-content: space-between;
200
+ gap: 10px; flex-wrap: wrap;
201
+ }
202
+ .footer-bar .caption { align-self: auto; }
203
+ .footer-actions { display: flex; align-items: center; gap: 7px; flex-shrink: 0; margin-left: auto; }
204
+
205
+ /* Translucent action pill — same glass language as .caption so the bar reads
206
+ as one hint layer rather than the old big white buttons. */
207
+ .cap-pill {
208
+ display: inline-flex; align-items: center; gap: 6px; cursor: pointer;
209
+ font: inherit; font-size: 11px; font-weight: 600; color: rgba(255,255,255,.92);
210
+ padding: 5px 12px; border-radius: 9px;
211
+ background: rgba(20,22,32,.28); backdrop-filter: blur(12px); -webkit-backdrop-filter: blur(12px);
212
+ border: 1px solid rgba(255,255,255,.14);
213
+ text-shadow: 0 0.5px 1.5px rgba(0,0,0,.3);
214
+ transition: transform .14s ease, background .15s ease, border-color .15s ease, color .15s ease;
215
+ animation: captionIn .4s ease both;
216
+ white-space: nowrap;
217
+ }
218
+ .cap-pill .ic { display: inline-flex; align-items: center; }
219
+ .cap-pill svg { width: 13px; height: 13px; opacity: .9; }
220
+ .cap-pill:hover { transform: translateY(-1px); background: rgba(20,22,32,.42); }
221
+ .cap-pill:active { transform: scale(.96); }
222
+ .cap-pill.act-new:hover { border-color: color-mix(in oklch, var(--accent) 55%, transparent); }
223
+ .cap-pill.act-close:hover { border-color: rgba(255,120,110,.5); color: #fff; }
224
+ [data-theme="dark"] .cap-pill { background: rgba(0,0,0,.34); }
225
+
196
226
  /* ============================================================
197
227
  Conversation card
198
228
  ============================================================ */