viveworker 0.4.7 → 0.4.8

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.css CHANGED
@@ -524,7 +524,9 @@ code {
524
524
 
525
525
  .settings-nav-row + .settings-nav-row,
526
526
  .settings-info-row + .settings-info-row,
527
- .settings-choice-row + .settings-choice-row {
527
+ .settings-choice-row + .settings-choice-row,
528
+ .settings-radio-row + .settings-radio-row,
529
+ .settings-info-row + .settings-radio-row {
528
530
  border-top: 1px solid rgba(255, 255, 255, 0.06);
529
531
  }
530
532
 
@@ -533,6 +535,80 @@ code {
533
535
  background: rgba(255, 255, 255, 0.035);
534
536
  }
535
537
 
538
+ .settings-group__description {
539
+ margin: 0;
540
+ padding: 0.72rem 1rem;
541
+ font-size: 0.83rem;
542
+ line-height: 1.45;
543
+ color: var(--muted);
544
+ border-bottom: 1px solid rgba(255, 255, 255, 0.06);
545
+ }
546
+
547
+ .settings-radio-row {
548
+ display: flex;
549
+ align-items: center;
550
+ gap: 0.78rem;
551
+ padding: 0.82rem 1rem;
552
+ cursor: pointer;
553
+ -webkit-tap-highlight-color: transparent;
554
+ }
555
+
556
+ .settings-radio-row:active {
557
+ background: rgba(255, 255, 255, 0.035);
558
+ }
559
+
560
+ .settings-radio-row input[type="radio"] {
561
+ appearance: none;
562
+ -webkit-appearance: none;
563
+ width: 1.25rem;
564
+ height: 1.25rem;
565
+ border: 2px solid rgba(156, 181, 197, 0.32);
566
+ border-radius: 50%;
567
+ background: transparent;
568
+ flex-shrink: 0;
569
+ position: relative;
570
+ cursor: pointer;
571
+ transition: border-color 160ms ease;
572
+ }
573
+
574
+ .settings-radio-row input[type="radio"]:checked {
575
+ border-color: rgba(121, 196, 255, 0.92);
576
+ }
577
+
578
+ .settings-radio-row input[type="radio"]:checked::after {
579
+ content: "";
580
+ position: absolute;
581
+ top: 50%;
582
+ left: 50%;
583
+ transform: translate(-50%, -50%);
584
+ width: 0.52rem;
585
+ height: 0.52rem;
586
+ border-radius: 50%;
587
+ background: rgba(121, 196, 255, 0.92);
588
+ }
589
+
590
+ .settings-radio-row input[type="radio"]:disabled {
591
+ opacity: 0.35;
592
+ cursor: not-allowed;
593
+ }
594
+
595
+ .settings-radio-row__label {
596
+ font-size: 0.94rem;
597
+ line-height: 1.25;
598
+ color: var(--text);
599
+ }
600
+
601
+ .settings-radio-row__label.muted {
602
+ color: var(--muted);
603
+ }
604
+
605
+ .settings-radio-row__badge {
606
+ margin-left: auto;
607
+ font-size: 0.74rem;
608
+ letter-spacing: 0.02em;
609
+ color: rgba(121, 196, 255, 0.82);
610
+ }
611
+
536
612
  .settings-row__icon {
537
613
  width: 1.95rem;
538
614
  height: 1.95rem;
@@ -2354,6 +2430,24 @@ code {
2354
2430
  border-color: var(--accent, #6ea8ff);
2355
2431
  }
2356
2432
 
2433
+ .a2a-task-response {
2434
+ width: 100%;
2435
+ padding: 0.85rem 0.95rem;
2436
+ border-radius: 14px;
2437
+ border: 1px solid var(--line);
2438
+ background: var(--surface, #1a1a2e);
2439
+ color: var(--fg);
2440
+ font-family: "SF Mono", SFMono-Regular, Menlo, Consolas, monospace;
2441
+ font-size: 0.85rem;
2442
+ line-height: 1.55;
2443
+ white-space: pre-wrap;
2444
+ word-break: break-word;
2445
+ overflow-x: auto;
2446
+ max-height: 24rem;
2447
+ overflow-y: auto;
2448
+ margin: 0;
2449
+ }
2450
+
2357
2451
  .compose-greeting {
2358
2452
  display: flex;
2359
2453
  align-items: center;
@@ -2655,6 +2749,34 @@ code {
2655
2749
  line-height: 1.45;
2656
2750
  }
2657
2751
 
2752
+ .reply-mode-switch--settings {
2753
+ grid-template-columns: minmax(0, 1fr) auto;
2754
+ align-items: start;
2755
+ }
2756
+
2757
+ .reply-mode-switch--settings__toggle {
2758
+ grid-column: 2;
2759
+ grid-row: 1;
2760
+ display: flex;
2761
+ flex-direction: column;
2762
+ align-items: center;
2763
+ gap: 0.32rem;
2764
+ }
2765
+
2766
+ .reply-mode-switch--settings .reply-mode-switch__hint {
2767
+ grid-column: 1;
2768
+ grid-row: 1;
2769
+ }
2770
+
2771
+ .reply-mode-switch__input:checked ~ .reply-mode-switch--settings__toggle .reply-mode-switch__track {
2772
+ background: rgba(67, 160, 255, 0.28);
2773
+ border-color: rgba(121, 196, 255, 0.34);
2774
+ }
2775
+
2776
+ .reply-mode-switch__input:checked ~ .reply-mode-switch--settings__toggle .reply-mode-switch__thumb {
2777
+ transform: translateX(1.06rem);
2778
+ }
2779
+
2658
2780
  @keyframes detail-loading-pulse {
2659
2781
  0% {
2660
2782
  background-position: 100% 0;
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";
@@ -612,12 +612,19 @@ function filteredDiffEntries() {
612
612
  }
613
613
 
614
614
  function syncTimelineThreadFilter() {
615
- const threads = Array.isArray(state.timeline?.threads) ? state.timeline.threads : [];
616
615
  if (!state.timelineThreadFilter || state.timelineThreadFilter === "all") {
617
616
  state.timelineThreadFilter = "all";
618
617
  return;
619
618
  }
620
- if (!threads.some((thread) => thread.id === state.timelineThreadFilter)) {
619
+ const provider = state.providerFilter || "all";
620
+ let validThreadIds;
621
+ if (provider === "all") {
622
+ const threads = Array.isArray(state.timeline?.threads) ? state.timeline.threads : [];
623
+ validThreadIds = new Set(threads.map((t) => t.id));
624
+ } else {
625
+ validThreadIds = new Set(timelineThreadsForProvider(provider).map((t) => t.id));
626
+ }
627
+ if (!validThreadIds.has(state.timelineThreadFilter)) {
621
628
  state.timelineThreadFilter = "all";
622
629
  }
623
630
  }
@@ -630,16 +637,47 @@ function syncTimelineKindFilter() {
630
637
  }
631
638
 
632
639
  function timelineKindFilterOptions() {
633
- return [
634
- { id: "all", label: L("timeline.kindFilter.all"), icon: "filter" },
640
+ const provider = state.providerFilter || "all";
641
+ const allOption = { id: "all", label: L("timeline.kindFilter.all"), icon: "filter" };
642
+
643
+ if (provider === "moltbook") {
644
+ return [
645
+ allOption,
646
+ { id: "moltbook_reply_drafts", label: L("timeline.kindFilter.moltbookReplyDrafts"), icon: "moltbook-reply" },
647
+ { id: "moltbook_post_drafts", label: L("timeline.kindFilter.moltbookPostDrafts"), icon: "moltbook-draft" },
648
+ { id: "moltbook_comments", label: L("timeline.kindFilter.moltbookComments"), icon: "moltbook-comment" },
649
+ ];
650
+ }
651
+ if (provider === "a2a") {
652
+ return [
653
+ allOption,
654
+ { id: "a2a_requests", label: L("timeline.kindFilter.a2aRequests"), icon: "item" },
655
+ { id: "a2a_results", label: L("timeline.kindFilter.a2aResults"), icon: "completion-item" },
656
+ ];
657
+ }
658
+
659
+ const codexClaudeOptions = [
660
+ allOption,
635
661
  { id: "messages", label: L("timeline.kindFilter.messages"), icon: "timeline" },
636
662
  { id: "files", label: L("timeline.kindFilter.files"), icon: "file-event" },
637
663
  { id: "approvals", label: L("timeline.kindFilter.approvals"), icon: "approval" },
638
664
  { id: "plans", label: L("timeline.kindFilter.plans"), icon: "plan" },
639
665
  { 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" },
666
+ { id: "final_answers", label: L("timeline.kindFilter.finalAnswers"), icon: "assistant-final" },
667
+ ];
668
+
669
+ if (provider === "codex" || provider === "claude") {
670
+ return codexClaudeOptions;
671
+ }
672
+
673
+ // "all" — union of everything
674
+ return [
675
+ ...codexClaudeOptions,
676
+ { id: "moltbook_reply_drafts", label: L("timeline.kindFilter.moltbookReplyDrafts"), icon: "moltbook-reply" },
677
+ { id: "moltbook_post_drafts", label: L("timeline.kindFilter.moltbookPostDrafts"), icon: "moltbook-draft" },
642
678
  { id: "moltbook_comments", label: L("timeline.kindFilter.moltbookComments"), icon: "moltbook-comment" },
679
+ { id: "a2a_requests", label: L("timeline.kindFilter.a2aRequests"), icon: "item" },
680
+ { id: "a2a_results", label: L("timeline.kindFilter.a2aResults"), icon: "completion-item" },
643
681
  ];
644
682
  }
645
683
 
@@ -665,10 +703,20 @@ function timelineEntryMatchesKindFilter(entry, filterId) {
665
703
  return kind === "choice";
666
704
  case "completions":
667
705
  return kind === "completion";
706
+ case "final_answers":
707
+ return kind === "assistant_final";
668
708
  case "moltbook_drafts":
669
709
  return kind === "moltbook_draft";
710
+ case "moltbook_reply_drafts":
711
+ return kind === "moltbook_draft" && entry?.draftType === "reply";
712
+ case "moltbook_post_drafts":
713
+ return kind === "moltbook_draft" && entry?.draftType !== "reply";
670
714
  case "moltbook_comments":
671
715
  return kind === "moltbook_reply";
716
+ case "a2a_requests":
717
+ return kind === "a2a_task";
718
+ case "a2a_results":
719
+ return kind === "a2a_task_result";
672
720
  default:
673
721
  return true;
674
722
  }
@@ -689,10 +737,19 @@ function completedThreads() {
689
737
  if (!items.length) {
690
738
  return [];
691
739
  }
740
+ const provider = state.providerFilter || "all";
692
741
  const byThread = new Map();
693
742
  for (const item of items) {
694
743
  const threadId = normalizeClientText(item.threadId || "");
695
- if (!threadId || isMoltbookThreadId(threadId, item)) {
744
+ if (!threadId) {
745
+ continue;
746
+ }
747
+ // Skip Moltbook threads unless Moltbook tab is active
748
+ if (provider !== "moltbook" && isMoltbookThreadId(threadId, item)) {
749
+ continue;
750
+ }
751
+ // Filter by provider
752
+ if (provider !== "all" && normalizeProviderClient(item.provider) !== provider) {
696
753
  continue;
697
754
  }
698
755
  const latestAtMs = Number(item.createdAtMs || 0);
@@ -1375,7 +1432,6 @@ function buildDetailLoadingSnapshot(itemRef = state.currentItem) {
1375
1432
  readOnly:
1376
1433
  entry?.status === "completed" ||
1377
1434
  TIMELINE_MESSAGE_KINDS.has(itemRef.kind) ||
1378
- itemRef.kind === "completion" ||
1379
1435
  (itemRef.kind === "choice" && item.supported === false),
1380
1436
  loading: true,
1381
1437
  };
@@ -1779,11 +1835,13 @@ function renderProviderFilter() {
1779
1835
  `;
1780
1836
  }
1781
1837
 
1838
+ const COMPLETED_CARD_KINDS = new Set(["assistant_final", "approval", "moltbook_reply", "moltbook_draft", "a2a_task_result", "thread_share"]);
1839
+
1782
1840
  function renderItemCard(entry, sourceTab, desktop) {
1783
- if (entry.status === "completed" && entry.item.kind === "completion") {
1841
+ if (entry.status === "completed" && COMPLETED_CARD_KINDS.has(entry.item.kind)) {
1784
1842
  return renderCompletedCompletionCard(entry, sourceTab);
1785
1843
  }
1786
- const kindInfo = kindMeta(entry.item.kind);
1844
+ const kindInfo = kindMeta(entry.item.kind, entry.item);
1787
1845
  const cardTitle = cardTitleForEntry(entry);
1788
1846
  const statusText = entry.status === "completed" ? L("common.completed") : L("common.actionNeeded");
1789
1847
  const intentText = itemIntentText(entry.item.kind, entry.status, entry.item.provider);
@@ -1852,10 +1910,12 @@ function cardTitleForEntry(entry) {
1852
1910
 
1853
1911
  function renderCompletedCompletionCard(entry, sourceTab) {
1854
1912
  const item = entry.item;
1855
- const kindInfo = kindMeta(item.kind);
1913
+ const kindInfo = kindMeta(item.kind, item);
1856
1914
  const summaryText = item.summary || fallbackSummaryForKind(item.kind, entry.status, item.provider);
1857
1915
  const threadLabel = timelineEntryThreadLabel(item, true);
1858
1916
  const timestampLabel = formatTimelineTimestamp(item.createdAtMs);
1917
+ const pillLabel = item.kind === "completion" ? L("common.task") : kindInfo.label;
1918
+ const pillTone = item.kind === "completion" ? "completion" : kindInfo.tone;
1859
1919
 
1860
1920
  return `
1861
1921
  <button
@@ -1867,7 +1927,7 @@ function renderCompletedCompletionCard(entry, sourceTab) {
1867
1927
  >
1868
1928
  <div class="item-card__header">
1869
1929
  <div class="item-card__meta">
1870
- <span class="type-pill type-pill--completion">${escapeHtml(L("common.task"))}</span>
1930
+ <span class="type-pill type-pill--${escapeHtml(pillTone)}">${escapeHtml(pillLabel)}</span>
1871
1931
  ${renderProviderBadge(item.provider)}
1872
1932
  </div>
1873
1933
  <div class="item-card__header-right">
@@ -1957,17 +2017,47 @@ function renderDiffPanel({ entries, desktop }) {
1957
2017
  `;
1958
2018
  }
1959
2019
 
2020
+ function timelineThreadsForProvider(provider) {
2021
+ const entries = Array.isArray(state.timeline?.entries) ? state.timeline.entries : [];
2022
+ const byThread = new Map();
2023
+ for (const entry of entries) {
2024
+ const threadId = entry.threadId || "";
2025
+ if (!threadId) continue;
2026
+ if (normalizeProviderClient(entry.provider) !== provider) continue;
2027
+ const latestAtMs = Number(entry.createdAtMs) || 0;
2028
+ const label = entry.threadLabel || "";
2029
+ const existing = byThread.get(threadId);
2030
+ if (!existing || latestAtMs > existing.latestAtMs) {
2031
+ byThread.set(threadId, { id: threadId, label, latestAtMs });
2032
+ }
2033
+ }
2034
+ return [...byThread.values()]
2035
+ .sort((a, b) => b.latestAtMs - a.latestAtMs)
2036
+ .map((t) => ({ id: t.id, label: dropdownThreadLabel(t.id, t.label) }));
2037
+ }
2038
+
1960
2039
  function renderTimelineThreadDropdown() {
1961
- const threads = Array.isArray(state.timeline?.threads) ? state.timeline.threads : [];
2040
+ const provider = state.providerFilter || "all";
2041
+ const kindFilterHtml = renderTimelineKindFilterControls();
2042
+
2043
+ let threads;
2044
+ if (provider === "all") {
2045
+ // "All" view — use the server-provided thread list
2046
+ threads = (Array.isArray(state.timeline?.threads) ? state.timeline.threads : []).map((thread) => ({
2047
+ id: thread.id,
2048
+ label: dropdownThreadLabel(thread.id, thread.label || ""),
2049
+ }));
2050
+ } else {
2051
+ // Provider-specific view — build from entries matching this provider
2052
+ threads = timelineThreadsForProvider(provider);
2053
+ }
2054
+
1962
2055
  return renderThreadDropdown({
1963
2056
  inputId: "timeline-thread-select",
1964
2057
  dataAttribute: "data-timeline-thread-select",
1965
2058
  selectedThreadId: state.timelineThreadFilter,
1966
- controlsHtml: renderTimelineKindFilterControls(),
1967
- threads: threads.map((thread) => ({
1968
- id: thread.id,
1969
- label: dropdownThreadLabel(thread.id, thread.label || ""),
1970
- })),
2059
+ controlsHtml: kindFilterHtml,
2060
+ threads,
1971
2061
  });
1972
2062
  }
1973
2063
 
@@ -2072,10 +2162,11 @@ function renderTimelineKindFilterControls() {
2072
2162
 
2073
2163
  function renderTimelineEntry(entry, { desktop }) {
2074
2164
  const item = entry.item;
2075
- const kindInfo = kindMeta(item.kind);
2165
+ const kindInfo = kindMeta(item.kind, item);
2076
2166
  const kindClassName = escapeHtml(kindInfo.tone || "neutral");
2077
2167
  const kindNameClass = escapeHtml(String(item.kind || "item").replace(/_/gu, "-"));
2078
- const isMessageLike = TIMELINE_MESSAGE_KINDS.has(item.kind) || item.kind === "completion";
2168
+ const isMoltbookOrA2A = item.kind === "moltbook_reply" || item.kind === "moltbook_draft" || item.kind === "a2a_task" || item.kind === "a2a_task_result" || item.kind === "thread_share";
2169
+ const isMessageLike = TIMELINE_MESSAGE_KINDS.has(item.kind) || isMoltbookOrA2A;
2079
2170
  const isFileEvent = item.kind === "file_event";
2080
2171
  const imageUrls = Array.isArray(item.imageUrls) ? item.imageUrls.filter(Boolean) : [];
2081
2172
  const fileRefs = normalizeClientFileRefs(item.fileRefs);
@@ -2932,6 +3023,9 @@ function settingsPageMeta(page) {
2932
3023
  description: L("settings.a2aRelay.copy"),
2933
3024
  icon: "link",
2934
3025
  };
3026
+ case "a2aExecutor":
3027
+ // Executor settings integrated into a2aRelay page — redirect.
3028
+ return settingsPageMeta("a2aRelay");
2935
3029
  default:
2936
3030
  return settingsPageMeta("notifications");
2937
3031
  }
@@ -3064,6 +3158,7 @@ function renderSettingsSubpage(context, { mobile }) {
3064
3158
  content = renderSettingsMoltbookPage(context);
3065
3159
  break;
3066
3160
  case "a2aRelay":
3161
+ case "a2aExecutor":
3067
3162
  content = renderSettingsA2aRelayPage(context);
3068
3163
  break;
3069
3164
  default:
@@ -3334,6 +3429,21 @@ function renderSettingsA2aRelayPage(context) {
3334
3429
  const profileUrl = `${relay.relayUrl}/${relay.userId}`;
3335
3430
  const userIdLink = `<a href="${escapeHtml(profileUrl)}" target="_blank" rel="noopener">${escapeHtml(relay.userId)}</a>`;
3336
3431
  const relayHost = (() => { try { return new URL(relay.relayUrl).host; } catch { return relay.relayUrl; } })();
3432
+ const publicChecked = relay.acceptPublicTasks === true;
3433
+
3434
+ // Executor preference section
3435
+ const executors = state.session?.a2aExecutors || { codex: false, claude: false };
3436
+ const currentExec = state.session?.a2aExecutorPreference || "ask";
3437
+ const bothAvailable = executors.codex && executors.claude;
3438
+ const execOptions = [
3439
+ { id: "ask", label: L("settings.a2aExecutor.ask") },
3440
+ ];
3441
+ if (executors.codex) execOptions.push({ id: "codex", label: L("settings.a2aExecutor.codex"), detected: true });
3442
+ if (executors.claude) execOptions.push({ id: "claude", label: L("settings.a2aExecutor.claude"), detected: true });
3443
+ if (bothAvailable) {
3444
+ execOptions.push({ id: "auto", label: L("settings.a2aExecutor.auto") });
3445
+ }
3446
+
3337
3447
  return `
3338
3448
  <div class="settings-page">
3339
3449
  ${renderSettingsGroup("", [
@@ -3345,6 +3455,28 @@ function renderSettingsA2aRelayPage(context) {
3345
3455
  ? renderSettingsInfoRow(L("settings.row.a2aLastPoll"), new Date(relay.lastPollAtMs).toLocaleString(state.locale))
3346
3456
  : "",
3347
3457
  ].filter(Boolean))}
3458
+ <section class="settings-group">
3459
+ <p class="settings-group__title">${escapeHtml(L("settings.a2aRelay.publicTasks.title"))}</p>
3460
+ <label class="reply-mode-switch reply-mode-switch--settings" data-a2a-public-toggle>
3461
+ <input type="checkbox" class="reply-mode-switch__input" ${publicChecked ? "checked" : ""} data-a2a-public-checkbox />
3462
+ <span class="reply-mode-switch--settings__toggle">
3463
+ <span class="reply-mode-switch__track" aria-hidden="true"><span class="reply-mode-switch__thumb"></span></span>
3464
+ <span class="reply-mode-switch__state">${escapeHtml(publicChecked ? L("settings.claudeAway.on") : L("settings.claudeAway.off"))}</span>
3465
+ </span>
3466
+ <span class="reply-mode-switch__hint">${escapeHtml(L("settings.a2aRelay.publicTasks.description"))}</span>
3467
+ </label>
3468
+ </section>
3469
+ ${renderSettingsGroup(L("settings.a2aExecutor.title"), [
3470
+ `<p class="settings-group__description">${escapeHtml(L("settings.a2aExecutor.copy"))}</p>`,
3471
+ ...execOptions.map((opt) => `
3472
+ <label class="settings-radio-row" data-a2a-executor-option="${escapeHtml(opt.id)}">
3473
+ <input type="radio" name="a2aExecutor" value="${escapeHtml(opt.id)}"
3474
+ ${currentExec === opt.id ? "checked" : ""} />
3475
+ <span class="settings-radio-row__label">${escapeHtml(opt.label)}</span>
3476
+ ${opt.detected ? `<span class="settings-radio-row__badge">✓ ${escapeHtml(L("settings.a2aExecutor.detected"))}</span>` : ""}
3477
+ </label>
3478
+ `),
3479
+ ])}
3348
3480
  </div>
3349
3481
  `;
3350
3482
  }
@@ -3455,8 +3587,8 @@ function renderDetailContent(detail, { mobile }) {
3455
3587
  }
3456
3588
 
3457
3589
  function renderStandardDetailDesktop(detail) {
3458
- const kindInfo = kindMeta(detail.kind);
3459
- const spaciousBodyDetail = TIMELINE_MESSAGE_KINDS.has(detail.kind) || detail.kind === "completion";
3590
+ const kindInfo = kindMeta(detail.kind, detail);
3591
+ const spaciousBodyDetail = TIMELINE_MESSAGE_KINDS.has(detail.kind);
3460
3592
  const plainIntro = renderDetailPlainIntro(detail);
3461
3593
  return `
3462
3594
  <div class="detail-shell">
@@ -3470,7 +3602,7 @@ function renderStandardDetailDesktop(detail) {
3470
3602
  ${renderA2ATaskDetail(detail)}
3471
3603
  ${renderThreadShareDetail(detail)}
3472
3604
  ${
3473
- detail.kind === "moltbook_draft" || detail.kind === "moltbook_reply" || detail.kind === "a2a_task" || detail.kind === "thread_share"
3605
+ detail.kind === "moltbook_draft" || detail.kind === "moltbook_reply" || detail.kind === "a2a_task" || detail.kind === "a2a_task_result" || detail.kind === "thread_share"
3474
3606
  ? ""
3475
3607
  : plainIntro
3476
3608
  ? plainIntro
@@ -3493,8 +3625,8 @@ function renderStandardDetailDesktop(detail) {
3493
3625
  }
3494
3626
 
3495
3627
  function renderStandardDetailMobile(detail) {
3496
- const kindInfo = kindMeta(detail.kind);
3497
- const spaciousBodyDetail = TIMELINE_MESSAGE_KINDS.has(detail.kind) || detail.kind === "completion";
3628
+ const kindInfo = kindMeta(detail.kind, detail);
3629
+ const spaciousBodyDetail = TIMELINE_MESSAGE_KINDS.has(detail.kind);
3498
3630
  const plainIntro = renderDetailPlainIntro(detail, { mobile: true });
3499
3631
  return `
3500
3632
  <div class="mobile-detail-screen">
@@ -3508,7 +3640,7 @@ function renderStandardDetailMobile(detail) {
3508
3640
  ${renderA2ATaskDetail(detail, { mobile: true })}
3509
3641
  ${renderThreadShareDetail(detail, { mobile: true })}
3510
3642
  ${
3511
- detail.kind === "moltbook_draft" || detail.kind === "moltbook_reply" || detail.kind === "a2a_task" || detail.kind === "thread_share"
3643
+ detail.kind === "moltbook_draft" || detail.kind === "moltbook_reply" || detail.kind === "a2a_task" || detail.kind === "a2a_task_result" || detail.kind === "thread_share"
3512
3644
  ? ""
3513
3645
  : plainIntro
3514
3646
  ? plainIntro
@@ -4066,7 +4198,7 @@ function renderMoltbookDraftComposer(detail, options = {}) {
4066
4198
  }
4067
4199
 
4068
4200
  function renderA2ATaskDetail(detail, options = {}) {
4069
- if (detail.kind !== "a2a_task") return "";
4201
+ if (detail.kind !== "a2a_task" && detail.kind !== "a2a_task_result") return "";
4070
4202
  const enabled = detail.a2aTaskEnabled !== false && detail.readOnly !== true;
4071
4203
  const instruction = detail.instruction || "";
4072
4204
  const callerIp = detail.callerInfo?.ip || "";
@@ -4084,10 +4216,32 @@ function renderA2ATaskDetail(detail, options = {}) {
4084
4216
  rejected: "a2a.task.statusRejected",
4085
4217
  }[detail.taskStatus] || "a2a.task.statusSubmitted";
4086
4218
 
4087
- const statusBadge = !enabled
4219
+ const statusBadge = !enabled && detail.kind === "a2a_task_result"
4088
4220
  ? `<span class="eyebrow-pill eyebrow-pill--subtle">${escapeHtml(L(statusKey))}</span>`
4089
4221
  : "";
4090
4222
 
4223
+ // Show executor selector when "ask" mode is active and both CLIs are available.
4224
+ const executors = state.session?.a2aExecutors || { codex: false, claude: false };
4225
+ const executorPref = state.session?.a2aExecutorPreference || "auto";
4226
+ const showExecutorPicker = enabled && executorPref === "ask" && executors.codex && executors.claude;
4227
+ const executorPicker = showExecutorPicker
4228
+ ? `
4229
+ <div class="reply-composer__instruction">
4230
+ <label class="field-label">${escapeHtml(L("a2a.task.executor"))}</label>
4231
+ <div class="a2a-executor-picker">
4232
+ <label class="a2a-executor-picker__option">
4233
+ <input type="radio" name="executor" value="codex" checked />
4234
+ <span>${escapeHtml(L("a2a.executor.codex"))}</span>
4235
+ </label>
4236
+ <label class="a2a-executor-picker__option">
4237
+ <input type="radio" name="executor" value="claude" />
4238
+ <span>${escapeHtml(L("a2a.executor.claude"))}</span>
4239
+ </label>
4240
+ </div>
4241
+ </div>
4242
+ `
4243
+ : "";
4244
+
4091
4245
  const buttons = enabled
4092
4246
  ? `
4093
4247
  <div class="actions actions--stack${options.mobile ? " actions--sticky" : ""}">
@@ -4111,6 +4265,13 @@ function renderA2ATaskDetail(detail, options = {}) {
4111
4265
  <label class="field-label">${escapeHtml(L("a2a.task.instruction"))}</label>
4112
4266
  <textarea name="instruction" class="reply-composer__textarea" rows="6" ${enabled ? "" : "readonly"}>${escapeHtml(instruction)}</textarea>
4113
4267
  </div>
4268
+ ${executorPicker}
4269
+ ${!enabled && detail.messageText ? `
4270
+ <div class="reply-composer__instruction">
4271
+ <label class="field-label">${escapeHtml(L("a2a.task.response"))}</label>
4272
+ <pre class="a2a-task-response">${escapeHtml(detail.messageText)}</pre>
4273
+ </div>
4274
+ ` : ""}
4114
4275
  ${buttonsWrapped}
4115
4276
  </form>
4116
4277
  </section>
@@ -4168,7 +4329,7 @@ function renderThreadShareDetail(detail, options = {}) {
4168
4329
  }
4169
4330
 
4170
4331
  function renderCompletionReplyComposer(detail, options = {}) {
4171
- if (detail.kind !== "completion" || detail.reply?.enabled !== true) {
4332
+ if ((detail.kind !== "completion" && detail.kind !== "assistant_final") || detail.reply?.enabled !== true) {
4172
4333
  return "";
4173
4334
  }
4174
4335
 
@@ -4238,7 +4399,7 @@ function renderCompletionReplyComposer(detail, options = {}) {
4238
4399
  </div>
4239
4400
  `
4240
4401
  : `
4241
- <form class="reply-composer__form" data-completion-reply-form data-token="${escapeHtml(detail.token)}" data-provider="${escapeHtml(normalizeProviderClient(detail?.provider))}">
4402
+ <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
4403
  <label class="field reply-field">
4243
4404
  <span class="field-label">${escapeHtml(L("reply.fieldLabel"))}</span>
4244
4405
  <div class="reply-field__shell">
@@ -4816,6 +4977,11 @@ function bindShellInteractions() {
4816
4977
  return;
4817
4978
  }
4818
4979
  state.providerFilter = next;
4980
+ // Reset thread/kind filters — each provider has its own set of valid options
4981
+ state.timelineThreadFilter = "all";
4982
+ state.timelineKindFilter = "all";
4983
+ state.timelineKindFilterOpen = false;
4984
+ state.completedThreadFilter = "all";
4819
4985
  alignCurrentItemToVisibleEntries();
4820
4986
  await renderShell();
4821
4987
  });
@@ -5161,6 +5327,33 @@ function bindShellInteractions() {
5161
5327
  });
5162
5328
  }
5163
5329
 
5330
+ for (const checkbox of document.querySelectorAll("[data-a2a-public-checkbox]")) {
5331
+ checkbox.addEventListener("change", async () => {
5332
+ const next = checkbox.checked === true;
5333
+ try {
5334
+ await apiPost("/api/a2a/public-tasks", { accept: next });
5335
+ state.a2aRelayStatus = await apiGet("/api/a2a/relay-status");
5336
+ } catch (error) {
5337
+ state.pushError = error.message || String(error);
5338
+ }
5339
+ await renderShell();
5340
+ });
5341
+ }
5342
+
5343
+ for (const radio of document.querySelectorAll("[data-a2a-executor-option] input[type='radio']")) {
5344
+ radio.addEventListener("change", async () => {
5345
+ if (!radio.checked) return;
5346
+ const preference = radio.value || "auto";
5347
+ try {
5348
+ await apiPost("/api/settings/a2a-executor", { preference });
5349
+ await refreshSession();
5350
+ } catch (error) {
5351
+ state.pushError = error.message || String(error);
5352
+ }
5353
+ await renderShell();
5354
+ });
5355
+ }
5356
+
5164
5357
  for (const button of document.querySelectorAll("[data-locale-option]")) {
5165
5358
  button.addEventListener("click", async () => {
5166
5359
  state.pushError = "";
@@ -5322,12 +5515,16 @@ function bindShellInteractions() {
5322
5515
  });
5323
5516
  if (textarea) textarea.readOnly = true;
5324
5517
  const instruction = normalizeClientText(new FormData(a2aTaskForm).get("instruction"));
5518
+ const executorRadio = a2aTaskForm.querySelector("input[name='executor']:checked");
5519
+ const executor = executorRadio ? executorRadio.value : "";
5325
5520
  try {
5521
+ const decisionBody = { action: submittedAction, instruction };
5522
+ if (executor) decisionBody.executor = executor;
5326
5523
  const res = await fetch(`/api/items/a2a-task/${encodeURIComponent(token)}/decision`, {
5327
5524
  method: "POST",
5328
5525
  headers: { "content-type": "application/json" },
5329
5526
  credentials: "same-origin",
5330
- body: JSON.stringify({ action: submittedAction, instruction }),
5527
+ body: JSON.stringify(decisionBody),
5331
5528
  });
5332
5529
  if (!res.ok) {
5333
5530
  const errBody = await res.json().catch(() => ({}));
@@ -5482,7 +5679,8 @@ function bindShellInteractions() {
5482
5679
  requestBody.append("image", attachment.file, attachment.name || attachment.file.name);
5483
5680
  }
5484
5681
  }
5485
- await apiPost(`/api/items/completion/${encodeURIComponent(token)}/reply`, requestBody);
5682
+ const replyKind = replyForm.dataset.replyKind || "completion";
5683
+ await apiPost(`/api/items/${encodeURIComponent(replyKind)}/${encodeURIComponent(token)}/reply`, requestBody);
5486
5684
  setCompletionReplyDraft(token, {
5487
5685
  text: "",
5488
5686
  sentText: text,
@@ -5880,10 +6078,10 @@ function inboxSubtabForItemKind(kind, sourceSubtab = "") {
5880
6078
  if (normalizeClientText(sourceSubtab || "") === "completed") {
5881
6079
  return "completed";
5882
6080
  }
5883
- return kind === "completion" ? "completed" : "pending";
6081
+ return kind === "completion" || kind === "assistant_final" ? "completed" : "pending";
5884
6082
  }
5885
6083
 
5886
- function kindMeta(kind) {
6084
+ function kindMeta(kind, item) {
5887
6085
  switch (kind) {
5888
6086
  case "user_message":
5889
6087
  return { label: L("common.userMessage"), tone: "neutral", icon: "user-message" };
@@ -5905,10 +6103,17 @@ function kindMeta(kind) {
5905
6103
  case "file_event":
5906
6104
  return { label: L("common.fileEvent"), tone: "neutral", icon: "file-event" };
5907
6105
  case "moltbook_reply":
6106
+ return { label: L("common.moltbookReply"), tone: "neutral", icon: "moltbook-comment" };
5908
6107
  case "moltbook_draft":
5909
- return { label: L("common.sns"), tone: "neutral", icon: "item" };
6108
+ return item?.draftType === "reply"
6109
+ ? { label: L("common.moltbookDraftReply"), tone: "neutral", icon: "moltbook-reply" }
6110
+ : { label: L("common.moltbookDraft"), tone: "neutral", icon: "moltbook-draft" };
5910
6111
  case "thread_share":
5911
6112
  return { label: L("common.threadShare"), tone: "neutral", icon: "link" };
6113
+ case "a2a_task":
6114
+ return { label: L("common.a2aTaskRequest"), tone: "neutral", icon: "item" };
6115
+ case "a2a_task_result":
6116
+ return { label: L("common.a2aTaskResult"), tone: "completion", icon: "completion-item" };
5912
6117
  default:
5913
6118
  return { label: L("common.item"), tone: "neutral", icon: "item" };
5914
6119
  }
@@ -6263,6 +6468,8 @@ function renderIcon(name) {
6263
6468
  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
6469
  case "moltbook-draft":
6265
6470
  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>`;
6471
+ case "moltbook-reply":
6472
+ 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
6473
  case "moltbook-comment":
6267
6474
  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
6475
  case "filter":