cmdzero 0.7.0 → 0.7.2

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 CHANGED
@@ -1,3 +1,7 @@
1
+ <p align="center">
2
+ <img src="https://raw.githubusercontent.com/adamcwade/cmdzero/main/assets/hero.png" alt="CmdZero — tweak your UI live in the browser; changes land in your source" width="900">
3
+ </p>
4
+
1
5
  # CmdZero
2
6
 
3
7
  Tweak your UI live in the browser — the changes are written straight to your source files.
@@ -85,8 +85,8 @@
85
85
  .cz-banner .cz-idle{color:#64748b}
86
86
  .cz-wrap{display:flex;flex-direction:column;align-items:flex-end;gap:6px}
87
87
  .cz-wrap.cz-expanded{max-height:calc(100vh - 90px);overflow-y:auto;overflow-x:hidden;padding:2px}
88
- .cz-wrap:not(.cz-expanded) > .cz-tweak:nth-child(n+4){display:none}
89
- .cz-fade{height:16px;width:160px;background:linear-gradient(to bottom,rgba(17,24,39,0),rgba(17,24,39,.55));pointer-events:none;margin-top:-10px}
88
+ .cz-wrap:not(.cz-expanded) > .cz-tweak:nth-last-child(n+4){display:none}
89
+ .cz-fade{height:14px;width:160px;background:linear-gradient(to top,rgba(17,24,39,0),rgba(17,24,39,.5));pointer-events:none;margin-bottom:-8px}
90
90
  .cz-history{background:#0b1220;color:#93c5fd;border:1px solid #263041;border-radius:8px;padding:4px 10px;font-size:11.5px;cursor:pointer;pointer-events:auto;box-shadow:0 4px 14px rgba(0,0,0,.3)}
91
91
  .cz-history:hover{border-color:#6366f1}
92
92
  .cz-tweak{background:#111827;color:#e5e7eb;border-radius:8px;padding:7px 11px;font-size:13px;display:flex;gap:8px;align-items:center;box-shadow:0 4px 14px rgba(0,0,0,.3);white-space:nowrap;width:max-content;max-width:720px;overflow:hidden;text-overflow:ellipsis}
@@ -546,10 +546,14 @@
546
546
  multiBar._draft = '';
547
547
  clearMulti();
548
548
  for (const t of targets) {
549
+ const tempId = 'nl-' + Date.now() + t.loc;
550
+ addTweak({ id: tempId, status: 'queued', label: `${shortLoc(t.loc)}: ${instruction.slice(0, 40)}` });
549
551
  try {
550
552
  const r = await api('nl', { loc: t.loc, instruction, model: state.model });
553
+ removeTweak(tempId);
551
554
  addTweak({ id: r.id, status: 'queued', model: r.model, label: `${shortLoc(t.loc)}: ${instruction.slice(0, 40)}` });
552
555
  } catch (e) {
556
+ removeTweak(tempId);
553
557
  addTweak({ id: 'x' + Date.now() + t.loc, status: 'error', label: `${shortLoc(t.loc)}: ${e.message}` });
554
558
  }
555
559
  }
@@ -962,7 +966,7 @@
962
966
 
963
967
  const nlWrap = el('div', 'cz-nlwrap');
964
968
  const input = el('textarea');
965
- input.placeholder = 'Describe a change… (⌘↵ to send)';
969
+ input.placeholder = 'Describe a change… ( to send · ⇧↵ newline)';
966
970
  input.rows = 3;
967
971
  const go = el('button', 'cz-primary', 'Go →');
968
972
  const send = async () => {
@@ -970,16 +974,23 @@
970
974
  if (!instruction) return;
971
975
  input.value = '';
972
976
  input.blur(); // so the reload after the model finishes isn't blocked by focus
977
+ // Immediate feedback — the server runs the router (which may call the
978
+ // classifier for ~1-3s) before it broadcasts its own 'queued', so show a
979
+ // placeholder now and swap in the real task once the request returns.
980
+ const tempId = 'nl-' + Date.now();
981
+ addTweak({ id: tempId, status: 'queued', label: instruction.slice(0, 60) });
973
982
  try {
974
983
  const r = await api('nl', { loc: s.loc, instruction, model: state.model });
984
+ removeTweak(tempId);
975
985
  addTweak({ id: r.id, status: 'queued', model: r.model, label: instruction.slice(0, 60) });
976
986
  } catch (e) {
977
- addTweak({ id: 'x' + Date.now(), status: 'error', label: e.message });
987
+ removeTweak(tempId);
988
+ addTweak({ id: 'x' + Date.now(), status: 'error', label: `${instruction.slice(0, 40)}: ${e.message}` });
978
989
  }
979
990
  };
980
991
  go.onclick = send;
981
- // ⌘↵ / Ctrl↵ sends; plain Enter inserts a newline (multi-line prompts).
982
- input.onkeydown = (e) => { if (e.key === 'Enter' && (e.metaKey || e.ctrlKey)) { e.preventDefault(); send(); } e.stopPropagation(); };
992
+ // Enter sends; Shift+Enter inserts a newline for multi-line prompts.
993
+ input.onkeydown = (e) => { if (e.key === 'Enter' && !e.shiftKey) { e.preventDefault(); send(); } e.stopPropagation(); };
983
994
  const goRow = el('div', 'cz-row');
984
995
  goRow.style.justifyContent = 'flex-end';
985
996
  goRow.appendChild(go);
@@ -1093,18 +1104,24 @@
1093
1104
  // ---------- tray ----------
1094
1105
  const tray = el('div', 'cz-tray');
1095
1106
  root.appendChild(tray);
1096
- // Alerts live in their own wrap so history can collapse independently.
1097
- // Newest is inserted at the top; the 3 newest stay visible (compressed; hover to expand) and older
1098
- // ones fold behind an expander.
1107
+ // Reads like a log: newest alert sits at the bottom-right corner, older ones
1108
+ // stack upward. The 3 newest stay visible (compressed; hover to expand); the
1109
+ // rest fold behind an expander that sits ABOVE them.
1110
+ const historyChip = el('div', 'cz-history');
1111
+ historyChip.style.display = 'none';
1099
1112
  const tweaksWrap = el('div', 'cz-wrap');
1100
- tray.appendChild(tweaksWrap);
1113
+ const pinNewestToBottom = () => { if (tweaksWrap.classList.contains('cz-expanded')) tweaksWrap.scrollTop = tweaksWrap.scrollHeight; };
1114
+ historyChip.onclick = () => {
1115
+ tweaksWrap.classList.toggle('cz-expanded');
1116
+ updateHistoryUI();
1117
+ // when expanded the list scrolls; keep the newest (bottom) in view
1118
+ requestAnimationFrame(pinNewestToBottom);
1119
+ };
1120
+ tray.appendChild(historyChip);
1101
1121
  const fade = el('div', 'cz-fade');
1102
1122
  fade.style.display = 'none';
1103
1123
  tray.appendChild(fade);
1104
- const historyChip = el('div', 'cz-history');
1105
- historyChip.style.display = 'none';
1106
- historyChip.onclick = () => { tweaksWrap.classList.toggle('cz-expanded'); updateHistoryUI(); };
1107
- tray.appendChild(historyChip);
1124
+ tray.appendChild(tweaksWrap);
1108
1125
  const tweaks = new Map();
1109
1126
  const tweakData = new Map(); // id -> merged record, persisted across reloads
1110
1127
  const HKEY = 'cz-history';
@@ -1160,6 +1177,15 @@
1160
1177
  }
1161
1178
  showTotals(null); // idle banner on load until /api/health arrives
1162
1179
 
1180
+ function removeTweak(id) {
1181
+ const key = String(id);
1182
+ const row = tweaks.get(key);
1183
+ if (row) row.remove();
1184
+ tweaks.delete(key);
1185
+ tweakData.delete(key);
1186
+ updateHistoryUI();
1187
+ }
1188
+
1163
1189
  function addTweak(t, hydrate) {
1164
1190
  const key = String(t.id);
1165
1191
  let row = tweaks.get(key);
@@ -1185,13 +1211,13 @@
1185
1211
  } catch (e) { row._meta.textContent = e.message; }
1186
1212
  };
1187
1213
  row.append(row._dot, row._label, row._meta, row._cancel, row._undo);
1188
- tweaksWrap.insertBefore(row, tweaksWrap.firstChild);
1214
+ tweaksWrap.appendChild(row); // newest at the end (bottom of the tray)
1189
1215
  tweaks.set(key, row);
1190
1216
  while (tweaksWrap.children.length > MAX_TWEAKS) {
1191
- const last = tweaksWrap.lastChild;
1192
- tweaks.delete(last._id);
1193
- tweakData.delete(last._id);
1194
- last.remove();
1217
+ const oldest = tweaksWrap.firstChild;
1218
+ tweaks.delete(oldest._id);
1219
+ tweakData.delete(oldest._id);
1220
+ oldest.remove();
1195
1221
  }
1196
1222
  }
1197
1223
  if (t.label) row._label.textContent = t.label;
@@ -1228,13 +1254,14 @@
1228
1254
  tweakData.set(key, { ...(tweakData.get(key) || {}), ...t, id: key });
1229
1255
  if (!hydrate) persistTweaks();
1230
1256
  updateHistoryUI();
1257
+ if (!hydrate) pinNewestToBottom(); // keep the newest alert in view when expanded
1231
1258
  }
1232
1259
 
1233
1260
  // Replay saved alerts after a page load so history survives reloads.
1234
1261
  (function hydrateTweaks() {
1235
1262
  let saved;
1236
1263
  try { saved = JSON.parse(localStorage.getItem(HKEY) || '[]'); } catch { saved = []; }
1237
- for (const rec of saved) addTweak(rec, true); // oldest→newest keeps newest on top
1264
+ for (const rec of saved) addTweak(rec, true); // chronological replay newest ends at the bottom
1238
1265
  })();
1239
1266
 
1240
1267
  try {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "cmdzero",
3
- "version": "0.7.0",
3
+ "version": "0.7.2",
4
4
  "description": "Tweak your UI live in the browser and write the changes straight to source. Copy and Tailwind edits cost zero tokens; everything else routes to a right-sized model via headless claude.",
5
5
  "license": "MIT",
6
6
  "type": "module",
package/src/server.js CHANGED
@@ -70,6 +70,9 @@ export function startServer({ root, port = 4100 }) {
70
70
  try {
71
71
  if (req.method === 'GET' && url.pathname === '/overlay.js') {
72
72
  res.setHeader('Content-Type', 'text/javascript');
73
+ // Never cache the overlay — the daemon is the source of truth, so a
74
+ // reload always picks up the latest build (no stale-overlay confusion).
75
+ res.setHeader('Cache-Control', 'no-store, must-revalidate');
73
76
  return res.end(fs.readFileSync(OVERLAY_PATH));
74
77
  }
75
78