viveworker 0.5.3 → 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/package.json +1 -1
- package/scripts/viveworker-bridge.mjs +305 -14
- package/web/app.css +100 -0
- package/web/app.js +184 -5
- package/web/i18n.js +40 -8
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "viveworker",
|
|
3
|
-
"version": "0.5.
|
|
3
|
+
"version": "0.5.5",
|
|
4
4
|
"description": "Local mobile companion for Codex Desktop and Claude Desktop — approvals, code review, Moltbook drafts, and A2A (Agent-to-Agent) task relay on your LAN.",
|
|
5
5
|
"author": "Yuta Hoshino <hoshino.lireneo@gmail.com>",
|
|
6
6
|
"license": "MIT",
|
|
@@ -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(
|
|
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,
|
|
4159
|
-
summary:
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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:
|
|
9884
|
+
kind: historyKind,
|
|
9642
9885
|
threadId: cleanText(extractConversationIdFromStableId(historyItem.stableId) || ""),
|
|
9643
9886
|
threadLabel: historyItem.threadLabel,
|
|
9644
|
-
title:
|
|
9645
|
-
summary:
|
|
9646
|
-
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 ||
|
package/web/app.css
CHANGED
|
@@ -611,6 +611,7 @@ code {
|
|
|
611
611
|
|
|
612
612
|
.settings-compose-badge {
|
|
613
613
|
display: inline-flex;
|
|
614
|
+
align-items: center;
|
|
614
615
|
align-self: flex-start;
|
|
615
616
|
flex: 0 0 auto;
|
|
616
617
|
padding: 0.12rem 0.42rem;
|
|
@@ -622,6 +623,16 @@ code {
|
|
|
622
623
|
white-space: nowrap;
|
|
623
624
|
}
|
|
624
625
|
|
|
626
|
+
.settings-compose-badge svg {
|
|
627
|
+
width: 0.85rem;
|
|
628
|
+
height: 0.85rem;
|
|
629
|
+
display: block;
|
|
630
|
+
}
|
|
631
|
+
|
|
632
|
+
.settings-compose-badge:has(> svg) {
|
|
633
|
+
padding: 0.18rem 0.3rem;
|
|
634
|
+
}
|
|
635
|
+
|
|
625
636
|
.settings-compose-badge--post {
|
|
626
637
|
background: rgba(67, 160, 255, 0.18);
|
|
627
638
|
color: rgba(121, 196, 255, 0.92);
|
|
@@ -2182,6 +2193,95 @@ code {
|
|
|
2182
2193
|
padding-inline: 0.1rem;
|
|
2183
2194
|
}
|
|
2184
2195
|
|
|
2196
|
+
.ambient-suggestions {
|
|
2197
|
+
display: grid;
|
|
2198
|
+
gap: 0.95rem;
|
|
2199
|
+
}
|
|
2200
|
+
|
|
2201
|
+
.ambient-suggestions__intro > :first-child {
|
|
2202
|
+
margin-top: 0;
|
|
2203
|
+
}
|
|
2204
|
+
|
|
2205
|
+
.ambient-suggestions__intro > :last-child {
|
|
2206
|
+
margin-bottom: 0;
|
|
2207
|
+
}
|
|
2208
|
+
|
|
2209
|
+
.ambient-suggestions__list {
|
|
2210
|
+
display: grid;
|
|
2211
|
+
gap: 0.8rem;
|
|
2212
|
+
}
|
|
2213
|
+
|
|
2214
|
+
.ambient-suggestion-card {
|
|
2215
|
+
display: grid;
|
|
2216
|
+
gap: 0.72rem;
|
|
2217
|
+
padding: 0.95rem 1rem;
|
|
2218
|
+
border-radius: 18px;
|
|
2219
|
+
background: rgba(10, 17, 22, 0.54);
|
|
2220
|
+
border: 1px solid rgba(156, 181, 197, 0.12);
|
|
2221
|
+
}
|
|
2222
|
+
|
|
2223
|
+
.ambient-suggestion-card__header {
|
|
2224
|
+
display: grid;
|
|
2225
|
+
grid-template-columns: minmax(0, 1fr) auto;
|
|
2226
|
+
gap: 0.7rem;
|
|
2227
|
+
align-items: start;
|
|
2228
|
+
}
|
|
2229
|
+
|
|
2230
|
+
.ambient-suggestion-card__title,
|
|
2231
|
+
.ambient-suggestion-card__description,
|
|
2232
|
+
.ambient-suggestion-card__prompt-label,
|
|
2233
|
+
.ambient-suggestion-card__prompt {
|
|
2234
|
+
margin: 0;
|
|
2235
|
+
}
|
|
2236
|
+
|
|
2237
|
+
.ambient-suggestion-card__title {
|
|
2238
|
+
font-size: 1rem;
|
|
2239
|
+
line-height: 1.4;
|
|
2240
|
+
color: var(--text);
|
|
2241
|
+
overflow-wrap: anywhere;
|
|
2242
|
+
}
|
|
2243
|
+
|
|
2244
|
+
.ambient-suggestion-card__description {
|
|
2245
|
+
color: var(--muted);
|
|
2246
|
+
line-height: 1.55;
|
|
2247
|
+
overflow-wrap: anywhere;
|
|
2248
|
+
}
|
|
2249
|
+
|
|
2250
|
+
.ambient-suggestion-card__copy-button {
|
|
2251
|
+
min-height: 2.35rem;
|
|
2252
|
+
padding: 0.65rem 0.9rem;
|
|
2253
|
+
border-radius: 14px;
|
|
2254
|
+
font-size: 0.84rem;
|
|
2255
|
+
white-space: nowrap;
|
|
2256
|
+
flex: 0 0 auto;
|
|
2257
|
+
}
|
|
2258
|
+
|
|
2259
|
+
.ambient-suggestion-card__prompt-wrap {
|
|
2260
|
+
display: grid;
|
|
2261
|
+
gap: 0.42rem;
|
|
2262
|
+
}
|
|
2263
|
+
|
|
2264
|
+
.ambient-suggestion-card__prompt-label {
|
|
2265
|
+
color: var(--muted);
|
|
2266
|
+
font-size: 0.78rem;
|
|
2267
|
+
font-weight: 700;
|
|
2268
|
+
letter-spacing: 0.01em;
|
|
2269
|
+
}
|
|
2270
|
+
|
|
2271
|
+
.ambient-suggestion-card__prompt {
|
|
2272
|
+
padding: 0.88rem 0.92rem;
|
|
2273
|
+
border-radius: 15px;
|
|
2274
|
+
background: rgba(255, 255, 255, 0.04);
|
|
2275
|
+
border: 1px solid rgba(156, 181, 197, 0.12);
|
|
2276
|
+
color: #eef8ff;
|
|
2277
|
+
font-size: 0.9rem;
|
|
2278
|
+
line-height: 1.55;
|
|
2279
|
+
white-space: pre-wrap;
|
|
2280
|
+
word-break: break-word;
|
|
2281
|
+
overflow-wrap: anywhere;
|
|
2282
|
+
font-family: "SFMono-Regular", ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
|
|
2283
|
+
}
|
|
2284
|
+
|
|
2185
2285
|
.detail-diff-thread__header {
|
|
2186
2286
|
width: 100%;
|
|
2187
2287
|
display: grid;
|
package/web/app.js
CHANGED
|
@@ -55,6 +55,7 @@ const state = {
|
|
|
55
55
|
a2aTaskExecutorPick: "codex",
|
|
56
56
|
pushNotice: "",
|
|
57
57
|
pushError: "",
|
|
58
|
+
ambientSuggestionCopyState: null,
|
|
58
59
|
deviceNotice: "",
|
|
59
60
|
deviceError: "",
|
|
60
61
|
imageViewer: null,
|
|
@@ -676,6 +677,7 @@ function timelineKindFilterOptions() {
|
|
|
676
677
|
const codexClaudeOptions = [
|
|
677
678
|
allOption,
|
|
678
679
|
{ id: "messages", label: L("timeline.kindFilter.messages"), icon: "timeline" },
|
|
680
|
+
{ id: "suggestions", label: L("timeline.kindFilter.suggestions"), icon: "suggestions" },
|
|
679
681
|
{ id: "files", label: L("timeline.kindFilter.files"), icon: "file-event" },
|
|
680
682
|
{ id: "approvals", label: L("timeline.kindFilter.approvals"), icon: "approval" },
|
|
681
683
|
{ id: "plans", label: L("timeline.kindFilter.plans"), icon: "plan" },
|
|
@@ -710,6 +712,8 @@ function timelineEntryMatchesKindFilter(entry, filterId) {
|
|
|
710
712
|
switch (filterId) {
|
|
711
713
|
case "messages":
|
|
712
714
|
return TIMELINE_MESSAGE_KINDS.has(kind);
|
|
715
|
+
case "suggestions":
|
|
716
|
+
return kind === "ambient_suggestions";
|
|
713
717
|
case "files":
|
|
714
718
|
return kind === "file_event";
|
|
715
719
|
case "approvals":
|
|
@@ -2347,6 +2351,10 @@ function timelineEntryThreadLabel(item, isMessage) {
|
|
|
2347
2351
|
}
|
|
2348
2352
|
|
|
2349
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
|
+
|
|
2350
2358
|
if (isMessageLike) {
|
|
2351
2359
|
return item.summary || fallbackSummaryForKind(item.kind, status, item.provider);
|
|
2352
2360
|
}
|
|
@@ -2359,6 +2367,10 @@ function timelineEntryPrimaryText(item, status, { isMessageLike = false, isFileE
|
|
|
2359
2367
|
}
|
|
2360
2368
|
|
|
2361
2369
|
function timelineEntrySecondaryText(item, status, primaryText, { isMessageLike = false, isFileEvent = false } = {}) {
|
|
2370
|
+
if (item?.kind === "ambient_suggestions") {
|
|
2371
|
+
return "";
|
|
2372
|
+
}
|
|
2373
|
+
|
|
2362
2374
|
if (isMessageLike) {
|
|
2363
2375
|
return "";
|
|
2364
2376
|
}
|
|
@@ -3607,8 +3619,9 @@ function renderSettingsA2aSharePage(context) {
|
|
|
3607
3619
|
const visible = items.slice(0, visibleCount);
|
|
3608
3620
|
const hasMore = items.length > visibleCount;
|
|
3609
3621
|
const filesList = visible.map((item) => {
|
|
3622
|
+
const passwordLabel = L("settings.a2aShare.passwordProtected");
|
|
3610
3623
|
const lock = item.hasPassword
|
|
3611
|
-
? `<span class="settings-compose-badge settings-compose-badge--reply" title="${escapeHtml(
|
|
3624
|
+
? `<span class="settings-compose-badge settings-compose-badge--reply" role="img" title="${escapeHtml(passwordLabel)}" aria-label="${escapeHtml(passwordLabel)}">${renderIcon("lock")}</span>`
|
|
3612
3625
|
: "";
|
|
3613
3626
|
const label = escapeHtml(item.originalName || item.slug);
|
|
3614
3627
|
const link = item.url
|
|
@@ -3825,8 +3838,9 @@ function renderStandardDetailDesktop(detail) {
|
|
|
3825
3838
|
${renderMoltbookDraftComposer(detail)}
|
|
3826
3839
|
${renderA2ATaskDetail(detail)}
|
|
3827
3840
|
${renderThreadShareDetail(detail)}
|
|
3841
|
+
${renderAmbientSuggestionsSection(detail)}
|
|
3828
3842
|
${
|
|
3829
|
-
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"
|
|
3830
3844
|
? ""
|
|
3831
3845
|
: plainIntro
|
|
3832
3846
|
? plainIntro
|
|
@@ -3863,8 +3877,9 @@ function renderStandardDetailMobile(detail) {
|
|
|
3863
3877
|
${renderMoltbookDraftComposer(detail, { mobile: true })}
|
|
3864
3878
|
${renderA2ATaskDetail(detail, { mobile: true })}
|
|
3865
3879
|
${renderThreadShareDetail(detail, { mobile: true })}
|
|
3880
|
+
${renderAmbientSuggestionsSection(detail, { mobile: true })}
|
|
3866
3881
|
${
|
|
3867
|
-
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"
|
|
3868
3883
|
? ""
|
|
3869
3884
|
: plainIntro
|
|
3870
3885
|
? plainIntro
|
|
@@ -3889,6 +3904,65 @@ function renderStandardDetailMobile(detail) {
|
|
|
3889
3904
|
`;
|
|
3890
3905
|
}
|
|
3891
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
|
+
|
|
3892
3966
|
function renderDetailPlainIntro(detail, options = {}) {
|
|
3893
3967
|
if (!["approval", "diff_thread", "file_event"].includes(detail?.kind || "")) {
|
|
3894
3968
|
return "";
|
|
@@ -5290,6 +5364,41 @@ function bindShellInteractions() {
|
|
|
5290
5364
|
});
|
|
5291
5365
|
}
|
|
5292
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
|
+
|
|
5293
5402
|
for (const button of document.querySelectorAll("[data-diff-thread-file-toggle]")) {
|
|
5294
5403
|
button.addEventListener("click", async (event) => {
|
|
5295
5404
|
event.preventDefault();
|
|
@@ -6314,7 +6423,7 @@ function tabForItemKind(kind, fallback) {
|
|
|
6314
6423
|
if (kind === "diff_thread") {
|
|
6315
6424
|
return "diff";
|
|
6316
6425
|
}
|
|
6317
|
-
if (kind === "file_event") {
|
|
6426
|
+
if (kind === "file_event" || kind === "ambient_suggestions") {
|
|
6318
6427
|
return "timeline";
|
|
6319
6428
|
}
|
|
6320
6429
|
if (TIMELINE_MESSAGE_KINDS.has(kind)) {
|
|
@@ -6346,6 +6455,8 @@ function kindMeta(kind, item) {
|
|
|
6346
6455
|
return { label: L("common.assistantCommentary"), tone: "plan", icon: "assistant-commentary" };
|
|
6347
6456
|
case "assistant_final":
|
|
6348
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" };
|
|
6349
6460
|
case "approval":
|
|
6350
6461
|
return { label: L("common.approval"), tone: "approval", icon: "approval" };
|
|
6351
6462
|
case "plan":
|
|
@@ -6391,6 +6502,9 @@ function itemIntentText(kind, status = "pending", provider) {
|
|
|
6391
6502
|
if (kind === "file_event") {
|
|
6392
6503
|
return L("intent.fileEvent");
|
|
6393
6504
|
}
|
|
6505
|
+
if (kind === "ambient_suggestions") {
|
|
6506
|
+
return L("intent.ambientSuggestions");
|
|
6507
|
+
}
|
|
6394
6508
|
if (kind === "user_message") {
|
|
6395
6509
|
return L("intent.userMessage");
|
|
6396
6510
|
}
|
|
@@ -6425,6 +6539,9 @@ function detailIntentText(detail) {
|
|
|
6425
6539
|
if (detail.kind === "file_event") {
|
|
6426
6540
|
return itemIntentText(detail.kind, "timeline", provider);
|
|
6427
6541
|
}
|
|
6542
|
+
if (detail.kind === "ambient_suggestions") {
|
|
6543
|
+
return itemIntentText(detail.kind, "timeline", provider);
|
|
6544
|
+
}
|
|
6428
6545
|
if (TIMELINE_MESSAGE_KINDS.has(detail.kind)) {
|
|
6429
6546
|
return itemIntentText(detail.kind, "timeline", provider);
|
|
6430
6547
|
}
|
|
@@ -6443,7 +6560,11 @@ function renderDetailTitle(detail) {
|
|
|
6443
6560
|
}
|
|
6444
6561
|
|
|
6445
6562
|
function detailDisplayTitle(detail) {
|
|
6446
|
-
const 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
|
+
}
|
|
6447
6568
|
if (threadLabel) {
|
|
6448
6569
|
return threadLabel;
|
|
6449
6570
|
}
|
|
@@ -6491,6 +6612,8 @@ function fallbackSummaryForKind(kind, status, provider) {
|
|
|
6491
6612
|
return L("summary.diffThread");
|
|
6492
6613
|
case "file_event":
|
|
6493
6614
|
return L("summary.fileEvent", vars);
|
|
6615
|
+
case "ambient_suggestions":
|
|
6616
|
+
return L("summary.ambientSuggestions", { count: 0, firstTitle: "", more: 0 });
|
|
6494
6617
|
case "user_message":
|
|
6495
6618
|
return L("summary.userMessage");
|
|
6496
6619
|
case "assistant_commentary":
|
|
@@ -6707,6 +6830,8 @@ function renderIcon(name) {
|
|
|
6707
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>`;
|
|
6708
6831
|
case "assistant-final":
|
|
6709
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>`;
|
|
6710
6835
|
case "completed":
|
|
6711
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>`;
|
|
6712
6837
|
case "settings":
|
|
@@ -6733,6 +6858,8 @@ function renderIcon(name) {
|
|
|
6733
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>`;
|
|
6734
6859
|
case "check":
|
|
6735
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>`;
|
|
6736
6863
|
case "back":
|
|
6737
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>`;
|
|
6738
6865
|
case "chevron-down":
|
|
@@ -6979,6 +7106,58 @@ function normalizeClientFileRefs(fileRefs) {
|
|
|
6979
7106
|
return deduped;
|
|
6980
7107
|
}
|
|
6981
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
|
+
|
|
6982
7161
|
function fileRefLabel(fileRef) {
|
|
6983
7162
|
const normalized = normalizeClientText(fileRef);
|
|
6984
7163
|
if (!normalized) {
|
package/web/i18n.js
CHANGED
|
@@ -47,6 +47,7 @@ const translations = {
|
|
|
47
47
|
"common.userMessage": "You",
|
|
48
48
|
"common.assistantCommentary": "Update",
|
|
49
49
|
"common.assistantFinal": "Final reply",
|
|
50
|
+
"common.ambientSuggestions": "Suggestions",
|
|
50
51
|
"common.item": "Item",
|
|
51
52
|
"common.sns": "SNS",
|
|
52
53
|
"common.moltbookReply": "Reply",
|
|
@@ -138,6 +139,11 @@ const translations = {
|
|
|
138
139
|
"detail.filesTitle": "Files",
|
|
139
140
|
"detail.diffTitle": "Diff",
|
|
140
141
|
"detail.diffUnavailable": "Diff is not available for this change. Showing touched files only.",
|
|
142
|
+
"detail.ambientSuggestions.copy": "Review the suggested next steps and copy any prompt into a new turn when you want to try it.",
|
|
143
|
+
"detail.ambientSuggestions.prompt": "Prompt",
|
|
144
|
+
"detail.ambientSuggestions.copyPrompt": "Copy prompt",
|
|
145
|
+
"detail.ambientSuggestions.copyPromptDone": "Copied",
|
|
146
|
+
"detail.ambientSuggestions.copyPromptFailed": "Copy failed",
|
|
141
147
|
"detail.diffThread.copy": ({ count }) =>
|
|
142
148
|
`This thread has ${count} ${count === 1 ? "changed file" : "changed files"}. Review each file below.`,
|
|
143
149
|
"detail.diffThread.copy.current": ({ count }) =>
|
|
@@ -194,6 +200,13 @@ const translations = {
|
|
|
194
200
|
"summary.userMessage": "Open the message again and read it in context.",
|
|
195
201
|
"summary.assistantCommentary": "Read {provider}'s latest working update.",
|
|
196
202
|
"summary.assistantFinal": "Read {provider}'s final reply for this turn.",
|
|
203
|
+
"summary.ambientSuggestions": ({ count, firstTitle, more }) => {
|
|
204
|
+
if (!count) return "Review suggested next steps.";
|
|
205
|
+
const parts = [`${count} ${count === 1 ? "suggestion" : "suggestions"}`];
|
|
206
|
+
if (firstTitle) parts.push(firstTitle);
|
|
207
|
+
if (more > 0) parts.push(`+${more} more`);
|
|
208
|
+
return parts.join(" · ");
|
|
209
|
+
},
|
|
197
210
|
"summary.approval": "Review what {provider} wants to do and decide whether to allow it.",
|
|
198
211
|
"summary.plan": "Check the proposed plan before {provider} starts implementing it.",
|
|
199
212
|
"summary.choice": "Pick the option {provider} needs to continue.",
|
|
@@ -206,6 +219,7 @@ const translations = {
|
|
|
206
219
|
"intent.completed": "Finished. Open to review the result.",
|
|
207
220
|
"intent.fileEvent": "Review the files involved in this step.",
|
|
208
221
|
"intent.diffThread": "Review the file changes for this thread.",
|
|
222
|
+
"intent.ambientSuggestions": "Review suggested next steps and copy a prompt if you want to try one.",
|
|
209
223
|
"intent.userMessage": "Open this message again.",
|
|
210
224
|
"intent.assistantCommentary": "Read the latest working update.",
|
|
211
225
|
"intent.assistantFinal": "Read {provider}'s final reply for this turn.",
|
|
@@ -223,6 +237,7 @@ const translations = {
|
|
|
223
237
|
"timeline.kindFilterButtonLabel": "Filter timeline events",
|
|
224
238
|
"timeline.kindFilter.all": "All events",
|
|
225
239
|
"timeline.kindFilter.messages": "Messages",
|
|
240
|
+
"timeline.kindFilter.suggestions": "Suggested next steps",
|
|
226
241
|
"timeline.kindFilter.files": "Files",
|
|
227
242
|
"timeline.kindFilter.approvals": "Approvals",
|
|
228
243
|
"timeline.kindFilter.plans": "Plans",
|
|
@@ -430,10 +445,10 @@ const translations = {
|
|
|
430
445
|
"settings.a2aExecutor.ask": "Ask every time",
|
|
431
446
|
"settings.a2aExecutor.unavailable": "Not detected",
|
|
432
447
|
"settings.a2aExecutor.detected": "Detected",
|
|
433
|
-
"settings.a2aShare.title": "
|
|
434
|
-
"settings.a2aShare.subtitle": "
|
|
435
|
-
"settings.a2aShare.copy": "Host static HTML
|
|
436
|
-
"settings.a2aShare.unavailable": "
|
|
448
|
+
"settings.a2aShare.title": "File Share",
|
|
449
|
+
"settings.a2aShare.subtitle": "File hosting",
|
|
450
|
+
"settings.a2aShare.copy": "Host static files (HTML, PDF, images, CSV) on a private URL. Shares the A2A credentials; no separate setup needed.",
|
|
451
|
+
"settings.a2aShare.unavailable": "File Share is not configured. Run `viveworker a2a setup` to provision credentials.",
|
|
437
452
|
"settings.a2aShare.storage.title": "Storage",
|
|
438
453
|
"settings.a2aShare.files.title": "Active shares",
|
|
439
454
|
"settings.a2aShare.files.empty": "No active shares yet.",
|
|
@@ -519,6 +534,7 @@ const translations = {
|
|
|
519
534
|
"server.title.userMessage": "User",
|
|
520
535
|
"server.title.assistantCommentary": "Update",
|
|
521
536
|
"server.title.assistantFinal": "Final reply",
|
|
537
|
+
"server.title.ambientSuggestions": "Suggested next steps",
|
|
522
538
|
"server.action.review": "Review",
|
|
523
539
|
"server.action.detail": "Detail",
|
|
524
540
|
"server.action.select": "Choose",
|
|
@@ -734,6 +750,7 @@ const translations = {
|
|
|
734
750
|
"common.userMessage": "あなた",
|
|
735
751
|
"common.assistantCommentary": "途中経過",
|
|
736
752
|
"common.assistantFinal": "最終回答",
|
|
753
|
+
"common.ambientSuggestions": "候補",
|
|
737
754
|
"common.item": "項目",
|
|
738
755
|
"common.sns": "SNS",
|
|
739
756
|
"common.moltbookReply": "返信",
|
|
@@ -824,6 +841,11 @@ const translations = {
|
|
|
824
841
|
"detail.filesTitle": "関連ファイル",
|
|
825
842
|
"detail.diffTitle": "差分",
|
|
826
843
|
"detail.diffUnavailable": "この変更の差分はまだ利用できません。対象ファイルのみ表示します。",
|
|
844
|
+
"detail.ambientSuggestions.copy": "次にやる候補を確認して、試したい prompt があればコピーできます。",
|
|
845
|
+
"detail.ambientSuggestions.prompt": "Prompt",
|
|
846
|
+
"detail.ambientSuggestions.copyPrompt": "Prompt をコピー",
|
|
847
|
+
"detail.ambientSuggestions.copyPromptDone": "コピーしました",
|
|
848
|
+
"detail.ambientSuggestions.copyPromptFailed": "コピーに失敗しました",
|
|
827
849
|
"detail.diffThread.copy": ({ count }) => `このスレッドで変更があるファイルは ${count}件です。下でファイルごとに確認できます。`,
|
|
828
850
|
"detail.diffThread.copy.current": ({ count }) => `このスレッドで現在未ステージの変更があるファイルは ${count}件です。下でファイルごとに確認できます。`,
|
|
829
851
|
"detail.diffThread.copy.latest": ({ count }) => `このスレッドで最後に観測した変更があるファイルは ${count}件です。下でファイルごとに確認できます。`,
|
|
@@ -877,6 +899,13 @@ const translations = {
|
|
|
877
899
|
"summary.userMessage": "このメッセージをもう一度開いて文脈付きで確認します。",
|
|
878
900
|
"summary.assistantCommentary": "{provider} の最新の途中経過を確認します。",
|
|
879
901
|
"summary.assistantFinal": "このターンの {provider} の最終回答を確認します。",
|
|
902
|
+
"summary.ambientSuggestions": ({ count, firstTitle, more }) => {
|
|
903
|
+
if (!count) return "次にやる候補を確認します。";
|
|
904
|
+
const parts = [`${count}件の候補`];
|
|
905
|
+
if (firstTitle) parts.push(firstTitle);
|
|
906
|
+
if (more > 0) parts.push(`ほか${more}件`);
|
|
907
|
+
return parts.join(" · ");
|
|
908
|
+
},
|
|
880
909
|
"summary.approval": "{provider} がやろうとしていることを確認して、許可するか決めます。",
|
|
881
910
|
"summary.plan": "{provider} が実装を始める前に、提案されたプランを確認します。",
|
|
882
911
|
"summary.choice": "{provider} が先へ進むために必要な選択肢を選びます。",
|
|
@@ -889,6 +918,7 @@ const translations = {
|
|
|
889
918
|
"intent.completed": "完了済みです。結果を確認できます。",
|
|
890
919
|
"intent.fileEvent": "このステップで触れたファイルを確認します。",
|
|
891
920
|
"intent.diffThread": "このスレッドのファイル変更を確認します。",
|
|
921
|
+
"intent.ambientSuggestions": "次にやる候補を確認して、必要なら prompt をコピーします。",
|
|
892
922
|
"intent.userMessage": "このメッセージを開き直します。",
|
|
893
923
|
"intent.assistantCommentary": "最新の途中経過を確認します。",
|
|
894
924
|
"intent.assistantFinal": "このターンの {provider} の最終回答を確認します。",
|
|
@@ -906,6 +936,7 @@ const translations = {
|
|
|
906
936
|
"timeline.kindFilterButtonLabel": "タイムラインのイベントを絞り込む",
|
|
907
937
|
"timeline.kindFilter.all": "すべてのイベント",
|
|
908
938
|
"timeline.kindFilter.messages": "メッセージ",
|
|
939
|
+
"timeline.kindFilter.suggestions": "候補提案",
|
|
909
940
|
"timeline.kindFilter.files": "ファイル",
|
|
910
941
|
"timeline.kindFilter.approvals": "承認",
|
|
911
942
|
"timeline.kindFilter.plans": "プラン",
|
|
@@ -1113,10 +1144,10 @@ const translations = {
|
|
|
1113
1144
|
"settings.a2aExecutor.ask": "毎回確認する",
|
|
1114
1145
|
"settings.a2aExecutor.unavailable": "未検出",
|
|
1115
1146
|
"settings.a2aExecutor.detected": "検出",
|
|
1116
|
-
"settings.a2aShare.title": "
|
|
1117
|
-
"settings.a2aShare.subtitle": "
|
|
1118
|
-
"settings.a2aShare.copy": "
|
|
1119
|
-
"settings.a2aShare.unavailable": "
|
|
1147
|
+
"settings.a2aShare.title": "File Share",
|
|
1148
|
+
"settings.a2aShare.subtitle": "ファイルホスティング",
|
|
1149
|
+
"settings.a2aShare.copy": "静的ファイル (HTML / PDF / 画像 / CSV) を非公開 URL でホストします。A2A と認証情報を共有するため追加の設定は不要です。",
|
|
1150
|
+
"settings.a2aShare.unavailable": "File Share が設定されていません。`viveworker a2a setup` を実行して認証情報を設定してください。",
|
|
1120
1151
|
"settings.a2aShare.storage.title": "ストレージ",
|
|
1121
1152
|
"settings.a2aShare.files.title": "公開中のファイル",
|
|
1122
1153
|
"settings.a2aShare.files.empty": "公開中のファイルはありません。",
|
|
@@ -1202,6 +1233,7 @@ const translations = {
|
|
|
1202
1233
|
"server.title.userMessage": "ユーザー",
|
|
1203
1234
|
"server.title.assistantCommentary": "途中経過",
|
|
1204
1235
|
"server.title.assistantFinal": "最終回答",
|
|
1236
|
+
"server.title.ambientSuggestions": "次の候補",
|
|
1205
1237
|
"server.action.review": "判断する",
|
|
1206
1238
|
"server.action.detail": "詳細",
|
|
1207
1239
|
"server.action.select": "選ぶ",
|