viveworker 0.2.0 → 0.3.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/web/app.js CHANGED
@@ -47,6 +47,7 @@ const state = {
47
47
  pairError: "",
48
48
  pairNotice: "",
49
49
  pushStatus: null,
50
+ moltbookScoutStatus: null,
50
51
  pushNotice: "",
51
52
  pushError: "",
52
53
  deviceNotice: "",
@@ -212,6 +213,7 @@ async function refreshAuthenticatedState() {
212
213
  await refreshTimeline();
213
214
  await refreshDevices();
214
215
  await refreshPushStatus();
216
+ await fetchMoltbookScoutStatus();
215
217
  ensureCurrentSelection();
216
218
  }
217
219
 
@@ -317,6 +319,18 @@ async function refreshPushStatus() {
317
319
  }
318
320
  }
319
321
 
322
+ async function fetchMoltbookScoutStatus() {
323
+ if (!state.session?.moltbookEnabled) {
324
+ state.moltbookScoutStatus = null;
325
+ return;
326
+ }
327
+ try {
328
+ state.moltbookScoutStatus = await apiGet("/api/moltbook/scout-status");
329
+ } catch {
330
+ state.moltbookScoutStatus = null;
331
+ }
332
+ }
333
+
320
334
  async function getClientPushState() {
321
335
  const registration = state.serviceWorkerRegistration || (await navigator.serviceWorker?.ready.catch(() => null));
322
336
  if (registration) {
@@ -526,11 +540,16 @@ function listInboxEntries() {
526
540
 
527
541
  function normalizeProviderClient(value) {
528
542
  const normalized = String(value || "").toLowerCase();
529
- return normalized === "claude" ? "claude" : "codex";
543
+ if (normalized === "claude") return "claude";
544
+ if (normalized === "moltbook") return "moltbook";
545
+ return "codex";
530
546
  }
531
547
 
532
548
  function providerDisplayName(provider) {
533
- return L(normalizeProviderClient(provider) === "claude" ? "common.claude" : "common.codex");
549
+ const p = normalizeProviderClient(provider);
550
+ if (p === "claude") return L("common.claude");
551
+ if (p === "moltbook") return "Moltbook";
552
+ return L("common.codex");
534
553
  }
535
554
 
536
555
  function entryMatchesProviderFilter(item) {
@@ -633,6 +652,16 @@ function timelineEntryMatchesKindFilter(entry, filterId) {
633
652
  }
634
653
  }
635
654
 
655
+ function isMoltbookThreadId(threadId, item) {
656
+ if (threadId === "moltbook") return true;
657
+ if (typeof threadId === "string" && threadId.startsWith("draft:")) return true;
658
+ const kind = normalizeClientText(item?.kind || "");
659
+ if (kind === "moltbook_draft" || kind === "moltbook_reply") return true;
660
+ const label = normalizeClientText(item?.threadLabel || "").toLowerCase();
661
+ if (label === "moltbook") return true;
662
+ return false;
663
+ }
664
+
636
665
  function completedThreads() {
637
666
  const items = Array.isArray(state.inbox?.completed) ? state.inbox.completed : [];
638
667
  if (!items.length) {
@@ -641,7 +670,7 @@ function completedThreads() {
641
670
  const byThread = new Map();
642
671
  for (const item of items) {
643
672
  const threadId = normalizeClientText(item.threadId || "");
644
- if (!threadId) {
673
+ if (!threadId || isMoltbookThreadId(threadId, item)) {
645
674
  continue;
646
675
  }
647
676
  const latestAtMs = Number(item.createdAtMs || 0);
@@ -666,7 +695,7 @@ function diffThreads() {
666
695
  const byThread = new Map();
667
696
  for (const item of items) {
668
697
  const threadId = normalizeClientText(item.threadId || "");
669
- if (!threadId) {
698
+ if (!threadId || isMoltbookThreadId(threadId, item)) {
670
699
  continue;
671
700
  }
672
701
  const latestAtMs = Number(item.createdAtMs || 0);
@@ -933,6 +962,27 @@ function shouldDeferRenderForActiveInteraction() {
933
962
  ) {
934
963
  return true;
935
964
  }
965
+ if (
966
+ activeElement instanceof HTMLTextAreaElement &&
967
+ activeElement.matches("[data-moltbook-draft-textarea]")
968
+ ) {
969
+ return true;
970
+ }
971
+ if (state.currentDetail?.kind === "moltbook_draft") {
972
+ return true;
973
+ }
974
+ if (
975
+ activeElement instanceof HTMLTextAreaElement &&
976
+ activeElement.matches("[data-claude-question-note]")
977
+ ) {
978
+ return true;
979
+ }
980
+ if (
981
+ activeElement instanceof HTMLInputElement &&
982
+ activeElement.matches("[data-moltbook-draft-title]")
983
+ ) {
984
+ return true;
985
+ }
936
986
  if (
937
987
  activeElement instanceof HTMLSelectElement &&
938
988
  activeElement.matches("[data-timeline-thread-select], [data-diff-thread-select], [data-completed-thread-select]")
@@ -1650,6 +1700,9 @@ function providerBadgeMeta(provider) {
1650
1700
  if (normalized === "claude") {
1651
1701
  return { id: "claude", label: L("common.claude"), glyph: "C" };
1652
1702
  }
1703
+ if (normalized === "moltbook") {
1704
+ return { id: "moltbook", label: "Moltbook", glyph: "M" };
1705
+ }
1653
1706
  return { id: "codex", label: L("common.codex"), glyph: "X" };
1654
1707
  }
1655
1708
 
@@ -1659,11 +1712,15 @@ function renderProviderBadge(provider) {
1659
1712
  }
1660
1713
 
1661
1714
  function providerFilterOptions() {
1662
- return [
1715
+ const options = [
1663
1716
  { id: "all", label: L("timeline.allThreads") },
1664
1717
  { id: "codex", label: L("common.codex") },
1665
1718
  { id: "claude", label: L("common.claude") },
1666
1719
  ];
1720
+ if (state.session?.moltbookEnabled === true) {
1721
+ options.push({ id: "moltbook", label: "Moltbook" });
1722
+ }
1723
+ return options;
1667
1724
  }
1668
1725
 
1669
1726
  function renderProviderFilter() {
@@ -2679,6 +2736,7 @@ function buildSettingsContext() {
2679
2736
  supportsPushValue,
2680
2737
  serverEnabled,
2681
2738
  }),
2739
+ moltbookScout: state.moltbookScoutStatus,
2682
2740
  };
2683
2741
  }
2684
2742
 
@@ -2825,6 +2883,13 @@ function settingsPageMeta(page) {
2825
2883
  description: L("settings.awayMode.copy"),
2826
2884
  icon: "settings",
2827
2885
  };
2886
+ case "moltbook":
2887
+ return {
2888
+ id: "moltbook",
2889
+ title: L("settings.moltbook.title"),
2890
+ description: L("settings.moltbook.copy"),
2891
+ icon: "item",
2892
+ };
2828
2893
  default:
2829
2894
  return settingsPageMeta("notifications");
2830
2895
  }
@@ -2894,6 +2959,15 @@ function renderSettingsRoot(context, { mobile }) {
2894
2959
  `
2895
2960
  }
2896
2961
  ${renderSettingsGroup(L("settings.group.general"), generalRows)}
2962
+ ${state.session?.moltbookEnabled ? renderSettingsGroup(L("common.sns"), [
2963
+ renderSettingsNavRow({
2964
+ page: "moltbook",
2965
+ icon: "item",
2966
+ title: L("settings.moltbook.title"),
2967
+ subtitle: L("settings.moltbook.subtitle"),
2968
+ value: context.moltbookScout?.enabled ? `${context.moltbookScout.sentToday} / ${context.moltbookScout.maxDaily}` : "",
2969
+ }),
2970
+ ]) : ""}
2897
2971
  ${renderSettingsGroup(L("settings.pairing.title"), deviceRows)}
2898
2972
  ${renderSettingsGroup(L("settings.group.advanced"), advancedRows)}
2899
2973
  </div>
@@ -2937,6 +3011,9 @@ function renderSettingsSubpage(context, { mobile }) {
2937
3011
  case "awayMode":
2938
3012
  content = renderSettingsAwayModePage();
2939
3013
  break;
3014
+ case "moltbook":
3015
+ content = renderSettingsMoltbookPage(context);
3016
+ break;
2940
3017
  default:
2941
3018
  content = "";
2942
3019
  }
@@ -2955,7 +3032,7 @@ function renderSettingsNotificationsPage(context) {
2955
3032
  const statusRows = [
2956
3033
  renderSettingsInfoRow(L("settings.row.status"), L(context.setupState.notifications.labelKey)),
2957
3034
  renderSettingsInfoRow(L("settings.row.notificationPermission"), permission),
2958
- renderSettingsInfoRow(L("settings.row.currentDeviceSubscribed"), push.serverSubscribed ? L("common.yes") : L("common.no")),
3035
+ renderSettingsInfoRow(L("settings.row.currentDeviceSubscribed"), push.serverSubscribed ? L("common.enabled") : L("common.disabled")),
2959
3036
  push.lastSuccessfulDeliveryAtMs
2960
3037
  ? renderSettingsInfoRow(
2961
3038
  L("settings.row.lastSuccessfulDelivery"),
@@ -2967,10 +3044,10 @@ function renderSettingsNotificationsPage(context) {
2967
3044
  <div class="settings-page">
2968
3045
  ${renderSettingsGroup("", statusRows)}
2969
3046
  ${renderSettingsGroup(L("settings.group.advanced"), [
2970
- renderSettingsInfoRow(L("settings.row.serverWebPush"), serverEnabled ? L("common.yes") : L("common.no")),
2971
- renderSettingsInfoRow(L("settings.row.secureContext"), secureContext ? L("common.yes") : L("common.no")),
2972
- renderSettingsInfoRow(L("settings.row.homeScreenApp"), standalone ? L("common.yes") : L("common.no")),
2973
- renderSettingsInfoRow(L("settings.row.browserSupport"), supportsPushValue ? L("common.yes") : L("common.no")),
3047
+ renderSettingsInfoRow(L("settings.row.serverWebPush"), serverEnabled ? L("common.enabled") : L("common.disabled")),
3048
+ renderSettingsInfoRow(L("settings.row.secureContext"), secureContext ? L("common.supported") : L("common.notSupported")),
3049
+ renderSettingsInfoRow(L("settings.row.homeScreenApp"), standalone ? L("common.supported") : L("common.notSupported")),
3050
+ renderSettingsInfoRow(L("settings.row.browserSupport"), supportsPushValue ? L("common.supported") : L("common.notSupported")),
2974
3051
  ])}
2975
3052
  ${state.pushNotice ? `<p class="inline-alert inline-alert--success">${escapeHtml(state.pushNotice)}</p>` : ""}
2976
3053
  ${state.pushError ? `<p class="inline-alert inline-alert--danger">${escapeHtml(state.pushError)}</p>` : ""}
@@ -3062,12 +3139,12 @@ function renderSettingsAdvancedPage(context) {
3062
3139
  <div class="settings-page">
3063
3140
  ${context.diagnostics.map((message) => `<p class="inline-alert">${escapeHtml(message)}</p>`).join("")}
3064
3141
  ${renderSettingsGroup("", [
3065
- renderSettingsInfoRow(L("settings.row.serverWebPush"), context.serverEnabled ? L("common.yes") : L("common.no")),
3066
- renderSettingsInfoRow(L("settings.row.secureContext"), context.secureContext ? L("common.yes") : L("common.no")),
3067
- renderSettingsInfoRow(L("settings.row.homeScreenApp"), context.standalone ? L("common.yes") : L("common.no")),
3142
+ renderSettingsInfoRow(L("settings.row.serverWebPush"), context.serverEnabled ? L("common.enabled") : L("common.disabled")),
3143
+ renderSettingsInfoRow(L("settings.row.secureContext"), context.secureContext ? L("common.supported") : L("common.notSupported")),
3144
+ renderSettingsInfoRow(L("settings.row.homeScreenApp"), context.standalone ? L("common.supported") : L("common.notSupported")),
3068
3145
  renderSettingsInfoRow(L("settings.row.notificationPermission"), context.permission),
3069
- renderSettingsInfoRow(L("settings.row.browserSupport"), context.supportsPushValue ? L("common.yes") : L("common.no")),
3070
- renderSettingsInfoRow(L("settings.row.currentDeviceSubscribed"), context.push.serverSubscribed ? L("common.yes") : L("common.no")),
3146
+ renderSettingsInfoRow(L("settings.row.browserSupport"), context.supportsPushValue ? L("common.supported") : L("common.notSupported")),
3147
+ renderSettingsInfoRow(L("settings.row.currentDeviceSubscribed"), context.push.serverSubscribed ? L("common.enabled") : L("common.disabled")),
3071
3148
  context.push.lastSuccessfulDeliveryAtMs
3072
3149
  ? renderSettingsInfoRow(
3073
3150
  L("settings.row.lastSuccessfulDelivery"),
@@ -3152,6 +3229,37 @@ function renderSettingsAwayModePage() {
3152
3229
  `;
3153
3230
  }
3154
3231
 
3232
+ function renderSettingsMoltbookPage(context) {
3233
+ const scout = context.moltbookScout;
3234
+ if (!scout?.enabled) {
3235
+ return `
3236
+ <div class="settings-page">
3237
+ <p class="settings-page-copy muted">${escapeHtml(L("settings.moltbook.unavailable"))}</p>
3238
+ </div>
3239
+ `;
3240
+ }
3241
+ const batchRows = scout.batch ? [
3242
+ renderSettingsInfoRow(L("settings.row.moltbookBatchCandidates"), String(scout.batch.candidateCount)),
3243
+ renderSettingsInfoRow(L("settings.row.moltbookBatchTopScore"), String(scout.batch.topScore)),
3244
+ renderSettingsInfoRow(L("settings.row.moltbookBatchRemaining"), `${Math.floor(scout.batch.remainingSeconds / 60)}:${String(scout.batch.remainingSeconds % 60).padStart(2, "0")}`),
3245
+ ] : [];
3246
+ return `
3247
+ <div class="settings-page">
3248
+ ${renderSettingsGroup("", [
3249
+ renderSettingsInfoRow(L("settings.row.moltbookQuota"), `${scout.sentToday} / ${scout.maxDaily}`),
3250
+ renderSettingsInfoRow(L("settings.row.moltbookComposed"), `${scout.composedToday || 0} / 3`),
3251
+ renderSettingsInfoRow(L("settings.row.moltbookSlots"), (scout.composeSlotsAttempted || []).join(", ") || "—"),
3252
+ renderSettingsInfoRow(L("settings.row.moltbookDate"), scout.day),
3253
+ renderSettingsInfoRow(L("settings.row.moltbookSeenPosts"), String(scout.seenPostCount)),
3254
+ ])}
3255
+ ${batchRows.length ? renderSettingsGroup(L("settings.moltbook.batchTitle"), batchRows) : ""}
3256
+ ${Array.isArray(scout.recentComposeTitles) && scout.recentComposeTitles.length
3257
+ ? renderSettingsGroup(L("settings.row.moltbookRecentTitles"), scout.recentComposeTitles.map((t) => renderSettingsInfoRow("", t)))
3258
+ : ""}
3259
+ </div>
3260
+ `;
3261
+ }
3262
+
3155
3263
  function renderSettingsInfoRow(label, value, options = {}) {
3156
3264
  const rowClassName = ["settings-info-row", options.rowClassName || ""].filter(Boolean).join(" ");
3157
3265
  const valueClassName = ["settings-info-row__value", options.valueClassName || ""].filter(Boolean).join(" ");
@@ -3193,7 +3301,7 @@ function renderDeviceSection(title, devices, emptyMessage) {
3193
3301
 
3194
3302
  function renderTrustedDeviceCard(device) {
3195
3303
  const localeLabel = localeDisplayName(device.locale, state.locale) || device.locale || L("common.unavailable");
3196
- const pushLabel = device.pushSubscribed ? L("common.yes") : L("common.no");
3304
+ const pushLabel = device.pushSubscribed ? L("common.enabled") : L("common.disabled");
3197
3305
  const badge = device.currentDevice
3198
3306
  ? `<span class="device-card__badge">${escapeHtml(L("settings.device.thisDevice"))}</span>`
3199
3307
  : "";
@@ -3264,13 +3372,17 @@ function renderStandardDetailDesktop(detail) {
3264
3372
  <div class="detail-shell">
3265
3373
  ${renderDetailMetaRow(detail, kindInfo)}
3266
3374
  <h2 class="detail-title detail-title--desktop">${escapeHtml(detailDisplayTitle(detail))}</h2>
3267
- ${detail.readOnly || detail.kind === "approval" ? "" : renderDetailLead(detail, kindInfo)}
3375
+ ${detail.readOnly || detail.kind === "approval" || detail.kind === "moltbook_draft" || detail.kind === "moltbook_reply" ? "" : renderDetailLead(detail, kindInfo)}
3268
3376
  ${renderPreviousContextCard(detail)}
3269
3377
  ${renderInterruptedDetailNotice(detail)}
3378
+ ${renderMoltbookReplyComposer(detail)}
3379
+ ${renderMoltbookDraftComposer(detail)}
3270
3380
  ${
3271
- plainIntro
3272
- ? plainIntro
3273
- : `
3381
+ detail.kind === "moltbook_draft" || detail.kind === "moltbook_reply"
3382
+ ? ""
3383
+ : plainIntro
3384
+ ? plainIntro
3385
+ : `
3274
3386
  <section class="detail-card detail-card--body ${spaciousBodyDetail ? "detail-card--message-body" : ""}">
3275
3387
  <div class="detail-body ${spaciousBodyDetail ? "detail-body--message " : ""}markdown">${detail.messageHtml || ""}</div>
3276
3388
  </section>
@@ -3299,10 +3411,14 @@ function renderStandardDetailMobile(detail) {
3299
3411
  ${renderDetailMetaRow(detail, kindInfo, { mobile: true })}
3300
3412
  ${renderPreviousContextCard(detail, { mobile: true })}
3301
3413
  ${renderInterruptedDetailNotice(detail, { mobile: true })}
3414
+ ${renderMoltbookReplyComposer(detail, { mobile: true })}
3415
+ ${renderMoltbookDraftComposer(detail, { mobile: true })}
3302
3416
  ${
3303
- plainIntro
3304
- ? plainIntro
3305
- : `
3417
+ detail.kind === "moltbook_draft" || detail.kind === "moltbook_reply"
3418
+ ? ""
3419
+ : plainIntro
3420
+ ? plainIntro
3421
+ : `
3306
3422
  <section class="detail-card detail-card--body detail-card--mobile ${spaciousBodyDetail ? "detail-card--message-body" : ""}">
3307
3423
  ${detail.readOnly || detail.kind === "approval" ? "" : renderDetailLead(detail, kindInfo, { mobile: true })}
3308
3424
  <div class="detail-body ${spaciousBodyDetail ? "detail-body--message " : ""}markdown">${detail.messageHtml || ""}</div>
@@ -3327,7 +3443,8 @@ function renderDetailPlainIntro(detail, options = {}) {
3327
3443
  if (!["approval", "diff_thread", "file_event"].includes(detail?.kind || "")) {
3328
3444
  return "";
3329
3445
  }
3330
- if (detail?.kind === "approval" && normalizeClientText(detail?.approvalKind || "") !== "file") {
3446
+ const ak = normalizeClientText(detail?.approvalKind || "");
3447
+ if (detail?.kind === "approval" && ak !== "file" && ak !== "plan" && ak !== "question") {
3331
3448
  return "";
3332
3449
  }
3333
3450
  if (!detail?.messageHtml) {
@@ -3732,7 +3849,7 @@ function renderClaudeQuestionSection(detail, options = {}) {
3732
3849
  ${noticeHtml}
3733
3850
  ${isReadOnly ? "" : `
3734
3851
  <div class="claude-question-actions">
3735
- <button type="submit" class="action-button action-button--primary" ${isSent || draft.sending ? "disabled" : ""}>
3852
+ <button type="submit" class="primary primary--wide" ${isSent || draft.sending ? "disabled" : ""}>
3736
3853
  ${escapeHtml(draft.sending ? L("claudeQuestion.send", { provider }) + "…" : sendLabel)}
3737
3854
  </button>
3738
3855
  </div>
@@ -3755,6 +3872,88 @@ function setClaudeQuestionDraft(token, partial) {
3755
3872
  return next;
3756
3873
  }
3757
3874
 
3875
+ function renderMoltbookReplyComposer(detail, options = {}) {
3876
+ if (detail.kind !== "moltbook_reply") return "";
3877
+ const rawTitle = normalizeClientText(detail.title || "");
3878
+ const match = rawTitle.match(/^@([^\s]+)/u);
3879
+ const authorHandle = match ? match[1] : detail.commentAuthor || "";
3880
+ const postUrl = detail.postUrl || "";
3881
+ const postLink = postUrl
3882
+ ? `<p class="reply-composer__author"><a href="${escapeHtml(postUrl)}" target="_blank" rel="noopener">${escapeHtml(postUrl)}</a></p>`
3883
+ : "";
3884
+ const bodyHtml = detail.messageHtml
3885
+ ? `<div class="reply-composer__context-body markdown">${detail.messageHtml}</div>`
3886
+ : "";
3887
+ return `
3888
+ <section class="detail-card detail-card--reply ${options.mobile ? "detail-card--mobile" : ""}">
3889
+ <div class="reply-composer reply-composer--readonly">
3890
+ <div class="reply-composer__copy">
3891
+ <span class="eyebrow-pill eyebrow-pill--quiet">Moltbook</span>
3892
+ ${authorHandle ? `<p class="reply-composer__author">from <strong>@${escapeHtml(authorHandle)}</strong></p>` : ""}
3893
+ ${postLink}
3894
+ </div>
3895
+ ${bodyHtml}
3896
+ </div>
3897
+ </section>
3898
+ `;
3899
+ }
3900
+
3901
+ function renderMoltbookDraftComposer(detail, options = {}) {
3902
+ if (detail.kind !== "moltbook_draft") return "";
3903
+ const enabled = detail.moltbookDraftEnabled !== false && detail.readOnly !== true;
3904
+ const draftText = detail.draftText || "";
3905
+ const isOriginalPost = detail.draftType === "original_post";
3906
+ const postAuthorLine = !isOriginalPost && detail.postAuthor
3907
+ ? `<p class="reply-composer__author-meta muted">@${escapeHtml(detail.postAuthor)}</p>`
3908
+ : "";
3909
+ const postLink = !isOriginalPost && detail.postUrl
3910
+ ? `<p class="reply-composer__author"><a href="${escapeHtml(detail.postUrl)}" target="_blank" rel="noopener">${escapeHtml(detail.postTitle || detail.postUrl)}</a></p>`
3911
+ : "";
3912
+ const postBodyBlock = !isOriginalPost && detail.postBody
3913
+ ? `<details class="reply-composer__context"><summary>元の投稿</summary><div class="reply-composer__context-body">${escapeHtml(detail.postBody).replace(/\n/g, "<br>")}</div></details>`
3914
+ : "";
3915
+ const intentBlock = detail.intent
3916
+ ? `<div class="reply-composer__intent"><span class="eyebrow-pill eyebrow-pill--quiet">${escapeHtml(L("moltbook.draft.intent"))}</span><p>${escapeHtml(detail.intent).replace(/\n/g, "<br>")}</p></div>`
3917
+ : "";
3918
+ const titleInput = isOriginalPost && enabled
3919
+ ? `<label class="field reply-field reply-field--title"><span class="field-label">${escapeHtml(L("moltbook.draft.titleLabel"))}</span><input type="text" name="title" class="reply-field__input" value="${escapeHtml(detail.postTitle || "")}" data-moltbook-draft-title /></label>`
3920
+ : isOriginalPost
3921
+ ? `<p class="reply-composer__title-display"><strong>${escapeHtml(detail.postTitle || "")}</strong></p>`
3922
+ : "";
3923
+ const submoltBadge = isOriginalPost && detail.submoltName
3924
+ ? `<span class="eyebrow-pill eyebrow-pill--subtle">${escapeHtml(detail.submoltName)}</span>`
3925
+ : "";
3926
+ const eyebrowLabel = isOriginalPost ? L("moltbook.draft.eyebrowPost") : L("moltbook.draft.eyebrowReply");
3927
+ const approveLabel = isOriginalPost ? L("moltbook.draft.approvePost") : L("moltbook.draft.approveReply");
3928
+ const buttons = enabled
3929
+ ? `
3930
+ <div class="actions actions--stack${options.mobile ? " actions--sticky" : ""}">
3931
+ <button type="submit" data-action="approve" class="primary primary--wide">${escapeHtml(approveLabel)}</button>
3932
+ <button type="submit" data-action="deny" class="danger danger--wide">Deny</button>
3933
+ </div>
3934
+ `
3935
+ : `<p class="muted reply-composer__description">${escapeHtml(L("moltbook.draft.resolved"))}</p>`;
3936
+ const buttonsWrapped = enabled && options.mobile ? `<div class="detail-action-bar">${buttons}</div>` : buttons;
3937
+ return `
3938
+ <section class="detail-card detail-card--reply ${options.mobile ? "detail-card--mobile" : ""}">
3939
+ <form class="reply-composer" data-moltbook-draft-form data-token="${escapeHtml(detail.token || "")}" ${isOriginalPost ? 'data-draft-type="original_post"' : ""}>
3940
+ <div class="reply-composer__copy">
3941
+ <span class="eyebrow-pill eyebrow-pill--quiet">${escapeHtml(eyebrowLabel)}</span>
3942
+ ${submoltBadge}
3943
+ ${postLink}
3944
+ ${postAuthorLine}
3945
+ ${titleInput}
3946
+ ${postBodyBlock}
3947
+ ${intentBlock}
3948
+ <p class="muted reply-composer__description">${escapeHtml(L("moltbook.draft.editHint"))}</p>
3949
+ </div>
3950
+ <textarea name="text" class="reply-composer__textarea" data-moltbook-draft-textarea rows="8" ${enabled ? "" : "readonly"}>${escapeHtml(draftText)}</textarea>
3951
+ ${buttonsWrapped}
3952
+ </form>
3953
+ </section>
3954
+ `;
3955
+ }
3956
+
3758
3957
  function renderCompletionReplyComposer(detail, options = {}) {
3759
3958
  if (detail.kind !== "completion" || detail.reply?.enabled !== true) {
3760
3959
  return "";
@@ -4811,6 +5010,77 @@ function bindShellInteractions() {
4811
5010
  });
4812
5011
  }
4813
5012
 
5013
+ const moltbookDraftForm = document.querySelector("[data-moltbook-draft-form]");
5014
+ if (moltbookDraftForm) {
5015
+ const token = moltbookDraftForm.dataset.token || "";
5016
+ let submittedAction = "approve";
5017
+ moltbookDraftForm.querySelectorAll("button[type='submit']").forEach((btn) => {
5018
+ btn.addEventListener("click", () => {
5019
+ submittedAction = btn.dataset.action || "approve";
5020
+ });
5021
+ });
5022
+ moltbookDraftForm.addEventListener("submit", async (event) => {
5023
+ event.preventDefault();
5024
+ if (moltbookDraftForm.dataset.submitting === "1") return;
5025
+ moltbookDraftForm.dataset.submitting = "1";
5026
+ const buttons = moltbookDraftForm.querySelectorAll("button[type='submit']");
5027
+ const textarea = moltbookDraftForm.querySelector("textarea");
5028
+ const labelCache = new Map();
5029
+ buttons.forEach((btn) => {
5030
+ labelCache.set(btn, btn.innerHTML);
5031
+ btn.disabled = true;
5032
+ if (btn.dataset.action === submittedAction) {
5033
+ btn.innerHTML = submittedAction === "approve" ? "送信中…" : "処理中…";
5034
+ btn.classList.add("is-loading");
5035
+ } else {
5036
+ btn.classList.add("is-dimmed");
5037
+ }
5038
+ });
5039
+ if (textarea) textarea.readOnly = true;
5040
+ const editedText = normalizeClientText(new FormData(moltbookDraftForm).get("text"));
5041
+ const titleInput = moltbookDraftForm.querySelector("[data-moltbook-draft-title]");
5042
+ const editedTitle = titleInput ? normalizeClientText(titleInput.value) : "";
5043
+ try {
5044
+ const decisionBody = { action: submittedAction, editedText };
5045
+ if (editedTitle) decisionBody.editedTitle = editedTitle;
5046
+ const res = await fetch(`/api/items/moltbook-draft/${encodeURIComponent(token)}/decision`, {
5047
+ method: "POST",
5048
+ headers: { "content-type": "application/json" },
5049
+ credentials: "same-origin",
5050
+ body: JSON.stringify(decisionBody),
5051
+ });
5052
+ if (!res.ok) {
5053
+ const errBody = await res.json().catch(() => ({}));
5054
+ alert(`Moltbook draft ${submittedAction} failed: ${errBody.error || res.status}`);
5055
+ buttons.forEach((btn) => {
5056
+ btn.disabled = false;
5057
+ btn.classList.remove("is-loading", "is-dimmed");
5058
+ btn.innerHTML = labelCache.get(btn);
5059
+ });
5060
+ if (textarea) textarea.readOnly = false;
5061
+ moltbookDraftForm.dataset.submitting = "";
5062
+ return;
5063
+ }
5064
+ // Mark local detail as resolved so re-render shows "already resolved" immediately.
5065
+ if (state.currentDetail?.kind === "moltbook_draft") {
5066
+ state.currentDetail.moltbookDraftEnabled = false;
5067
+ state.currentDetail.readOnly = true;
5068
+ }
5069
+ await refreshAuthenticatedState();
5070
+ await renderShell();
5071
+ } catch (error) {
5072
+ alert(`Moltbook draft error: ${error.message}`);
5073
+ buttons.forEach((btn) => {
5074
+ btn.disabled = false;
5075
+ btn.classList.remove("is-loading", "is-dimmed");
5076
+ btn.innerHTML = labelCache.get(btn);
5077
+ });
5078
+ if (textarea) textarea.readOnly = false;
5079
+ moltbookDraftForm.dataset.submitting = "";
5080
+ }
5081
+ });
5082
+ }
5083
+
4814
5084
  const replyForm = document.querySelector("[data-completion-reply-form]");
4815
5085
  if (replyForm) {
4816
5086
  const token = replyForm.dataset.token || "";
@@ -5289,6 +5559,9 @@ function kindMeta(kind) {
5289
5559
  return { label: L("common.diff"), tone: "neutral", icon: "diff" };
5290
5560
  case "file_event":
5291
5561
  return { label: L("common.fileEvent"), tone: "neutral", icon: "file-event" };
5562
+ case "moltbook_reply":
5563
+ case "moltbook_draft":
5564
+ return { label: L("common.sns"), tone: "neutral", icon: "item" };
5292
5565
  default:
5293
5566
  return { label: L("common.item"), tone: "neutral", icon: "item" };
5294
5567
  }
package/web/i18n.js CHANGED
@@ -29,6 +29,10 @@ const translations = {
29
29
  "common.unavailable": "unavailable",
30
30
  "common.yes": "Yes",
31
31
  "common.no": "No",
32
+ "common.enabled": "Enabled",
33
+ "common.disabled": "Disabled",
34
+ "common.supported": "Supported",
35
+ "common.notSupported": "Not supported",
32
36
  "common.none": "none",
33
37
  "common.settings": "Settings",
34
38
  "common.pending": "Pending",
@@ -44,6 +48,7 @@ const translations = {
44
48
  "common.assistantCommentary": "Update",
45
49
  "common.assistantFinal": "Final reply",
46
50
  "common.item": "Item",
51
+ "common.sns": "SNS",
47
52
  "common.untitledItem": "Untitled item",
48
53
  "common.useDeviceLanguage": "Use device language",
49
54
  "language.option.en": "English",
@@ -339,6 +344,28 @@ const translations = {
339
344
  "settings.row.currentLanguage": "Current language",
340
345
  "settings.row.languageSource": "Language source",
341
346
  "settings.row.defaultLanguage": "Setup default",
347
+ "settings.moltbook.title": "Moltbook",
348
+ "settings.moltbook.subtitle": "Auto-scout posting quota",
349
+ "settings.moltbook.copy": "View your daily auto-scout posting status and quota.",
350
+ "settings.moltbook.unavailable": "Moltbook is not configured.",
351
+ "settings.row.moltbookQuota": "Posted today",
352
+ "settings.row.moltbookDate": "Date",
353
+ "settings.row.moltbookSeenPosts": "Seen posts",
354
+ "settings.moltbook.batchTitle": "Current batch",
355
+ "settings.row.moltbookBatchCandidates": "Candidates",
356
+ "settings.row.moltbookBatchTopScore": "Top score",
357
+ "settings.row.moltbookBatchRemaining": "Time remaining",
358
+ "settings.row.moltbookComposed": "Original posts today",
359
+ "settings.row.moltbookSlots": "Slots attempted",
360
+ "settings.row.moltbookRecentTitles": "Recent post titles",
361
+ "moltbook.draft.eyebrowPost": "Moltbook new post",
362
+ "moltbook.draft.eyebrowReply": "Moltbook draft",
363
+ "moltbook.draft.intent": "Intent",
364
+ "moltbook.draft.titleLabel": "Title",
365
+ "moltbook.draft.approvePost": "Approve & publish",
366
+ "moltbook.draft.approveReply": "Approve & post",
367
+ "moltbook.draft.editHint": "Edit if needed, then approve to publish or deny to skip.",
368
+ "moltbook.draft.resolved": "This draft has already been resolved.",
342
369
  "notice.notificationsEnabled": "Notifications are enabled on this device.",
343
370
  "notice.notificationsDisabled": "Notifications are disabled on this device.",
344
371
  "notice.testNotificationSent": "Test notification sent.",
@@ -575,6 +602,10 @@ const translations = {
575
602
  "common.unavailable": "利用不可",
576
603
  "common.yes": "はい",
577
604
  "common.no": "いいえ",
605
+ "common.enabled": "有効",
606
+ "common.disabled": "無効",
607
+ "common.supported": "対応",
608
+ "common.notSupported": "非対応",
578
609
  "common.none": "なし",
579
610
  "common.settings": "設定",
580
611
  "common.pending": "未対応",
@@ -590,6 +621,7 @@ const translations = {
590
621
  "common.assistantCommentary": "途中経過",
591
622
  "common.assistantFinal": "最終回答",
592
623
  "common.item": "項目",
624
+ "common.sns": "SNS",
593
625
  "common.untitledItem": "無題の項目",
594
626
  "common.useDeviceLanguage": "端末の言語を使う",
595
627
  "language.option.en": "English",
@@ -881,6 +913,28 @@ const translations = {
881
913
  "settings.row.currentLanguage": "現在の言語",
882
914
  "settings.row.languageSource": "言語ソース",
883
915
  "settings.row.defaultLanguage": "setup 時の既定値",
916
+ "settings.moltbook.title": "Moltbook",
917
+ "settings.moltbook.subtitle": "自動スカウトの投稿枠",
918
+ "settings.moltbook.copy": "本日の自動スカウト投稿状況と上限を確認できます。",
919
+ "settings.moltbook.unavailable": "Moltbook が設定されていません。",
920
+ "settings.row.moltbookQuota": "本日の投稿数",
921
+ "settings.row.moltbookDate": "日付",
922
+ "settings.row.moltbookSeenPosts": "確認済み投稿数",
923
+ "settings.moltbook.batchTitle": "現在のバッチ",
924
+ "settings.row.moltbookBatchCandidates": "候補数",
925
+ "settings.row.moltbookBatchTopScore": "最高スコア",
926
+ "settings.row.moltbookBatchRemaining": "残り時間",
927
+ "settings.row.moltbookComposed": "本日の新規投稿数",
928
+ "settings.row.moltbookSlots": "提案済みスロット",
929
+ "settings.row.moltbookRecentTitles": "最近の投稿タイトル",
930
+ "moltbook.draft.eyebrowPost": "Moltbook 新規投稿",
931
+ "moltbook.draft.eyebrowReply": "Moltbook ドラフト",
932
+ "moltbook.draft.intent": "意図",
933
+ "moltbook.draft.titleLabel": "タイトル",
934
+ "moltbook.draft.approvePost": "承認して投稿",
935
+ "moltbook.draft.approveReply": "承認して返信",
936
+ "moltbook.draft.editHint": "必要に応じて編集し、承認して投稿するか、拒否してスキップします。",
937
+ "moltbook.draft.resolved": "このドラフトは既に処理済みです。",
884
938
  "notice.notificationsEnabled": "この端末で通知を有効にしました。",
885
939
  "notice.notificationsDisabled": "この端末の通知を無効にしました。",
886
940
  "notice.testNotificationSent": "テスト通知を送信しました。",
@@ -1012,7 +1066,7 @@ const translations = {
1012
1066
  "cli.setup.webPushEnabled": "Web Push: enabled (HTTPS)",
1013
1067
  "cli.setup.webPushDisabled": "Web Push: disabled",
1014
1068
  "cli.setup.claudeHooksSkipped": "Claude hooks はスキップしました(~/.claude ディレクトリが見つかりません)。Claude Desktop をインストール後に `viveworker setup` を再実行すると Claude 連携が有効になります。",
1015
- "cli.setup.completePending": "viveworker の setup は完了しましたが、まだ health 応答は確認できていません。
1069
+ "cli.setup.completePending": "viveworker の setup は完了しましたが、まだ health 応答は確認できていません。",
1016
1070
  "cli.setup.caFlow.title": "端末で viveworker を開く前に、必要ならローカルの証明書をインストールしてください:",
1017
1071
  "cli.setup.caFlow.step1": "1. 下の CA download URL を開くか QR を読み取り、端末で rootCA.pem を取得します。",
1018
1072
  "cli.setup.caFlow.step2": "2. rootCA.pem をインストールし、必要なら端末側でその証明書を信頼します。",
package/web/sw.js CHANGED
@@ -1,4 +1,4 @@
1
- const CACHE_NAME = "viveworker-v7";
1
+ const CACHE_NAME = "viveworker-v20";
2
2
  const NOTIFICATION_INTENT_CACHE = "viveworker-notification-intent-v1";
3
3
  const NOTIFICATION_INTENT_PATH = "/__viveworker_notification_intent__";
4
4
  const APP_ASSETS = ["/app.css", "/app.js", "/i18n.js"];