viveworker 0.5.2 → 0.5.5

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
@@ -50,9 +50,12 @@ const state = {
50
50
  moltbookScoutStatus: null,
51
51
  moltbookRecentTitlesExpanded: 0,
52
52
  a2aRelayStatus: null,
53
+ a2aShareStatus: null,
54
+ a2aShareRecentExpanded: 0,
53
55
  a2aTaskExecutorPick: "codex",
54
56
  pushNotice: "",
55
57
  pushError: "",
58
+ ambientSuggestionCopyState: null,
56
59
  deviceNotice: "",
57
60
  deviceError: "",
58
61
  imageViewer: null,
@@ -218,6 +221,7 @@ async function refreshAuthenticatedState() {
218
221
  await refreshPushStatus();
219
222
  await fetchMoltbookScoutStatus();
220
223
  await fetchA2aRelayStatus();
224
+ await fetchA2aShareStatus();
221
225
  ensureCurrentSelection();
222
226
  }
223
227
 
@@ -347,6 +351,18 @@ async function fetchA2aRelayStatus() {
347
351
  }
348
352
  }
349
353
 
354
+ async function fetchA2aShareStatus() {
355
+ if (!state.session?.a2aShareEnabled) {
356
+ state.a2aShareStatus = null;
357
+ return;
358
+ }
359
+ try {
360
+ state.a2aShareStatus = await apiGet("/api/share/status");
361
+ } catch {
362
+ state.a2aShareStatus = null;
363
+ }
364
+ }
365
+
350
366
  async function getClientPushState() {
351
367
  const registration = state.serviceWorkerRegistration || (await navigator.serviceWorker?.ready.catch(() => null));
352
368
  if (registration) {
@@ -661,6 +677,7 @@ function timelineKindFilterOptions() {
661
677
  const codexClaudeOptions = [
662
678
  allOption,
663
679
  { id: "messages", label: L("timeline.kindFilter.messages"), icon: "timeline" },
680
+ { id: "suggestions", label: L("timeline.kindFilter.suggestions"), icon: "suggestions" },
664
681
  { id: "files", label: L("timeline.kindFilter.files"), icon: "file-event" },
665
682
  { id: "approvals", label: L("timeline.kindFilter.approvals"), icon: "approval" },
666
683
  { id: "plans", label: L("timeline.kindFilter.plans"), icon: "plan" },
@@ -695,6 +712,8 @@ function timelineEntryMatchesKindFilter(entry, filterId) {
695
712
  switch (filterId) {
696
713
  case "messages":
697
714
  return TIMELINE_MESSAGE_KINDS.has(kind);
715
+ case "suggestions":
716
+ return kind === "ambient_suggestions";
698
717
  case "files":
699
718
  return kind === "file_event";
700
719
  case "approvals":
@@ -2332,6 +2351,10 @@ function timelineEntryThreadLabel(item, isMessage) {
2332
2351
  }
2333
2352
 
2334
2353
  function timelineEntryPrimaryText(item, status, { isMessageLike = false, isFileEvent = false } = {}) {
2354
+ if (item?.kind === "ambient_suggestions") {
2355
+ return item.summary || fallbackSummaryForKind(item.kind, status, item.provider);
2356
+ }
2357
+
2335
2358
  if (isMessageLike) {
2336
2359
  return item.summary || fallbackSummaryForKind(item.kind, status, item.provider);
2337
2360
  }
@@ -2344,6 +2367,10 @@ function timelineEntryPrimaryText(item, status, { isMessageLike = false, isFileE
2344
2367
  }
2345
2368
 
2346
2369
  function timelineEntrySecondaryText(item, status, primaryText, { isMessageLike = false, isFileEvent = false } = {}) {
2370
+ if (item?.kind === "ambient_suggestions") {
2371
+ return "";
2372
+ }
2373
+
2347
2374
  if (isMessageLike) {
2348
2375
  return "";
2349
2376
  }
@@ -2865,6 +2892,7 @@ function buildSettingsContext() {
2865
2892
  }),
2866
2893
  moltbookScout: state.moltbookScoutStatus,
2867
2894
  a2aRelay: state.a2aRelayStatus,
2895
+ a2aShare: state.a2aShareStatus,
2868
2896
  };
2869
2897
  }
2870
2898
 
@@ -3025,6 +3053,13 @@ function settingsPageMeta(page) {
3025
3053
  description: L("settings.a2aRelay.copy"),
3026
3054
  icon: "link",
3027
3055
  };
3056
+ case "a2aShare":
3057
+ return {
3058
+ id: "a2aShare",
3059
+ title: L("settings.a2aShare.title"),
3060
+ description: L("settings.a2aShare.copy"),
3061
+ icon: "link",
3062
+ };
3028
3063
  case "a2aExecutor":
3029
3064
  // Executor settings integrated into a2aRelay page — redirect.
3030
3065
  return settingsPageMeta("a2aRelay");
@@ -3097,13 +3132,13 @@ function renderSettingsRoot(context, { mobile }) {
3097
3132
  `
3098
3133
  }
3099
3134
  ${renderSettingsGroup(L("settings.group.general"), generalRows)}
3100
- ${(state.session?.moltbookEnabled || state.session?.a2aRelayEnabled) ? renderSettingsGroup(L("settings.group.integrations"), [
3135
+ ${(state.session?.moltbookEnabled || state.session?.a2aRelayEnabled || state.session?.a2aShareEnabled) ? renderSettingsGroup(L("settings.group.integrations"), [
3101
3136
  state.session?.moltbookEnabled ? renderSettingsNavRow({
3102
3137
  page: "moltbook",
3103
3138
  icon: "item",
3104
3139
  title: L("settings.moltbook.title"),
3105
3140
  subtitle: L("settings.moltbook.subtitle"),
3106
- value: context.moltbookScout?.enabled ? `${context.moltbookScout.sentToday} / ${context.moltbookScout.maxDaily}` : "",
3141
+ value: context.moltbookScout?.enabled ? L("settings.status.enabled") : L("settings.status.disabled"),
3107
3142
  }) : "",
3108
3143
  state.session?.a2aRelayEnabled ? renderSettingsNavRow({
3109
3144
  page: "a2aRelay",
@@ -3112,6 +3147,13 @@ function renderSettingsRoot(context, { mobile }) {
3112
3147
  subtitle: L("settings.a2aRelay.subtitle"),
3113
3148
  value: context.a2aRelay?.connected ? L("settings.status.connected") : L("settings.a2aRelay.status.disconnected"),
3114
3149
  }) : "",
3150
+ state.session?.a2aShareEnabled ? renderSettingsNavRow({
3151
+ page: "a2aShare",
3152
+ icon: "link",
3153
+ title: L("settings.a2aShare.title"),
3154
+ subtitle: L("settings.a2aShare.subtitle"),
3155
+ value: context.a2aShare?.enabled ? L("settings.status.enabled") : L("settings.status.disabled"),
3156
+ }) : "",
3115
3157
  ].filter(Boolean)) : ""}
3116
3158
  ${renderSettingsGroup(L("settings.pairing.title"), deviceRows)}
3117
3159
  ${renderSettingsGroup(L("settings.group.advanced"), advancedRows)}
@@ -3163,6 +3205,9 @@ function renderSettingsSubpage(context, { mobile }) {
3163
3205
  case "a2aExecutor":
3164
3206
  content = renderSettingsA2aRelayPage(context);
3165
3207
  break;
3208
+ case "a2aShare":
3209
+ content = renderSettingsA2aSharePage(context);
3210
+ break;
3166
3211
  default:
3167
3212
  content = "";
3168
3213
  }
@@ -3392,8 +3437,16 @@ function renderSettingsMoltbookPage(context) {
3392
3437
  renderSettingsInfoRow(L("settings.row.moltbookBatchTopScore"), String(scout.batch.topScore)),
3393
3438
  renderSettingsInfoRow(L("settings.row.moltbookBatchRemaining"), `${Math.floor(scout.batch.remainingSeconds / 60)}:${String(scout.batch.remainingSeconds % 60).padStart(2, "0")}`),
3394
3439
  ] : [];
3440
+ const accountRow = scout.account?.name && scout.account?.profileUrl
3441
+ ? renderSettingsInfoRow(
3442
+ L("settings.row.moltbookAccount"),
3443
+ `<a href="${escapeHtml(scout.account.profileUrl)}" target="_blank" rel="noopener">${escapeHtml(scout.account.name)}</a>`,
3444
+ { rawValue: true }
3445
+ )
3446
+ : null;
3395
3447
  return `
3396
3448
  <div class="settings-page">
3449
+ ${accountRow ? renderSettingsGroup("", [accountRow]) : ""}
3397
3450
  ${renderSettingsGroup("", [
3398
3451
  renderSettingsInfoRow(L("settings.row.moltbookQuota"), `${scout.sentToday} / ${scout.maxDaily}`),
3399
3452
  renderSettingsInfoRow(L("settings.row.moltbookComposed"), `${scout.composedToday || 0} / 3`),
@@ -3417,7 +3470,19 @@ function renderSettingsMoltbookPage(context) {
3417
3470
  const link = postId
3418
3471
  ? `<a href="https://www.moltbook.com/post/${escapeHtml(postId)}" target="_blank" rel="noopener">${escapeHtml(title)}</a>`
3419
3472
  : escapeHtml(title);
3420
- return `<div class="settings-compose-entry">${badge}<span class="settings-compose-entry__title">${link}</span></div>`;
3473
+ const iconName = type === "reply" ? "moltbook-reply" : "moltbook-draft";
3474
+ const iconTone = type === "reply" ? "settings-icon-entry__icon--reply" : "settings-icon-entry__icon--post";
3475
+ return `
3476
+ <div class="settings-compose-entry settings-icon-entry">
3477
+ <span class="settings-icon-entry__icon ${iconTone}" aria-hidden="true">${renderIcon(iconName)}</span>
3478
+ <span class="settings-icon-entry__body">
3479
+ <span class="settings-icon-entry__title-row">
3480
+ <span class="settings-compose-entry__title">${link}</span>
3481
+ ${badge}
3482
+ </span>
3483
+ </span>
3484
+ </div>
3485
+ `;
3421
3486
  });
3422
3487
  if (hasMore) {
3423
3488
  const remaining = titles.length - visibleCount;
@@ -3445,7 +3510,7 @@ function renderSettingsA2aRelayPage(context) {
3445
3510
  : relay.polling
3446
3511
  ? L("settings.a2aRelay.status.polling")
3447
3512
  : L("settings.a2aRelay.status.disconnected");
3448
- const profileUrl = `${relay.relayUrl}/${relay.userId}`;
3513
+ const profileUrl = `${relay.relayUrl}/u/${relay.userId}`;
3449
3514
  const userIdLink = `<a href="${escapeHtml(profileUrl)}" target="_blank" rel="noopener">${escapeHtml(relay.userId)}</a>`;
3450
3515
  const relayHost = (() => { try { return new URL(relay.relayUrl).host; } catch { return relay.relayUrl; } })();
3451
3516
  const publicChecked = relay.acceptPublicTasks === true;
@@ -3465,20 +3530,22 @@ function renderSettingsA2aRelayPage(context) {
3465
3530
 
3466
3531
  return `
3467
3532
  <div class="settings-page">
3533
+ ${renderSettingsGroup("", [
3534
+ renderSettingsInfoRow(L("settings.row.a2aStatus"), statusLabel),
3535
+ renderSettingsInfoRow(L("settings.row.a2aUserId"), userIdLink, { rawValue: true }),
3536
+ renderSettingsInfoRow(L("settings.row.a2aRelay"), relayHost),
3537
+ renderSettingsInfoRow(L("settings.row.a2aApiKey"), relay.apiKeyConfigured ? L("settings.a2aRelay.apiKey.configured") : L("settings.a2aRelay.apiKey.notConfigured")),
3538
+ relay.lastPollAtMs
3539
+ ? renderSettingsInfoRow(L("settings.row.a2aLastPoll"), new Date(relay.lastPollAtMs).toLocaleString(state.locale))
3540
+ : "",
3541
+ ].filter(Boolean))}
3468
3542
  ${(() => {
3469
3543
  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
- : "",
3544
+ return renderSettingsGroup(L("settings.a2aRelay.taskStats.title"), [
3478
3545
  renderSettingsInfoRow(L("settings.row.a2aTaskReceived"), String(stats.received)),
3479
3546
  renderSettingsInfoRow(L("settings.row.a2aTaskCompleted"), String(stats.completed)),
3480
3547
  renderSettingsInfoRow(L("settings.row.a2aTaskDenied"), String(stats.denied)),
3481
- ].filter(Boolean));
3548
+ ]);
3482
3549
  })()}
3483
3550
  <section class="settings-group">
3484
3551
  <p class="settings-group__title">${escapeHtml(L("settings.a2aRelay.publicTasks.title"))}</p>
@@ -3506,6 +3573,151 @@ function renderSettingsA2aRelayPage(context) {
3506
3573
  `;
3507
3574
  }
3508
3575
 
3576
+ function renderSettingsA2aSharePage(context) {
3577
+ const share = context.a2aShare;
3578
+ if (!share?.enabled) {
3579
+ return `
3580
+ <div class="settings-page">
3581
+ <p class="settings-page-copy muted">${escapeHtml(L("settings.a2aShare.unavailable"))}</p>
3582
+ </div>
3583
+ `;
3584
+ }
3585
+ const quota = share.quota || { bytes: 0, maxBytes: 0, count: 0, maxCount: 0 };
3586
+ const limits = share.limits || {};
3587
+ const items = Array.isArray(share.items) ? share.items : [];
3588
+ const statusLabel = share.error
3589
+ ? L("settings.a2aShare.status.unreachable")
3590
+ : L("settings.status.enabled");
3591
+ const formatBytes = (bytes) => {
3592
+ if (!Number.isFinite(bytes)) return "—";
3593
+ if (bytes < 1024) return `${bytes} B`;
3594
+ if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`;
3595
+ return `${(bytes / (1024 * 1024)).toFixed(2)} MB`;
3596
+ };
3597
+ const storageValue = share.quota
3598
+ ? `${formatBytes(quota.bytes)} / ${formatBytes(quota.maxBytes || limits.maxTotalBytes || 0)}`
3599
+ : "—";
3600
+ const filesValue = share.quota
3601
+ ? `${quota.count} / ${quota.maxCount || limits.maxFiles || 0}`
3602
+ : "—";
3603
+ const storageRows = [
3604
+ renderSettingsInfoRow(L("settings.row.a2aShareStorage"), storageValue),
3605
+ renderSettingsInfoRow(L("settings.row.a2aShareFiles"), filesValue),
3606
+ renderSettingsInfoRow(L("settings.row.a2aShareMaxFileSize"), formatBytes(limits.maxFileBytes || 0)),
3607
+ renderSettingsInfoRow(
3608
+ L("settings.row.a2aShareDefaultExpiry"),
3609
+ L("settings.a2aShare.days", { count: limits.defaultExpiresDays || 30 })
3610
+ ),
3611
+ renderSettingsInfoRow(
3612
+ L("settings.row.a2aShareUploadRate"),
3613
+ L("settings.a2aShare.ratePerHour", { count: limits.uploadRatePerHour || 10 })
3614
+ ),
3615
+ ];
3616
+
3617
+ const PAGE_SIZE = 5;
3618
+ const visibleCount = state.a2aShareRecentExpanded || PAGE_SIZE;
3619
+ const visible = items.slice(0, visibleCount);
3620
+ const hasMore = items.length > visibleCount;
3621
+ const filesList = visible.map((item) => {
3622
+ const passwordLabel = L("settings.a2aShare.passwordProtected");
3623
+ const lock = item.hasPassword
3624
+ ? `<span class="settings-compose-badge settings-compose-badge--reply" role="img" title="${escapeHtml(passwordLabel)}" aria-label="${escapeHtml(passwordLabel)}">${renderIcon("lock")}</span>`
3625
+ : "";
3626
+ const label = escapeHtml(item.originalName || item.slug);
3627
+ const link = item.url
3628
+ ? `<a href="${escapeHtml(item.url)}" target="_blank" rel="noopener">${label}</a>`
3629
+ : label;
3630
+ const sizeText = escapeHtml(formatBytes(item.size || 0));
3631
+ const createdText = item.createdAtMs
3632
+ ? escapeHtml(formatRelativeAge(Date.now() - item.createdAtMs))
3633
+ : "";
3634
+ const expiresText = item.expiresAtMs
3635
+ ? escapeHtml(formatExpiresIn(item.expiresAtMs - Date.now()))
3636
+ : "";
3637
+ const meta = [sizeText, createdText, expiresText].filter(Boolean).join(" · ");
3638
+ return `
3639
+ <div class="settings-compose-entry settings-icon-entry">
3640
+ <span class="settings-icon-entry__icon settings-icon-entry__icon--file" aria-hidden="true">${renderIcon("file-event")}</span>
3641
+ <span class="settings-icon-entry__body">
3642
+ <span class="settings-icon-entry__title-row">
3643
+ <span class="settings-compose-entry__title">${link}</span>
3644
+ ${lock}
3645
+ </span>
3646
+ ${meta ? `<span class="settings-compose-entry__meta muted">${meta}</span>` : ""}
3647
+ </span>
3648
+ </div>
3649
+ `;
3650
+ });
3651
+ if (hasMore) {
3652
+ const remaining = items.length - visibleCount;
3653
+ filesList.push(
3654
+ `<button type="button" class="settings-compose-more" data-a2a-share-files-more>${escapeHtml(L("settings.a2aShare.showMore", { count: remaining }))}</button>`
3655
+ );
3656
+ } else if (items.length > PAGE_SIZE) {
3657
+ filesList.push(
3658
+ `<button type="button" class="settings-compose-more" data-a2a-share-files-collapse>${escapeHtml(L("settings.a2aShare.showLess"))}</button>`
3659
+ );
3660
+ }
3661
+
3662
+ return `
3663
+ <div class="settings-page">
3664
+ ${renderSettingsGroup("", [
3665
+ renderSettingsInfoRow(L("settings.row.a2aShareStatus"), statusLabel),
3666
+ renderSettingsInfoRow(L("settings.row.a2aShareEndpoint"), share.shareHost || share.shareUrl || ""),
3667
+ renderSettingsInfoRow(L("settings.row.a2aShareUserId"), share.userId || ""),
3668
+ ])}
3669
+ ${renderSettingsGroup(L("settings.a2aShare.storage.title"), storageRows)}
3670
+ ${items.length
3671
+ ? renderSettingsGroup(L("settings.a2aShare.files.title"), filesList)
3672
+ : renderSettingsGroup(L("settings.a2aShare.files.title"), [
3673
+ `<p class="settings-group__description muted">${escapeHtml(L("settings.a2aShare.files.empty"))}</p>`,
3674
+ ])}
3675
+ ${share.error ? `<p class="settings-page-copy muted">${escapeHtml(L("settings.a2aShare.error", { reason: share.error }))}</p>` : ""}
3676
+ </div>
3677
+ `;
3678
+ }
3679
+
3680
+ function formatRelativeAge(ms) {
3681
+ if (!Number.isFinite(ms) || ms < 0) return "";
3682
+ // Intl.RelativeTimeFormat with numeric:"auto" gives us locale-aware phrasing
3683
+ // ("5 minutes ago" / "5分前") without needing dedicated translation keys.
3684
+ const locale = state.locale || DEFAULT_LOCALE;
3685
+ let rtf;
3686
+ try {
3687
+ rtf = new Intl.RelativeTimeFormat(locale, { numeric: "auto" });
3688
+ } catch {
3689
+ rtf = null;
3690
+ }
3691
+ const sec = Math.floor(ms / 1000);
3692
+ const pick = (value, unit, fallback) => (rtf ? rtf.format(-value, unit) : fallback);
3693
+ if (sec < 60) return pick(sec, "second", `${sec}s ago`);
3694
+ const min = Math.floor(sec / 60);
3695
+ if (min < 60) return pick(min, "minute", `${min}m ago`);
3696
+ const hr = Math.floor(min / 60);
3697
+ if (hr < 24) return pick(hr, "hour", `${hr}h ago`);
3698
+ const day = Math.floor(hr / 24);
3699
+ if (day < 30) return pick(day, "day", `${day}d ago`);
3700
+ const mo = Math.floor(day / 30);
3701
+ if (mo < 12) return pick(mo, "month", `${mo}mo ago`);
3702
+ return pick(Math.floor(mo / 12), "year", `${Math.floor(mo / 12)}y ago`);
3703
+ }
3704
+
3705
+ function formatExpiresIn(ms) {
3706
+ if (!Number.isFinite(ms)) return "";
3707
+ if (ms <= 0) return L("settings.a2aShare.expired");
3708
+ const locale = state.locale || DEFAULT_LOCALE;
3709
+ let rtf;
3710
+ try {
3711
+ rtf = new Intl.RelativeTimeFormat(locale, { numeric: "always" });
3712
+ } catch {
3713
+ rtf = null;
3714
+ }
3715
+ const day = Math.floor(ms / 86400000);
3716
+ if (day >= 1) return rtf ? rtf.format(day, "day") : `in ${day}d`;
3717
+ const hr = Math.max(1, Math.floor(ms / 3600000));
3718
+ return rtf ? rtf.format(hr, "hour") : `in ${hr}h`;
3719
+ }
3720
+
3509
3721
  function renderSettingsInfoRow(label, value, options = {}) {
3510
3722
  const rowClassName = ["settings-info-row", options.rowClassName || ""].filter(Boolean).join(" ");
3511
3723
  const valueClassName = ["settings-info-row__value", options.valueClassName || ""].filter(Boolean).join(" ");
@@ -3626,8 +3838,9 @@ function renderStandardDetailDesktop(detail) {
3626
3838
  ${renderMoltbookDraftComposer(detail)}
3627
3839
  ${renderA2ATaskDetail(detail)}
3628
3840
  ${renderThreadShareDetail(detail)}
3841
+ ${renderAmbientSuggestionsSection(detail)}
3629
3842
  ${
3630
- detail.kind === "moltbook_draft" || detail.kind === "moltbook_reply" || detail.kind === "a2a_task" || detail.kind === "a2a_task_result" || detail.kind === "thread_share"
3843
+ detail.kind === "moltbook_draft" || detail.kind === "moltbook_reply" || detail.kind === "a2a_task" || detail.kind === "a2a_task_result" || detail.kind === "thread_share" || detail.kind === "ambient_suggestions"
3631
3844
  ? ""
3632
3845
  : plainIntro
3633
3846
  ? plainIntro
@@ -3664,8 +3877,9 @@ function renderStandardDetailMobile(detail) {
3664
3877
  ${renderMoltbookDraftComposer(detail, { mobile: true })}
3665
3878
  ${renderA2ATaskDetail(detail, { mobile: true })}
3666
3879
  ${renderThreadShareDetail(detail, { mobile: true })}
3880
+ ${renderAmbientSuggestionsSection(detail, { mobile: true })}
3667
3881
  ${
3668
- detail.kind === "moltbook_draft" || detail.kind === "moltbook_reply" || detail.kind === "a2a_task" || detail.kind === "a2a_task_result" || detail.kind === "thread_share"
3882
+ detail.kind === "moltbook_draft" || detail.kind === "moltbook_reply" || detail.kind === "a2a_task" || detail.kind === "a2a_task_result" || detail.kind === "thread_share" || detail.kind === "ambient_suggestions"
3669
3883
  ? ""
3670
3884
  : plainIntro
3671
3885
  ? plainIntro
@@ -3690,6 +3904,65 @@ function renderStandardDetailMobile(detail) {
3690
3904
  `;
3691
3905
  }
3692
3906
 
3907
+ function renderAmbientSuggestionsSection(detail, options = {}) {
3908
+ if (detail?.kind !== "ambient_suggestions") {
3909
+ return "";
3910
+ }
3911
+
3912
+ const suggestions = normalizeClientAmbientSuggestions(detail?.suggestions);
3913
+ if (suggestions.length === 0) {
3914
+ return `
3915
+ <section class="detail-card detail-card--body ${options.mobile ? "detail-card--mobile" : ""}">
3916
+ <div class="detail-body markdown">${detail.messageHtml || ""}</div>
3917
+ </section>
3918
+ `;
3919
+ }
3920
+
3921
+ return `
3922
+ <section class="detail-card detail-card--body ${options.mobile ? "detail-card--mobile" : ""}">
3923
+ <div class="ambient-suggestions">
3924
+ ${detail.messageHtml ? `<div class="ambient-suggestions__intro markdown">${detail.messageHtml}</div>` : ""}
3925
+ <div class="ambient-suggestions__list">
3926
+ ${suggestions
3927
+ .map((suggestion, index) => renderAmbientSuggestionCard(detail, suggestion, index))
3928
+ .join("")}
3929
+ </div>
3930
+ </div>
3931
+ </section>
3932
+ `;
3933
+ }
3934
+
3935
+ function renderAmbientSuggestionCard(detail, suggestion, index) {
3936
+ const copyKey = ambientSuggestionCopyKey(detail?.token || "", suggestion?.id || "", index);
3937
+ const copyStatus = state.ambientSuggestionCopyState?.key === copyKey
3938
+ ? state.ambientSuggestionCopyState?.status || ""
3939
+ : "";
3940
+ const copyLabel = copyStatus === "success"
3941
+ ? L("detail.ambientSuggestions.copyPromptDone")
3942
+ : copyStatus === "error"
3943
+ ? L("detail.ambientSuggestions.copyPromptFailed")
3944
+ : L("detail.ambientSuggestions.copyPrompt");
3945
+ return `
3946
+ <article class="ambient-suggestion-card">
3947
+ <div class="ambient-suggestion-card__header">
3948
+ <h3 class="ambient-suggestion-card__title">${escapeHtml(suggestion.title)}</h3>
3949
+ <button
3950
+ type="button"
3951
+ class="secondary ambient-suggestion-card__copy-button"
3952
+ data-copy-ambient-suggestion
3953
+ data-copy-ambient-suggestion-token="${escapeHtml(detail?.token || "")}"
3954
+ data-copy-ambient-suggestion-index="${escapeHtml(String(index))}"
3955
+ >${escapeHtml(copyLabel)}</button>
3956
+ </div>
3957
+ ${suggestion.description ? `<p class="ambient-suggestion-card__description">${escapeHtml(suggestion.description)}</p>` : ""}
3958
+ <div class="ambient-suggestion-card__prompt-wrap">
3959
+ <p class="ambient-suggestion-card__prompt-label">${escapeHtml(L("detail.ambientSuggestions.prompt"))}</p>
3960
+ <pre class="ambient-suggestion-card__prompt">${escapeHtml(suggestion.prompt)}</pre>
3961
+ </div>
3962
+ </article>
3963
+ `;
3964
+ }
3965
+
3693
3966
  function renderDetailPlainIntro(detail, options = {}) {
3694
3967
  if (!["approval", "diff_thread", "file_event"].includes(detail?.kind || "")) {
3695
3968
  return "";
@@ -5091,6 +5364,41 @@ function bindShellInteractions() {
5091
5364
  });
5092
5365
  }
5093
5366
 
5367
+ for (const button of document.querySelectorAll("[data-copy-ambient-suggestion]")) {
5368
+ button.addEventListener("click", async (event) => {
5369
+ event.preventDefault();
5370
+ event.stopPropagation();
5371
+ const detail = state.currentDetail;
5372
+ if (!detail || detail.kind !== "ambient_suggestions") {
5373
+ return;
5374
+ }
5375
+ if ((button.dataset.copyAmbientSuggestionToken || "") !== (detail.token || "")) {
5376
+ return;
5377
+ }
5378
+ const index = Math.max(0, Number(button.dataset.copyAmbientSuggestionIndex) || 0);
5379
+ const suggestions = normalizeClientAmbientSuggestions(detail.suggestions);
5380
+ const suggestion = suggestions[index];
5381
+ if (!suggestion?.prompt) {
5382
+ return;
5383
+ }
5384
+ const copyKey = ambientSuggestionCopyKey(detail.token || "", suggestion.id || "", index);
5385
+ try {
5386
+ await copyTextToClipboard(suggestion.prompt);
5387
+ state.ambientSuggestionCopyState = { key: copyKey, status: "success" };
5388
+ } catch {
5389
+ state.ambientSuggestionCopyState = { key: copyKey, status: "error" };
5390
+ }
5391
+ await renderShell();
5392
+ window.setTimeout(async () => {
5393
+ if (state.ambientSuggestionCopyState?.key !== copyKey) {
5394
+ return;
5395
+ }
5396
+ state.ambientSuggestionCopyState = null;
5397
+ await renderShell();
5398
+ }, 1600);
5399
+ });
5400
+ }
5401
+
5094
5402
  for (const button of document.querySelectorAll("[data-diff-thread-file-toggle]")) {
5095
5403
  button.addEventListener("click", async (event) => {
5096
5404
  event.preventDefault();
@@ -5393,6 +5701,19 @@ function bindShellInteractions() {
5393
5701
  });
5394
5702
  }
5395
5703
 
5704
+ for (const btn of document.querySelectorAll("[data-a2a-share-files-more]")) {
5705
+ btn.addEventListener("click", async () => {
5706
+ state.a2aShareRecentExpanded = (state.a2aShareRecentExpanded || 5) + 5;
5707
+ await renderShell();
5708
+ });
5709
+ }
5710
+ for (const btn of document.querySelectorAll("[data-a2a-share-files-collapse]")) {
5711
+ btn.addEventListener("click", async () => {
5712
+ state.a2aShareRecentExpanded = 0;
5713
+ await renderShell();
5714
+ });
5715
+ }
5716
+
5396
5717
  for (const button of document.querySelectorAll("[data-locale-option]")) {
5397
5718
  button.addEventListener("click", async () => {
5398
5719
  state.pushError = "";
@@ -6102,7 +6423,7 @@ function tabForItemKind(kind, fallback) {
6102
6423
  if (kind === "diff_thread") {
6103
6424
  return "diff";
6104
6425
  }
6105
- if (kind === "file_event") {
6426
+ if (kind === "file_event" || kind === "ambient_suggestions") {
6106
6427
  return "timeline";
6107
6428
  }
6108
6429
  if (TIMELINE_MESSAGE_KINDS.has(kind)) {
@@ -6134,6 +6455,8 @@ function kindMeta(kind, item) {
6134
6455
  return { label: L("common.assistantCommentary"), tone: "plan", icon: "assistant-commentary" };
6135
6456
  case "assistant_final":
6136
6457
  return { label: L("common.assistantFinal"), tone: "completion", icon: "assistant-final" };
6458
+ case "ambient_suggestions":
6459
+ return { label: L("common.ambientSuggestions"), tone: "neutral", icon: "suggestions" };
6137
6460
  case "approval":
6138
6461
  return { label: L("common.approval"), tone: "approval", icon: "approval" };
6139
6462
  case "plan":
@@ -6179,6 +6502,9 @@ function itemIntentText(kind, status = "pending", provider) {
6179
6502
  if (kind === "file_event") {
6180
6503
  return L("intent.fileEvent");
6181
6504
  }
6505
+ if (kind === "ambient_suggestions") {
6506
+ return L("intent.ambientSuggestions");
6507
+ }
6182
6508
  if (kind === "user_message") {
6183
6509
  return L("intent.userMessage");
6184
6510
  }
@@ -6213,6 +6539,9 @@ function detailIntentText(detail) {
6213
6539
  if (detail.kind === "file_event") {
6214
6540
  return itemIntentText(detail.kind, "timeline", provider);
6215
6541
  }
6542
+ if (detail.kind === "ambient_suggestions") {
6543
+ return itemIntentText(detail.kind, "timeline", provider);
6544
+ }
6216
6545
  if (TIMELINE_MESSAGE_KINDS.has(detail.kind)) {
6217
6546
  return itemIntentText(detail.kind, "timeline", provider);
6218
6547
  }
@@ -6231,7 +6560,11 @@ function renderDetailTitle(detail) {
6231
6560
  }
6232
6561
 
6233
6562
  function detailDisplayTitle(detail) {
6234
- const threadLabel = normalizeClientText(detail?.threadLabel || "");
6563
+ const threadLabel = sanitizeThreadLabelForDisplay(detail?.threadLabel || "", detail?.threadId || "");
6564
+ if (detail?.kind === "ambient_suggestions") {
6565
+ const ambientTitle = sanitizeThreadLabelForDisplay(detail?.title || "", detail?.threadId || "");
6566
+ return ambientTitle || L("common.ambientSuggestions");
6567
+ }
6235
6568
  if (threadLabel) {
6236
6569
  return threadLabel;
6237
6570
  }
@@ -6279,6 +6612,8 @@ function fallbackSummaryForKind(kind, status, provider) {
6279
6612
  return L("summary.diffThread");
6280
6613
  case "file_event":
6281
6614
  return L("summary.fileEvent", vars);
6615
+ case "ambient_suggestions":
6616
+ return L("summary.ambientSuggestions", { count: 0, firstTitle: "", more: 0 });
6282
6617
  case "user_message":
6283
6618
  return L("summary.userMessage");
6284
6619
  case "assistant_commentary":
@@ -6495,6 +6830,8 @@ function renderIcon(name) {
6495
6830
  return `<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.85" stroke-linecap="round" stroke-linejoin="round"><path d="M12 6.2v5.6"/><path d="M9.2 9h5.6"/><path d="M6 14.8a6.7 6.7 0 0 0 12 0"/><path d="M8 4.8a7.6 7.6 0 0 1 8 0"/></svg>`;
6496
6831
  case "assistant-final":
6497
6832
  return `<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.85" stroke-linecap="round" stroke-linejoin="round"><path d="M6 6.5h12a1.8 1.8 0 0 1 1.8 1.8v6.1A1.8 1.8 0 0 1 18 16.2h-5.3L9 19.4v-3.2H6a1.8 1.8 0 0 1-1.8-1.8V8.3A1.8 1.8 0 0 1 6 6.5Z"/><path d="m9.2 11.3 1.7 1.7 3.6-3.8"/></svg>`;
6833
+ case "suggestions":
6834
+ return `<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.85" stroke-linecap="round" stroke-linejoin="round"><path d="M12 3.8 13 6.7l3 .5-2.2 2.2.5 3-2.3-1.2-2.3 1.2.5-3L8 7.2l3-.5Z"/><path d="M6.2 14.6h11.6"/><path d="M8.4 18.2h7.2"/></svg>`;
6498
6835
  case "completed":
6499
6836
  return `<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="8.5"/><path d="m8.7 12.2 2.1 2.1 4.6-4.8"/></svg>`;
6500
6837
  case "settings":
@@ -6521,6 +6858,8 @@ function renderIcon(name) {
6521
6858
  return `<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.9" stroke-linecap="round" stroke-linejoin="round"><path d="M5 7h14"/><path d="M8 12h8"/><path d="M10.5 17h3"/></svg>`;
6522
6859
  case "check":
6523
6860
  return `<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round"><path d="m6.8 12.5 3.2 3.2 7.2-7.4"/></svg>`;
6861
+ case "lock":
6862
+ return `<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.9" stroke-linecap="round" stroke-linejoin="round"><rect x="5.5" y="10.5" width="13" height="9" rx="2"/><path d="M8 10.5V7.5a4 4 0 0 1 8 0v3"/></svg>`;
6524
6863
  case "back":
6525
6864
  return `<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="m15 18-6-6 6-6"/></svg>`;
6526
6865
  case "chevron-down":
@@ -6767,6 +7106,58 @@ function normalizeClientFileRefs(fileRefs) {
6767
7106
  return deduped;
6768
7107
  }
6769
7108
 
7109
+ function normalizeClientAmbientSuggestions(value) {
7110
+ if (!Array.isArray(value)) {
7111
+ return [];
7112
+ }
7113
+ return value
7114
+ .map((entry) => {
7115
+ const title = normalizeClientText(entry?.title);
7116
+ const prompt = normalizeClientText(entry?.prompt);
7117
+ if (!title || !prompt) {
7118
+ return null;
7119
+ }
7120
+ return {
7121
+ id: normalizeClientText(entry?.id),
7122
+ title,
7123
+ prompt,
7124
+ description: normalizeClientText(entry?.description),
7125
+ };
7126
+ })
7127
+ .filter(Boolean);
7128
+ }
7129
+
7130
+ function ambientSuggestionCopyKey(token, suggestionId, index) {
7131
+ return [normalizeClientText(token), normalizeClientText(suggestionId), String(Math.max(0, Number(index) || 0))].join(":");
7132
+ }
7133
+
7134
+ async function copyTextToClipboard(text) {
7135
+ const normalized = String(text ?? "");
7136
+ if (!normalized) {
7137
+ throw new Error("empty");
7138
+ }
7139
+
7140
+ if (navigator.clipboard?.writeText) {
7141
+ await navigator.clipboard.writeText(normalized);
7142
+ return;
7143
+ }
7144
+
7145
+ const textarea = document.createElement("textarea");
7146
+ textarea.value = normalized;
7147
+ textarea.setAttribute("readonly", "true");
7148
+ textarea.style.position = "fixed";
7149
+ textarea.style.opacity = "0";
7150
+ textarea.style.pointerEvents = "none";
7151
+ document.body.appendChild(textarea);
7152
+ textarea.select();
7153
+ textarea.setSelectionRange(0, textarea.value.length);
7154
+ const copied = document.execCommand("copy");
7155
+ document.body.removeChild(textarea);
7156
+ if (!copied) {
7157
+ throw new Error("copy-failed");
7158
+ }
7159
+ }
7160
+
6770
7161
  function fileRefLabel(fileRef) {
6771
7162
  const normalized = normalizeClientText(fileRef);
6772
7163
  if (!normalized) {