viveworker 0.2.1 → 0.4.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,18 @@ 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
+ if (normalized === "a2a") return "a2a";
546
+ return "codex";
530
547
  }
531
548
 
532
549
  function providerDisplayName(provider) {
533
- return L(normalizeProviderClient(provider) === "claude" ? "common.claude" : "common.codex");
550
+ const p = normalizeProviderClient(provider);
551
+ if (p === "claude") return L("common.claude");
552
+ if (p === "moltbook") return "Moltbook";
553
+ if (p === "a2a") return "A2A";
554
+ return L("common.codex");
534
555
  }
535
556
 
536
557
  function entryMatchesProviderFilter(item) {
@@ -633,6 +654,16 @@ function timelineEntryMatchesKindFilter(entry, filterId) {
633
654
  }
634
655
  }
635
656
 
657
+ function isMoltbookThreadId(threadId, item) {
658
+ if (threadId === "moltbook") return true;
659
+ if (typeof threadId === "string" && threadId.startsWith("draft:")) return true;
660
+ const kind = normalizeClientText(item?.kind || "");
661
+ if (kind === "moltbook_draft" || kind === "moltbook_reply") return true;
662
+ const label = normalizeClientText(item?.threadLabel || "").toLowerCase();
663
+ if (label === "moltbook") return true;
664
+ return false;
665
+ }
666
+
636
667
  function completedThreads() {
637
668
  const items = Array.isArray(state.inbox?.completed) ? state.inbox.completed : [];
638
669
  if (!items.length) {
@@ -641,7 +672,7 @@ function completedThreads() {
641
672
  const byThread = new Map();
642
673
  for (const item of items) {
643
674
  const threadId = normalizeClientText(item.threadId || "");
644
- if (!threadId) {
675
+ if (!threadId || isMoltbookThreadId(threadId, item)) {
645
676
  continue;
646
677
  }
647
678
  const latestAtMs = Number(item.createdAtMs || 0);
@@ -666,7 +697,7 @@ function diffThreads() {
666
697
  const byThread = new Map();
667
698
  for (const item of items) {
668
699
  const threadId = normalizeClientText(item.threadId || "");
669
- if (!threadId) {
700
+ if (!threadId || isMoltbookThreadId(threadId, item)) {
670
701
  continue;
671
702
  }
672
703
  const latestAtMs = Number(item.createdAtMs || 0);
@@ -933,6 +964,27 @@ function shouldDeferRenderForActiveInteraction() {
933
964
  ) {
934
965
  return true;
935
966
  }
967
+ if (
968
+ activeElement instanceof HTMLTextAreaElement &&
969
+ activeElement.matches("[data-moltbook-draft-textarea]")
970
+ ) {
971
+ return true;
972
+ }
973
+ if (state.currentDetail?.kind === "moltbook_draft") {
974
+ return true;
975
+ }
976
+ if (
977
+ activeElement instanceof HTMLTextAreaElement &&
978
+ activeElement.matches("[data-claude-question-note]")
979
+ ) {
980
+ return true;
981
+ }
982
+ if (
983
+ activeElement instanceof HTMLInputElement &&
984
+ activeElement.matches("[data-moltbook-draft-title]")
985
+ ) {
986
+ return true;
987
+ }
936
988
  if (
937
989
  activeElement instanceof HTMLSelectElement &&
938
990
  activeElement.matches("[data-timeline-thread-select], [data-diff-thread-select], [data-completed-thread-select]")
@@ -1650,6 +1702,12 @@ function providerBadgeMeta(provider) {
1650
1702
  if (normalized === "claude") {
1651
1703
  return { id: "claude", label: L("common.claude"), glyph: "C" };
1652
1704
  }
1705
+ if (normalized === "moltbook") {
1706
+ return { id: "moltbook", label: "Moltbook", glyph: "M" };
1707
+ }
1708
+ if (normalized === "a2a") {
1709
+ return { id: "a2a", label: "A2A", glyph: "A" };
1710
+ }
1653
1711
  return { id: "codex", label: L("common.codex"), glyph: "X" };
1654
1712
  }
1655
1713
 
@@ -1659,11 +1717,18 @@ function renderProviderBadge(provider) {
1659
1717
  }
1660
1718
 
1661
1719
  function providerFilterOptions() {
1662
- return [
1720
+ const options = [
1663
1721
  { id: "all", label: L("timeline.allThreads") },
1664
1722
  { id: "codex", label: L("common.codex") },
1665
1723
  { id: "claude", label: L("common.claude") },
1666
1724
  ];
1725
+ if (state.session?.moltbookEnabled === true) {
1726
+ options.push({ id: "moltbook", label: "Moltbook" });
1727
+ }
1728
+ if (state.session?.a2aEnabled === true) {
1729
+ options.push({ id: "a2a", label: "A2A" });
1730
+ }
1731
+ return options;
1667
1732
  }
1668
1733
 
1669
1734
  function renderProviderFilter() {
@@ -2679,6 +2744,7 @@ function buildSettingsContext() {
2679
2744
  supportsPushValue,
2680
2745
  serverEnabled,
2681
2746
  }),
2747
+ moltbookScout: state.moltbookScoutStatus,
2682
2748
  };
2683
2749
  }
2684
2750
 
@@ -2825,6 +2891,13 @@ function settingsPageMeta(page) {
2825
2891
  description: L("settings.awayMode.copy"),
2826
2892
  icon: "settings",
2827
2893
  };
2894
+ case "moltbook":
2895
+ return {
2896
+ id: "moltbook",
2897
+ title: L("settings.moltbook.title"),
2898
+ description: L("settings.moltbook.copy"),
2899
+ icon: "item",
2900
+ };
2828
2901
  default:
2829
2902
  return settingsPageMeta("notifications");
2830
2903
  }
@@ -2894,6 +2967,15 @@ function renderSettingsRoot(context, { mobile }) {
2894
2967
  `
2895
2968
  }
2896
2969
  ${renderSettingsGroup(L("settings.group.general"), generalRows)}
2970
+ ${state.session?.moltbookEnabled ? renderSettingsGroup(L("common.sns"), [
2971
+ renderSettingsNavRow({
2972
+ page: "moltbook",
2973
+ icon: "item",
2974
+ title: L("settings.moltbook.title"),
2975
+ subtitle: L("settings.moltbook.subtitle"),
2976
+ value: context.moltbookScout?.enabled ? `${context.moltbookScout.sentToday} / ${context.moltbookScout.maxDaily}` : "",
2977
+ }),
2978
+ ]) : ""}
2897
2979
  ${renderSettingsGroup(L("settings.pairing.title"), deviceRows)}
2898
2980
  ${renderSettingsGroup(L("settings.group.advanced"), advancedRows)}
2899
2981
  </div>
@@ -2937,6 +3019,9 @@ function renderSettingsSubpage(context, { mobile }) {
2937
3019
  case "awayMode":
2938
3020
  content = renderSettingsAwayModePage();
2939
3021
  break;
3022
+ case "moltbook":
3023
+ content = renderSettingsMoltbookPage(context);
3024
+ break;
2940
3025
  default:
2941
3026
  content = "";
2942
3027
  }
@@ -2954,8 +3039,8 @@ function renderSettingsNotificationsPage(context) {
2954
3039
  const { push, permission, secureContext, standalone, supportsPushValue, serverEnabled } = context;
2955
3040
  const statusRows = [
2956
3041
  renderSettingsInfoRow(L("settings.row.status"), L(context.setupState.notifications.labelKey)),
2957
- renderSettingsInfoRow(L("settings.row.notificationPermission"), permission),
2958
- renderSettingsInfoRow(L("settings.row.currentDeviceSubscribed"), push.serverSubscribed ? L("common.yes") : L("common.no")),
3042
+ renderSettingsInfoRow(L("settings.row.notificationPermission"), L(`settings.permission.${permission}`) || permission),
3043
+ renderSettingsInfoRow(L("settings.row.currentDeviceSubscribed"), push.serverSubscribed ? L("common.enabled") : L("common.disabled")),
2959
3044
  push.lastSuccessfulDeliveryAtMs
2960
3045
  ? renderSettingsInfoRow(
2961
3046
  L("settings.row.lastSuccessfulDelivery"),
@@ -2967,10 +3052,10 @@ function renderSettingsNotificationsPage(context) {
2967
3052
  <div class="settings-page">
2968
3053
  ${renderSettingsGroup("", statusRows)}
2969
3054
  ${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")),
3055
+ renderSettingsInfoRow(L("settings.row.serverWebPush"), serverEnabled ? L("common.enabled") : L("common.disabled")),
3056
+ renderSettingsInfoRow(L("settings.row.secureContext"), secureContext ? L("common.supported") : L("common.notSupported")),
3057
+ renderSettingsInfoRow(L("settings.row.homeScreenApp"), standalone ? L("common.supported") : L("common.notSupported")),
3058
+ renderSettingsInfoRow(L("settings.row.browserSupport"), supportsPushValue ? L("common.supported") : L("common.notSupported")),
2974
3059
  ])}
2975
3060
  ${state.pushNotice ? `<p class="inline-alert inline-alert--success">${escapeHtml(state.pushNotice)}</p>` : ""}
2976
3061
  ${state.pushError ? `<p class="inline-alert inline-alert--danger">${escapeHtml(state.pushError)}</p>` : ""}
@@ -3062,12 +3147,12 @@ function renderSettingsAdvancedPage(context) {
3062
3147
  <div class="settings-page">
3063
3148
  ${context.diagnostics.map((message) => `<p class="inline-alert">${escapeHtml(message)}</p>`).join("")}
3064
3149
  ${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")),
3068
- 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")),
3150
+ renderSettingsInfoRow(L("settings.row.serverWebPush"), context.serverEnabled ? L("common.enabled") : L("common.disabled")),
3151
+ renderSettingsInfoRow(L("settings.row.secureContext"), context.secureContext ? L("common.supported") : L("common.notSupported")),
3152
+ renderSettingsInfoRow(L("settings.row.homeScreenApp"), context.standalone ? L("common.supported") : L("common.notSupported")),
3153
+ renderSettingsInfoRow(L("settings.row.notificationPermission"), L(`settings.permission.${context.permission}`) || context.permission),
3154
+ renderSettingsInfoRow(L("settings.row.browserSupport"), context.supportsPushValue ? L("common.supported") : L("common.notSupported")),
3155
+ renderSettingsInfoRow(L("settings.row.currentDeviceSubscribed"), context.push.serverSubscribed ? L("common.enabled") : L("common.disabled")),
3071
3156
  context.push.lastSuccessfulDeliveryAtMs
3072
3157
  ? renderSettingsInfoRow(
3073
3158
  L("settings.row.lastSuccessfulDelivery"),
@@ -3152,13 +3237,50 @@ function renderSettingsAwayModePage() {
3152
3237
  `;
3153
3238
  }
3154
3239
 
3240
+ function renderSettingsMoltbookPage(context) {
3241
+ const scout = context.moltbookScout;
3242
+ if (!scout?.enabled) {
3243
+ return `
3244
+ <div class="settings-page">
3245
+ <p class="settings-page-copy muted">${escapeHtml(L("settings.moltbook.unavailable"))}</p>
3246
+ </div>
3247
+ `;
3248
+ }
3249
+ const batchRows = scout.batch ? [
3250
+ renderSettingsInfoRow(L("settings.row.moltbookBatchCandidates"), String(scout.batch.candidateCount)),
3251
+ renderSettingsInfoRow(L("settings.row.moltbookBatchTopScore"), String(scout.batch.topScore)),
3252
+ renderSettingsInfoRow(L("settings.row.moltbookBatchRemaining"), `${Math.floor(scout.batch.remainingSeconds / 60)}:${String(scout.batch.remainingSeconds % 60).padStart(2, "0")}`),
3253
+ ] : [];
3254
+ return `
3255
+ <div class="settings-page">
3256
+ ${renderSettingsGroup("", [
3257
+ renderSettingsInfoRow(L("settings.row.moltbookQuota"), `${scout.sentToday} / ${scout.maxDaily}`),
3258
+ renderSettingsInfoRow(L("settings.row.moltbookComposed"), `${scout.composedToday || 0} / 3`),
3259
+ renderSettingsInfoRow(L("settings.row.moltbookSeenPosts"), String(scout.seenPostCount)),
3260
+ ])}
3261
+ ${batchRows.length ? renderSettingsGroup(L("settings.moltbook.batchTitle"), batchRows) : ""}
3262
+ ${Array.isArray(scout.recentComposeTitles) && scout.recentComposeTitles.length
3263
+ ? renderSettingsGroup(L("settings.row.moltbookRecentTitles"), scout.recentComposeTitles.map((t) => {
3264
+ const title = typeof t === "string" ? t : (t.title || "");
3265
+ const postId = typeof t === "object" ? t.postId : "";
3266
+ const display = postId
3267
+ ? `<a href="https://www.moltbook.com/post/${escapeHtml(postId)}" target="_blank" rel="noopener">${escapeHtml(title)}</a>`
3268
+ : escapeHtml(title);
3269
+ return renderSettingsInfoRow("", display, { rawValue: true, rowClassName: "settings-info-row--stacked" });
3270
+ }))
3271
+ : ""}
3272
+ </div>
3273
+ `;
3274
+ }
3275
+
3155
3276
  function renderSettingsInfoRow(label, value, options = {}) {
3156
3277
  const rowClassName = ["settings-info-row", options.rowClassName || ""].filter(Boolean).join(" ");
3157
3278
  const valueClassName = ["settings-info-row__value", options.valueClassName || ""].filter(Boolean).join(" ");
3279
+ const displayValue = options.rawValue ? value : escapeHtml(value);
3158
3280
  return `
3159
3281
  <div class="${rowClassName}">
3160
3282
  <span class="settings-info-row__label">${escapeHtml(label)}</span>
3161
- <span class="${valueClassName}">${escapeHtml(value)}</span>
3283
+ <span class="${valueClassName}">${displayValue}</span>
3162
3284
  </div>
3163
3285
  `;
3164
3286
  }
@@ -3193,7 +3315,7 @@ function renderDeviceSection(title, devices, emptyMessage) {
3193
3315
 
3194
3316
  function renderTrustedDeviceCard(device) {
3195
3317
  const localeLabel = localeDisplayName(device.locale, state.locale) || device.locale || L("common.unavailable");
3196
- const pushLabel = device.pushSubscribed ? L("common.yes") : L("common.no");
3318
+ const pushLabel = device.pushSubscribed ? L("common.enabled") : L("common.disabled");
3197
3319
  const badge = device.currentDevice
3198
3320
  ? `<span class="device-card__badge">${escapeHtml(L("settings.device.thisDevice"))}</span>`
3199
3321
  : "";
@@ -3264,13 +3386,18 @@ function renderStandardDetailDesktop(detail) {
3264
3386
  <div class="detail-shell">
3265
3387
  ${renderDetailMetaRow(detail, kindInfo)}
3266
3388
  <h2 class="detail-title detail-title--desktop">${escapeHtml(detailDisplayTitle(detail))}</h2>
3267
- ${detail.readOnly || detail.kind === "approval" ? "" : renderDetailLead(detail, kindInfo)}
3389
+ ${detail.readOnly || detail.kind === "approval" || detail.kind === "moltbook_draft" || detail.kind === "moltbook_reply" ? "" : renderDetailLead(detail, kindInfo)}
3268
3390
  ${renderPreviousContextCard(detail)}
3269
3391
  ${renderInterruptedDetailNotice(detail)}
3392
+ ${renderMoltbookReplyComposer(detail)}
3393
+ ${renderMoltbookDraftComposer(detail)}
3394
+ ${renderA2ATaskDetail(detail)}
3270
3395
  ${
3271
- plainIntro
3272
- ? plainIntro
3273
- : `
3396
+ detail.kind === "moltbook_draft" || detail.kind === "moltbook_reply" || detail.kind === "a2a_task"
3397
+ ? ""
3398
+ : plainIntro
3399
+ ? plainIntro
3400
+ : `
3274
3401
  <section class="detail-card detail-card--body ${spaciousBodyDetail ? "detail-card--message-body" : ""}">
3275
3402
  <div class="detail-body ${spaciousBodyDetail ? "detail-body--message " : ""}markdown">${detail.messageHtml || ""}</div>
3276
3403
  </section>
@@ -3299,10 +3426,15 @@ function renderStandardDetailMobile(detail) {
3299
3426
  ${renderDetailMetaRow(detail, kindInfo, { mobile: true })}
3300
3427
  ${renderPreviousContextCard(detail, { mobile: true })}
3301
3428
  ${renderInterruptedDetailNotice(detail, { mobile: true })}
3429
+ ${renderMoltbookReplyComposer(detail, { mobile: true })}
3430
+ ${renderMoltbookDraftComposer(detail, { mobile: true })}
3431
+ ${renderA2ATaskDetail(detail, { mobile: true })}
3302
3432
  ${
3303
- plainIntro
3304
- ? plainIntro
3305
- : `
3433
+ detail.kind === "moltbook_draft" || detail.kind === "moltbook_reply" || detail.kind === "a2a_task"
3434
+ ? ""
3435
+ : plainIntro
3436
+ ? plainIntro
3437
+ : `
3306
3438
  <section class="detail-card detail-card--body detail-card--mobile ${spaciousBodyDetail ? "detail-card--message-body" : ""}">
3307
3439
  ${detail.readOnly || detail.kind === "approval" ? "" : renderDetailLead(detail, kindInfo, { mobile: true })}
3308
3440
  <div class="detail-body ${spaciousBodyDetail ? "detail-body--message " : ""}markdown">${detail.messageHtml || ""}</div>
@@ -3327,7 +3459,8 @@ function renderDetailPlainIntro(detail, options = {}) {
3327
3459
  if (!["approval", "diff_thread", "file_event"].includes(detail?.kind || "")) {
3328
3460
  return "";
3329
3461
  }
3330
- if (detail?.kind === "approval" && normalizeClientText(detail?.approvalKind || "") !== "file") {
3462
+ const ak = normalizeClientText(detail?.approvalKind || "");
3463
+ if (detail?.kind === "approval" && ak !== "file" && ak !== "plan" && ak !== "question") {
3331
3464
  return "";
3332
3465
  }
3333
3466
  if (!detail?.messageHtml) {
@@ -3732,7 +3865,7 @@ function renderClaudeQuestionSection(detail, options = {}) {
3732
3865
  ${noticeHtml}
3733
3866
  ${isReadOnly ? "" : `
3734
3867
  <div class="claude-question-actions">
3735
- <button type="submit" class="action-button action-button--primary" ${isSent || draft.sending ? "disabled" : ""}>
3868
+ <button type="submit" class="primary primary--wide" ${isSent || draft.sending ? "disabled" : ""}>
3736
3869
  ${escapeHtml(draft.sending ? L("claudeQuestion.send", { provider }) + "…" : sendLabel)}
3737
3870
  </button>
3738
3871
  </div>
@@ -3755,6 +3888,150 @@ function setClaudeQuestionDraft(token, partial) {
3755
3888
  return next;
3756
3889
  }
3757
3890
 
3891
+ function renderMoltbookReplyComposer(detail, options = {}) {
3892
+ if (detail.kind !== "moltbook_reply") return "";
3893
+ const rawTitle = normalizeClientText(detail.title || "");
3894
+ const match = rawTitle.match(/^@([^\s]+)/u);
3895
+ const authorHandle = match ? match[1] : detail.commentAuthor || "";
3896
+ const postUrl = detail.postUrl || "";
3897
+ const postLink = postUrl
3898
+ ? `<p class="reply-composer__author"><a href="${escapeHtml(postUrl)}" target="_blank" rel="noopener">${escapeHtml(postUrl)}</a></p>`
3899
+ : "";
3900
+ const bodyHtml = detail.messageHtml
3901
+ ? `<div class="reply-composer__context-body markdown">${detail.messageHtml}</div>`
3902
+ : "";
3903
+ return `
3904
+ <section class="detail-card detail-card--reply ${options.mobile ? "detail-card--mobile" : ""}">
3905
+ <div class="reply-composer reply-composer--readonly">
3906
+ <div class="reply-composer__copy">
3907
+ <span class="eyebrow-pill eyebrow-pill--quiet">Moltbook</span>
3908
+ ${authorHandle ? `<p class="reply-composer__author">from <strong>@${escapeHtml(authorHandle)}</strong></p>` : ""}
3909
+ ${postLink}
3910
+ </div>
3911
+ ${bodyHtml}
3912
+ </div>
3913
+ </section>
3914
+ `;
3915
+ }
3916
+
3917
+ function renderMoltbookDraftComposer(detail, options = {}) {
3918
+ if (detail.kind !== "moltbook_draft") return "";
3919
+ const enabled = detail.moltbookDraftEnabled !== false && detail.readOnly !== true;
3920
+ const draftText = detail.draftText || "";
3921
+ const isOriginalPost = detail.draftType === "original_post";
3922
+ const postAuthorLine = !isOriginalPost && detail.postAuthor
3923
+ ? `<p class="reply-composer__author-meta muted">@${escapeHtml(detail.postAuthor)}</p>`
3924
+ : "";
3925
+ const postLink = !isOriginalPost && detail.postUrl
3926
+ ? `<p class="reply-composer__author"><a href="${escapeHtml(detail.postUrl)}" target="_blank" rel="noopener">${escapeHtml(detail.postTitle || detail.postUrl)}</a></p>`
3927
+ : "";
3928
+ const postBodyBlock = !isOriginalPost && detail.postBody
3929
+ ? `<details class="reply-composer__context"><summary>元の投稿</summary><div class="reply-composer__context-body">${escapeHtml(detail.postBody).replace(/\n/g, "<br>")}</div></details>`
3930
+ : "";
3931
+ const intentBlock = detail.intent
3932
+ ? `<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>`
3933
+ : "";
3934
+ const titleInput = isOriginalPost && enabled
3935
+ ? `<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>`
3936
+ : isOriginalPost
3937
+ ? `<p class="reply-composer__title-display"><strong>${escapeHtml(detail.postTitle || "")}</strong></p>`
3938
+ : "";
3939
+ const submoltBadge = isOriginalPost && detail.submoltName
3940
+ ? `<span class="eyebrow-pill eyebrow-pill--subtle">${escapeHtml(detail.submoltName)}</span>`
3941
+ : "";
3942
+ const eyebrowLabel = isOriginalPost ? L("moltbook.draft.eyebrowPost") : L("moltbook.draft.eyebrowReply");
3943
+ const approveLabel = isOriginalPost ? L("moltbook.draft.approvePost") : L("moltbook.draft.approveReply");
3944
+ const greetingBlock = (() => {
3945
+ if (!isOriginalPost) return "";
3946
+ const slot = detail.slot || "";
3947
+ const icons = { morning: "\u2600\ufe0f", noon: "\u26c5", evening: "\ud83c\udf19" };
3948
+ const keys = { morning: "moltbook.draft.greetMorning", noon: "moltbook.draft.greetNoon", evening: "moltbook.draft.greetEvening" };
3949
+ const icon = icons[slot] || "\u270d\ufe0f";
3950
+ const msg = keys[slot] ? L(keys[slot]) : L("moltbook.draft.greetFallback");
3951
+ return `<div class="compose-greeting"><span class="compose-greeting__icon">${icon}</span><span class="compose-greeting__text">${escapeHtml(msg)}</span></div>`;
3952
+ })();
3953
+ const buttons = enabled
3954
+ ? `
3955
+ <div class="actions actions--stack${options.mobile ? " actions--sticky" : ""}">
3956
+ <button type="submit" data-action="approve" class="primary primary--wide">${escapeHtml(approveLabel)}</button>
3957
+ <button type="submit" data-action="deny" class="danger danger--wide">Deny</button>
3958
+ </div>
3959
+ `
3960
+ : `<p class="muted reply-composer__description">${escapeHtml(L("moltbook.draft.resolved"))}</p>`;
3961
+ const buttonsWrapped = enabled && options.mobile ? `<div class="detail-action-bar">${buttons}</div>` : buttons;
3962
+ return `
3963
+ <section class="detail-card detail-card--reply ${options.mobile ? "detail-card--mobile" : ""}">
3964
+ <form class="reply-composer" data-moltbook-draft-form data-token="${escapeHtml(detail.token || "")}" ${isOriginalPost ? 'data-draft-type="original_post"' : ""}>
3965
+ <div class="reply-composer__copy">
3966
+ <span class="eyebrow-pill eyebrow-pill--quiet">${escapeHtml(eyebrowLabel)}</span>
3967
+ ${submoltBadge}
3968
+ ${greetingBlock}
3969
+ ${postLink}
3970
+ ${postAuthorLine}
3971
+ ${titleInput}
3972
+ ${postBodyBlock}
3973
+ ${intentBlock}
3974
+ <p class="muted reply-composer__description">${escapeHtml(L("moltbook.draft.editHint"))}</p>
3975
+ </div>
3976
+ <textarea name="text" class="reply-composer__textarea" data-moltbook-draft-textarea rows="8" ${enabled ? "" : "readonly"}>${escapeHtml(draftText)}</textarea>
3977
+ ${buttonsWrapped}
3978
+ </form>
3979
+ </section>
3980
+ `;
3981
+ }
3982
+
3983
+ function renderA2ATaskDetail(detail, options = {}) {
3984
+ if (detail.kind !== "a2a_task") return "";
3985
+ const enabled = detail.a2aTaskEnabled !== false && detail.readOnly !== true;
3986
+ const instruction = detail.instruction || "";
3987
+ const callerIp = detail.callerInfo?.ip || "";
3988
+ const callerAgent = detail.callerInfo?.userAgent || "";
3989
+ const callerLine = callerIp
3990
+ ? `<p class="reply-composer__author-meta muted">${escapeHtml(L("a2a.task.from"))}: ${escapeHtml(callerIp)}${callerAgent ? ` (${escapeHtml(callerAgent.slice(0, 60))})` : ""}</p>`
3991
+ : "";
3992
+
3993
+ const statusKey = {
3994
+ submitted: "a2a.task.statusSubmitted",
3995
+ working: "a2a.task.statusWorking",
3996
+ completed: "a2a.task.statusCompleted",
3997
+ failed: "a2a.task.statusFailed",
3998
+ canceled: "a2a.task.statusCanceled",
3999
+ rejected: "a2a.task.statusRejected",
4000
+ }[detail.taskStatus] || "a2a.task.statusSubmitted";
4001
+
4002
+ const statusBadge = !enabled
4003
+ ? `<span class="eyebrow-pill eyebrow-pill--subtle">${escapeHtml(L(statusKey))}</span>`
4004
+ : "";
4005
+
4006
+ const buttons = enabled
4007
+ ? `
4008
+ <div class="actions actions--stack${options.mobile ? " actions--sticky" : ""}">
4009
+ <button type="submit" data-action="approve" class="primary primary--wide">${escapeHtml(L("a2a.task.approve"))}</button>
4010
+ <button type="submit" data-action="deny" class="danger danger--wide">${escapeHtml(L("a2a.task.deny"))}</button>
4011
+ </div>
4012
+ `
4013
+ : `<p class="muted reply-composer__description">${escapeHtml(L("a2a.task.resolved"))}</p>`;
4014
+ const buttonsWrapped = enabled && options.mobile ? `<div class="detail-action-bar">${buttons}</div>` : buttons;
4015
+
4016
+ return `
4017
+ <section class="detail-card detail-card--reply ${options.mobile ? "detail-card--mobile" : ""}">
4018
+ <form class="reply-composer" data-a2a-task-form data-token="${escapeHtml(detail.token || "")}">
4019
+ <div class="reply-composer__copy">
4020
+ <span class="eyebrow-pill eyebrow-pill--quiet">${escapeHtml(L("a2a.task.eyebrow"))}</span>
4021
+ ${statusBadge}
4022
+ ${callerLine}
4023
+ <p class="muted reply-composer__description">${enabled ? escapeHtml(L("a2a.task.editHint")) : ""}</p>
4024
+ </div>
4025
+ <div class="reply-composer__instruction">
4026
+ <label class="field-label">${escapeHtml(L("a2a.task.instruction"))}</label>
4027
+ <textarea name="instruction" class="reply-composer__textarea" rows="6" ${enabled ? "" : "readonly"}>${escapeHtml(instruction)}</textarea>
4028
+ </div>
4029
+ ${buttonsWrapped}
4030
+ </form>
4031
+ </section>
4032
+ `;
4033
+ }
4034
+
3758
4035
  function renderCompletionReplyComposer(detail, options = {}) {
3759
4036
  if (detail.kind !== "completion" || detail.reply?.enabled !== true) {
3760
4037
  return "";
@@ -4811,6 +5088,143 @@ function bindShellInteractions() {
4811
5088
  });
4812
5089
  }
4813
5090
 
5091
+ const moltbookDraftForm = document.querySelector("[data-moltbook-draft-form]");
5092
+ if (moltbookDraftForm) {
5093
+ const token = moltbookDraftForm.dataset.token || "";
5094
+ let submittedAction = "approve";
5095
+ moltbookDraftForm.querySelectorAll("button[type='submit']").forEach((btn) => {
5096
+ btn.addEventListener("click", () => {
5097
+ submittedAction = btn.dataset.action || "approve";
5098
+ });
5099
+ });
5100
+ moltbookDraftForm.addEventListener("submit", async (event) => {
5101
+ event.preventDefault();
5102
+ if (moltbookDraftForm.dataset.submitting === "1") return;
5103
+ moltbookDraftForm.dataset.submitting = "1";
5104
+ const buttons = moltbookDraftForm.querySelectorAll("button[type='submit']");
5105
+ const textarea = moltbookDraftForm.querySelector("textarea");
5106
+ const labelCache = new Map();
5107
+ buttons.forEach((btn) => {
5108
+ labelCache.set(btn, btn.innerHTML);
5109
+ btn.disabled = true;
5110
+ if (btn.dataset.action === submittedAction) {
5111
+ btn.innerHTML = submittedAction === "approve" ? "送信中…" : "処理中…";
5112
+ btn.classList.add("is-loading");
5113
+ } else {
5114
+ btn.classList.add("is-dimmed");
5115
+ }
5116
+ });
5117
+ if (textarea) textarea.readOnly = true;
5118
+ const editedText = normalizeClientText(new FormData(moltbookDraftForm).get("text"));
5119
+ const titleInput = moltbookDraftForm.querySelector("[data-moltbook-draft-title]");
5120
+ const editedTitle = titleInput ? normalizeClientText(titleInput.value) : "";
5121
+ try {
5122
+ const decisionBody = { action: submittedAction, editedText };
5123
+ if (editedTitle) decisionBody.editedTitle = editedTitle;
5124
+ const res = await fetch(`/api/items/moltbook-draft/${encodeURIComponent(token)}/decision`, {
5125
+ method: "POST",
5126
+ headers: { "content-type": "application/json" },
5127
+ credentials: "same-origin",
5128
+ body: JSON.stringify(decisionBody),
5129
+ });
5130
+ if (!res.ok) {
5131
+ const errBody = await res.json().catch(() => ({}));
5132
+ alert(`Moltbook draft ${submittedAction} failed: ${errBody.error || res.status}`);
5133
+ buttons.forEach((btn) => {
5134
+ btn.disabled = false;
5135
+ btn.classList.remove("is-loading", "is-dimmed");
5136
+ btn.innerHTML = labelCache.get(btn);
5137
+ });
5138
+ if (textarea) textarea.readOnly = false;
5139
+ moltbookDraftForm.dataset.submitting = "";
5140
+ return;
5141
+ }
5142
+ // Mark local detail as resolved so re-render shows "already resolved" immediately.
5143
+ if (state.currentDetail?.kind === "moltbook_draft") {
5144
+ state.currentDetail.moltbookDraftEnabled = false;
5145
+ state.currentDetail.readOnly = true;
5146
+ }
5147
+ await refreshAuthenticatedState();
5148
+ await renderShell();
5149
+ } catch (error) {
5150
+ alert(`Moltbook draft error: ${error.message}`);
5151
+ buttons.forEach((btn) => {
5152
+ btn.disabled = false;
5153
+ btn.classList.remove("is-loading", "is-dimmed");
5154
+ btn.innerHTML = labelCache.get(btn);
5155
+ });
5156
+ if (textarea) textarea.readOnly = false;
5157
+ moltbookDraftForm.dataset.submitting = "";
5158
+ }
5159
+ });
5160
+ }
5161
+
5162
+ const a2aTaskForm = document.querySelector("[data-a2a-task-form]");
5163
+ if (a2aTaskForm) {
5164
+ const token = a2aTaskForm.dataset.token || "";
5165
+ let submittedAction = "approve";
5166
+ a2aTaskForm.querySelectorAll("button[type='submit']").forEach((btn) => {
5167
+ btn.addEventListener("click", () => {
5168
+ submittedAction = btn.dataset.action || "approve";
5169
+ });
5170
+ });
5171
+ a2aTaskForm.addEventListener("submit", async (event) => {
5172
+ event.preventDefault();
5173
+ if (a2aTaskForm.dataset.submitting === "1") return;
5174
+ a2aTaskForm.dataset.submitting = "1";
5175
+ const buttons = a2aTaskForm.querySelectorAll("button[type='submit']");
5176
+ const textarea = a2aTaskForm.querySelector("textarea");
5177
+ const labelCache = new Map();
5178
+ buttons.forEach((btn) => {
5179
+ labelCache.set(btn, btn.innerHTML);
5180
+ btn.disabled = true;
5181
+ if (btn.dataset.action === submittedAction) {
5182
+ btn.innerHTML = submittedAction === "approve" ? "Executing…" : "Denying…";
5183
+ btn.classList.add("is-loading");
5184
+ } else {
5185
+ btn.classList.add("is-dimmed");
5186
+ }
5187
+ });
5188
+ if (textarea) textarea.readOnly = true;
5189
+ const instruction = normalizeClientText(new FormData(a2aTaskForm).get("instruction"));
5190
+ try {
5191
+ const res = await fetch(`/api/items/a2a-task/${encodeURIComponent(token)}/decision`, {
5192
+ method: "POST",
5193
+ headers: { "content-type": "application/json" },
5194
+ credentials: "same-origin",
5195
+ body: JSON.stringify({ action: submittedAction, instruction }),
5196
+ });
5197
+ if (!res.ok) {
5198
+ const errBody = await res.json().catch(() => ({}));
5199
+ alert(`A2A task ${submittedAction} failed: ${errBody.error || res.status}`);
5200
+ buttons.forEach((btn) => {
5201
+ btn.disabled = false;
5202
+ btn.classList.remove("is-loading", "is-dimmed");
5203
+ btn.innerHTML = labelCache.get(btn);
5204
+ });
5205
+ if (textarea) textarea.readOnly = false;
5206
+ a2aTaskForm.dataset.submitting = "";
5207
+ return;
5208
+ }
5209
+ if (state.currentDetail?.kind === "a2a_task") {
5210
+ state.currentDetail.a2aTaskEnabled = false;
5211
+ state.currentDetail.readOnly = true;
5212
+ }
5213
+ await refreshAuthenticatedState();
5214
+ await renderShell();
5215
+ } catch (error) {
5216
+ alert(`A2A task error: ${error.message}`);
5217
+ buttons.forEach((btn) => {
5218
+ btn.disabled = false;
5219
+ btn.classList.remove("is-loading", "is-dimmed");
5220
+ btn.innerHTML = labelCache.get(btn);
5221
+ });
5222
+ if (textarea) textarea.readOnly = false;
5223
+ a2aTaskForm.dataset.submitting = "";
5224
+ }
5225
+ });
5226
+ }
5227
+
4814
5228
  const replyForm = document.querySelector("[data-completion-reply-form]");
4815
5229
  if (replyForm) {
4816
5230
  const token = replyForm.dataset.token || "";
@@ -5289,6 +5703,9 @@ function kindMeta(kind) {
5289
5703
  return { label: L("common.diff"), tone: "neutral", icon: "diff" };
5290
5704
  case "file_event":
5291
5705
  return { label: L("common.fileEvent"), tone: "neutral", icon: "file-event" };
5706
+ case "moltbook_reply":
5707
+ case "moltbook_draft":
5708
+ return { label: L("common.sns"), tone: "neutral", icon: "item" };
5292
5709
  default:
5293
5710
  return { label: L("common.item"), tone: "neutral", icon: "item" };
5294
5711
  }