viveworker 0.8.9 → 0.9.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -148,7 +148,7 @@ async function handlePermissionRequest() {
148
148
  } else if (toolName === "Bash") {
149
149
  approvalKind = "command";
150
150
  const cmd = String(toolInput.command || "");
151
- messageText = `Command approval needed.\n\`\`\`\n${cmd.slice(0, 500)}\n\`\`\``;
151
+ messageText = `Command approval needed.\n\`\`\`\n${escapeMarkdownCodeFenceBody(cmd.slice(0, 500))}\n\`\`\``;
152
152
  } else if (toolName === "ExitPlanMode") {
153
153
  approvalKind = "plan";
154
154
  messageText = "Plan approval needed.";
@@ -708,6 +708,10 @@ function sanitizeForPath(s) {
708
708
  return s.replace(/[^a-zA-Z0-9_-]/gu, "_").slice(0, 64);
709
709
  }
710
710
 
711
+ function escapeMarkdownCodeFenceBody(text) {
712
+ return String(text || "").replace(/```/gu, "\\`\\`\\`");
713
+ }
714
+
711
715
  function encodeFilePathForSnapshot(filePath) {
712
716
  return filePath.replace(/[/\\]/gu, "_") + ".snap";
713
717
  }
@@ -837,6 +837,7 @@ async function installMoltbookScout({ cliOptions, suppressOutput = false }) {
837
837
  // Use the auto script which runs scout (no LLM), then drafts via harness, then proposes.
838
838
  const envVars = [
839
839
  `SCOUT_HARNESS=${harness.kind}`,
840
+ harness.bin ? `SCOUT_HARNESS_BIN=${shellQuote(harness.bin)}` : "",
840
841
  submoltsFlag ? `SCOUT_FLAGS="${submoltsFlag.trim()}${maxDailyFlag}"` : maxDailyFlag ? `SCOUT_FLAGS="${maxDailyFlag.trim()}"` : "",
841
842
  ].filter(Boolean).join(" ");
842
843
  inner =
@@ -2778,6 +2779,10 @@ function escapeXml(value) {
2778
2779
  .replace(/"/gu, """);
2779
2780
  }
2780
2781
 
2782
+ function shellQuote(value) {
2783
+ return `'${String(value ?? "").replace(/'/gu, "'\\''")}'`;
2784
+ }
2785
+
2781
2786
  async function printPairingInfo(locale, config, { sectioned = false } = {}) {
2782
2787
  const baseUrl = String(config.NATIVE_APPROVAL_SERVER_PUBLIC_BASE_URL || "").trim();
2783
2788
  const pairCode = String(config.PAIRING_CODE || "").trim();
@@ -34,9 +34,14 @@ Keep onboarding calm and step-by-step. Do not make the user debug config files u
34
34
  ## Tool Selection
35
35
 
36
36
  - Use `viveworker_notify` for informational milestones that should appear on the phone or timeline, such as "build finished", "review ready", or "agent is blocked".
37
+ - Use `viveworker_stats` when the user asks for viveworker package adoption or usage stats.
38
+ - Use `viveworker_share_list` when the user asks what is currently in File Share or asks for share usage metrics.
39
+ - Use `viveworker_a2a_activity` or `viveworker_a2a_card` when troubleshooting A2A activity or local agent-card settings.
40
+ - Use `viveworker_moltbook_list`, `viveworker_moltbook_show`, or `viveworker_moltbook_thread` when inspecting Moltbook inbox comments or threads.
37
41
  - Use `viveworker_ask` when the next step depends on a human preference, missing requirement, or choice that should not be guessed.
38
42
  - Use `viveworker_request_approval` before actions that are externally visible, hard to undo, risky, sensitive, delegated, paid, or likely to surprise the user.
39
43
  - Use `viveworker_share_file` when the user wants a workspace deliverable shared as a limited File Share URL. Good fits include `.html`, `.htm`, `.pdf`, `.png`, `.jpg`, `.jpeg`, `.gif`, `.webp`, and `.csv`.
44
+ - Use `viveworker_share_replace` when the user wants to replace or update the file behind an existing File Share slug.
40
45
  - Use `viveworker_thread_share` when context should move to another Codex session, Claude session, or viveworker inbox while preserving a human-visible handoff.
41
46
  - Use `viveworker_send_a2a_task` only for registered A2A target aliases and bounded tasks with clear acceptance criteria.
42
47
 
@@ -47,10 +52,12 @@ Keep onboarding calm and step-by-step. Do not make the user debug config files u
47
52
  - Include relevant file refs, but never include secrets, tokens, private keys, or unnecessary file contents.
48
53
  - Treat timeout, rejection, or transport failure as "not approved". Do not continue as if approval was granted.
49
54
  - After the tool returns, summarize the decision or returned artifact to the user.
55
+ - Read-only inspection tools use fixed command allowlists. Do not try to route arbitrary shell commands or side-effectful viveworker CLI actions through MCP.
50
56
 
51
57
  ## File Share Rules
52
58
 
53
59
  - Share only files inside the workspace root.
60
+ - Replacing an existing share changes what existing recipients see at that URL; require phone approval and make that consequence clear.
54
61
  - Do not share `.env`, credential files, private keys, `.ssh`, `.aws`, `.gnupg`, or secret-looking paths.
55
62
  - Prefer sharing final deliverables, not internal source files, unless the user explicitly asks.
56
63
  - For unsupported file types, explain the accepted types and suggest exporting to HTML, PDF, image, or CSV first.
@@ -23,10 +23,18 @@ Then restart the Claude Code session. If the tools are not available, ask the us
23
23
  ## Tools
24
24
 
25
25
  - `viveworker_status` checks bridge, pairing, Remote connection, A2A, File Share, and Moltbook state.
26
+ - `viveworker_stats` reads package adoption and usage stats.
27
+ - `viveworker_share_list` lists File Share uploads and optional usage metrics.
28
+ - `viveworker_a2a_activity` reads local A2A activity.
29
+ - `viveworker_a2a_card` reads the current local A2A agent card.
30
+ - `viveworker_moltbook_list` lists Moltbook inbox comments.
31
+ - `viveworker_moltbook_show` reads one Moltbook comment.
32
+ - `viveworker_moltbook_thread` reads the thread for one Moltbook comment.
26
33
  - `viveworker_notify` sends an informational phone notification and records a timeline entry.
27
34
  - `viveworker_ask` asks the paired phone a question and waits for the answer.
28
35
  - `viveworker_request_approval` asks the phone to approve or reject a proposed action.
29
36
  - `viveworker_share_file` uploads a workspace file to File Share after phone approval. For password-protected handoff, pass `tokenize: true` to return a short-lived passwordless `?t=` URL.
37
+ - `viveworker_share_replace` replaces the file behind an existing File Share slug after phone approval.
30
38
  - `viveworker_share_link` mints a short-lived passwordless `?t=` URL for an existing password-protected File Share slug after phone approval.
31
39
  - `viveworker_thread_share` shares context into another Codex / Claude / inbox thread.
32
40
  - `viveworker_send_a2a_task` sends a task to a registered A2A target after phone approval.
@@ -36,9 +44,11 @@ Then restart the Claude Code session. If the tools are not available, ask the us
36
44
  - If the user says "ask me on my phone", "スマホに聞いて", or a short decision blocks progress, use `viveworker_ask`.
37
45
  - If the user asks you to proceed with a risky, external, irreversible, payment-related, or user-visible action, use `viveworker_request_approval`.
38
46
  - If the user asks for a report, prototype, screenshot, PDF, CSV, or standalone HTML to become a shareable link, use `viveworker_share_file`.
47
+ - If the user asks to replace or update the file at an existing File Share link, use `viveworker_share_replace`.
39
48
  - If the user asks for a password-protected share but wants the recipient to open it without knowing the password, use `viveworker_share_file` with `password` and `tokenize: true`, or use `viveworker_share_link` for an existing slug.
40
49
  - If the user says "share this with Codex/Claude", "Aの内容をBに共有して", or wants context handed to another session, use `viveworker_thread_share`.
41
50
  - If the user wants another registered agent to do work, use `viveworker_send_a2a_task`.
51
+ - For troubleshooting or inspection, prefer the read-only MCP tools above before asking the user to run CLI commands.
42
52
 
43
53
  ## Prompting rules
44
54
 
@@ -64,6 +74,7 @@ Claude may not automatically notice newly shared messages from another session.
64
74
 
65
75
  - MCP is a control-plane surface, not a shell executor.
66
76
  - Do not run shell strings through MCP.
67
- - File Share and A2A task sending require phone approval.
77
+ - Read-only inspection tools use fixed command allowlists and should not be used to mutate local state.
78
+ - File Share uploads/replacements and A2A task sending require phone approval.
68
79
  - Use registered A2A aliases only; do not ask the user to paste API keys into prompts.
69
80
  - Prefer `viveworker_status` before troubleshooting a missing notification, stale pairing, or remote connectivity issue.
package/web/app.css CHANGED
@@ -391,6 +391,11 @@ pre code {
391
391
  color: #d8e7f1;
392
392
  }
393
393
 
394
+ .status-pill--in_progress {
395
+ background: rgba(83, 154, 214, 0.14);
396
+ color: #cfeaff;
397
+ }
398
+
394
399
  .count-chip {
395
400
  min-width: 2.2rem;
396
401
  background: rgba(255, 255, 255, 0.08);
@@ -2352,6 +2357,99 @@ pre code {
2352
2357
  inset 0 1px 0 rgba(255, 255, 255, 0.04);
2353
2358
  }
2354
2359
 
2360
+ .work-health {
2361
+ display: flex;
2362
+ align-items: flex-start;
2363
+ gap: 0.7rem;
2364
+ padding: 0.78rem 0;
2365
+ border-top: 1px solid rgba(230, 176, 84, 0.24);
2366
+ border-bottom: 1px solid rgba(230, 176, 84, 0.24);
2367
+ color: #f5dfb2;
2368
+ }
2369
+
2370
+ .work-health__icon,
2371
+ .work-caught-up__icon,
2372
+ .work-section__more-icon {
2373
+ width: 1rem;
2374
+ height: 1rem;
2375
+ display: inline-flex;
2376
+ flex: 0 0 auto;
2377
+ }
2378
+
2379
+ .work-health__copy {
2380
+ min-width: 0;
2381
+ display: grid;
2382
+ gap: 0.22rem;
2383
+ font-size: 0.84rem;
2384
+ line-height: 1.4;
2385
+ }
2386
+
2387
+ .work-caught-up {
2388
+ display: flex;
2389
+ align-items: center;
2390
+ gap: 0.55rem;
2391
+ color: #bfe9d6;
2392
+ font-size: 0.88rem;
2393
+ font-weight: 700;
2394
+ }
2395
+
2396
+ .work-sections {
2397
+ display: grid;
2398
+ gap: 1.25rem;
2399
+ }
2400
+
2401
+ .work-section {
2402
+ min-width: 0;
2403
+ display: grid;
2404
+ gap: 0.65rem;
2405
+ }
2406
+
2407
+ .work-section__header {
2408
+ display: flex;
2409
+ align-items: center;
2410
+ justify-content: space-between;
2411
+ gap: 0.75rem;
2412
+ min-width: 0;
2413
+ }
2414
+
2415
+ .work-section__title {
2416
+ margin: 0;
2417
+ color: var(--text);
2418
+ font-size: 0.9rem;
2419
+ line-height: 1.3;
2420
+ }
2421
+
2422
+ .work-section__count {
2423
+ min-width: 1.45rem;
2424
+ height: 1.45rem;
2425
+ display: inline-flex;
2426
+ align-items: center;
2427
+ justify-content: center;
2428
+ padding: 0 0.38rem;
2429
+ border-radius: var(--radius-pill);
2430
+ background: rgba(255, 255, 255, 0.08);
2431
+ color: var(--muted);
2432
+ font-size: 0.74rem;
2433
+ font-weight: 800;
2434
+ }
2435
+
2436
+ .work-section__more {
2437
+ width: 100%;
2438
+ min-height: 2.5rem;
2439
+ display: flex;
2440
+ align-items: center;
2441
+ justify-content: space-between;
2442
+ gap: 0.65rem;
2443
+ padding: 0.4rem 0;
2444
+ border: 0;
2445
+ border-top: 1px solid var(--line);
2446
+ background: transparent;
2447
+ color: #cfe2ef;
2448
+ font-size: 0.84rem;
2449
+ font-weight: 700;
2450
+ text-align: left;
2451
+ }
2452
+
2355
2453
  .timeline-thread-filter__row {
2356
2454
  display: grid;
2357
2455
  grid-template-columns: minmax(0, 1fr) auto;
@@ -5500,6 +5598,14 @@ button[aria-busy="true"]:not(.is-loading) {
5500
5598
  color: #81c784;
5501
5599
  background: rgba(129, 199, 132, 0.12);
5502
5600
  }
5601
+ .provider-badge--mcp {
5602
+ color: #d6c287;
5603
+ background: rgba(214, 194, 135, 0.1);
5604
+ }
5605
+ .provider-badge--viveworker {
5606
+ color: #9bc4ce;
5607
+ background: rgba(155, 196, 206, 0.1);
5608
+ }
5503
5609
 
5504
5610
  .provider-filter {
5505
5611
  width: 100%;
package/web/app.js CHANGED
@@ -15,7 +15,7 @@ const INITIAL_DETECTED_LOCALE = detectBrowserLocale();
15
15
  const TIMELINE_MESSAGE_KINDS = new Set(["user_message", "assistant_commentary", "assistant_final"]);
16
16
  const TIMELINE_OPERATIONAL_KINDS = new Set(["approval", "plan", "plan_ready", "choice"]);
17
17
  const EXTERNAL_TARGET_TABS = new Set(["inbox", "timeline", "diff"]);
18
- const EXTERNAL_TARGET_INBOX_SUBTABS = new Set(["pending", "completed"]);
18
+ const EXTERNAL_TARGET_INBOX_SUBTABS = new Set(["today", "pending", "completed"]);
19
19
  const THREAD_FILTER_INTERACTION_DEFER_MS = 8000;
20
20
  const SCROLLABLE_CONTENT_INTERACTION_DEFER_MS = 8000;
21
21
  const MAX_COMPLETION_REPLY_IMAGE_COUNT = 4;
@@ -27,7 +27,7 @@ const REMOTE_PAIRING_STATE_SCHEMA_VERSION = 2;
27
27
  const REMOTE_PAIRING_STATE_LEGACY_SCHEMA_VERSION = 1;
28
28
  const BOOT_SPLASH_SLOW_HINT_MS = 10000;
29
29
  const BOOT_SPLASH_REMOTE_SWITCHING_MIN_MS = 650;
30
- const BOOTSTRAP_REMOTE_TIMEOUT_MS = 12_000;
30
+ const BOOTSTRAP_REMOTE_TIMEOUT_MS = 50_000;
31
31
  const REMOTE_PAIRING_TOKEN_REFRESH_MS = 30 * 24 * 60 * 60 * 1000;
32
32
  const BOOT_TRACE_MAX_EVENTS = 90;
33
33
  const BOOT_TRACE_MAX_VALUE_LENGTH = 120;
@@ -52,6 +52,7 @@ const CLIENT_EVENT_REPORT_TIMEOUT_MS = 1_800;
52
52
  const TIMELINE_LIVE_REFRESH_TIMEOUT_MS = 2_000;
53
53
  const TIMELINE_LIVE_RETRY_MS = 5_000;
54
54
  const HAZBASE_ACTION_TIMEOUT_MS = 30_000;
55
+ const TODAY_COMPLETED_ITEM_LIMIT = 6;
55
56
  const timelineImageObjectUrlCache = new Map();
56
57
 
57
58
  function wait(ms) {
@@ -92,7 +93,7 @@ const state = {
92
93
  currentDetailLoading: false,
93
94
  detailLoadingItem: null,
94
95
  detailOpen: false,
95
- inboxSubtab: "pending",
96
+ inboxSubtab: "today",
96
97
  timelineThreadFilter: "all",
97
98
  timelineKindFilter: "all",
98
99
  providerFilter: "all",
@@ -317,6 +318,7 @@ function flushBootTrace(reason, extra = {}) {
317
318
  totalMs: Math.max(0, Math.round(bootTraceNow() - bootTraceStartPerfMs)),
318
319
  remoteRouteSeen: bootSplashRemoteRouteSeen,
319
320
  finalStage: bootSplashStatusStage,
321
+ origin: window.location.origin,
320
322
  userAgent: navigator.userAgent || "",
321
323
  events: bootTraceEvents,
322
324
  ...extra,
@@ -502,7 +504,6 @@ function handleBootRoutingStatus(event) {
502
504
  case "lan-checking":
503
505
  setBootSplashStatus("boot.status.checkingLan", BOOT_SPLASH_STAGE.checking);
504
506
  break;
505
- case "lan-failed":
506
507
  case "remote-switching":
507
508
  showBootRemoteSwitchingStatus();
508
509
  break;
@@ -980,7 +981,7 @@ async function refreshSession() {
980
981
  }
981
982
 
982
983
  // One-shot boot fetch: hits `/api/bootstrap` which bundles session,
983
- // inbox (pending + completed), timeline, and devices into a single
984
+ // inbox work-item buckets, timeline, and devices into a single
984
985
  // HTTPS round-trip. Saves 3 additional TLS handshakes versus calling
985
986
  // the four endpoints in parallel, which is the dominant boot cost on
986
987
  // iOS PWAs where connection reuse is aggressive. Leaves the diff and
@@ -990,6 +991,11 @@ async function refreshBootstrap() {
990
991
  const bootstrap = await apiGet("/api/bootstrap", {
991
992
  timeoutMs: BOOTSTRAP_REMOTE_TIMEOUT_MS,
992
993
  preferRelayError: true,
994
+ raceLanRecoveryWithRelay: true,
995
+ confirmLanReachabilityBeforeRelay: true,
996
+ retryLanAfterReachabilityProbe: true,
997
+ lanReachabilityProbeTimeoutMs: 1_500,
998
+ lanRecoveryTimeoutMs: 7_000,
993
999
  });
994
1000
  recordBootTraceEvent("bootstrap-response", { url: "/api/bootstrap" });
995
1001
  state.session = bootstrap?.session || null;
@@ -1007,8 +1013,10 @@ async function refreshBootstrap() {
1007
1013
  const previousDiff = Array.isArray(state.inbox?.diff) ? state.inbox.diff : [];
1008
1014
  state.inbox = {
1009
1015
  pending: Array.isArray(fastInbox.pending) ? fastInbox.pending : [],
1016
+ inProgress: Array.isArray(fastInbox.inProgress) ? fastInbox.inProgress : [],
1010
1017
  completed: Array.isArray(fastInbox.completed) ? fastInbox.completed : [],
1011
1018
  diff: previousDiff,
1019
+ work: fastInbox?.work && typeof fastInbox.work === "object" ? fastInbox.work : null,
1012
1020
  };
1013
1021
  syncDiffThreadFilter();
1014
1022
  syncCompletedThreadFilter();
@@ -1279,7 +1287,7 @@ async function getClientPushState() {
1279
1287
 
1280
1288
  async function refreshInbox(opts = {}) {
1281
1289
  const fast = await apiGet("/api/inbox", opts);
1282
- // `/api/inbox` now returns only `{ pending, completed }`. The `diff`
1290
+ // `/api/inbox` returns fast work-item buckets without diff data. The `diff`
1283
1291
  // half lives at `/api/inbox/diff` because it spawns `git` subprocesses
1284
1292
  // server-side and was blocking first paint of the completed/pending
1285
1293
  // lists. Preserve whatever diff entries we already have in memory so
@@ -1287,8 +1295,10 @@ async function refreshInbox(opts = {}) {
1287
1295
  const previousDiff = Array.isArray(state.inbox?.diff) ? state.inbox.diff : [];
1288
1296
  state.inbox = {
1289
1297
  pending: Array.isArray(fast?.pending) ? fast.pending : [],
1298
+ inProgress: Array.isArray(fast?.inProgress) ? fast.inProgress : [],
1290
1299
  completed: Array.isArray(fast?.completed) ? fast.completed : [],
1291
1300
  diff: previousDiff,
1301
+ work: fast?.work && typeof fast.work === "object" ? fast.work : null,
1292
1302
  };
1293
1303
  syncDiffThreadFilter();
1294
1304
  syncCompletedThreadFilter();
@@ -1298,11 +1308,13 @@ async function refreshInbox(opts = {}) {
1298
1308
  async function refreshInboxDiff() {
1299
1309
  try {
1300
1310
  const response = await apiGet("/api/inbox/diff");
1301
- const previous = state.inbox || { pending: [], completed: [] };
1311
+ const previous = state.inbox || { pending: [], inProgress: [], completed: [] };
1302
1312
  state.inbox = {
1303
1313
  pending: Array.isArray(previous.pending) ? previous.pending : [],
1314
+ inProgress: Array.isArray(previous.inProgress) ? previous.inProgress : [],
1304
1315
  completed: Array.isArray(previous.completed) ? previous.completed : [],
1305
1316
  diff: Array.isArray(response?.diff) ? response.diff : [],
1317
+ work: previous.work && typeof previous.work === "object" ? previous.work : null,
1306
1318
  };
1307
1319
  syncDiffThreadFilter();
1308
1320
  syncInboxSubtab();
@@ -1685,6 +1697,7 @@ function allInboxEntries() {
1685
1697
  }
1686
1698
  return [
1687
1699
  ...(Array.isArray(state.inbox.pending) ? state.inbox.pending.map((item) => ({ item, status: "pending" })) : []),
1700
+ ...(Array.isArray(state.inbox.inProgress) ? state.inbox.inProgress.map((item) => ({ item, status: "in_progress" })) : []),
1688
1701
  ...(Array.isArray(state.inbox.diff) ? state.inbox.diff.map((item) => ({ item, status: "diff" })) : []),
1689
1702
  ...(Array.isArray(state.inbox.completed) ? state.inbox.completed.map((item) => ({ item, status: "completed" })) : []),
1690
1703
  ];
@@ -1780,6 +1793,9 @@ function listInboxEntries() {
1780
1793
  if (!state.inbox) {
1781
1794
  return [];
1782
1795
  }
1796
+ if (state.inboxSubtab === "today") {
1797
+ return todayInboxEntries();
1798
+ }
1783
1799
  if (state.inboxSubtab === "completed") {
1784
1800
  return filteredCompletedEntries().map((item) => ({ item, status: "completed" }));
1785
1801
  }
@@ -1788,6 +1804,35 @@ function listInboxEntries() {
1788
1804
  .map((item) => ({ item, status: "pending" }));
1789
1805
  }
1790
1806
 
1807
+ function isTodayTimestamp(value, now = new Date()) {
1808
+ const timestamp = Number(value) || 0;
1809
+ if (!timestamp) {
1810
+ return false;
1811
+ }
1812
+ const date = new Date(timestamp);
1813
+ return date.getFullYear() === now.getFullYear() &&
1814
+ date.getMonth() === now.getMonth() &&
1815
+ date.getDate() === now.getDate();
1816
+ }
1817
+
1818
+ function todayInboxGroups() {
1819
+ const pending = (Array.isArray(state.inbox?.pending) ? state.inbox.pending : [])
1820
+ .filter((item) => entryMatchesProviderFilter(item))
1821
+ .map((item) => ({ item, status: "pending" }));
1822
+ const inProgress = (Array.isArray(state.inbox?.inProgress) ? state.inbox.inProgress : [])
1823
+ .filter((item) => entryMatchesProviderFilter(item))
1824
+ .map((item) => ({ item, status: "in_progress" }));
1825
+ const completed = (Array.isArray(state.inbox?.completed) ? state.inbox.completed : [])
1826
+ .filter((item) => entryMatchesProviderFilter(item) && isTodayTimestamp(item?.createdAtMs))
1827
+ .map((item) => ({ item, status: "completed" }));
1828
+ return { pending, inProgress, completed };
1829
+ }
1830
+
1831
+ function todayInboxEntries() {
1832
+ const groups = todayInboxGroups();
1833
+ return [...groups.pending, ...groups.inProgress, ...groups.completed];
1834
+ }
1835
+
1791
1836
  function normalizeProviderClient(value) {
1792
1837
  const normalized = String(value || "").toLowerCase();
1793
1838
  if (normalized === "claude") return "claude";
@@ -2069,11 +2114,12 @@ function syncCompletedThreadFilter() {
2069
2114
  }
2070
2115
 
2071
2116
  function syncInboxSubtab() {
2072
- if (state.inboxSubtab === "completed") {
2073
- state.inboxSubtab = "completed";
2074
- return;
2075
- }
2076
- state.inboxSubtab = "pending";
2117
+ state.inboxSubtab = normalizeInboxSubtab(state.inboxSubtab);
2118
+ }
2119
+
2120
+ function normalizeInboxSubtab(value, fallback = "today") {
2121
+ const normalized = normalizeClientText(value || "");
2122
+ return EXTERNAL_TARGET_INBOX_SUBTABS.has(normalized) ? normalized : fallback;
2077
2123
  }
2078
2124
 
2079
2125
  function renderPair() {
@@ -3305,11 +3351,13 @@ function renderInboxPanel({ entries, desktop }) {
3305
3351
  const subtabControls = renderInboxSubtabs();
3306
3352
  const providerFilterHtml = renderProviderFilter();
3307
3353
  const threadFilterHtml = state.inboxSubtab === "completed" ? renderCompletedThreadDropdown() : "";
3308
- const bodyHtml = entries.length
3309
- ? `<div class="card-list ${desktop ? "card-list--desktop" : ""}">
3310
- ${entries.map((entry) => renderItemCard(entry, "inbox", desktop)).join("")}
3311
- </div>`
3312
- : renderInboxEmptyState();
3354
+ const bodyHtml = state.inboxSubtab === "today"
3355
+ ? renderTodayInboxBody({ desktop })
3356
+ : entries.length
3357
+ ? `<div class="card-list ${desktop ? "card-list--desktop" : ""}">
3358
+ ${entries.map((entry) => renderItemCard(entry, "inbox", desktop)).join("")}
3359
+ </div>`
3360
+ : renderInboxEmptyState();
3313
3361
 
3314
3362
  if (!desktop) {
3315
3363
  return `
@@ -3378,17 +3426,94 @@ function renderInboxSubtabs() {
3378
3426
 
3379
3427
  function inboxSubtabOptions() {
3380
3428
  return [
3429
+ { id: "today", label: L("inbox.subtab.today") },
3381
3430
  { id: "pending", label: L("inbox.subtab.pending") },
3382
3431
  { id: "completed", label: L("inbox.subtab.completed") },
3383
3432
  ];
3384
3433
  }
3385
3434
 
3435
+ function renderTodayInboxBody({ desktop }) {
3436
+ const groups = todayInboxGroups();
3437
+ const visibleCompleted = groups.completed.slice(0, TODAY_COMPLETED_ITEM_LIMIT);
3438
+ const hiddenCompletedCount = Math.max(0, groups.completed.length - visibleCompleted.length);
3439
+ const healthNotice = renderWorkHealthNotice();
3440
+ const hasAnyItems = groups.pending.length > 0 || groups.inProgress.length > 0 || groups.completed.length > 0;
3441
+ if (!hasAnyItems) {
3442
+ return `${healthNotice}${renderInboxEmptyState()}`;
3443
+ }
3444
+
3445
+ return `
3446
+ ${healthNotice}
3447
+ ${groups.pending.length === 0 ? `
3448
+ <div class="work-caught-up" role="status">
3449
+ <span class="work-caught-up__icon" aria-hidden="true">${renderIcon("completed")}</span>
3450
+ <span>${escapeHtml(L("work.today.caughtUp"))}</span>
3451
+ </div>
3452
+ ` : ""}
3453
+ <div class="work-sections">
3454
+ ${renderTodayWorkSection("needs-action", L("work.section.needsAction"), groups.pending, desktop)}
3455
+ ${renderTodayWorkSection("in-progress", L("work.section.inProgress"), groups.inProgress, desktop)}
3456
+ ${renderTodayWorkSection("completed", L("work.section.completedToday"), visibleCompleted, desktop, {
3457
+ hiddenCount: hiddenCompletedCount,
3458
+ })}
3459
+ </div>
3460
+ `;
3461
+ }
3462
+
3463
+ function renderTodayWorkSection(id, label, entries, desktop, { hiddenCount = 0 } = {}) {
3464
+ if (!entries.length) {
3465
+ return "";
3466
+ }
3467
+ return `
3468
+ <section class="work-section work-section--${escapeHtml(id)}" aria-labelledby="work-section-${escapeHtml(id)}">
3469
+ <div class="work-section__header">
3470
+ <h3 class="work-section__title" id="work-section-${escapeHtml(id)}">${escapeHtml(label)}</h3>
3471
+ <span class="work-section__count">${entries.length + hiddenCount}</span>
3472
+ </div>
3473
+ <div class="card-list ${desktop ? "card-list--desktop" : ""}">
3474
+ ${entries.map((entry) => renderItemCard(entry, "inbox", desktop)).join("")}
3475
+ </div>
3476
+ ${hiddenCount > 0 ? `
3477
+ <button class="work-section__more" type="button" data-inbox-subtab="completed">
3478
+ <span>${escapeHtml(L("work.today.viewAllCompleted", { count: hiddenCount }))}</span>
3479
+ <span class="work-section__more-icon" aria-hidden="true">${renderIcon("chevron-right")}</span>
3480
+ </button>
3481
+ ` : ""}
3482
+ </section>
3483
+ `;
3484
+ }
3485
+
3486
+ function renderWorkHealthNotice() {
3487
+ const attention = Array.isArray(state.inbox?.work?.health?.attention)
3488
+ ? state.inbox.work.health.attention
3489
+ : [];
3490
+ if (!attention.length) {
3491
+ return "";
3492
+ }
3493
+ const messages = attention.map((issue) => {
3494
+ if (issue?.code === "connection-required") {
3495
+ return L("work.health.connectionRequired", { provider: providerDisplayName(issue.source) });
3496
+ }
3497
+ return L("work.health.degraded");
3498
+ });
3499
+ return `
3500
+ <div class="work-health" role="status">
3501
+ <span class="work-health__icon" aria-hidden="true">${renderIcon("lock")}</span>
3502
+ <div class="work-health__copy">
3503
+ <strong>${escapeHtml(L("work.health.title"))}</strong>
3504
+ ${messages.map((message) => `<span>${escapeHtml(message)}</span>`).join("")}
3505
+ </div>
3506
+ </div>
3507
+ `;
3508
+ }
3509
+
3386
3510
  function renderInboxEmptyState() {
3387
3511
  const isCompletedView = state.inboxSubtab === "completed";
3512
+ const isTodayView = state.inboxSubtab === "today";
3388
3513
  return `
3389
3514
  <div class="empty-state">
3390
- <p class="empty-state__title">${escapeHtml(L(isCompletedView ? "inbox.subtab.completed" : "inbox.subtab.pending"))}</p>
3391
- <p class="muted">${escapeHtml(L(isCompletedView ? "empty.completed" : "empty.pending"))}</p>
3515
+ <p class="empty-state__title">${escapeHtml(L(isCompletedView ? "inbox.subtab.completed" : isTodayView ? "work.today.emptyTitle" : "inbox.subtab.pending"))}</p>
3516
+ <p class="muted">${escapeHtml(L(isCompletedView ? "empty.completed" : isTodayView ? "work.today.emptyCopy" : "empty.pending"))}</p>
3392
3517
  </div>
3393
3518
  `;
3394
3519
  }
@@ -3404,6 +3529,12 @@ function providerBadgeMeta(provider) {
3404
3529
  if (normalized === "a2a") {
3405
3530
  return { id: "a2a", label: "A2A", glyph: "A" };
3406
3531
  }
3532
+ if (normalized === "mcp") {
3533
+ return { id: "mcp", label: "MCP", glyph: "M" };
3534
+ }
3535
+ if (normalized === "viveworker") {
3536
+ return { id: "viveworker", label: L("common.appName"), glyph: "V" };
3537
+ }
3407
3538
  return { id: "codex", label: L("common.codex"), glyph: "X" };
3408
3539
  }
3409
3540
 
@@ -3449,7 +3580,7 @@ function renderProviderFilter() {
3449
3580
  `;
3450
3581
  }
3451
3582
 
3452
- const COMPLETED_CARD_KINDS = new Set(["assistant_final", "approval", "moltbook_reply", "moltbook_draft", "a2a_task_result", "thread_share"]);
3583
+ const COMPLETED_CARD_KINDS = new Set(["completion", "assistant_final", "approval", "moltbook_reply", "moltbook_draft", "a2a_task_result", "thread_share"]);
3453
3584
 
3454
3585
  function renderItemCard(entry, sourceTab, desktop) {
3455
3586
  if (entry.status === "completed" && COMPLETED_CARD_KINDS.has(entry.item.kind)) {
@@ -3457,9 +3588,13 @@ function renderItemCard(entry, sourceTab, desktop) {
3457
3588
  }
3458
3589
  const kindInfo = kindMeta(entry.item.kind, entry.item);
3459
3590
  const cardTitle = cardTitleForEntry(entry);
3460
- const statusText = entry.status === "completed" ? L("common.completed") : L("common.actionNeeded");
3591
+ const statusText = entry.status === "completed"
3592
+ ? L("common.completed")
3593
+ : entry.status === "in_progress"
3594
+ ? L("common.inProgress")
3595
+ : L("common.actionNeeded");
3461
3596
  const intentText = itemIntentText(entry.item.kind, entry.status, entry.item.provider);
3462
- const showCompletedTimestamp = entry.status === "completed" && sourceTab === "completed";
3597
+ const showCompletedTimestamp = entry.status === "completed";
3463
3598
  const timestampLabel = showCompletedTimestamp ? formatTimelineTimestamp(entry.item.createdAtMs) : "";
3464
3599
  return `
3465
3600
  <button
@@ -3467,6 +3602,7 @@ function renderItemCard(entry, sourceTab, desktop) {
3467
3602
  data-open-item-kind="${escapeHtml(entry.item.kind)}"
3468
3603
  data-open-item-token="${escapeHtml(entry.item.token)}"
3469
3604
  data-source-tab="${escapeHtml(sourceTab)}"
3605
+ data-source-subtab="${escapeHtml(sourceTab === "inbox" ? state.inboxSubtab : "")}"
3470
3606
  >
3471
3607
  <div class="item-card__header">
3472
3608
  <div class="item-card__meta">
@@ -3491,7 +3627,7 @@ function renderItemCard(entry, sourceTab, desktop) {
3491
3627
  </p>
3492
3628
  <p class="item-card__summary">${escapeHtml(entry.item.summary || fallbackSummaryForKind(entry.item.kind, entry.status, entry.item.provider))}</p>
3493
3629
  ${
3494
- !desktop && sourceTab === "inbox"
3630
+ !desktop && sourceTab === "inbox" && state.inboxSubtab !== "today"
3495
3631
  ? `<p class="item-card__status-note">${escapeHtml(statusText)}</p>`
3496
3632
  : ""
3497
3633
  }
@@ -3543,7 +3679,7 @@ function renderCompletedCompletionCard(entry, sourceTab) {
3543
3679
  data-open-item-kind="${escapeHtml(item.kind)}"
3544
3680
  data-open-item-token="${escapeHtml(item.token)}"
3545
3681
  data-source-tab="${escapeHtml(sourceTab)}"
3546
- data-source-subtab="completed"
3682
+ data-source-subtab="${escapeHtml(sourceTab === "inbox" ? state.inboxSubtab : "completed")}"
3547
3683
  >
3548
3684
  <div class="item-card__header">
3549
3685
  <div class="item-card__meta">
@@ -9237,7 +9373,7 @@ function bindShellInteractions() {
9237
9373
 
9238
9374
  for (const button of document.querySelectorAll("[data-inbox-subtab]")) {
9239
9375
  button.addEventListener("click", async () => {
9240
- const nextSubtab = button.dataset.inboxSubtab === "completed" ? "completed" : "pending";
9376
+ const nextSubtab = normalizeInboxSubtab(button.dataset.inboxSubtab);
9241
9377
  if (nextSubtab === state.inboxSubtab) {
9242
9378
  return;
9243
9379
  }
@@ -11094,7 +11230,7 @@ function bindPartialListSurfaceInteractions(listSurface) {
11094
11230
 
11095
11231
  const subtabButton = target?.closest("[data-inbox-subtab]");
11096
11232
  if (subtabButton) {
11097
- const nextSubtab = subtabButton.dataset.inboxSubtab === "completed" ? "completed" : "pending";
11233
+ const nextSubtab = normalizeInboxSubtab(subtabButton.dataset.inboxSubtab);
11098
11234
  if (nextSubtab === state.inboxSubtab) {
11099
11235
  return;
11100
11236
  }
@@ -11347,8 +11483,12 @@ function tabForItemKind(kind, fallback) {
11347
11483
  }
11348
11484
 
11349
11485
  function inboxSubtabForItemKind(kind, sourceSubtab = "") {
11350
- if (normalizeClientText(sourceSubtab || "") === "completed") {
11351
- return "completed";
11486
+ const normalizedSourceSubtab = normalizeClientText(sourceSubtab || "");
11487
+ if (EXTERNAL_TARGET_INBOX_SUBTABS.has(normalizedSourceSubtab)) {
11488
+ return normalizedSourceSubtab;
11489
+ }
11490
+ if (kind === "activity_status") {
11491
+ return "today";
11352
11492
  }
11353
11493
  const completedKinds = new Set(["completion", "assistant_final", "plan_ready", "moltbook_reply", "thread_share", "a2a_task_result"]);
11354
11494
  return completedKinds.has(kind) ? "completed" : "pending";
@@ -11428,6 +11568,9 @@ function renderTypePillContent(kindInfo) {
11428
11568
 
11429
11569
  function itemIntentText(kind, status = "pending", provider) {
11430
11570
  const vars = { provider: providerDisplayName(provider) };
11571
+ if (status === "in_progress" || kind === "activity_status") {
11572
+ return L("intent.inProgress", vars);
11573
+ }
11431
11574
  if (kind === "diff_thread") {
11432
11575
  return L("intent.diffThread");
11433
11576
  }
@@ -11461,6 +11604,12 @@ function itemIntentText(kind, status = "pending", provider) {
11461
11604
  return L("intent.choice", vars);
11462
11605
  case "completion":
11463
11606
  return L("intent.completed");
11607
+ case "moltbook_draft":
11608
+ return L("intent.draftReview");
11609
+ case "a2a_task":
11610
+ return L("intent.delegationReview");
11611
+ case "thread_share":
11612
+ return L("intent.handoffReview");
11464
11613
  default:
11465
11614
  return L("summary.default");
11466
11615
  }
@@ -11468,6 +11617,9 @@ function itemIntentText(kind, status = "pending", provider) {
11468
11617
 
11469
11618
  function detailIntentText(detail) {
11470
11619
  const provider = detail?.provider;
11620
+ if (detail.kind === "activity_status" || (detail.kind === "a2a_task" && ["working", "input-required"].includes(normalizeClientText(detail.taskStatus)))) {
11621
+ return itemIntentText(detail.kind, "in_progress", provider);
11622
+ }
11471
11623
  if (detail.kind === "diff_thread") {
11472
11624
  return itemIntentText(detail.kind, "diff", provider);
11473
11625
  }
@@ -11542,6 +11694,9 @@ function detailDisplayTitle(detail) {
11542
11694
 
11543
11695
  function fallbackSummaryForKind(kind, status, provider) {
11544
11696
  const vars = { provider: providerDisplayName(provider) };
11697
+ if (status === "in_progress" || kind === "activity_status") {
11698
+ return L("summary.inProgress", vars);
11699
+ }
11545
11700
  if (status === "completed") {
11546
11701
  return L("summary.completed");
11547
11702
  }
package/web/build-id.js CHANGED
@@ -1 +1 @@
1
- export const APP_BUILD_ID = "20260525-wallet-network-count";
1
+ export const APP_BUILD_ID = "20260713-lan-recovery-v2";