pennyrouter 0.3.45 → 0.3.46

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": "pennyrouter",
3
- "version": "0.3.45",
3
+ "version": "0.3.46",
4
4
  "description": "Install and manage PennyRouter local coding-agent integrations.",
5
5
  "homepage": "https://pennyrouter.com",
6
6
  "bugs": {
@@ -315,6 +315,7 @@
315
315
  <div class="segmented" role="radiogroup" aria-label="Featured page">
316
316
  <label><input type="radio" name="publish-cover" value="penny" checked><span>Penny page</span></label>
317
317
  <label><input type="radio" name="publish-cover" value="native"><span>Native page</span></label>
318
+ <label><input type="radio" name="publish-cover" value="none"><span>No page</span></label>
318
319
  </div>
319
320
  <label>Your name, as shown publicly
320
321
  <input id="publish-name" maxlength="40" required autocomplete="nickname"></label>
@@ -962,13 +963,9 @@
962
963
  }
963
964
  }
964
965
  const sideOf = (role) => (paneRoles.left === role ? 'left' : 'right');
965
- const publishBlocker = () => {
966
- if (!gallery) return 'Publishing needs a native pane and a Penny pane on the same harness.';
967
- if (!snapshot[sideOf('NATIVE')].length || !snapshot[sideOf('PENNY')].length) {
968
- return 'Publishing needs an HTML page in each pane.';
969
- }
970
- return '';
971
- };
966
+ // Pages are optional: a comparison can be shared with just its numbers and a screenshot.
967
+ const publishBlocker = () =>
968
+ gallery ? '' : 'Publishing needs a native pane and a Penny pane on the same harness.';
972
969
  const prepareGallery = (message) => {
973
970
  lastReport = message;
974
971
  gallery = message.gallery || null;
@@ -993,7 +990,22 @@
993
990
  const pagePolicy = "default-src 'none'; script-src 'unsafe-inline'; style-src 'unsafe-inline'; img-src data: blob:; " +
994
991
  "font-src data:; connect-src 'none'; frame-src 'none'; object-src 'none'; base-uri 'none'; form-action 'none'";
995
992
  const sandboxed = (html) => `<meta http-equiv="Content-Security-Policy" content="${pagePolicy}"><meta name="referrer" content="no-referrer">${html}`;
996
- const chosen = (role) => snapshot[sideOf(role)][Number($(role === 'NATIVE' ? 'publish-native' : 'publish-penny').value) || 0];
993
+ // The page chosen for a side, or undefined when "No page" is selected.
994
+ const chosen = (role) => {
995
+ const value = $(role === 'NATIVE' ? 'publish-native' : 'publish-penny').value;
996
+ return value === '' ? undefined : snapshot[sideOf(role)][Number(value)];
997
+ };
998
+ // The featured page must be one that is shared; fall back to the other page, then to none.
999
+ const syncFeatured = () => {
1000
+ const available = { penny: Boolean(chosen('PENNY')), native: Boolean(chosen('NATIVE')) };
1001
+ for (const radio of document.querySelectorAll('input[name="publish-cover"]')) {
1002
+ radio.disabled = radio.value !== 'none' && !available[radio.value];
1003
+ }
1004
+ if (document.querySelector('input[name="publish-cover"]:checked').disabled) {
1005
+ const next = available.penny ? 'penny' : available.native ? 'native' : 'none';
1006
+ document.querySelector(`input[name="publish-cover"][value="${next}"]`).checked = true;
1007
+ }
1008
+ };
997
1009
  const checkedValue = (name) => document.querySelector(`input[name="${name}"]:checked`).value;
998
1010
  const savingsText = (savings) => {
999
1011
  if (!savings) return '';
@@ -1037,8 +1049,11 @@
1037
1049
  $('preview-savings').classList.toggle('loss', Boolean(savings && savings.cost < 0));
1038
1050
  const model = (meterSnapshots[sideOf('PENNY')]?.model || '').split('/').pop();
1039
1051
  $('preview-meta').textContent = [model, $('publish-name').value.trim()].filter(Boolean).join(' · ');
1040
- const cover = chosen(checkedValue('publish-cover') === 'native' ? 'NATIVE' : 'PENNY');
1041
- const key = `${checkedValue('publish-cover')}:${cover?.path}`;
1052
+ syncFeatured();
1053
+ const featured = checkedValue('publish-cover');
1054
+ const cover = featured === 'none' ? undefined : chosen(featured === 'native' ? 'NATIVE' : 'PENNY');
1055
+ $('publish-preview').hidden = !cover;
1056
+ const key = `${featured}:${cover?.path}`;
1042
1057
  if (cover && key !== previewedCover) {
1043
1058
  previewedCover = key;
1044
1059
  $('publish-preview').srcdoc = sandboxed(cover.content);
@@ -1048,7 +1063,9 @@
1048
1063
  for (const [role, id] of [['NATIVE', 'publish-native'], ['PENNY', 'publish-penny']]) {
1049
1064
  const select = $(id);
1050
1065
  if (!select.options.length) {
1066
+ select.add(new Option('No page', ''));
1051
1067
  snapshot[sideOf(role)].forEach((file, index) => select.add(new Option(file.path, String(index))));
1068
+ select.value = snapshot[sideOf(role)].length ? '0' : '';
1052
1069
  }
1053
1070
  }
1054
1071
  if (!$('publish-name').value) $('publish-name').value = remembered('penny-compare-display-name');
@@ -1083,6 +1100,7 @@
1083
1100
  if (published) return;
1084
1101
  const native = chosen('NATIVE');
1085
1102
  const penny = chosen('PENNY');
1103
+ const featured = checkedValue('publish-cover');
1086
1104
  const video = $('publish-video').files[0] || null;
1087
1105
  const videoType = video && (video.type || (/\.webm$/i.test(video.name) ? 'video/webm' : /\.mp4$/i.test(video.name) ? 'video/mp4' : ''));
1088
1106
  $('publish-error').textContent = '';
@@ -1096,7 +1114,8 @@
1096
1114
  }
1097
1115
  const meta = { title: $('publish-title').value.trim(), comment: $('publish-comment').value.trim(),
1098
1116
  tag: checkedValue('publish-tag'), display_name: $('publish-name').value.trim(),
1099
- native_file: native.path, penny_file: penny.path, cover: checkedValue('publish-cover'),
1117
+ native_file: native?.path ?? null, penny_file: penny?.path ?? null,
1118
+ cover: featured === 'none' ? null : featured,
1100
1119
  video_type: video ? videoType : null };
1101
1120
  const submit = $('publish-submit');
1102
1121
  submit.disabled = true;
@@ -1106,13 +1125,13 @@
1106
1125
  try {
1107
1126
  if (hosted) {
1108
1127
  result = await relayToParent({ report: gallery.report, signature: gallery.signature,
1109
- reportNonce: gallery.reportNonce, meta, native: native.content, penny: penny.content, video,
1128
+ reportNonce: gallery.reportNonce, meta, native: native?.content ?? null, penny: penny?.content ?? null, video,
1110
1129
  cover: coverImage });
1111
1130
  } else {
1112
1131
  const form = new FormData();
1113
1132
  form.set('meta', JSON.stringify(meta));
1114
- form.set('native', new Blob([native.content], { type: 'text/html' }), 'native.html');
1115
- form.set('penny', new Blob([penny.content], { type: 'text/html' }), 'penny.html');
1133
+ if (native) form.set('native', new Blob([native.content], { type: 'text/html' }), 'native.html');
1134
+ if (penny) form.set('penny', new Blob([penny.content], { type: 'text/html' }), 'penny.html');
1116
1135
  if (video) form.set('video', video, video.name);
1117
1136
  if (coverImage) form.set('cover', coverImage, 'cover');
1118
1137
  const response = await fetch('/gallery/submit', { method: 'POST',
@@ -1173,8 +1192,8 @@
1173
1192
  window.addEventListener('message', (event) => {
1174
1193
  if (event.origin !== config.parentOrigin || event.source !== window.parent) return;
1175
1194
  if (event.data?.type === 'compare-demo-start' && authenticated) {
1176
- const { harness, nativeKey, pennyKey, environment: chosen } = event.data;
1177
- send({ type: 'start', harness, nativeKey, pennyKey, environment: chosen });
1195
+ const { harness, model, nativeKey, pennyKey, environment: chosen } = event.data;
1196
+ send({ type: 'start', harness, model, nativeKey, pennyKey, environment: chosen });
1178
1197
  setStatus('Starting terminals…');
1179
1198
  return;
1180
1199
  }
@@ -25,7 +25,7 @@ export async function savedPennyKey() {
25
25
  }
26
26
  }
27
27
 
28
- /** Sends the report, the chosen pages, and an optional video; returns the new entry's id. A
28
+ /** Sends the report, the chosen pages (each optional), and an optional video; returns the new entry's id. A
29
29
  * report signed by its demo sandbox (the subscription launcher) carries that signature. */
30
30
  export async function publishToGallery(report, form, { signature = '', reportNonce = '' } = {}) {
31
31
  const key = await savedPennyKey();
@@ -37,8 +37,7 @@ export async function publishToGallery(report, form, { signature = '', reportNon
37
37
  outgoing.set('meta', String(form.get('meta') ?? ''));
38
38
  for (const side of ['native', 'penny']) {
39
39
  const page = form.get(side);
40
- if (!(page instanceof Blob)) throw new Error('Choose a native page and a Penny page.');
41
- outgoing.set(side, page, `${side}.html`);
40
+ if (page instanceof Blob) outgoing.set(side, page, `${side}.html`);
42
41
  }
43
42
  const cover = form.get('cover');
44
43
  if (cover instanceof Blob && cover.size) outgoing.set('cover', cover, 'cover');