viveworker 0.4.7 → 0.4.9

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
@@ -5,7 +5,7 @@ const INSTALL_BANNER_DISMISS_KEY = "viveworker-install-banner-dismissed-v2";
5
5
  const PUSH_BANNER_DISMISS_KEY = "viveworker-push-banner-dismissed-v1";
6
6
  const INITIAL_DETECTED_LOCALE = detectBrowserLocale();
7
7
  const TIMELINE_MESSAGE_KINDS = new Set(["user_message", "assistant_commentary", "assistant_final"]);
8
- const TIMELINE_OPERATIONAL_KINDS = new Set(["approval", "plan", "plan_ready", "choice", "completion"]);
8
+ const TIMELINE_OPERATIONAL_KINDS = new Set(["approval", "plan", "plan_ready", "choice"]);
9
9
  const THREAD_FILTER_INTERACTION_DEFER_MS = 8000;
10
10
  const MAX_COMPLETION_REPLY_IMAGE_COUNT = 4;
11
11
  const NOTIFICATION_INTENT_CACHE = "viveworker-notification-intent-v1";
@@ -48,7 +48,9 @@ const state = {
48
48
  pairNotice: "",
49
49
  pushStatus: null,
50
50
  moltbookScoutStatus: null,
51
+ moltbookRecentTitlesExpanded: 0,
51
52
  a2aRelayStatus: null,
53
+ a2aTaskExecutorPick: "codex",
52
54
  pushNotice: "",
53
55
  pushError: "",
54
56
  deviceNotice: "",
@@ -612,12 +614,19 @@ function filteredDiffEntries() {
612
614
  }
613
615
 
614
616
  function syncTimelineThreadFilter() {
615
- const threads = Array.isArray(state.timeline?.threads) ? state.timeline.threads : [];
616
617
  if (!state.timelineThreadFilter || state.timelineThreadFilter === "all") {
617
618
  state.timelineThreadFilter = "all";
618
619
  return;
619
620
  }
620
- if (!threads.some((thread) => thread.id === state.timelineThreadFilter)) {
621
+ const provider = state.providerFilter || "all";
622
+ let validThreadIds;
623
+ if (provider === "all") {
624
+ const threads = Array.isArray(state.timeline?.threads) ? state.timeline.threads : [];
625
+ validThreadIds = new Set(threads.map((t) => t.id));
626
+ } else {
627
+ validThreadIds = new Set(timelineThreadsForProvider(provider).map((t) => t.id));
628
+ }
629
+ if (!validThreadIds.has(state.timelineThreadFilter)) {
621
630
  state.timelineThreadFilter = "all";
622
631
  }
623
632
  }
@@ -630,16 +639,47 @@ function syncTimelineKindFilter() {
630
639
  }
631
640
 
632
641
  function timelineKindFilterOptions() {
633
- return [
634
- { id: "all", label: L("timeline.kindFilter.all"), icon: "filter" },
642
+ const provider = state.providerFilter || "all";
643
+ const allOption = { id: "all", label: L("timeline.kindFilter.all"), icon: "filter" };
644
+
645
+ if (provider === "moltbook") {
646
+ return [
647
+ allOption,
648
+ { id: "moltbook_reply_drafts", label: L("timeline.kindFilter.moltbookReplyDrafts"), icon: "moltbook-reply" },
649
+ { id: "moltbook_post_drafts", label: L("timeline.kindFilter.moltbookPostDrafts"), icon: "moltbook-draft" },
650
+ { id: "moltbook_comments", label: L("timeline.kindFilter.moltbookComments"), icon: "moltbook-comment" },
651
+ ];
652
+ }
653
+ if (provider === "a2a") {
654
+ return [
655
+ allOption,
656
+ { id: "a2a_requests", label: L("timeline.kindFilter.a2aRequests"), icon: "item" },
657
+ { id: "a2a_results", label: L("timeline.kindFilter.a2aResults"), icon: "completion-item" },
658
+ ];
659
+ }
660
+
661
+ const codexClaudeOptions = [
662
+ allOption,
635
663
  { id: "messages", label: L("timeline.kindFilter.messages"), icon: "timeline" },
636
664
  { id: "files", label: L("timeline.kindFilter.files"), icon: "file-event" },
637
665
  { id: "approvals", label: L("timeline.kindFilter.approvals"), icon: "approval" },
638
666
  { id: "plans", label: L("timeline.kindFilter.plans"), icon: "plan" },
639
667
  { id: "choices", label: L("timeline.kindFilter.choices"), icon: "choice" },
640
- { id: "completions", label: L("timeline.kindFilter.completions"), icon: "completion-item" },
641
- { id: "moltbook_drafts", label: L("timeline.kindFilter.moltbookDrafts"), icon: "moltbook-draft" },
668
+ { id: "final_answers", label: L("timeline.kindFilter.finalAnswers"), icon: "assistant-final" },
669
+ ];
670
+
671
+ if (provider === "codex" || provider === "claude") {
672
+ return codexClaudeOptions;
673
+ }
674
+
675
+ // "all" — union of everything
676
+ return [
677
+ ...codexClaudeOptions,
678
+ { id: "moltbook_reply_drafts", label: L("timeline.kindFilter.moltbookReplyDrafts"), icon: "moltbook-reply" },
679
+ { id: "moltbook_post_drafts", label: L("timeline.kindFilter.moltbookPostDrafts"), icon: "moltbook-draft" },
642
680
  { id: "moltbook_comments", label: L("timeline.kindFilter.moltbookComments"), icon: "moltbook-comment" },
681
+ { id: "a2a_requests", label: L("timeline.kindFilter.a2aRequests"), icon: "item" },
682
+ { id: "a2a_results", label: L("timeline.kindFilter.a2aResults"), icon: "completion-item" },
643
683
  ];
644
684
  }
645
685
 
@@ -665,10 +705,20 @@ function timelineEntryMatchesKindFilter(entry, filterId) {
665
705
  return kind === "choice";
666
706
  case "completions":
667
707
  return kind === "completion";
708
+ case "final_answers":
709
+ return kind === "assistant_final";
668
710
  case "moltbook_drafts":
669
711
  return kind === "moltbook_draft";
712
+ case "moltbook_reply_drafts":
713
+ return kind === "moltbook_draft" && entry?.draftType === "reply";
714
+ case "moltbook_post_drafts":
715
+ return kind === "moltbook_draft" && entry?.draftType !== "reply";
670
716
  case "moltbook_comments":
671
717
  return kind === "moltbook_reply";
718
+ case "a2a_requests":
719
+ return kind === "a2a_task";
720
+ case "a2a_results":
721
+ return kind === "a2a_task_result";
672
722
  default:
673
723
  return true;
674
724
  }
@@ -689,10 +739,19 @@ function completedThreads() {
689
739
  if (!items.length) {
690
740
  return [];
691
741
  }
742
+ const provider = state.providerFilter || "all";
692
743
  const byThread = new Map();
693
744
  for (const item of items) {
694
745
  const threadId = normalizeClientText(item.threadId || "");
695
- if (!threadId || isMoltbookThreadId(threadId, item)) {
746
+ if (!threadId) {
747
+ continue;
748
+ }
749
+ // Skip Moltbook threads unless Moltbook tab is active
750
+ if (provider !== "moltbook" && isMoltbookThreadId(threadId, item)) {
751
+ continue;
752
+ }
753
+ // Filter by provider
754
+ if (provider !== "all" && normalizeProviderClient(item.provider) !== provider) {
696
755
  continue;
697
756
  }
698
757
  const latestAtMs = Number(item.createdAtMs || 0);
@@ -1375,7 +1434,6 @@ function buildDetailLoadingSnapshot(itemRef = state.currentItem) {
1375
1434
  readOnly:
1376
1435
  entry?.status === "completed" ||
1377
1436
  TIMELINE_MESSAGE_KINDS.has(itemRef.kind) ||
1378
- itemRef.kind === "completion" ||
1379
1437
  (itemRef.kind === "choice" && item.supported === false),
1380
1438
  loading: true,
1381
1439
  };
@@ -1779,11 +1837,13 @@ function renderProviderFilter() {
1779
1837
  `;
1780
1838
  }
1781
1839
 
1840
+ const COMPLETED_CARD_KINDS = new Set(["assistant_final", "approval", "moltbook_reply", "moltbook_draft", "a2a_task_result", "thread_share"]);
1841
+
1782
1842
  function renderItemCard(entry, sourceTab, desktop) {
1783
- if (entry.status === "completed" && entry.item.kind === "completion") {
1843
+ if (entry.status === "completed" && COMPLETED_CARD_KINDS.has(entry.item.kind)) {
1784
1844
  return renderCompletedCompletionCard(entry, sourceTab);
1785
1845
  }
1786
- const kindInfo = kindMeta(entry.item.kind);
1846
+ const kindInfo = kindMeta(entry.item.kind, entry.item);
1787
1847
  const cardTitle = cardTitleForEntry(entry);
1788
1848
  const statusText = entry.status === "completed" ? L("common.completed") : L("common.actionNeeded");
1789
1849
  const intentText = itemIntentText(entry.item.kind, entry.status, entry.item.provider);
@@ -1852,10 +1912,12 @@ function cardTitleForEntry(entry) {
1852
1912
 
1853
1913
  function renderCompletedCompletionCard(entry, sourceTab) {
1854
1914
  const item = entry.item;
1855
- const kindInfo = kindMeta(item.kind);
1915
+ const kindInfo = kindMeta(item.kind, item);
1856
1916
  const summaryText = item.summary || fallbackSummaryForKind(item.kind, entry.status, item.provider);
1857
1917
  const threadLabel = timelineEntryThreadLabel(item, true);
1858
1918
  const timestampLabel = formatTimelineTimestamp(item.createdAtMs);
1919
+ const pillLabel = item.kind === "completion" ? L("common.task") : kindInfo.label;
1920
+ const pillTone = item.kind === "completion" ? "completion" : kindInfo.tone;
1859
1921
 
1860
1922
  return `
1861
1923
  <button
@@ -1867,7 +1929,7 @@ function renderCompletedCompletionCard(entry, sourceTab) {
1867
1929
  >
1868
1930
  <div class="item-card__header">
1869
1931
  <div class="item-card__meta">
1870
- <span class="type-pill type-pill--completion">${escapeHtml(L("common.task"))}</span>
1932
+ <span class="type-pill type-pill--${escapeHtml(pillTone)}">${escapeHtml(pillLabel)}</span>
1871
1933
  ${renderProviderBadge(item.provider)}
1872
1934
  </div>
1873
1935
  <div class="item-card__header-right">
@@ -1957,17 +2019,47 @@ function renderDiffPanel({ entries, desktop }) {
1957
2019
  `;
1958
2020
  }
1959
2021
 
2022
+ function timelineThreadsForProvider(provider) {
2023
+ const entries = Array.isArray(state.timeline?.entries) ? state.timeline.entries : [];
2024
+ const byThread = new Map();
2025
+ for (const entry of entries) {
2026
+ const threadId = entry.threadId || "";
2027
+ if (!threadId) continue;
2028
+ if (normalizeProviderClient(entry.provider) !== provider) continue;
2029
+ const latestAtMs = Number(entry.createdAtMs) || 0;
2030
+ const label = entry.threadLabel || "";
2031
+ const existing = byThread.get(threadId);
2032
+ if (!existing || latestAtMs > existing.latestAtMs) {
2033
+ byThread.set(threadId, { id: threadId, label, latestAtMs });
2034
+ }
2035
+ }
2036
+ return [...byThread.values()]
2037
+ .sort((a, b) => b.latestAtMs - a.latestAtMs)
2038
+ .map((t) => ({ id: t.id, label: dropdownThreadLabel(t.id, t.label) }));
2039
+ }
2040
+
1960
2041
  function renderTimelineThreadDropdown() {
1961
- const threads = Array.isArray(state.timeline?.threads) ? state.timeline.threads : [];
2042
+ const provider = state.providerFilter || "all";
2043
+ const kindFilterHtml = renderTimelineKindFilterControls();
2044
+
2045
+ let threads;
2046
+ if (provider === "all") {
2047
+ // "All" view — use the server-provided thread list
2048
+ threads = (Array.isArray(state.timeline?.threads) ? state.timeline.threads : []).map((thread) => ({
2049
+ id: thread.id,
2050
+ label: dropdownThreadLabel(thread.id, thread.label || ""),
2051
+ }));
2052
+ } else {
2053
+ // Provider-specific view — build from entries matching this provider
2054
+ threads = timelineThreadsForProvider(provider);
2055
+ }
2056
+
1962
2057
  return renderThreadDropdown({
1963
2058
  inputId: "timeline-thread-select",
1964
2059
  dataAttribute: "data-timeline-thread-select",
1965
2060
  selectedThreadId: state.timelineThreadFilter,
1966
- controlsHtml: renderTimelineKindFilterControls(),
1967
- threads: threads.map((thread) => ({
1968
- id: thread.id,
1969
- label: dropdownThreadLabel(thread.id, thread.label || ""),
1970
- })),
2061
+ controlsHtml: kindFilterHtml,
2062
+ threads,
1971
2063
  });
1972
2064
  }
1973
2065
 
@@ -2072,10 +2164,11 @@ function renderTimelineKindFilterControls() {
2072
2164
 
2073
2165
  function renderTimelineEntry(entry, { desktop }) {
2074
2166
  const item = entry.item;
2075
- const kindInfo = kindMeta(item.kind);
2167
+ const kindInfo = kindMeta(item.kind, item);
2076
2168
  const kindClassName = escapeHtml(kindInfo.tone || "neutral");
2077
2169
  const kindNameClass = escapeHtml(String(item.kind || "item").replace(/_/gu, "-"));
2078
- const isMessageLike = TIMELINE_MESSAGE_KINDS.has(item.kind) || item.kind === "completion";
2170
+ const isMoltbookOrA2A = item.kind === "moltbook_reply" || item.kind === "moltbook_draft" || item.kind === "a2a_task" || item.kind === "a2a_task_result" || item.kind === "thread_share";
2171
+ const isMessageLike = TIMELINE_MESSAGE_KINDS.has(item.kind) || isMoltbookOrA2A;
2079
2172
  const isFileEvent = item.kind === "file_event";
2080
2173
  const imageUrls = Array.isArray(item.imageUrls) ? item.imageUrls.filter(Boolean) : [];
2081
2174
  const fileRefs = normalizeClientFileRefs(item.fileRefs);
@@ -2932,6 +3025,9 @@ function settingsPageMeta(page) {
2932
3025
  description: L("settings.a2aRelay.copy"),
2933
3026
  icon: "link",
2934
3027
  };
3028
+ case "a2aExecutor":
3029
+ // Executor settings integrated into a2aRelay page — redirect.
3030
+ return settingsPageMeta("a2aRelay");
2935
3031
  default:
2936
3032
  return settingsPageMeta("notifications");
2937
3033
  }
@@ -3064,6 +3160,7 @@ function renderSettingsSubpage(context, { mobile }) {
3064
3160
  content = renderSettingsMoltbookPage(context);
3065
3161
  break;
3066
3162
  case "a2aRelay":
3163
+ case "a2aExecutor":
3067
3164
  content = renderSettingsA2aRelayPage(context);
3068
3165
  break;
3069
3166
  default:
@@ -3303,16 +3400,33 @@ function renderSettingsMoltbookPage(context) {
3303
3400
  renderSettingsInfoRow(L("settings.row.moltbookSeenPosts"), String(scout.seenPostCount)),
3304
3401
  ])}
3305
3402
  ${batchRows.length ? renderSettingsGroup(L("settings.moltbook.batchTitle"), batchRows) : ""}
3306
- ${Array.isArray(scout.recentComposeTitles) && scout.recentComposeTitles.length
3307
- ? renderSettingsGroup(L("settings.row.moltbookRecentTitles"), scout.recentComposeTitles.map((t) => {
3308
- const title = typeof t === "string" ? t : (t.title || "");
3309
- const postId = typeof t === "object" ? t.postId : "";
3310
- const display = postId
3311
- ? `<a href="https://www.moltbook.com/post/${escapeHtml(postId)}" target="_blank" rel="noopener">${escapeHtml(title)}</a>`
3312
- : escapeHtml(title);
3313
- return renderSettingsInfoRow("", display, { rawValue: true, rowClassName: "settings-info-row--stacked" });
3314
- }))
3315
- : ""}
3403
+ ${(() => {
3404
+ const titles = Array.isArray(scout.recentComposeTitles) ? scout.recentComposeTitles : [];
3405
+ if (!titles.length) return "";
3406
+ const PAGE_SIZE = 5;
3407
+ const visibleCount = state.moltbookRecentTitlesExpanded || PAGE_SIZE;
3408
+ const visible = titles.slice(0, visibleCount);
3409
+ const hasMore = titles.length > visibleCount;
3410
+ const rows = visible.map((t) => {
3411
+ const title = typeof t === "string" ? t : (t.title || "");
3412
+ const postId = typeof t === "object" ? t.postId : "";
3413
+ const type = typeof t === "object" ? (t.type || "post") : "post";
3414
+ const badge = type === "reply"
3415
+ ? `<span class="settings-compose-badge settings-compose-badge--reply">${escapeHtml(L("settings.moltbook.typeReply"))}</span>`
3416
+ : `<span class="settings-compose-badge settings-compose-badge--post">${escapeHtml(L("settings.moltbook.typePost"))}</span>`;
3417
+ const link = postId
3418
+ ? `<a href="https://www.moltbook.com/post/${escapeHtml(postId)}" target="_blank" rel="noopener">${escapeHtml(title)}</a>`
3419
+ : escapeHtml(title);
3420
+ return `<div class="settings-compose-entry">${badge}<span class="settings-compose-entry__title">${link}</span></div>`;
3421
+ });
3422
+ if (hasMore) {
3423
+ const remaining = titles.length - visibleCount;
3424
+ rows.push(`<button type="button" class="settings-compose-more" data-moltbook-titles-more>${escapeHtml(L("settings.moltbook.showMore", { count: remaining }))}</button>`);
3425
+ } else if (titles.length > PAGE_SIZE) {
3426
+ rows.push(`<button type="button" class="settings-compose-more" data-moltbook-titles-collapse>${escapeHtml(L("settings.moltbook.showLess"))}</button>`);
3427
+ }
3428
+ return renderSettingsGroup(L("settings.row.moltbookRecentTitles"), rows);
3429
+ })()}
3316
3430
  </div>
3317
3431
  `;
3318
3432
  }
@@ -3334,17 +3448,60 @@ function renderSettingsA2aRelayPage(context) {
3334
3448
  const profileUrl = `${relay.relayUrl}/${relay.userId}`;
3335
3449
  const userIdLink = `<a href="${escapeHtml(profileUrl)}" target="_blank" rel="noopener">${escapeHtml(relay.userId)}</a>`;
3336
3450
  const relayHost = (() => { try { return new URL(relay.relayUrl).host; } catch { return relay.relayUrl; } })();
3451
+ const publicChecked = relay.acceptPublicTasks === true;
3452
+
3453
+ // Executor preference section
3454
+ const executors = state.session?.a2aExecutors || { codex: false, claude: false };
3455
+ const currentExec = state.session?.a2aExecutorPreference || "ask";
3456
+ const bothAvailable = executors.codex && executors.claude;
3457
+ const execOptions = [
3458
+ { id: "ask", label: L("settings.a2aExecutor.ask") },
3459
+ ];
3460
+ if (executors.codex) execOptions.push({ id: "codex", label: L("settings.a2aExecutor.codex"), detected: true });
3461
+ if (executors.claude) execOptions.push({ id: "claude", label: L("settings.a2aExecutor.claude"), detected: true });
3462
+ if (bothAvailable) {
3463
+ execOptions.push({ id: "auto", label: L("settings.a2aExecutor.auto") });
3464
+ }
3465
+
3337
3466
  return `
3338
3467
  <div class="settings-page">
3339
- ${renderSettingsGroup("", [
3340
- renderSettingsInfoRow(L("settings.row.a2aStatus"), statusLabel),
3341
- renderSettingsInfoRow(L("settings.row.a2aUserId"), userIdLink, { rawValue: true }),
3342
- renderSettingsInfoRow(L("settings.row.a2aRelay"), relayHost),
3343
- renderSettingsInfoRow(L("settings.row.a2aApiKey"), relay.apiKeyConfigured ? L("settings.a2aRelay.apiKey.configured") : L("settings.a2aRelay.apiKey.notConfigured")),
3344
- relay.lastPollAtMs
3345
- ? renderSettingsInfoRow(L("settings.row.a2aLastPoll"), new Date(relay.lastPollAtMs).toLocaleString(state.locale))
3346
- : "",
3347
- ].filter(Boolean))}
3468
+ ${(() => {
3469
+ const stats = relay.taskStats || { received: 0, completed: 0, denied: 0 };
3470
+ return renderSettingsGroup("", [
3471
+ renderSettingsInfoRow(L("settings.row.a2aStatus"), statusLabel),
3472
+ renderSettingsInfoRow(L("settings.row.a2aUserId"), userIdLink, { rawValue: true }),
3473
+ renderSettingsInfoRow(L("settings.row.a2aRelay"), relayHost),
3474
+ renderSettingsInfoRow(L("settings.row.a2aApiKey"), relay.apiKeyConfigured ? L("settings.a2aRelay.apiKey.configured") : L("settings.a2aRelay.apiKey.notConfigured")),
3475
+ relay.lastPollAtMs
3476
+ ? renderSettingsInfoRow(L("settings.row.a2aLastPoll"), new Date(relay.lastPollAtMs).toLocaleString(state.locale))
3477
+ : "",
3478
+ renderSettingsInfoRow(L("settings.row.a2aTaskReceived"), String(stats.received)),
3479
+ renderSettingsInfoRow(L("settings.row.a2aTaskCompleted"), String(stats.completed)),
3480
+ renderSettingsInfoRow(L("settings.row.a2aTaskDenied"), String(stats.denied)),
3481
+ ].filter(Boolean));
3482
+ })()}
3483
+ <section class="settings-group">
3484
+ <p class="settings-group__title">${escapeHtml(L("settings.a2aRelay.publicTasks.title"))}</p>
3485
+ <label class="reply-mode-switch reply-mode-switch--settings" data-a2a-public-toggle>
3486
+ <input type="checkbox" class="reply-mode-switch__input" ${publicChecked ? "checked" : ""} data-a2a-public-checkbox />
3487
+ <span class="reply-mode-switch--settings__toggle">
3488
+ <span class="reply-mode-switch__track" aria-hidden="true"><span class="reply-mode-switch__thumb"></span></span>
3489
+ <span class="reply-mode-switch__state">${escapeHtml(publicChecked ? L("settings.claudeAway.on") : L("settings.claudeAway.off"))}</span>
3490
+ </span>
3491
+ <span class="reply-mode-switch__hint">${escapeHtml(L("settings.a2aRelay.publicTasks.description"))}</span>
3492
+ </label>
3493
+ </section>
3494
+ ${renderSettingsGroup(L("settings.a2aExecutor.title"), [
3495
+ `<p class="settings-group__description">${escapeHtml(L("settings.a2aExecutor.copy"))}</p>`,
3496
+ ...execOptions.map((opt) => `
3497
+ <label class="settings-radio-row" data-a2a-executor-option="${escapeHtml(opt.id)}">
3498
+ <input type="radio" name="a2aExecutor" value="${escapeHtml(opt.id)}"
3499
+ ${currentExec === opt.id ? "checked" : ""} />
3500
+ <span class="settings-radio-row__label">${escapeHtml(opt.label)}</span>
3501
+ ${opt.detected ? `<span class="settings-radio-row__badge">✓ ${escapeHtml(L("settings.a2aExecutor.detected"))}</span>` : ""}
3502
+ </label>
3503
+ `),
3504
+ ])}
3348
3505
  </div>
3349
3506
  `;
3350
3507
  }
@@ -3455,8 +3612,8 @@ function renderDetailContent(detail, { mobile }) {
3455
3612
  }
3456
3613
 
3457
3614
  function renderStandardDetailDesktop(detail) {
3458
- const kindInfo = kindMeta(detail.kind);
3459
- const spaciousBodyDetail = TIMELINE_MESSAGE_KINDS.has(detail.kind) || detail.kind === "completion";
3615
+ const kindInfo = kindMeta(detail.kind, detail);
3616
+ const spaciousBodyDetail = TIMELINE_MESSAGE_KINDS.has(detail.kind);
3460
3617
  const plainIntro = renderDetailPlainIntro(detail);
3461
3618
  return `
3462
3619
  <div class="detail-shell">
@@ -3470,7 +3627,7 @@ function renderStandardDetailDesktop(detail) {
3470
3627
  ${renderA2ATaskDetail(detail)}
3471
3628
  ${renderThreadShareDetail(detail)}
3472
3629
  ${
3473
- detail.kind === "moltbook_draft" || detail.kind === "moltbook_reply" || detail.kind === "a2a_task" || detail.kind === "thread_share"
3630
+ detail.kind === "moltbook_draft" || detail.kind === "moltbook_reply" || detail.kind === "a2a_task" || detail.kind === "a2a_task_result" || detail.kind === "thread_share"
3474
3631
  ? ""
3475
3632
  : plainIntro
3476
3633
  ? plainIntro
@@ -3493,8 +3650,8 @@ function renderStandardDetailDesktop(detail) {
3493
3650
  }
3494
3651
 
3495
3652
  function renderStandardDetailMobile(detail) {
3496
- const kindInfo = kindMeta(detail.kind);
3497
- const spaciousBodyDetail = TIMELINE_MESSAGE_KINDS.has(detail.kind) || detail.kind === "completion";
3653
+ const kindInfo = kindMeta(detail.kind, detail);
3654
+ const spaciousBodyDetail = TIMELINE_MESSAGE_KINDS.has(detail.kind);
3498
3655
  const plainIntro = renderDetailPlainIntro(detail, { mobile: true });
3499
3656
  return `
3500
3657
  <div class="mobile-detail-screen">
@@ -3508,7 +3665,7 @@ function renderStandardDetailMobile(detail) {
3508
3665
  ${renderA2ATaskDetail(detail, { mobile: true })}
3509
3666
  ${renderThreadShareDetail(detail, { mobile: true })}
3510
3667
  ${
3511
- detail.kind === "moltbook_draft" || detail.kind === "moltbook_reply" || detail.kind === "a2a_task" || detail.kind === "thread_share"
3668
+ detail.kind === "moltbook_draft" || detail.kind === "moltbook_reply" || detail.kind === "a2a_task" || detail.kind === "a2a_task_result" || detail.kind === "thread_share"
3512
3669
  ? ""
3513
3670
  : plainIntro
3514
3671
  ? plainIntro
@@ -4066,7 +4223,7 @@ function renderMoltbookDraftComposer(detail, options = {}) {
4066
4223
  }
4067
4224
 
4068
4225
  function renderA2ATaskDetail(detail, options = {}) {
4069
- if (detail.kind !== "a2a_task") return "";
4226
+ if (detail.kind !== "a2a_task" && detail.kind !== "a2a_task_result") return "";
4070
4227
  const enabled = detail.a2aTaskEnabled !== false && detail.readOnly !== true;
4071
4228
  const instruction = detail.instruction || "";
4072
4229
  const callerIp = detail.callerInfo?.ip || "";
@@ -4084,10 +4241,33 @@ function renderA2ATaskDetail(detail, options = {}) {
4084
4241
  rejected: "a2a.task.statusRejected",
4085
4242
  }[detail.taskStatus] || "a2a.task.statusSubmitted";
4086
4243
 
4087
- const statusBadge = !enabled
4244
+ const statusBadge = !enabled && detail.kind === "a2a_task_result"
4088
4245
  ? `<span class="eyebrow-pill eyebrow-pill--subtle">${escapeHtml(L(statusKey))}</span>`
4089
4246
  : "";
4090
4247
 
4248
+ // Show executor selector when "ask" mode is active and both CLIs are available.
4249
+ const executors = state.session?.a2aExecutors || { codex: false, claude: false };
4250
+ const executorPref = state.session?.a2aExecutorPreference || "auto";
4251
+ const showExecutorPicker = enabled && executorPref === "ask" && executors.codex && executors.claude;
4252
+ const pickedExecutor = state.a2aTaskExecutorPick || "codex";
4253
+ const executorPicker = showExecutorPicker
4254
+ ? `
4255
+ <div class="reply-composer__instruction">
4256
+ <label class="field-label">${escapeHtml(L("a2a.task.executor"))}</label>
4257
+ <div class="a2a-executor-picker">
4258
+ <label class="a2a-executor-picker__option">
4259
+ <input type="radio" name="executor" value="codex" ${pickedExecutor === "codex" ? "checked" : ""} />
4260
+ <span>${escapeHtml(L("a2a.executor.codex"))}</span>
4261
+ </label>
4262
+ <label class="a2a-executor-picker__option">
4263
+ <input type="radio" name="executor" value="claude" ${pickedExecutor === "claude" ? "checked" : ""} />
4264
+ <span>${escapeHtml(L("a2a.executor.claude"))}</span>
4265
+ </label>
4266
+ </div>
4267
+ </div>
4268
+ `
4269
+ : "";
4270
+
4091
4271
  const buttons = enabled
4092
4272
  ? `
4093
4273
  <div class="actions actions--stack${options.mobile ? " actions--sticky" : ""}">
@@ -4111,6 +4291,13 @@ function renderA2ATaskDetail(detail, options = {}) {
4111
4291
  <label class="field-label">${escapeHtml(L("a2a.task.instruction"))}</label>
4112
4292
  <textarea name="instruction" class="reply-composer__textarea" rows="6" ${enabled ? "" : "readonly"}>${escapeHtml(instruction)}</textarea>
4113
4293
  </div>
4294
+ ${executorPicker}
4295
+ ${!enabled && detail.messageText ? `
4296
+ <div class="reply-composer__instruction">
4297
+ <label class="field-label">${escapeHtml(L("a2a.task.response"))}</label>
4298
+ <pre class="a2a-task-response">${escapeHtml(detail.messageText)}</pre>
4299
+ </div>
4300
+ ` : ""}
4114
4301
  ${buttonsWrapped}
4115
4302
  </form>
4116
4303
  </section>
@@ -4168,7 +4355,7 @@ function renderThreadShareDetail(detail, options = {}) {
4168
4355
  }
4169
4356
 
4170
4357
  function renderCompletionReplyComposer(detail, options = {}) {
4171
- if (detail.kind !== "completion" || detail.reply?.enabled !== true) {
4358
+ if ((detail.kind !== "completion" && detail.kind !== "assistant_final") || detail.reply?.enabled !== true) {
4172
4359
  return "";
4173
4360
  }
4174
4361
 
@@ -4238,7 +4425,7 @@ function renderCompletionReplyComposer(detail, options = {}) {
4238
4425
  </div>
4239
4426
  `
4240
4427
  : `
4241
- <form class="reply-composer__form" data-completion-reply-form data-token="${escapeHtml(detail.token)}" data-provider="${escapeHtml(normalizeProviderClient(detail?.provider))}">
4428
+ <form class="reply-composer__form" data-completion-reply-form data-token="${escapeHtml(detail.token)}" data-provider="${escapeHtml(normalizeProviderClient(detail?.provider))}" data-reply-kind="${escapeHtml(detail.kind)}">
4242
4429
  <label class="field reply-field">
4243
4430
  <span class="field-label">${escapeHtml(L("reply.fieldLabel"))}</span>
4244
4431
  <div class="reply-field__shell">
@@ -4816,6 +5003,11 @@ function bindShellInteractions() {
4816
5003
  return;
4817
5004
  }
4818
5005
  state.providerFilter = next;
5006
+ // Reset thread/kind filters — each provider has its own set of valid options
5007
+ state.timelineThreadFilter = "all";
5008
+ state.timelineKindFilter = "all";
5009
+ state.timelineKindFilterOpen = false;
5010
+ state.completedThreadFilter = "all";
4819
5011
  alignCurrentItemToVisibleEntries();
4820
5012
  await renderShell();
4821
5013
  });
@@ -5161,6 +5353,46 @@ function bindShellInteractions() {
5161
5353
  });
5162
5354
  }
5163
5355
 
5356
+ for (const checkbox of document.querySelectorAll("[data-a2a-public-checkbox]")) {
5357
+ checkbox.addEventListener("change", async () => {
5358
+ const next = checkbox.checked === true;
5359
+ try {
5360
+ await apiPost("/api/a2a/public-tasks", { accept: next });
5361
+ state.a2aRelayStatus = await apiGet("/api/a2a/relay-status");
5362
+ } catch (error) {
5363
+ state.pushError = error.message || String(error);
5364
+ }
5365
+ await renderShell();
5366
+ });
5367
+ }
5368
+
5369
+ for (const radio of document.querySelectorAll("[data-a2a-executor-option] input[type='radio']")) {
5370
+ radio.addEventListener("change", async () => {
5371
+ if (!radio.checked) return;
5372
+ const preference = radio.value || "auto";
5373
+ try {
5374
+ await apiPost("/api/settings/a2a-executor", { preference });
5375
+ await refreshSession();
5376
+ } catch (error) {
5377
+ state.pushError = error.message || String(error);
5378
+ }
5379
+ await renderShell();
5380
+ });
5381
+ }
5382
+
5383
+ for (const btn of document.querySelectorAll("[data-moltbook-titles-more]")) {
5384
+ btn.addEventListener("click", async () => {
5385
+ state.moltbookRecentTitlesExpanded = (state.moltbookRecentTitlesExpanded || 5) + 5;
5386
+ await renderShell();
5387
+ });
5388
+ }
5389
+ for (const btn of document.querySelectorAll("[data-moltbook-titles-collapse]")) {
5390
+ btn.addEventListener("click", async () => {
5391
+ state.moltbookRecentTitlesExpanded = 0;
5392
+ await renderShell();
5393
+ });
5394
+ }
5395
+
5164
5396
  for (const button of document.querySelectorAll("[data-locale-option]")) {
5165
5397
  button.addEventListener("click", async () => {
5166
5398
  state.pushError = "";
@@ -5294,6 +5526,12 @@ function bindShellInteractions() {
5294
5526
  });
5295
5527
  }
5296
5528
 
5529
+ for (const radio of document.querySelectorAll(".a2a-executor-picker input[type='radio']")) {
5530
+ radio.addEventListener("change", () => {
5531
+ if (radio.checked) state.a2aTaskExecutorPick = radio.value;
5532
+ });
5533
+ }
5534
+
5297
5535
  const a2aTaskForm = document.querySelector("[data-a2a-task-form]");
5298
5536
  if (a2aTaskForm) {
5299
5537
  const token = a2aTaskForm.dataset.token || "";
@@ -5322,12 +5560,16 @@ function bindShellInteractions() {
5322
5560
  });
5323
5561
  if (textarea) textarea.readOnly = true;
5324
5562
  const instruction = normalizeClientText(new FormData(a2aTaskForm).get("instruction"));
5563
+ const executorRadio = a2aTaskForm.querySelector("input[name='executor']:checked");
5564
+ const executor = executorRadio ? executorRadio.value : "";
5325
5565
  try {
5566
+ const decisionBody = { action: submittedAction, instruction };
5567
+ if (executor) decisionBody.executor = executor;
5326
5568
  const res = await fetch(`/api/items/a2a-task/${encodeURIComponent(token)}/decision`, {
5327
5569
  method: "POST",
5328
5570
  headers: { "content-type": "application/json" },
5329
5571
  credentials: "same-origin",
5330
- body: JSON.stringify({ action: submittedAction, instruction }),
5572
+ body: JSON.stringify(decisionBody),
5331
5573
  });
5332
5574
  if (!res.ok) {
5333
5575
  const errBody = await res.json().catch(() => ({}));
@@ -5482,7 +5724,8 @@ function bindShellInteractions() {
5482
5724
  requestBody.append("image", attachment.file, attachment.name || attachment.file.name);
5483
5725
  }
5484
5726
  }
5485
- await apiPost(`/api/items/completion/${encodeURIComponent(token)}/reply`, requestBody);
5727
+ const replyKind = replyForm.dataset.replyKind || "completion";
5728
+ await apiPost(`/api/items/${encodeURIComponent(replyKind)}/${encodeURIComponent(token)}/reply`, requestBody);
5486
5729
  setCompletionReplyDraft(token, {
5487
5730
  text: "",
5488
5731
  sentText: text,
@@ -5880,10 +6123,10 @@ function inboxSubtabForItemKind(kind, sourceSubtab = "") {
5880
6123
  if (normalizeClientText(sourceSubtab || "") === "completed") {
5881
6124
  return "completed";
5882
6125
  }
5883
- return kind === "completion" ? "completed" : "pending";
6126
+ return kind === "completion" || kind === "assistant_final" ? "completed" : "pending";
5884
6127
  }
5885
6128
 
5886
- function kindMeta(kind) {
6129
+ function kindMeta(kind, item) {
5887
6130
  switch (kind) {
5888
6131
  case "user_message":
5889
6132
  return { label: L("common.userMessage"), tone: "neutral", icon: "user-message" };
@@ -5905,10 +6148,17 @@ function kindMeta(kind) {
5905
6148
  case "file_event":
5906
6149
  return { label: L("common.fileEvent"), tone: "neutral", icon: "file-event" };
5907
6150
  case "moltbook_reply":
6151
+ return { label: L("common.moltbookReply"), tone: "neutral", icon: "moltbook-comment" };
5908
6152
  case "moltbook_draft":
5909
- return { label: L("common.sns"), tone: "neutral", icon: "item" };
6153
+ return item?.draftType === "reply"
6154
+ ? { label: L("common.moltbookDraftReply"), tone: "neutral", icon: "moltbook-reply" }
6155
+ : { label: L("common.moltbookDraft"), tone: "neutral", icon: "moltbook-draft" };
5910
6156
  case "thread_share":
5911
6157
  return { label: L("common.threadShare"), tone: "neutral", icon: "link" };
6158
+ case "a2a_task":
6159
+ return { label: L("common.a2aTaskRequest"), tone: "neutral", icon: "item" };
6160
+ case "a2a_task_result":
6161
+ return { label: L("common.a2aTaskResult"), tone: "completion", icon: "completion-item" };
5912
6162
  default:
5913
6163
  return { label: L("common.item"), tone: "neutral", icon: "item" };
5914
6164
  }
@@ -6263,6 +6513,8 @@ function renderIcon(name) {
6263
6513
  return `<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.9" stroke-linecap="round" stroke-linejoin="round"><path d="m9.5 12.5 5.9-5.9a3 3 0 1 1 4.2 4.2l-7.7 7.7a5 5 0 1 1-7.1-7.1l8.1-8.1"/></svg>`;
6264
6514
  case "moltbook-draft":
6265
6515
  return `<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.9" stroke-linecap="round" stroke-linejoin="round"><path d="M15.2 3.8 20.2 8.8 8.5 20.5 3.5 20.5 3.5 15.5Z"/><path d="M12.5 6.5l5 5"/></svg>`;
6516
+ case "moltbook-reply":
6517
+ return `<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.9" stroke-linecap="round" stroke-linejoin="round"><path d="M9 4 4 9l5 5"/><path d="M4 9h11a4 4 0 0 1 4 4v3"/></svg>`;
6266
6518
  case "moltbook-comment":
6267
6519
  return `<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.9" stroke-linecap="round" stroke-linejoin="round"><path d="M4.5 5.5h15a2 2 0 0 1 2 2v7a2 2 0 0 1-2 2H11l-4 3.5v-3.5H4.5a2 2 0 0 1-2-2v-7a2 2 0 0 1 2-2Z"/><path d="M8 10h8"/><path d="M8 13h5"/></svg>`;
6268
6520
  case "filter":