viveworker 0.4.6 → 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.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);
@@ -993,6 +1050,9 @@ function shouldDeferRenderForActiveInteraction() {
993
1050
  if (state.currentDetail?.kind === "moltbook_draft") {
994
1051
  return true;
995
1052
  }
1053
+ if (state.currentDetail?.kind === "thread_share" && state.currentDetail?.threadShareEnabled) {
1054
+ return true;
1055
+ }
996
1056
  if (
997
1057
  activeElement instanceof HTMLTextAreaElement &&
998
1058
  activeElement.matches("[data-claude-question-note]")
@@ -1372,7 +1432,6 @@ function buildDetailLoadingSnapshot(itemRef = state.currentItem) {
1372
1432
  readOnly:
1373
1433
  entry?.status === "completed" ||
1374
1434
  TIMELINE_MESSAGE_KINDS.has(itemRef.kind) ||
1375
- itemRef.kind === "completion" ||
1376
1435
  (itemRef.kind === "choice" && item.supported === false),
1377
1436
  loading: true,
1378
1437
  };
@@ -1776,11 +1835,13 @@ function renderProviderFilter() {
1776
1835
  `;
1777
1836
  }
1778
1837
 
1838
+ const COMPLETED_CARD_KINDS = new Set(["assistant_final", "approval", "moltbook_reply", "moltbook_draft", "a2a_task_result", "thread_share"]);
1839
+
1779
1840
  function renderItemCard(entry, sourceTab, desktop) {
1780
- if (entry.status === "completed" && entry.item.kind === "completion") {
1841
+ if (entry.status === "completed" && COMPLETED_CARD_KINDS.has(entry.item.kind)) {
1781
1842
  return renderCompletedCompletionCard(entry, sourceTab);
1782
1843
  }
1783
- const kindInfo = kindMeta(entry.item.kind);
1844
+ const kindInfo = kindMeta(entry.item.kind, entry.item);
1784
1845
  const cardTitle = cardTitleForEntry(entry);
1785
1846
  const statusText = entry.status === "completed" ? L("common.completed") : L("common.actionNeeded");
1786
1847
  const intentText = itemIntentText(entry.item.kind, entry.status, entry.item.provider);
@@ -1849,10 +1910,12 @@ function cardTitleForEntry(entry) {
1849
1910
 
1850
1911
  function renderCompletedCompletionCard(entry, sourceTab) {
1851
1912
  const item = entry.item;
1852
- const kindInfo = kindMeta(item.kind);
1913
+ const kindInfo = kindMeta(item.kind, item);
1853
1914
  const summaryText = item.summary || fallbackSummaryForKind(item.kind, entry.status, item.provider);
1854
1915
  const threadLabel = timelineEntryThreadLabel(item, true);
1855
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;
1856
1919
 
1857
1920
  return `
1858
1921
  <button
@@ -1864,7 +1927,7 @@ function renderCompletedCompletionCard(entry, sourceTab) {
1864
1927
  >
1865
1928
  <div class="item-card__header">
1866
1929
  <div class="item-card__meta">
1867
- <span class="type-pill type-pill--completion">${escapeHtml(L("common.task"))}</span>
1930
+ <span class="type-pill type-pill--${escapeHtml(pillTone)}">${escapeHtml(pillLabel)}</span>
1868
1931
  ${renderProviderBadge(item.provider)}
1869
1932
  </div>
1870
1933
  <div class="item-card__header-right">
@@ -1954,17 +2017,47 @@ function renderDiffPanel({ entries, desktop }) {
1954
2017
  `;
1955
2018
  }
1956
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
+
1957
2039
  function renderTimelineThreadDropdown() {
1958
- 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
+
1959
2055
  return renderThreadDropdown({
1960
2056
  inputId: "timeline-thread-select",
1961
2057
  dataAttribute: "data-timeline-thread-select",
1962
2058
  selectedThreadId: state.timelineThreadFilter,
1963
- controlsHtml: renderTimelineKindFilterControls(),
1964
- threads: threads.map((thread) => ({
1965
- id: thread.id,
1966
- label: dropdownThreadLabel(thread.id, thread.label || ""),
1967
- })),
2059
+ controlsHtml: kindFilterHtml,
2060
+ threads,
1968
2061
  });
1969
2062
  }
1970
2063
 
@@ -2069,10 +2162,11 @@ function renderTimelineKindFilterControls() {
2069
2162
 
2070
2163
  function renderTimelineEntry(entry, { desktop }) {
2071
2164
  const item = entry.item;
2072
- const kindInfo = kindMeta(item.kind);
2165
+ const kindInfo = kindMeta(item.kind, item);
2073
2166
  const kindClassName = escapeHtml(kindInfo.tone || "neutral");
2074
2167
  const kindNameClass = escapeHtml(String(item.kind || "item").replace(/_/gu, "-"));
2075
- 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;
2076
2170
  const isFileEvent = item.kind === "file_event";
2077
2171
  const imageUrls = Array.isArray(item.imageUrls) ? item.imageUrls.filter(Boolean) : [];
2078
2172
  const fileRefs = normalizeClientFileRefs(item.fileRefs);
@@ -2929,6 +3023,9 @@ function settingsPageMeta(page) {
2929
3023
  description: L("settings.a2aRelay.copy"),
2930
3024
  icon: "link",
2931
3025
  };
3026
+ case "a2aExecutor":
3027
+ // Executor settings integrated into a2aRelay page — redirect.
3028
+ return settingsPageMeta("a2aRelay");
2932
3029
  default:
2933
3030
  return settingsPageMeta("notifications");
2934
3031
  }
@@ -3061,6 +3158,7 @@ function renderSettingsSubpage(context, { mobile }) {
3061
3158
  content = renderSettingsMoltbookPage(context);
3062
3159
  break;
3063
3160
  case "a2aRelay":
3161
+ case "a2aExecutor":
3064
3162
  content = renderSettingsA2aRelayPage(context);
3065
3163
  break;
3066
3164
  default:
@@ -3331,6 +3429,21 @@ function renderSettingsA2aRelayPage(context) {
3331
3429
  const profileUrl = `${relay.relayUrl}/${relay.userId}`;
3332
3430
  const userIdLink = `<a href="${escapeHtml(profileUrl)}" target="_blank" rel="noopener">${escapeHtml(relay.userId)}</a>`;
3333
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
+
3334
3447
  return `
3335
3448
  <div class="settings-page">
3336
3449
  ${renderSettingsGroup("", [
@@ -3342,6 +3455,28 @@ function renderSettingsA2aRelayPage(context) {
3342
3455
  ? renderSettingsInfoRow(L("settings.row.a2aLastPoll"), new Date(relay.lastPollAtMs).toLocaleString(state.locale))
3343
3456
  : "",
3344
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
+ ])}
3345
3480
  </div>
3346
3481
  `;
3347
3482
  }
@@ -3452,21 +3587,22 @@ function renderDetailContent(detail, { mobile }) {
3452
3587
  }
3453
3588
 
3454
3589
  function renderStandardDetailDesktop(detail) {
3455
- const kindInfo = kindMeta(detail.kind);
3456
- 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);
3457
3592
  const plainIntro = renderDetailPlainIntro(detail);
3458
3593
  return `
3459
3594
  <div class="detail-shell">
3460
3595
  ${renderDetailMetaRow(detail, kindInfo)}
3461
3596
  <h2 class="detail-title detail-title--desktop">${renderDetailTitle(detail)}</h2>
3462
- ${detail.readOnly || detail.kind === "approval" || detail.kind === "moltbook_draft" || detail.kind === "moltbook_reply" ? "" : renderDetailLead(detail, kindInfo)}
3597
+ ${detail.readOnly || detail.kind === "approval" || detail.kind === "moltbook_draft" || detail.kind === "moltbook_reply" || detail.kind === "thread_share" ? "" : renderDetailLead(detail, kindInfo)}
3463
3598
  ${renderPreviousContextCard(detail)}
3464
3599
  ${renderInterruptedDetailNotice(detail)}
3465
3600
  ${renderMoltbookReplyComposer(detail)}
3466
3601
  ${renderMoltbookDraftComposer(detail)}
3467
3602
  ${renderA2ATaskDetail(detail)}
3603
+ ${renderThreadShareDetail(detail)}
3468
3604
  ${
3469
- detail.kind === "moltbook_draft" || detail.kind === "moltbook_reply" || detail.kind === "a2a_task"
3605
+ detail.kind === "moltbook_draft" || detail.kind === "moltbook_reply" || detail.kind === "a2a_task" || detail.kind === "a2a_task_result" || detail.kind === "thread_share"
3470
3606
  ? ""
3471
3607
  : plainIntro
3472
3608
  ? plainIntro
@@ -3489,8 +3625,8 @@ function renderStandardDetailDesktop(detail) {
3489
3625
  }
3490
3626
 
3491
3627
  function renderStandardDetailMobile(detail) {
3492
- const kindInfo = kindMeta(detail.kind);
3493
- 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);
3494
3630
  const plainIntro = renderDetailPlainIntro(detail, { mobile: true });
3495
3631
  return `
3496
3632
  <div class="mobile-detail-screen">
@@ -3502,8 +3638,9 @@ function renderStandardDetailMobile(detail) {
3502
3638
  ${renderMoltbookReplyComposer(detail, { mobile: true })}
3503
3639
  ${renderMoltbookDraftComposer(detail, { mobile: true })}
3504
3640
  ${renderA2ATaskDetail(detail, { mobile: true })}
3641
+ ${renderThreadShareDetail(detail, { mobile: true })}
3505
3642
  ${
3506
- detail.kind === "moltbook_draft" || detail.kind === "moltbook_reply" || detail.kind === "a2a_task"
3643
+ detail.kind === "moltbook_draft" || detail.kind === "moltbook_reply" || detail.kind === "a2a_task" || detail.kind === "a2a_task_result" || detail.kind === "thread_share"
3507
3644
  ? ""
3508
3645
  : plainIntro
3509
3646
  ? plainIntro
@@ -4061,7 +4198,7 @@ function renderMoltbookDraftComposer(detail, options = {}) {
4061
4198
  }
4062
4199
 
4063
4200
  function renderA2ATaskDetail(detail, options = {}) {
4064
- if (detail.kind !== "a2a_task") return "";
4201
+ if (detail.kind !== "a2a_task" && detail.kind !== "a2a_task_result") return "";
4065
4202
  const enabled = detail.a2aTaskEnabled !== false && detail.readOnly !== true;
4066
4203
  const instruction = detail.instruction || "";
4067
4204
  const callerIp = detail.callerInfo?.ip || "";
@@ -4079,10 +4216,32 @@ function renderA2ATaskDetail(detail, options = {}) {
4079
4216
  rejected: "a2a.task.statusRejected",
4080
4217
  }[detail.taskStatus] || "a2a.task.statusSubmitted";
4081
4218
 
4082
- const statusBadge = !enabled
4219
+ const statusBadge = !enabled && detail.kind === "a2a_task_result"
4083
4220
  ? `<span class="eyebrow-pill eyebrow-pill--subtle">${escapeHtml(L(statusKey))}</span>`
4084
4221
  : "";
4085
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
+
4086
4245
  const buttons = enabled
4087
4246
  ? `
4088
4247
  <div class="actions actions--stack${options.mobile ? " actions--sticky" : ""}">
@@ -4106,6 +4265,63 @@ function renderA2ATaskDetail(detail, options = {}) {
4106
4265
  <label class="field-label">${escapeHtml(L("a2a.task.instruction"))}</label>
4107
4266
  <textarea name="instruction" class="reply-composer__textarea" rows="6" ${enabled ? "" : "readonly"}>${escapeHtml(instruction)}</textarea>
4108
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
+ ` : ""}
4275
+ ${buttonsWrapped}
4276
+ </form>
4277
+ </section>
4278
+ `;
4279
+ }
4280
+
4281
+ function renderThreadShareDetail(detail, options = {}) {
4282
+ if (detail.kind !== "thread_share") return "";
4283
+ const enabled = detail.threadShareEnabled !== false && detail.readOnly !== true;
4284
+ const content = detail.shareContent || "";
4285
+ const fromLabel = detail.sourceLabel || detail.sourceTool || "agent";
4286
+ const toLabel = detail.targetLabel || detail.targetConversationId?.slice(0, 8) || detail.targetTool || "thread";
4287
+ const contextFiles = Array.isArray(detail.contextFiles) ? detail.contextFiles : [];
4288
+ const shareTypeLabel = {
4289
+ plan_review: L("threadShare.type.planReview"),
4290
+ handoff: L("threadShare.type.handoff"),
4291
+ message: L("threadShare.type.message"),
4292
+ }[detail.shareType] || L("threadShare.type.message");
4293
+
4294
+ const contextFilesHtml = contextFiles.length > 0
4295
+ ? `<div class="reply-composer__instruction">
4296
+ <label class="field-label">${escapeHtml(L("threadShare.contextFiles"))}</label>
4297
+ <ul class="context-files-list">${contextFiles.map((f) => `<li class="context-file-item"><code>${escapeHtml(f)}</code></li>`).join("")}</ul>
4298
+ </div>`
4299
+ : "";
4300
+
4301
+ const buttons = enabled
4302
+ ? `
4303
+ <div class="actions actions--stack${options.mobile ? " actions--sticky" : ""}">
4304
+ <button type="submit" data-action="approve" class="primary primary--wide">${escapeHtml(L("threadShare.approve"))}</button>
4305
+ <button type="submit" data-action="deny" class="danger danger--wide">${escapeHtml(L("threadShare.deny"))}</button>
4306
+ </div>
4307
+ `
4308
+ : `<p class="muted reply-composer__description">${escapeHtml(L("threadShare.resolved"))}</p>`;
4309
+ const buttonsWrapped = enabled && options.mobile ? `<div class="detail-action-bar">${buttons}</div>` : buttons;
4310
+
4311
+ return `
4312
+ <section class="detail-card detail-card--reply ${options.mobile ? "detail-card--mobile" : ""}">
4313
+ <form class="reply-composer" data-thread-share-form data-token="${escapeHtml(detail.token || "")}">
4314
+ <div class="reply-composer__copy">
4315
+ <span class="eyebrow-pill eyebrow-pill--quiet">${escapeHtml(L("threadShare.eyebrow"))}</span>
4316
+ <span class="eyebrow-pill eyebrow-pill--subtle">${escapeHtml(shareTypeLabel)}</span>
4317
+ <p class="reply-composer__author-meta muted">${escapeHtml(fromLabel)} → ${escapeHtml(toLabel)}</p>
4318
+ <p class="muted reply-composer__description">${enabled ? escapeHtml(L("threadShare.editHint")) : ""}</p>
4319
+ </div>
4320
+ ${contextFilesHtml}
4321
+ <div class="reply-composer__instruction">
4322
+ <label class="field-label">${escapeHtml(L("threadShare.content"))}</label>
4323
+ <textarea name="shareContent" class="reply-composer__textarea" rows="12" ${enabled ? "" : "readonly"}>${escapeHtml(content)}</textarea>
4324
+ </div>
4109
4325
  ${buttonsWrapped}
4110
4326
  </form>
4111
4327
  </section>
@@ -4113,7 +4329,7 @@ function renderA2ATaskDetail(detail, options = {}) {
4113
4329
  }
4114
4330
 
4115
4331
  function renderCompletionReplyComposer(detail, options = {}) {
4116
- if (detail.kind !== "completion" || detail.reply?.enabled !== true) {
4332
+ if ((detail.kind !== "completion" && detail.kind !== "assistant_final") || detail.reply?.enabled !== true) {
4117
4333
  return "";
4118
4334
  }
4119
4335
 
@@ -4183,7 +4399,7 @@ function renderCompletionReplyComposer(detail, options = {}) {
4183
4399
  </div>
4184
4400
  `
4185
4401
  : `
4186
- <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)}">
4187
4403
  <label class="field reply-field">
4188
4404
  <span class="field-label">${escapeHtml(L("reply.fieldLabel"))}</span>
4189
4405
  <div class="reply-field__shell">
@@ -4761,6 +4977,11 @@ function bindShellInteractions() {
4761
4977
  return;
4762
4978
  }
4763
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";
4764
4985
  alignCurrentItemToVisibleEntries();
4765
4986
  await renderShell();
4766
4987
  });
@@ -5106,6 +5327,33 @@ function bindShellInteractions() {
5106
5327
  });
5107
5328
  }
5108
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
+
5109
5357
  for (const button of document.querySelectorAll("[data-locale-option]")) {
5110
5358
  button.addEventListener("click", async () => {
5111
5359
  state.pushError = "";
@@ -5267,12 +5515,16 @@ function bindShellInteractions() {
5267
5515
  });
5268
5516
  if (textarea) textarea.readOnly = true;
5269
5517
  const instruction = normalizeClientText(new FormData(a2aTaskForm).get("instruction"));
5518
+ const executorRadio = a2aTaskForm.querySelector("input[name='executor']:checked");
5519
+ const executor = executorRadio ? executorRadio.value : "";
5270
5520
  try {
5521
+ const decisionBody = { action: submittedAction, instruction };
5522
+ if (executor) decisionBody.executor = executor;
5271
5523
  const res = await fetch(`/api/items/a2a-task/${encodeURIComponent(token)}/decision`, {
5272
5524
  method: "POST",
5273
5525
  headers: { "content-type": "application/json" },
5274
5526
  credentials: "same-origin",
5275
- body: JSON.stringify({ action: submittedAction, instruction }),
5527
+ body: JSON.stringify(decisionBody),
5276
5528
  });
5277
5529
  if (!res.ok) {
5278
5530
  const errBody = await res.json().catch(() => ({}));
@@ -5305,6 +5557,72 @@ function bindShellInteractions() {
5305
5557
  });
5306
5558
  }
5307
5559
 
5560
+ const threadShareForm = document.querySelector("[data-thread-share-form]");
5561
+ if (threadShareForm) {
5562
+ const token = threadShareForm.dataset.token || "";
5563
+ let submittedAction = "approve";
5564
+ threadShareForm.querySelectorAll("button[type='submit']").forEach((btn) => {
5565
+ btn.addEventListener("click", () => {
5566
+ submittedAction = btn.dataset.action || "approve";
5567
+ });
5568
+ });
5569
+ threadShareForm.addEventListener("submit", async (event) => {
5570
+ event.preventDefault();
5571
+ if (threadShareForm.dataset.submitting === "1") return;
5572
+ threadShareForm.dataset.submitting = "1";
5573
+ const buttons = threadShareForm.querySelectorAll("button[type='submit']");
5574
+ const textarea = threadShareForm.querySelector("textarea");
5575
+ const labelCache = new Map();
5576
+ buttons.forEach((btn) => {
5577
+ labelCache.set(btn, btn.innerHTML);
5578
+ btn.disabled = true;
5579
+ if (btn.dataset.action === submittedAction) {
5580
+ btn.innerHTML = submittedAction === "approve" ? "Sharing…" : "Denying…";
5581
+ btn.classList.add("is-loading");
5582
+ } else {
5583
+ btn.classList.add("is-dimmed");
5584
+ }
5585
+ });
5586
+ if (textarea) textarea.readOnly = true;
5587
+ const editedContent = normalizeClientText(new FormData(threadShareForm).get("shareContent"));
5588
+ try {
5589
+ const res = await fetch(`/api/threads/share/${encodeURIComponent(token)}/decision`, {
5590
+ method: "POST",
5591
+ headers: { "content-type": "application/json" },
5592
+ credentials: "same-origin",
5593
+ body: JSON.stringify({ decision: submittedAction, editedContent }),
5594
+ });
5595
+ if (!res.ok) {
5596
+ const errBody = await res.json().catch(() => ({}));
5597
+ alert(`Thread share ${submittedAction} failed: ${errBody.error || res.status}`);
5598
+ buttons.forEach((btn) => {
5599
+ btn.disabled = false;
5600
+ btn.classList.remove("is-loading", "is-dimmed");
5601
+ btn.innerHTML = labelCache.get(btn);
5602
+ });
5603
+ if (textarea) textarea.readOnly = false;
5604
+ threadShareForm.dataset.submitting = "";
5605
+ return;
5606
+ }
5607
+ if (state.currentDetail?.kind === "thread_share") {
5608
+ state.currentDetail.threadShareEnabled = false;
5609
+ state.currentDetail.readOnly = true;
5610
+ }
5611
+ await refreshAuthenticatedState();
5612
+ await renderShell();
5613
+ } catch (error) {
5614
+ alert(`Thread share error: ${error.message}`);
5615
+ buttons.forEach((btn) => {
5616
+ btn.disabled = false;
5617
+ btn.classList.remove("is-loading", "is-dimmed");
5618
+ btn.innerHTML = labelCache.get(btn);
5619
+ });
5620
+ if (textarea) textarea.readOnly = false;
5621
+ threadShareForm.dataset.submitting = "";
5622
+ }
5623
+ });
5624
+ }
5625
+
5308
5626
  const replyForm = document.querySelector("[data-completion-reply-form]");
5309
5627
  if (replyForm) {
5310
5628
  const token = replyForm.dataset.token || "";
@@ -5361,7 +5679,8 @@ function bindShellInteractions() {
5361
5679
  requestBody.append("image", attachment.file, attachment.name || attachment.file.name);
5362
5680
  }
5363
5681
  }
5364
- 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);
5365
5684
  setCompletionReplyDraft(token, {
5366
5685
  text: "",
5367
5686
  sentText: text,
@@ -5759,10 +6078,10 @@ function inboxSubtabForItemKind(kind, sourceSubtab = "") {
5759
6078
  if (normalizeClientText(sourceSubtab || "") === "completed") {
5760
6079
  return "completed";
5761
6080
  }
5762
- return kind === "completion" ? "completed" : "pending";
6081
+ return kind === "completion" || kind === "assistant_final" ? "completed" : "pending";
5763
6082
  }
5764
6083
 
5765
- function kindMeta(kind) {
6084
+ function kindMeta(kind, item) {
5766
6085
  switch (kind) {
5767
6086
  case "user_message":
5768
6087
  return { label: L("common.userMessage"), tone: "neutral", icon: "user-message" };
@@ -5784,8 +6103,17 @@ function kindMeta(kind) {
5784
6103
  case "file_event":
5785
6104
  return { label: L("common.fileEvent"), tone: "neutral", icon: "file-event" };
5786
6105
  case "moltbook_reply":
6106
+ return { label: L("common.moltbookReply"), tone: "neutral", icon: "moltbook-comment" };
5787
6107
  case "moltbook_draft":
5788
- 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" };
6111
+ case "thread_share":
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" };
5789
6117
  default:
5790
6118
  return { label: L("common.item"), tone: "neutral", icon: "item" };
5791
6119
  }
@@ -6140,6 +6468,8 @@ function renderIcon(name) {
6140
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>`;
6141
6469
  case "moltbook-draft":
6142
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>`;
6143
6473
  case "moltbook-comment":
6144
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>`;
6145
6475
  case "filter":