viveworker 0.5.3 → 0.6.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.
@@ -29,7 +29,7 @@ const sessionCookieName = "viveworker_session";
29
29
  const deviceCookieName = "viveworker_device";
30
30
  const historyKinds = new Set(["completion", "assistant_final", "plan_ready", "approval", "plan", "choice", "info", "moltbook_reply", "moltbook_draft", "thread_share", "a2a_task", "a2a_task_result"]);
31
31
  const timelineMessageKinds = new Set(["user_message", "assistant_commentary", "assistant_final"]);
32
- const timelineKinds = new Set([...timelineMessageKinds, "approval", "plan", "choice", "plan_ready", "file_event", "moltbook_reply", "moltbook_draft", "thread_share", "a2a_task", "a2a_task_result"]);
32
+ const timelineKinds = new Set([...timelineMessageKinds, "ambient_suggestions", "approval", "plan", "choice", "plan_ready", "file_event", "moltbook_reply", "moltbook_draft", "thread_share", "a2a_task", "a2a_task_result"]);
33
33
  const SQLITE_COMPLETION_BATCH_SIZE = 200;
34
34
  const DEFAULT_DEVICE_TRUST_TTL_MS = 30 * 24 * 60 * 60 * 1000;
35
35
  const MAX_PAIRED_DEVICES = 200;
@@ -161,6 +161,7 @@ runtime.recentHistoryItems = initialHistoryItems;
161
161
  runtime.recentTimelineEntries = initialTimelineEntries;
162
162
  state.recentHistoryItems = initialHistoryItems;
163
163
  state.recentTimelineEntries = initialTimelineEntries;
164
+ const backfilledAmbientSuggestionsStateChanged = backfillAmbientSuggestionsState({ config, runtime, state });
164
165
  const migratedRecentCodeEventsStateChanged = migrateRecentCodeEventsState({ config, runtime, state });
165
166
  const restoredTimelineImagePathsStateChanged = await backfillPersistedTimelineImagePaths({ config, runtime, state });
166
167
  const backfilledMoltbookInboxChanged = await backfillMoltbookInboxHistory({ config, runtime, state });
@@ -291,6 +292,8 @@ function kindTitle(locale, kind) {
291
292
  return t(locale, "server.title.assistantCommentary");
292
293
  case "assistant_final":
293
294
  return t(locale, "server.title.assistantFinal");
295
+ case "ambient_suggestions":
296
+ return t(locale, "server.title.ambientSuggestions");
294
297
  case "approval":
295
298
  return t(locale, "server.title.approval");
296
299
  case "plan":
@@ -319,6 +322,117 @@ function kindTitle(locale, kind) {
319
322
  }
320
323
  }
321
324
 
325
+ function normalizeAmbientSuggestion(raw) {
326
+ if (!isPlainObject(raw)) {
327
+ return null;
328
+ }
329
+
330
+ const title = cleanText(raw.title ?? "");
331
+ const prompt = normalizeLongText(raw.prompt ?? "");
332
+ if (!title || !prompt) {
333
+ return null;
334
+ }
335
+
336
+ const threadAction = isPlainObject(raw.threadAction)
337
+ ? {
338
+ ...(cleanText(raw.threadAction.type ?? "") ? { type: cleanText(raw.threadAction.type) } : {}),
339
+ }
340
+ : null;
341
+ const appIds = Array.isArray(raw.appIds)
342
+ ? raw.appIds.map((value) => cleanText(value ?? "")).filter(Boolean)
343
+ : [];
344
+
345
+ return {
346
+ ...(cleanText(raw.id ?? "") ? { id: cleanText(raw.id) } : {}),
347
+ title,
348
+ prompt,
349
+ ...(cleanText(raw.description ?? "") ? { description: cleanText(raw.description) } : {}),
350
+ ...(threadAction && threadAction.type ? { threadAction } : {}),
351
+ ...(appIds.length > 0 ? { appIds } : {}),
352
+ };
353
+ }
354
+
355
+ function normalizeAmbientSuggestions(raw) {
356
+ if (!Array.isArray(raw)) {
357
+ return [];
358
+ }
359
+ return raw.map((entry) => normalizeAmbientSuggestion(entry)).filter(Boolean);
360
+ }
361
+
362
+ function parseAmbientSuggestionsEnvelope(value) {
363
+ const normalized = cleanText(value ?? "");
364
+ if (!normalized) {
365
+ return null;
366
+ }
367
+
368
+ let payload;
369
+ try {
370
+ payload = JSON.parse(normalized);
371
+ } catch {
372
+ return null;
373
+ }
374
+
375
+ if (!isPlainObject(payload)) {
376
+ return null;
377
+ }
378
+
379
+ const hasSuggestionsField = Array.isArray(payload.suggestions);
380
+ const hasExcludeField = Array.isArray(payload.exclude);
381
+ if (!hasSuggestionsField && !hasExcludeField) {
382
+ return null;
383
+ }
384
+
385
+ const suggestions = hasSuggestionsField ? normalizeAmbientSuggestions(payload.suggestions) : [];
386
+ if (suggestions.length > 0) {
387
+ return { type: "suggestions", suggestions };
388
+ }
389
+
390
+ return {
391
+ type: "metadata",
392
+ hasSuggestionsField,
393
+ hasExcludeField,
394
+ };
395
+ }
396
+
397
+ function parseAmbientSuggestionsPayload(value) {
398
+ const envelope = parseAmbientSuggestionsEnvelope(value);
399
+ return envelope?.type === "suggestions" ? envelope.suggestions : null;
400
+ }
401
+
402
+ function isAmbientSuggestionsMetadataPayload(value) {
403
+ return parseAmbientSuggestionsEnvelope(value) !== null;
404
+ }
405
+
406
+ function ambientSuggestionsSummary(locale, suggestions) {
407
+ const count = Math.max(0, Array.isArray(suggestions) ? suggestions.length : 0);
408
+ if (count <= 0) {
409
+ return "";
410
+ }
411
+ const firstTitle = cleanText(suggestions[0]?.title || "");
412
+ return t(locale, "summary.ambientSuggestions", {
413
+ count,
414
+ firstTitle,
415
+ more: Math.max(0, count - 1),
416
+ });
417
+ }
418
+
419
+ function ambientSuggestionsMessageText(locale, suggestions) {
420
+ if (!Array.isArray(suggestions) || suggestions.length === 0) {
421
+ return "";
422
+ }
423
+ return [
424
+ kindTitle(locale, "ambient_suggestions"),
425
+ "",
426
+ ...suggestions.flatMap((suggestion, index) => {
427
+ const description = cleanText(suggestion.description || "");
428
+ return [
429
+ `${index + 1}. ${cleanText(suggestion.title || "")}`,
430
+ ...(description ? [description] : []),
431
+ ];
432
+ }),
433
+ ].join("\n");
434
+ }
435
+
322
436
  function looksLikeGeneratedThreadTitle(value) {
323
437
  const normalized = cleanText(value || "");
324
438
  if (!normalized.includes("|")) {
@@ -2250,6 +2364,8 @@ function timelineKindSortPriority(kind) {
2250
2364
  case "plan_ready":
2251
2365
  case "choice":
2252
2366
  return 60;
2367
+ case "ambient_suggestions":
2368
+ return 52;
2253
2369
  case "file_event":
2254
2370
  return 45;
2255
2371
  case "assistant_final":
@@ -2281,6 +2397,10 @@ function normalizeTimelineEntry(raw) {
2281
2397
  const diffText = normalizeTimelineDiffText(raw.diffText ?? "");
2282
2398
  const diffSource = normalizeTimelineDiffSource(raw.diffSource ?? "");
2283
2399
  const diffCounts = diffLineCounts(diffText);
2400
+ const suggestions = normalizeAmbientSuggestions(raw.suggestions ?? []);
2401
+ if (kind === "ambient_suggestions" && suggestions.length === 0) {
2402
+ return null;
2403
+ }
2284
2404
  const summary =
2285
2405
  normalizeNotificationText(raw.summary ?? "") ||
2286
2406
  formatNotificationBody(messageText, 180) ||
@@ -2351,6 +2471,7 @@ function normalizeTimelineEntry(raw) {
2351
2471
  // A2A task-specific fields
2352
2472
  ...(raw.instruction != null ? { instruction: cleanText(raw.instruction) } : {}),
2353
2473
  ...(raw.taskStatus != null ? { taskStatus: cleanText(raw.taskStatus) } : {}),
2474
+ ...(suggestions.length > 0 ? { suggestions } : {}),
2354
2475
  };
2355
2476
  }
2356
2477
 
@@ -2525,6 +2646,9 @@ function historyItemFromEvent(event) {
2525
2646
 
2526
2647
  const kind = event.kind === "task_complete" ? "completion" : "plan_ready";
2527
2648
  const messageText = normalizeLongText(event.detailText || event.message || "");
2649
+ if (kind === "completion" && isAmbientSuggestionsMetadataPayload(messageText)) {
2650
+ return null;
2651
+ }
2528
2652
  return normalizeHistoryItem({
2529
2653
  stableId: event.id,
2530
2654
  kind,
@@ -2558,6 +2682,74 @@ function recordHistoryItem({ config, runtime, state, item }) {
2558
2682
  return changed;
2559
2683
  }
2560
2684
 
2685
+ function backfillAmbientSuggestionsState({ config, runtime, state }) {
2686
+ const nextTimelineEntries = normalizeTimelineEntries(
2687
+ (runtime.recentTimelineEntries ?? []).map((entry) => {
2688
+ const entryKind = cleanText(entry?.kind || "");
2689
+ if (entryKind === "ambient_suggestions") {
2690
+ const suggestions = normalizeAmbientSuggestions(entry?.suggestions ?? []);
2691
+ if (suggestions.length === 0) {
2692
+ return null;
2693
+ }
2694
+ return {
2695
+ ...entry,
2696
+ title: kindTitle(DEFAULT_LOCALE, "ambient_suggestions"),
2697
+ summary: ambientSuggestionsSummary(DEFAULT_LOCALE, suggestions),
2698
+ messageText: ambientSuggestionsMessageText(DEFAULT_LOCALE, suggestions),
2699
+ suggestions,
2700
+ };
2701
+ }
2702
+ if (entryKind !== "assistant_final") {
2703
+ return entry;
2704
+ }
2705
+ const envelope = parseAmbientSuggestionsEnvelope(entry?.messageText ?? "");
2706
+ if (!envelope) {
2707
+ return entry;
2708
+ }
2709
+ if (envelope.type !== "suggestions") {
2710
+ return null;
2711
+ }
2712
+ const suggestions = envelope.suggestions;
2713
+ return {
2714
+ ...entry,
2715
+ kind: "ambient_suggestions",
2716
+ title: kindTitle(DEFAULT_LOCALE, "ambient_suggestions"),
2717
+ summary: ambientSuggestionsSummary(DEFAULT_LOCALE, suggestions),
2718
+ messageText: ambientSuggestionsMessageText(DEFAULT_LOCALE, suggestions),
2719
+ suggestions,
2720
+ };
2721
+ }),
2722
+ config.maxTimelineEntries
2723
+ );
2724
+
2725
+ const nextHistoryItems = normalizeHistoryItems(
2726
+ (runtime.recentHistoryItems ?? []).filter((item) => {
2727
+ const kind = cleanText(item?.kind || "");
2728
+ if (kind !== "assistant_final" && kind !== "completion") {
2729
+ return true;
2730
+ }
2731
+ return !isAmbientSuggestionsMetadataPayload(item?.messageText ?? "");
2732
+ }),
2733
+ config.maxHistoryItems
2734
+ );
2735
+
2736
+ const timelineChanged =
2737
+ JSON.stringify(nextTimelineEntries) !== JSON.stringify(runtime.recentTimelineEntries ?? []);
2738
+ const historyChanged =
2739
+ JSON.stringify(nextHistoryItems) !== JSON.stringify(runtime.recentHistoryItems ?? []);
2740
+
2741
+ if (timelineChanged) {
2742
+ runtime.recentTimelineEntries = nextTimelineEntries;
2743
+ state.recentTimelineEntries = nextTimelineEntries;
2744
+ }
2745
+ if (historyChanged) {
2746
+ runtime.recentHistoryItems = nextHistoryItems;
2747
+ state.recentHistoryItems = nextHistoryItems;
2748
+ }
2749
+
2750
+ return timelineChanged || historyChanged;
2751
+ }
2752
+
2561
2753
  // Flip a previously-recorded history item's readOnly flag without disturbing
2562
2754
  // its ordering or other fields. Used by moltbook_reply / moltbook_draft flows
2563
2755
  // to transition pending → resolved so the entry starts matching the completed
@@ -4140,6 +4332,20 @@ function buildSqliteTimelineEntry({ row, config, runtime }) {
4140
4332
  if (!messageText) {
4141
4333
  return null;
4142
4334
  }
4335
+ const ambientSuggestionsEnvelope = kind === "assistant_final" ? parseAmbientSuggestionsEnvelope(messageText) : null;
4336
+ if (ambientSuggestionsEnvelope?.type === "metadata") {
4337
+ return null;
4338
+ }
4339
+ const ambientSuggestions = ambientSuggestionsEnvelope?.type === "suggestions"
4340
+ ? ambientSuggestionsEnvelope.suggestions
4341
+ : null;
4342
+ const entryKind = ambientSuggestions ? "ambient_suggestions" : kind;
4343
+ const entryMessageText = ambientSuggestions
4344
+ ? ambientSuggestionsMessageText(config.defaultLocale, ambientSuggestions)
4345
+ : messageText;
4346
+ const entrySummary = ambientSuggestions
4347
+ ? ambientSuggestionsSummary(config.defaultLocale, ambientSuggestions)
4348
+ : formatNotificationBody(messageText, 180) || messageText;
4143
4349
 
4144
4350
  const createdAtMs = Math.max(0, Number(row.ts) || 0) * 1000 || Date.now();
4145
4351
  const threadLabel = getNativeThreadLabel({
@@ -4147,17 +4353,18 @@ function buildSqliteTimelineEntry({ row, config, runtime }) {
4147
4353
  conversationId: threadId,
4148
4354
  cwd: "",
4149
4355
  });
4150
- const stableId = messageTimelineStableId(kind, threadId, item.id || row.id, messageText, createdAtMs);
4356
+ const stableId = messageTimelineStableId(entryKind, threadId, item.id || row.id, entryMessageText, createdAtMs);
4151
4357
 
4152
4358
  return normalizeTimelineEntry({
4153
4359
  stableId,
4154
4360
  token: historyToken(stableId),
4155
- kind,
4361
+ kind: entryKind,
4156
4362
  threadId,
4157
4363
  threadLabel,
4158
- title: threadLabel || kindTitle(config.defaultLocale, kind),
4159
- summary: formatNotificationBody(messageText, 180) || messageText,
4160
- messageText,
4364
+ title: ambientSuggestions ? kindTitle(config.defaultLocale, entryKind) : threadLabel || kindTitle(config.defaultLocale, entryKind),
4365
+ summary: entrySummary,
4366
+ messageText: entryMessageText,
4367
+ ...(ambientSuggestions ? { suggestions: ambientSuggestions } : {}),
4161
4368
  createdAtMs,
4162
4369
  readOnly: true,
4163
4370
  });
@@ -4609,7 +4816,11 @@ async function processScannedEvent({ config, runtime, state, event }) {
4609
4816
  item: historyItemFromEvent(event),
4610
4817
  }) || dirty;
4611
4818
 
4612
- if (event.kind === "task_complete") {
4819
+ const isAmbientSuggestionsCompletion =
4820
+ event.kind === "task_complete" &&
4821
+ isAmbientSuggestionsMetadataPayload(normalizeLongText(event.detailText || event.message || ""));
4822
+
4823
+ if (event.kind === "task_complete" && !isAmbientSuggestionsCompletion) {
4613
4824
  dirty =
4614
4825
  (await deliverWebPushItem({
4615
4826
  config,
@@ -4626,7 +4837,7 @@ async function processScannedEvent({ config, runtime, state, event }) {
4626
4837
  })) || dirty;
4627
4838
  }
4628
4839
 
4629
- if (config.enableNtfy) {
4840
+ if (config.enableNtfy && !isAmbientSuggestionsCompletion) {
4630
4841
  try {
4631
4842
  await publishNtfy(config, event);
4632
4843
  } catch (error) {
@@ -9351,7 +9562,18 @@ function buildCompletedInboxItems(runtime, state, config, locale) {
9351
9562
  const items = normalizeHistoryItems(state.recentHistoryItems ?? runtime.recentHistoryItems, config.maxHistoryItems);
9352
9563
  runtime.recentHistoryItems = items;
9353
9564
  const completedKinds = new Set(["assistant_final", "approval", "moltbook_reply", "moltbook_draft", "thread_share", "a2a_task_result"]);
9354
- return items
9565
+
9566
+ // Infrequent kinds (A2A, thread_share) get evicted from the fixed-size
9567
+ // history FIFO by the high volume of approval/assistant_final entries.
9568
+ // Fall back to recentTimelineEntries so completed A2A tasks are never lost
9569
+ // while they're still within the timeline window.
9570
+ const infrequentKinds = new Set(["a2a_task_result", "thread_share"]);
9571
+ const historyStableIds = new Set(items.map((i) => i.stableId).filter(Boolean));
9572
+ const timelineEntries = (runtime.recentTimelineEntries ?? [])
9573
+ .filter((e) => infrequentKinds.has(cleanText(e?.kind || "")) && !historyStableIds.has(e.stableId));
9574
+ const merged = [...items, ...timelineEntries];
9575
+
9576
+ return merged
9355
9577
  .filter((item) => {
9356
9578
  const k = cleanText(item?.kind || "");
9357
9579
  if (!completedKinds.has(k)) return false;
@@ -9631,22 +9853,44 @@ function buildOperationalTimelineEntries(runtime, state, config, locale) {
9631
9853
  }
9632
9854
 
9633
9855
  for (const historyItem of normalizeHistoryItems(state.recentHistoryItems ?? runtime.recentHistoryItems, config.maxHistoryItems)) {
9634
- if (!timelineKinds.has(historyItem.kind)) {
9856
+ let historyKind = historyItem.kind;
9857
+ let historySummary = historyItem.summary;
9858
+ let historyMessageText = historyItem.messageText;
9859
+ let historyTitle = historyItem.threadLabel ? formatTitle(kindTitle(locale, historyItem.kind), historyItem.threadLabel) : historyItem.title;
9860
+ let historySuggestions = [];
9861
+
9862
+ if (historyItem.kind === "assistant_final" || historyItem.kind === "completion") {
9863
+ const ambientSuggestionsEnvelope = parseAmbientSuggestionsEnvelope(historyItem.messageText ?? "");
9864
+ if (ambientSuggestionsEnvelope?.type === "metadata") {
9865
+ continue;
9866
+ }
9867
+ if (ambientSuggestionsEnvelope?.type === "suggestions") {
9868
+ const ambientSuggestions = ambientSuggestionsEnvelope.suggestions;
9869
+ historyKind = "ambient_suggestions";
9870
+ historyTitle = kindTitle(locale, historyKind);
9871
+ historySummary = ambientSuggestionsSummary(locale, ambientSuggestions);
9872
+ historyMessageText = ambientSuggestionsMessageText(locale, ambientSuggestions);
9873
+ historySuggestions = ambientSuggestions;
9874
+ }
9875
+ }
9876
+
9877
+ if (!timelineKinds.has(historyKind)) {
9635
9878
  continue;
9636
9879
  }
9637
9880
  items.push(
9638
9881
  normalizeTimelineEntry({
9639
9882
  stableId: historyItem.stableId,
9640
9883
  token: historyItem.token,
9641
- kind: historyItem.kind,
9884
+ kind: historyKind,
9642
9885
  threadId: cleanText(extractConversationIdFromStableId(historyItem.stableId) || ""),
9643
9886
  threadLabel: historyItem.threadLabel,
9644
- title: historyItem.threadLabel ? formatTitle(kindTitle(locale, historyItem.kind), historyItem.threadLabel) : historyItem.title,
9645
- summary: historyItem.summary,
9646
- messageText: historyItem.messageText,
9887
+ title: historyTitle,
9888
+ summary: historySummary,
9889
+ messageText: historyMessageText,
9647
9890
  outcome: historyItem.outcome,
9648
9891
  createdAtMs: historyItem.createdAtMs,
9649
9892
  provider: normalizeProvider(historyItem.provider),
9893
+ ...(historySuggestions.length > 0 && historyKind === "ambient_suggestions" ? { suggestions: historySuggestions } : {}),
9650
9894
  })
9651
9895
  );
9652
9896
  }
@@ -10070,6 +10314,25 @@ function buildTimelineMessageDetail(entry, locale, runtime = null) {
10070
10314
  };
10071
10315
  }
10072
10316
 
10317
+ function buildAmbientSuggestionsDetail(entry, locale) {
10318
+ const suggestions = normalizeAmbientSuggestions(entry?.suggestions ?? []);
10319
+ return {
10320
+ kind: "ambient_suggestions",
10321
+ token: cleanText(entry?.token || ""),
10322
+ threadId: cleanText(entry?.threadId || ""),
10323
+ title: cleanText(entry?.title || "") || kindTitle(locale, "ambient_suggestions"),
10324
+ threadLabel: cleanText(entry?.threadLabel || ""),
10325
+ createdAtMs: Number(entry?.createdAtMs) || 0,
10326
+ messageHtml: renderMessageHtml(
10327
+ t(locale, "detail.ambientSuggestions.copy"),
10328
+ `<p>${escapeHtml(t(locale, "detail.detailUnavailable"))}</p>`
10329
+ ),
10330
+ suggestions,
10331
+ readOnly: true,
10332
+ actions: [],
10333
+ };
10334
+ }
10335
+
10073
10336
  function buildTimelineFileEventDetail(entry, locale) {
10074
10337
  const fileEventType = normalizeTimelineFileEventType(entry?.fileEventType ?? "");
10075
10338
  return {
@@ -10791,6 +11054,33 @@ async function buildApiItemDetail({ config, runtime, state, kind, token, locale
10791
11054
  const entry = timelineEntryByToken(runtime, token, kind);
10792
11055
  return entry ? buildTimelineFileEventDetail(entry, locale) : null;
10793
11056
  }
11057
+ if (kind === "ambient_suggestions") {
11058
+ const entry = timelineEntryByToken(runtime, token, kind);
11059
+ if (entry) {
11060
+ return buildAmbientSuggestionsDetail(entry, locale);
11061
+ }
11062
+ const historicalSource = (runtime.recentHistoryItems ?? []).find((item) => {
11063
+ if (cleanText(item?.token || "") !== cleanText(token || "")) {
11064
+ return false;
11065
+ }
11066
+ const itemKind = cleanText(item?.kind || "");
11067
+ if (itemKind !== "assistant_final" && itemKind !== "completion") {
11068
+ return false;
11069
+ }
11070
+ return parseAmbientSuggestionsPayload(item?.messageText ?? "") !== null;
11071
+ });
11072
+ if (!historicalSource) {
11073
+ return null;
11074
+ }
11075
+ return buildAmbientSuggestionsDetail({
11076
+ token: historicalSource.token,
11077
+ threadId: historyItemThreadId(historicalSource),
11078
+ threadLabel: historicalSource.threadLabel,
11079
+ title: kindTitle(locale, "ambient_suggestions"),
11080
+ createdAtMs: historicalSource.createdAtMs,
11081
+ suggestions: parseAmbientSuggestionsPayload(historicalSource.messageText ?? "") ?? [],
11082
+ }, locale);
11083
+ }
10794
11084
  if (timelineMessageKinds.has(kind)) {
10795
11085
  const entry = timelineEntryByToken(runtime, token, kind);
10796
11086
  if (!entry) return null;
@@ -16213,6 +16503,7 @@ async function main() {
16213
16503
  migratedPairedDevicesStateChanged ||
16214
16504
  restoredPendingPlanStateChanged ||
16215
16505
  restoredTimelineImagePathsStateChanged ||
16506
+ backfilledAmbientSuggestionsStateChanged ||
16216
16507
  migratedRecentCodeEventsStateChanged ||
16217
16508
  restoredPendingUserInputStateChanged ||
16218
16509
  backfilledMoltbookInboxChanged ||