viveworker 0.8.2 → 0.8.3
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/README.md +5 -4
- package/package.json +1 -1
- package/scripts/com.viveworker.moltbook-scout.plist.sample +2 -2
- package/scripts/moltbook-api.mjs +178 -2
- package/scripts/moltbook-cli.mjs +134 -8
- package/scripts/moltbook-scout-auto.sh +100 -10
- package/scripts/moltbook-watcher.mjs +10 -0
- package/scripts/viveworker-bridge.mjs +716 -38
- package/web/app.css +33 -0
- package/web/app.js +78 -18
- package/web/build-id.js +1 -1
- package/web/i18n.js +24 -2
- package/web/remote-pairing/api-router.js +168 -15
|
@@ -21,7 +21,7 @@ import { generatePairingCredentials, shouldRotatePairing, upsertEnvText } from "
|
|
|
21
21
|
import { renderMarkdownHtml } from "./lib/markdown-render.mjs";
|
|
22
22
|
import { buildAgentCard, handleA2ARequest, resolveA2ATaskDecision, completeA2ATask, failA2ATask } from "./a2a-handler.mjs";
|
|
23
23
|
import { registerWithRelay, startRelayPolling, stopRelayPolling, postRelayResult, getRelayStatus, updatePublicTasksFlag } from "./a2a-relay-client.mjs";
|
|
24
|
-
import { createMoltbookClient, readScoutState, writeScoutState, rollScoutDayIfNeeded, markPostSeen, recordComposeAttempt, writeDraft, readDraft, deleteDraft, listPendingDrafts, solveVerificationPuzzle, solvePuzzleWithLLM, recordPuzzleAttempt, listInboxItems } from "./moltbook-api.mjs";
|
|
24
|
+
import { createMoltbookClient, readScoutState, writeScoutState, rollScoutDayIfNeeded, markPostSeen, recordComposeAttempt, writeDraft, readDraft, deleteDraft, listPendingDrafts, solveVerificationPuzzle, solvePuzzleWithLLM, recordPuzzleAttempt, listInboxItems, updateInboxStatus, getMoltbookReplyQuotaState } from "./moltbook-api.mjs";
|
|
25
25
|
import { startRemotePairingRelay, DEFAULT_RELAY_URL } from "./lib/remote-pairing/orchestrator.mjs";
|
|
26
26
|
import { restartRemotePairingRelay, persistRemotePairingEnv, getRemotePairingStatus } from "./lib/remote-pairing/control.mjs";
|
|
27
27
|
import { appendRemotePairingAuditEvent, readRemotePairingAuditEvents } from "./lib/remote-pairing/audit.mjs";
|
|
@@ -55,7 +55,7 @@ const sessionCookieName = "viveworker_session";
|
|
|
55
55
|
const deviceCookieName = "viveworker_device";
|
|
56
56
|
const historyKinds = new Set(["completion", "assistant_final", "plan_ready", "approval", "plan", "choice", "info", "moltbook_reply", "moltbook_draft", "thread_share", "a2a_task", "a2a_task_result"]);
|
|
57
57
|
const timelineMessageKinds = new Set(["user_message", "assistant_commentary", "assistant_final"]);
|
|
58
|
-
const timelineKinds = new Set([...timelineMessageKinds, "ambient_suggestions", "approval", "plan", "choice", "plan_ready", "file_event", "command_event", "moltbook_reply", "moltbook_draft", "thread_share", "a2a_task", "a2a_task_result"]);
|
|
58
|
+
const timelineKinds = new Set([...timelineMessageKinds, "activity_status", "ambient_suggestions", "approval", "plan", "choice", "plan_ready", "file_event", "command_event", "moltbook_reply", "moltbook_draft", "thread_share", "a2a_task", "a2a_task_result"]);
|
|
59
59
|
const SQLITE_COMPLETION_BATCH_SIZE = 200;
|
|
60
60
|
const DEFAULT_DEVICE_TRUST_TTL_MS = 30 * 24 * 60 * 60 * 1000;
|
|
61
61
|
const MAX_PAIRED_DEVICES = 200;
|
|
@@ -70,6 +70,7 @@ const COMPLETION_PUSH_CONTENT_DEDUPE_WINDOW_MS = 2 * 60 * 1000;
|
|
|
70
70
|
const HAZBASE_METADATA_TIMEOUT_MS = 1500;
|
|
71
71
|
const NPM_VERSION_CHECK_TIMEOUT_MS = 2500;
|
|
72
72
|
const NPM_VERSION_CHECK_CACHE_TTL_MS = 6 * 60 * 60 * 1000;
|
|
73
|
+
const TIMELINE_ACTIVITY_TTL_MS = 2 * 60 * 1000;
|
|
73
74
|
|
|
74
75
|
// Shared memo for buildDiffThreadGroups. Each call spawns 3 git subprocesses
|
|
75
76
|
// per tracked repo (`git diff --name-status`, `git status --porcelain`,
|
|
@@ -265,6 +266,7 @@ const runtime = {
|
|
|
265
266
|
completionDetailsByToken: new Map(),
|
|
266
267
|
moltbookItemsByToken: new Map(),
|
|
267
268
|
moltbookDraftsByToken: new Map(),
|
|
269
|
+
moltbookReplyQuotaQueue: Promise.resolve(),
|
|
268
270
|
a2aTasksByToken: new Map(),
|
|
269
271
|
threadSharesByToken: new Map(),
|
|
270
272
|
threadRegistry: new Map(),
|
|
@@ -272,6 +274,7 @@ const runtime = {
|
|
|
272
274
|
recentHistoryItems: [],
|
|
273
275
|
recentTimelineEntries: [],
|
|
274
276
|
recentCodeEvents: [],
|
|
277
|
+
activeTimelineActivitiesByKey: new Map(),
|
|
275
278
|
timelineBus: null,
|
|
276
279
|
timelineRevision: 0,
|
|
277
280
|
timelineLiveWatchers: [],
|
|
@@ -346,6 +349,58 @@ setInterval(async () => {
|
|
|
346
349
|
}
|
|
347
350
|
}
|
|
348
351
|
}, 60_000).unref?.();
|
|
352
|
+
|
|
353
|
+
async function withMoltbookReplyQuotaLock(fn) {
|
|
354
|
+
const previous = runtime.moltbookReplyQuotaQueue || Promise.resolve();
|
|
355
|
+
let release;
|
|
356
|
+
const next = new Promise((resolve) => { release = resolve; });
|
|
357
|
+
runtime.moltbookReplyQuotaQueue = previous.then(() => next, () => next);
|
|
358
|
+
await previous.catch(() => {});
|
|
359
|
+
try {
|
|
360
|
+
return await fn();
|
|
361
|
+
} finally {
|
|
362
|
+
release();
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
async function reserveMoltbookReplyQuotaSlot(draft, maxDaily = 5) {
|
|
367
|
+
if (!draft || draft.draftType === "original_post") return { reserved: false };
|
|
368
|
+
return withMoltbookReplyQuotaLock(async () => {
|
|
369
|
+
const scoutState = rollScoutDayIfNeeded(await readScoutState());
|
|
370
|
+
if ((Number(scoutState.sentToday) || 0) >= maxDaily) {
|
|
371
|
+
return {
|
|
372
|
+
reserved: false,
|
|
373
|
+
quotaReached: true,
|
|
374
|
+
day: scoutState.day,
|
|
375
|
+
sentToday: Number(scoutState.sentToday) || 0,
|
|
376
|
+
maxDaily,
|
|
377
|
+
};
|
|
378
|
+
}
|
|
379
|
+
scoutState.sentToday = (Number(scoutState.sentToday) || 0) + 1;
|
|
380
|
+
markPostSeen(scoutState, draft.postId, "reserved");
|
|
381
|
+
await writeScoutState(scoutState);
|
|
382
|
+
draft.replyQuotaReserved = true;
|
|
383
|
+
draft.replyQuotaReservedDay = scoutState.day;
|
|
384
|
+
draft.replyQuotaReservedAtMs = Date.now();
|
|
385
|
+
return {
|
|
386
|
+
reserved: true,
|
|
387
|
+
day: scoutState.day,
|
|
388
|
+
sentToday: scoutState.sentToday,
|
|
389
|
+
maxDaily,
|
|
390
|
+
};
|
|
391
|
+
});
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
async function releaseMoltbookReplyQuotaSlot(draft) {
|
|
395
|
+
if (!draft?.replyQuotaReserved || draft.draftType === "original_post") return;
|
|
396
|
+
await withMoltbookReplyQuotaLock(async () => {
|
|
397
|
+
const scoutState = rollScoutDayIfNeeded(await readScoutState());
|
|
398
|
+
if (draft.replyQuotaReservedDay && scoutState.day !== draft.replyQuotaReservedDay) return;
|
|
399
|
+
scoutState.sentToday = Math.max(0, (Number(scoutState.sentToday) || 0) - 1);
|
|
400
|
+
await writeScoutState(scoutState);
|
|
401
|
+
});
|
|
402
|
+
draft.replyQuotaReserved = false;
|
|
403
|
+
}
|
|
349
404
|
const initialHistoryItems = normalizeHistoryItems(state.recentHistoryItems ?? [], config.maxHistoryItems);
|
|
350
405
|
const initialTimelineEntries = normalizeTimelineEntries(state.recentTimelineEntries ?? [], config.maxTimelineEntries);
|
|
351
406
|
const normalizedHistoryStateChanged =
|
|
@@ -514,6 +569,8 @@ function kindTitle(locale, kind) {
|
|
|
514
569
|
return t(locale, "server.title.assistantCommentary");
|
|
515
570
|
case "assistant_final":
|
|
516
571
|
return t(locale, "server.title.assistantFinal");
|
|
572
|
+
case "activity_status":
|
|
573
|
+
return t(locale, "server.title.activityStatus");
|
|
517
574
|
case "ambient_suggestions":
|
|
518
575
|
return t(locale, "server.title.ambientSuggestions");
|
|
519
576
|
case "approval":
|
|
@@ -829,7 +886,7 @@ function normalizeTimelineOutcome(value) {
|
|
|
829
886
|
|
|
830
887
|
function normalizeTimelineFileEventType(value) {
|
|
831
888
|
const normalized = cleanText(value || "").toLowerCase();
|
|
832
|
-
return ["read", "search", "command", "write", "create", "delete", "rename"].includes(normalized) ? normalized : "";
|
|
889
|
+
return ["read", "search", "git", "command", "write", "create", "delete", "rename"].includes(normalized) ? normalized : "";
|
|
833
890
|
}
|
|
834
891
|
|
|
835
892
|
function fileEventTitle(locale, fileEventType) {
|
|
@@ -838,6 +895,8 @@ function fileEventTitle(locale, fileEventType) {
|
|
|
838
895
|
return t(locale, "fileEvent.read");
|
|
839
896
|
case "search":
|
|
840
897
|
return t(locale, "fileEvent.search");
|
|
898
|
+
case "git":
|
|
899
|
+
return t(locale, "fileEvent.command");
|
|
841
900
|
case "command":
|
|
842
901
|
return t(locale, "fileEvent.command");
|
|
843
902
|
case "write":
|
|
@@ -860,6 +919,8 @@ function fileEventDetailCopy(locale, fileEventType, provider) {
|
|
|
860
919
|
return t(locale, "detail.fileEvent.read", vars);
|
|
861
920
|
case "search":
|
|
862
921
|
return t(locale, "detail.fileEvent.search", vars);
|
|
922
|
+
case "git":
|
|
923
|
+
return t(locale, "detail.fileEvent.command", vars);
|
|
863
924
|
case "command":
|
|
864
925
|
return t(locale, "detail.fileEvent.command", vars);
|
|
865
926
|
case "write":
|
|
@@ -1531,7 +1592,7 @@ function evaluateTrustedReadCommandPolicy({ commandText, cwd, workspaceRoot }) {
|
|
|
1531
1592
|
return null;
|
|
1532
1593
|
}
|
|
1533
1594
|
|
|
1534
|
-
const command =
|
|
1595
|
+
const command = commandBaseName(tokens[0]);
|
|
1535
1596
|
if (!command || /^[A-Za-z_][A-Za-z0-9_]*=.*/u.test(command)) {
|
|
1536
1597
|
return null;
|
|
1537
1598
|
}
|
|
@@ -1606,8 +1667,7 @@ function classifyTimelineCommand(commandText) {
|
|
|
1606
1667
|
if (["rg", "grep", "ag", "ack", "fd", "find"].includes(command) || gitSubcommand === "grep") {
|
|
1607
1668
|
fileEventType = "search";
|
|
1608
1669
|
} else if (
|
|
1609
|
-
["cat", "sed", "nl", "head", "tail", "wc", "ls", "pwd", "less", "more"].includes(command)
|
|
1610
|
-
(command === "git" && ["status", "diff", "show", "log", "ls-files", "branch"].includes(gitSubcommand))
|
|
1670
|
+
["cat", "sed", "nl", "head", "tail", "wc", "ls", "pwd", "less", "more"].includes(command)
|
|
1611
1671
|
) {
|
|
1612
1672
|
fileEventType = "read";
|
|
1613
1673
|
}
|
|
@@ -1668,15 +1728,19 @@ function extractReadFileRefsFromCommand(commandText) {
|
|
|
1668
1728
|
return [];
|
|
1669
1729
|
}
|
|
1670
1730
|
|
|
1671
|
-
if (command === "cat" || command === "nl") {
|
|
1672
|
-
return normalizeTimelineFileRefs(tokens
|
|
1731
|
+
if (command === "cat" || command === "nl" || command === "wc" || command === "less" || command === "more" || command === "ls") {
|
|
1732
|
+
return normalizeTimelineFileRefs(readCommandPathArgs(tokens));
|
|
1733
|
+
}
|
|
1734
|
+
|
|
1735
|
+
if (command === "head" || command === "tail") {
|
|
1736
|
+
return normalizeTimelineFileRefs(readCommandPathArgs(tokens, { valueOptions: new Set(["-n", "--lines", "-c", "--bytes"]) }));
|
|
1673
1737
|
}
|
|
1674
1738
|
|
|
1675
1739
|
if (command === "sed") {
|
|
1676
1740
|
if (!tokens.includes("-n")) {
|
|
1677
1741
|
return [];
|
|
1678
1742
|
}
|
|
1679
|
-
return normalizeTimelineFileRefs(tokens
|
|
1743
|
+
return normalizeTimelineFileRefs(sedCommandPathArgs(tokens));
|
|
1680
1744
|
}
|
|
1681
1745
|
|
|
1682
1746
|
if (command === "rg") {
|
|
@@ -1707,6 +1771,57 @@ function extractReadFileRefsFromCommand(commandText) {
|
|
|
1707
1771
|
return [];
|
|
1708
1772
|
}
|
|
1709
1773
|
|
|
1774
|
+
function readCommandPathArgs(tokens, { valueOptions = new Set() } = {}) {
|
|
1775
|
+
const args = [];
|
|
1776
|
+
for (let index = 1; index < tokens.length; index += 1) {
|
|
1777
|
+
const token = cleanText(tokens[index] || "");
|
|
1778
|
+
if (!token) {
|
|
1779
|
+
continue;
|
|
1780
|
+
}
|
|
1781
|
+
if (token === "--") {
|
|
1782
|
+
args.push(...tokens.slice(index + 1));
|
|
1783
|
+
break;
|
|
1784
|
+
}
|
|
1785
|
+
if (token.startsWith("-")) {
|
|
1786
|
+
const optionName = token.includes("=") ? token.slice(0, token.indexOf("=")) : token;
|
|
1787
|
+
if (valueOptions.has(optionName) && !token.includes("=")) {
|
|
1788
|
+
index += 1;
|
|
1789
|
+
}
|
|
1790
|
+
continue;
|
|
1791
|
+
}
|
|
1792
|
+
args.push(token);
|
|
1793
|
+
}
|
|
1794
|
+
return args;
|
|
1795
|
+
}
|
|
1796
|
+
|
|
1797
|
+
function sedCommandPathArgs(tokens) {
|
|
1798
|
+
const args = [];
|
|
1799
|
+
let scriptSeen = false;
|
|
1800
|
+
for (let index = 1; index < tokens.length; index += 1) {
|
|
1801
|
+
const token = cleanText(tokens[index] || "");
|
|
1802
|
+
if (!token) {
|
|
1803
|
+
continue;
|
|
1804
|
+
}
|
|
1805
|
+
if (token === "--") {
|
|
1806
|
+
args.push(...tokens.slice(index + 1));
|
|
1807
|
+
break;
|
|
1808
|
+
}
|
|
1809
|
+
if (token.startsWith("-")) {
|
|
1810
|
+
if (token === "-e" || token === "-f") {
|
|
1811
|
+
index += 1;
|
|
1812
|
+
scriptSeen = true;
|
|
1813
|
+
}
|
|
1814
|
+
continue;
|
|
1815
|
+
}
|
|
1816
|
+
if (!scriptSeen) {
|
|
1817
|
+
scriptSeen = true;
|
|
1818
|
+
continue;
|
|
1819
|
+
}
|
|
1820
|
+
args.push(token);
|
|
1821
|
+
}
|
|
1822
|
+
return args;
|
|
1823
|
+
}
|
|
1824
|
+
|
|
1710
1825
|
function autoPilotApprovalMessage(locale, commandText) {
|
|
1711
1826
|
const prefix = t(locale, "server.message.autoPilotTrustedReadApproved");
|
|
1712
1827
|
const commandBlock = cleanText(commandText || "")
|
|
@@ -3377,6 +3492,8 @@ function migrateRecentCodeEventsState({ config, runtime, state }) {
|
|
|
3377
3492
|
|
|
3378
3493
|
function timelineKindSortPriority(kind) {
|
|
3379
3494
|
switch (cleanText(kind || "")) {
|
|
3495
|
+
case "activity_status":
|
|
3496
|
+
return 90;
|
|
3380
3497
|
case "completion":
|
|
3381
3498
|
return 70;
|
|
3382
3499
|
case "approval":
|
|
@@ -3490,6 +3607,8 @@ function normalizeTimelineEntry(raw) {
|
|
|
3490
3607
|
tone: cleanText(raw.tone ?? "") || "secondary",
|
|
3491
3608
|
cwd: resolvePath(cleanText(raw.cwd || "")),
|
|
3492
3609
|
provider: normalizeProvider(raw.provider),
|
|
3610
|
+
...(raw.activityPhase != null ? { activityPhase: cleanText(raw.activityPhase) } : {}),
|
|
3611
|
+
...(raw.commandText != null ? { commandText: cleanText(raw.commandText) } : {}),
|
|
3493
3612
|
// Moltbook-specific fields — preserved for detail view fallback after
|
|
3494
3613
|
// the in-memory draft/item expires.
|
|
3495
3614
|
...(raw.draftText != null ? { draftText: cleanText(raw.draftText) } : {}),
|
|
@@ -3583,6 +3702,229 @@ function publishTimelineUpdate({ config, runtime, entry, source = "unknown" }) {
|
|
|
3583
3702
|
ensureTimelineBus(runtime).emit("timeline:update", payload);
|
|
3584
3703
|
}
|
|
3585
3704
|
|
|
3705
|
+
function timelineActivityKey(provider, threadId) {
|
|
3706
|
+
const normalizedProvider = normalizeProvider(provider) || "codex";
|
|
3707
|
+
const normalizedThreadId = cleanText(threadId || "");
|
|
3708
|
+
if (!normalizedThreadId) {
|
|
3709
|
+
return "";
|
|
3710
|
+
}
|
|
3711
|
+
return `${normalizedProvider}:${normalizedThreadId}`;
|
|
3712
|
+
}
|
|
3713
|
+
|
|
3714
|
+
function timelineActivityTitle(locale, phase) {
|
|
3715
|
+
switch (cleanText(phase || "")) {
|
|
3716
|
+
case "thinking":
|
|
3717
|
+
return t(locale, "server.activity.thinking");
|
|
3718
|
+
case "reading":
|
|
3719
|
+
return t(locale, "server.activity.reading");
|
|
3720
|
+
case "searching":
|
|
3721
|
+
return t(locale, "server.activity.searching");
|
|
3722
|
+
case "editing":
|
|
3723
|
+
return t(locale, "server.activity.editing");
|
|
3724
|
+
case "running_command":
|
|
3725
|
+
return t(locale, "server.activity.runningCommand");
|
|
3726
|
+
case "awaiting_approval":
|
|
3727
|
+
return t(locale, "server.activity.awaitingApproval");
|
|
3728
|
+
default:
|
|
3729
|
+
return t(locale, "server.activity.working");
|
|
3730
|
+
}
|
|
3731
|
+
}
|
|
3732
|
+
|
|
3733
|
+
function timelineActivitySummary(activity) {
|
|
3734
|
+
const phase = cleanText(activity?.phase || "");
|
|
3735
|
+
const refs = normalizeTimelineFileRefs(activity?.fileRefs || []);
|
|
3736
|
+
if (refs.length === 1 && ["reading", "searching", "editing"].includes(phase)) {
|
|
3737
|
+
return compactPath(refs[0]);
|
|
3738
|
+
}
|
|
3739
|
+
if (refs.length > 1 && ["reading", "searching", "editing"].includes(phase)) {
|
|
3740
|
+
return `${refs.length} files`;
|
|
3741
|
+
}
|
|
3742
|
+
const commandText = cleanText(activity?.commandText || "");
|
|
3743
|
+
if (commandText && phase !== "running_command") {
|
|
3744
|
+
return timelineCommandSummary(commandText);
|
|
3745
|
+
}
|
|
3746
|
+
return cleanText(activity?.summary || "");
|
|
3747
|
+
}
|
|
3748
|
+
|
|
3749
|
+
function buildTimelineActivityEntry(activity, locale = DEFAULT_LOCALE) {
|
|
3750
|
+
if (!isPlainObject(activity)) {
|
|
3751
|
+
return null;
|
|
3752
|
+
}
|
|
3753
|
+
const provider = normalizeProvider(activity.provider);
|
|
3754
|
+
const threadId = cleanText(activity.threadId || "");
|
|
3755
|
+
const key = cleanText(activity.key || timelineActivityKey(provider, threadId));
|
|
3756
|
+
if (!key || !threadId) {
|
|
3757
|
+
return null;
|
|
3758
|
+
}
|
|
3759
|
+
const phase = cleanText(activity.phase || "working");
|
|
3760
|
+
const updatedAtMs = Number(activity.updatedAtMs) || Date.now();
|
|
3761
|
+
return normalizeTimelineEntry({
|
|
3762
|
+
stableId: `activity_status:${key}`,
|
|
3763
|
+
token: historyToken(`activity_status:${key}`),
|
|
3764
|
+
kind: "activity_status",
|
|
3765
|
+
threadId,
|
|
3766
|
+
threadLabel: cleanText(activity.threadLabel || ""),
|
|
3767
|
+
title: timelineActivityTitle(locale, phase),
|
|
3768
|
+
summary: timelineActivitySummary(activity),
|
|
3769
|
+
messageText: cleanText(activity.commandText || activity.summary || ""),
|
|
3770
|
+
fileRefs: normalizeTimelineFileRefs(activity.fileRefs || []),
|
|
3771
|
+
commandText: cleanText(activity.commandText || ""),
|
|
3772
|
+
activityPhase: phase,
|
|
3773
|
+
createdAtMs: updatedAtMs,
|
|
3774
|
+
cwd: cleanText(activity.cwd || ""),
|
|
3775
|
+
readOnly: true,
|
|
3776
|
+
provider,
|
|
3777
|
+
});
|
|
3778
|
+
}
|
|
3779
|
+
|
|
3780
|
+
function publishTimelineActivityUpdate({ config, runtime, activity, source = "activity-status" }) {
|
|
3781
|
+
const entry = buildTimelineActivityEntry(activity, DEFAULT_LOCALE);
|
|
3782
|
+
if (entry) {
|
|
3783
|
+
publishTimelineUpdate({ config, runtime, entry, source });
|
|
3784
|
+
}
|
|
3785
|
+
}
|
|
3786
|
+
|
|
3787
|
+
function upsertTimelineActivity({
|
|
3788
|
+
config,
|
|
3789
|
+
runtime,
|
|
3790
|
+
provider,
|
|
3791
|
+
threadId,
|
|
3792
|
+
threadLabel = "",
|
|
3793
|
+
phase = "working",
|
|
3794
|
+
summary = "",
|
|
3795
|
+
commandText = "",
|
|
3796
|
+
fileRefs = [],
|
|
3797
|
+
cwd = "",
|
|
3798
|
+
source = "activity-status",
|
|
3799
|
+
atMs = Date.now(),
|
|
3800
|
+
}) {
|
|
3801
|
+
if (!config?.webUiEnabled || !runtime) {
|
|
3802
|
+
return false;
|
|
3803
|
+
}
|
|
3804
|
+
if (!(runtime.activeTimelineActivitiesByKey instanceof Map)) {
|
|
3805
|
+
runtime.activeTimelineActivitiesByKey = new Map();
|
|
3806
|
+
}
|
|
3807
|
+
const normalizedProvider = normalizeProvider(provider) || "codex";
|
|
3808
|
+
const normalizedThreadId = cleanText(threadId || "");
|
|
3809
|
+
const key = timelineActivityKey(normalizedProvider, normalizedThreadId);
|
|
3810
|
+
if (!key) {
|
|
3811
|
+
return false;
|
|
3812
|
+
}
|
|
3813
|
+
const next = {
|
|
3814
|
+
key,
|
|
3815
|
+
provider: normalizedProvider,
|
|
3816
|
+
threadId: normalizedThreadId,
|
|
3817
|
+
threadLabel: cleanText(threadLabel || ""),
|
|
3818
|
+
phase: cleanText(phase || "working"),
|
|
3819
|
+
summary: cleanText(summary || ""),
|
|
3820
|
+
commandText: cleanText(commandText || ""),
|
|
3821
|
+
fileRefs: normalizeTimelineFileRefs(fileRefs),
|
|
3822
|
+
cwd: cleanText(cwd || ""),
|
|
3823
|
+
updatedAtMs: Number(atMs) || Date.now(),
|
|
3824
|
+
};
|
|
3825
|
+
const previous = runtime.activeTimelineActivitiesByKey.get(key);
|
|
3826
|
+
const changed =
|
|
3827
|
+
!previous ||
|
|
3828
|
+
previous.phase !== next.phase ||
|
|
3829
|
+
previous.summary !== next.summary ||
|
|
3830
|
+
previous.commandText !== next.commandText ||
|
|
3831
|
+
previous.threadLabel !== next.threadLabel ||
|
|
3832
|
+
previous.cwd !== next.cwd ||
|
|
3833
|
+
JSON.stringify(previous.fileRefs || []) !== JSON.stringify(next.fileRefs || []);
|
|
3834
|
+
runtime.activeTimelineActivitiesByKey.set(key, next);
|
|
3835
|
+
if (changed) {
|
|
3836
|
+
publishTimelineActivityUpdate({ config, runtime, activity: next, source });
|
|
3837
|
+
}
|
|
3838
|
+
return changed;
|
|
3839
|
+
}
|
|
3840
|
+
|
|
3841
|
+
function clearTimelineActivity({ config, runtime, provider, threadId, source = "activity-clear", atMs = Date.now() }) {
|
|
3842
|
+
if (!runtime?.activeTimelineActivitiesByKey) {
|
|
3843
|
+
return false;
|
|
3844
|
+
}
|
|
3845
|
+
const key = timelineActivityKey(provider, threadId);
|
|
3846
|
+
if (!key || !runtime.activeTimelineActivitiesByKey.has(key)) {
|
|
3847
|
+
return false;
|
|
3848
|
+
}
|
|
3849
|
+
const previous = runtime.activeTimelineActivitiesByKey.get(key);
|
|
3850
|
+
runtime.activeTimelineActivitiesByKey.delete(key);
|
|
3851
|
+
publishTimelineActivityUpdate({
|
|
3852
|
+
config,
|
|
3853
|
+
runtime,
|
|
3854
|
+
activity: {
|
|
3855
|
+
...(isPlainObject(previous) ? previous : {}),
|
|
3856
|
+
key,
|
|
3857
|
+
provider: normalizeProvider(provider),
|
|
3858
|
+
threadId: cleanText(threadId || ""),
|
|
3859
|
+
phase: "working",
|
|
3860
|
+
updatedAtMs: Number(atMs) || Date.now(),
|
|
3861
|
+
},
|
|
3862
|
+
source,
|
|
3863
|
+
});
|
|
3864
|
+
return true;
|
|
3865
|
+
}
|
|
3866
|
+
|
|
3867
|
+
function timelineActivityFromCommand({ commandText, cwd = "" }) {
|
|
3868
|
+
const classified = classifyTimelineCommand(commandText);
|
|
3869
|
+
const fileEventType = cleanText(classified.fileEventType || "");
|
|
3870
|
+
const phase = fileEventType === "read"
|
|
3871
|
+
? "reading"
|
|
3872
|
+
: fileEventType === "search"
|
|
3873
|
+
? "searching"
|
|
3874
|
+
: "running_command";
|
|
3875
|
+
return {
|
|
3876
|
+
phase,
|
|
3877
|
+
commandText: classified.commandText || commandText,
|
|
3878
|
+
fileRefs: classified.fileRefs || [],
|
|
3879
|
+
cwd,
|
|
3880
|
+
};
|
|
3881
|
+
}
|
|
3882
|
+
|
|
3883
|
+
function timelineActivityFromClaudeTool(toolName, input = {}) {
|
|
3884
|
+
const lowerToolName = cleanText(toolName || "").toLowerCase();
|
|
3885
|
+
if (lowerToolName === "bash") {
|
|
3886
|
+
return timelineActivityFromCommand({ commandText: cleanText(input.command || ""), cwd: cleanText(input.cwd || "") });
|
|
3887
|
+
}
|
|
3888
|
+
if (["read", "ls"].includes(lowerToolName)) {
|
|
3889
|
+
return {
|
|
3890
|
+
phase: "reading",
|
|
3891
|
+
summary: cleanText(toolName || ""),
|
|
3892
|
+
fileRefs: normalizeTimelineFileRefs([input.file_path || input.path || input.filePath].filter(Boolean)),
|
|
3893
|
+
};
|
|
3894
|
+
}
|
|
3895
|
+
if (["grep", "glob", "websearch", "webfetch"].includes(lowerToolName)) {
|
|
3896
|
+
return {
|
|
3897
|
+
phase: "searching",
|
|
3898
|
+
summary: cleanText(input.pattern || input.query || input.url || toolName || ""),
|
|
3899
|
+
fileRefs: normalizeTimelineFileRefs([input.path || input.file_path || input.filePath].filter(Boolean)),
|
|
3900
|
+
};
|
|
3901
|
+
}
|
|
3902
|
+
if (["write", "edit", "multiedit", "todowrite"].includes(lowerToolName)) {
|
|
3903
|
+
return {
|
|
3904
|
+
phase: "editing",
|
|
3905
|
+
summary: cleanText(toolName || ""),
|
|
3906
|
+
fileRefs: normalizeTimelineFileRefs([input.file_path || input.path || input.filePath].filter(Boolean)),
|
|
3907
|
+
};
|
|
3908
|
+
}
|
|
3909
|
+
if (["askuserquestion", "exitplanmode"].includes(lowerToolName)) {
|
|
3910
|
+
return { phase: "awaiting_approval", summary: cleanText(toolName || "") };
|
|
3911
|
+
}
|
|
3912
|
+
return { phase: "working", summary: cleanText(toolName || "") };
|
|
3913
|
+
}
|
|
3914
|
+
|
|
3915
|
+
function timelineActivityFromClaudeContent(content) {
|
|
3916
|
+
if (!Array.isArray(content)) {
|
|
3917
|
+
return null;
|
|
3918
|
+
}
|
|
3919
|
+
for (const block of content) {
|
|
3920
|
+
if (!isPlainObject(block) || block.type !== "tool_use") {
|
|
3921
|
+
continue;
|
|
3922
|
+
}
|
|
3923
|
+
return timelineActivityFromClaudeTool(block.name || "", isPlainObject(block.input) ? block.input : {});
|
|
3924
|
+
}
|
|
3925
|
+
return null;
|
|
3926
|
+
}
|
|
3927
|
+
|
|
3586
3928
|
function recordTimelineEntry({ config, runtime, state, entry, source = "unknown" }) {
|
|
3587
3929
|
const normalized = normalizeTimelineEntry(entry);
|
|
3588
3930
|
if (!normalized) {
|
|
@@ -4599,6 +4941,17 @@ async function processRolloutFile({ filePath, config, runtime, state, now }) {
|
|
|
4599
4941
|
entry: timelineEntry,
|
|
4600
4942
|
source: "codex-rollout",
|
|
4601
4943
|
}) || dirty;
|
|
4944
|
+
upsertTimelineActivity({
|
|
4945
|
+
config,
|
|
4946
|
+
runtime,
|
|
4947
|
+
provider: "codex",
|
|
4948
|
+
threadId: timelineEntry.threadId,
|
|
4949
|
+
threadLabel: timelineEntry.threadLabel,
|
|
4950
|
+
phase: "thinking",
|
|
4951
|
+
cwd: fileState.cwd || "",
|
|
4952
|
+
source: "codex-user-message",
|
|
4953
|
+
atMs: Date.parse(record.timestamp ?? "") || Date.now(),
|
|
4954
|
+
});
|
|
4602
4955
|
}
|
|
4603
4956
|
|
|
4604
4957
|
const fileTimelineEntries = await buildRolloutFileTimelineEntries({
|
|
@@ -5032,6 +5385,23 @@ async function processClaudeTranscriptFile({ filePath, config, runtime, state, n
|
|
|
5032
5385
|
for (const toolEntry of toolEntries) {
|
|
5033
5386
|
dirty = recordTimelineEntry({ config, runtime, state, entry: toolEntry, source: "claude-tool" }) || dirty;
|
|
5034
5387
|
}
|
|
5388
|
+
const toolActivity = timelineActivityFromClaudeContent(content);
|
|
5389
|
+
if (toolActivity) {
|
|
5390
|
+
upsertTimelineActivity({
|
|
5391
|
+
config,
|
|
5392
|
+
runtime,
|
|
5393
|
+
provider: "claude",
|
|
5394
|
+
threadId,
|
|
5395
|
+
threadLabel,
|
|
5396
|
+
phase: toolActivity.phase,
|
|
5397
|
+
summary: toolActivity.summary,
|
|
5398
|
+
commandText: toolActivity.commandText,
|
|
5399
|
+
fileRefs: toolActivity.fileRefs,
|
|
5400
|
+
cwd: fileState.cwd,
|
|
5401
|
+
source: "claude-tool-start",
|
|
5402
|
+
atMs: createdAtMs,
|
|
5403
|
+
});
|
|
5404
|
+
}
|
|
5035
5405
|
}
|
|
5036
5406
|
|
|
5037
5407
|
let text = "";
|
|
@@ -5071,6 +5441,28 @@ async function processClaudeTranscriptFile({ filePath, config, runtime, state, n
|
|
|
5071
5441
|
: stopReason === "end_turn"
|
|
5072
5442
|
? "assistant_final"
|
|
5073
5443
|
: "assistant_commentary";
|
|
5444
|
+
if (kind === "user_message") {
|
|
5445
|
+
upsertTimelineActivity({
|
|
5446
|
+
config,
|
|
5447
|
+
runtime,
|
|
5448
|
+
provider: "claude",
|
|
5449
|
+
threadId,
|
|
5450
|
+
threadLabel,
|
|
5451
|
+
phase: "thinking",
|
|
5452
|
+
cwd: fileState.cwd,
|
|
5453
|
+
source: "claude-user-message",
|
|
5454
|
+
atMs: createdAtMs,
|
|
5455
|
+
});
|
|
5456
|
+
} else if (kind === "assistant_final") {
|
|
5457
|
+
clearTimelineActivity({
|
|
5458
|
+
config,
|
|
5459
|
+
runtime,
|
|
5460
|
+
provider: "claude",
|
|
5461
|
+
threadId,
|
|
5462
|
+
source: "claude-final",
|
|
5463
|
+
atMs: createdAtMs,
|
|
5464
|
+
});
|
|
5465
|
+
}
|
|
5074
5466
|
// Use kind-independent stableId so re-scans with corrected classification
|
|
5075
5467
|
// replace old entries rather than creating duplicates.
|
|
5076
5468
|
const stableId = `claude_msg:${threadId}:${uuid}`;
|
|
@@ -6367,6 +6759,20 @@ async function buildRolloutFileTimelineEntries({ config, record, fileState, runt
|
|
|
6367
6759
|
return [];
|
|
6368
6760
|
}
|
|
6369
6761
|
const classified = classifyTimelineCommand(commandText);
|
|
6762
|
+
const activity = timelineActivityFromCommand({ commandText, cwd: cleanText(args.workdir || fileState.cwd || "") });
|
|
6763
|
+
upsertTimelineActivity({
|
|
6764
|
+
config,
|
|
6765
|
+
runtime,
|
|
6766
|
+
provider: "codex",
|
|
6767
|
+
threadId,
|
|
6768
|
+
threadLabel,
|
|
6769
|
+
phase: activity.phase,
|
|
6770
|
+
commandText: activity.commandText,
|
|
6771
|
+
fileRefs: activity.fileRefs,
|
|
6772
|
+
cwd: activity.cwd,
|
|
6773
|
+
source: "codex-tool-start",
|
|
6774
|
+
atMs: createdAtMs,
|
|
6775
|
+
});
|
|
6370
6776
|
return [
|
|
6371
6777
|
buildToolTimelineEntry({
|
|
6372
6778
|
provider: "codex",
|
|
@@ -6384,11 +6790,34 @@ async function buildRolloutFileTimelineEntries({ config, record, fileState, runt
|
|
|
6384
6790
|
|
|
6385
6791
|
if (payloadType === "custom_tool_call") {
|
|
6386
6792
|
rememberApplyPatchInput(fileState, payload, createdAtMs);
|
|
6793
|
+
upsertTimelineActivity({
|
|
6794
|
+
config,
|
|
6795
|
+
runtime,
|
|
6796
|
+
provider: "codex",
|
|
6797
|
+
threadId,
|
|
6798
|
+
threadLabel,
|
|
6799
|
+
phase: "editing",
|
|
6800
|
+
summary: cleanText(payload?.name || "apply_patch"),
|
|
6801
|
+
cwd: fileState.cwd || "",
|
|
6802
|
+
source: "codex-tool-start",
|
|
6803
|
+
atMs: createdAtMs,
|
|
6804
|
+
});
|
|
6387
6805
|
return [];
|
|
6388
6806
|
}
|
|
6389
6807
|
|
|
6390
6808
|
if (payloadType === "function_call_output") {
|
|
6391
6809
|
if (findStoredToolEventInput(fileState, callId)) {
|
|
6810
|
+
upsertTimelineActivity({
|
|
6811
|
+
config,
|
|
6812
|
+
runtime,
|
|
6813
|
+
provider: "codex",
|
|
6814
|
+
threadId,
|
|
6815
|
+
threadLabel,
|
|
6816
|
+
phase: "thinking",
|
|
6817
|
+
cwd: fileState.cwd || "",
|
|
6818
|
+
source: "codex-tool-output",
|
|
6819
|
+
atMs: createdAtMs,
|
|
6820
|
+
});
|
|
6392
6821
|
return [];
|
|
6393
6822
|
}
|
|
6394
6823
|
const commandText = extractCommandLineFromFunctionOutput(payload.output ?? "");
|
|
@@ -6396,6 +6825,17 @@ async function buildRolloutFileTimelineEntries({ config, record, fileState, runt
|
|
|
6396
6825
|
return [];
|
|
6397
6826
|
}
|
|
6398
6827
|
const classified = classifyTimelineCommand(commandText);
|
|
6828
|
+
upsertTimelineActivity({
|
|
6829
|
+
config,
|
|
6830
|
+
runtime,
|
|
6831
|
+
provider: "codex",
|
|
6832
|
+
threadId,
|
|
6833
|
+
threadLabel,
|
|
6834
|
+
phase: "thinking",
|
|
6835
|
+
cwd: fileState.cwd || "",
|
|
6836
|
+
source: "codex-tool-output",
|
|
6837
|
+
atMs: createdAtMs,
|
|
6838
|
+
});
|
|
6399
6839
|
return [
|
|
6400
6840
|
buildToolTimelineEntry({
|
|
6401
6841
|
provider: "codex",
|
|
@@ -6554,6 +6994,18 @@ async function buildRolloutFileTimelineEntries({ config, record, fileState, runt
|
|
|
6554
6994
|
fileState.applyPatchInputsByCallId.delete(callId);
|
|
6555
6995
|
}
|
|
6556
6996
|
|
|
6997
|
+
upsertTimelineActivity({
|
|
6998
|
+
config,
|
|
6999
|
+
runtime,
|
|
7000
|
+
provider: "codex",
|
|
7001
|
+
threadId,
|
|
7002
|
+
threadLabel,
|
|
7003
|
+
phase: "thinking",
|
|
7004
|
+
cwd: fileState.cwd || "",
|
|
7005
|
+
source: "codex-tool-output",
|
|
7006
|
+
atMs: createdAtMs,
|
|
7007
|
+
});
|
|
7008
|
+
|
|
6557
7009
|
return entries.filter(Boolean);
|
|
6558
7010
|
}
|
|
6559
7011
|
|
|
@@ -6610,8 +7062,24 @@ async function processScannedEvent({ config, runtime, state, event }) {
|
|
|
6610
7062
|
let dirty = false;
|
|
6611
7063
|
|
|
6612
7064
|
if (event.kind === "task_complete") {
|
|
7065
|
+
clearTimelineActivity({
|
|
7066
|
+
config,
|
|
7067
|
+
runtime,
|
|
7068
|
+
provider: "codex",
|
|
7069
|
+
threadId: event.threadId ?? event.conversationId ?? "",
|
|
7070
|
+
source: "codex-task-complete",
|
|
7071
|
+
atMs: event.timestampMs || Date.now(),
|
|
7072
|
+
});
|
|
6613
7073
|
attachCompletionDetails({ config, runtime, event });
|
|
6614
7074
|
} else if (event.kind === "plan_ready") {
|
|
7075
|
+
clearTimelineActivity({
|
|
7076
|
+
config,
|
|
7077
|
+
runtime,
|
|
7078
|
+
provider: "codex",
|
|
7079
|
+
threadId: event.threadId ?? event.conversationId ?? "",
|
|
7080
|
+
source: "codex-plan-ready",
|
|
7081
|
+
atMs: event.timestampMs || Date.now(),
|
|
7082
|
+
});
|
|
6615
7083
|
attachPlanDetails({ config, runtime, event });
|
|
6616
7084
|
}
|
|
6617
7085
|
|
|
@@ -10170,9 +10638,10 @@ function formatNativeApprovalMessage(kind, params, locale = config?.defaultLocal
|
|
|
10170
10638
|
}
|
|
10171
10639
|
|
|
10172
10640
|
function formatCommandApprovalMessage(params, locale = config?.defaultLocale || DEFAULT_LOCALE) {
|
|
10641
|
+
const safeParams = isPlainObject(params) ? params : {};
|
|
10173
10642
|
const parts = [];
|
|
10174
|
-
const reason = truncate(cleanText(
|
|
10175
|
-
const command = truncate(cleanText(
|
|
10643
|
+
const reason = truncate(cleanText(safeParams.reason ?? safeParams.justification ?? ""), 220);
|
|
10644
|
+
const command = truncate(cleanText(safeParams.command ?? safeParams.cmd ?? ""), 1200);
|
|
10176
10645
|
if (reason) {
|
|
10177
10646
|
parts.push(reason);
|
|
10178
10647
|
} else {
|
|
@@ -10185,21 +10654,41 @@ function formatCommandApprovalMessage(params, locale = config?.defaultLocale ||
|
|
|
10185
10654
|
}
|
|
10186
10655
|
|
|
10187
10656
|
function formatFileApprovalMessage(params, locale = config?.defaultLocale || DEFAULT_LOCALE) {
|
|
10657
|
+
const safeParams = isPlainObject(params) ? params : {};
|
|
10188
10658
|
const parts = [];
|
|
10189
|
-
const reason = truncate(cleanText(
|
|
10659
|
+
const reason = truncate(cleanText(safeParams.reason ?? ""), 220);
|
|
10190
10660
|
if (reason) {
|
|
10191
10661
|
parts.push(reason);
|
|
10192
10662
|
} else {
|
|
10193
10663
|
parts.push(t(locale, "server.message.fileApprovalNeeded"));
|
|
10194
10664
|
}
|
|
10195
|
-
if (
|
|
10196
|
-
parts.push(t(locale, "server.message.pathPrefix", { path: compactPath(
|
|
10197
|
-
} else if (
|
|
10198
|
-
parts.push(t(locale, "server.message.pathPrefix", { path: compactPath(
|
|
10665
|
+
if (safeParams.grantRoot) {
|
|
10666
|
+
parts.push(t(locale, "server.message.pathPrefix", { path: compactPath(safeParams.grantRoot) }));
|
|
10667
|
+
} else if (safeParams.cwd) {
|
|
10668
|
+
parts.push(t(locale, "server.message.pathPrefix", { path: compactPath(safeParams.cwd) }));
|
|
10199
10669
|
}
|
|
10200
10670
|
return truncate(parts.join("\n"), 1024);
|
|
10201
10671
|
}
|
|
10202
10672
|
|
|
10673
|
+
function claudeApprovalRawParams(body, kind) {
|
|
10674
|
+
const toolInput = isPlainObject(body?.toolInput) ? cloneJson(body.toolInput) : {};
|
|
10675
|
+
const rawParams = {
|
|
10676
|
+
...toolInput,
|
|
10677
|
+
...(body?.cwd ? { cwd: String(body.cwd) } : {}),
|
|
10678
|
+
};
|
|
10679
|
+
const messageText = cleanText(body?.messageText || "");
|
|
10680
|
+
if (messageText && !cleanText(rawParams.reason ?? rawParams.justification ?? "")) {
|
|
10681
|
+
rawParams.reason = messageText;
|
|
10682
|
+
}
|
|
10683
|
+
if (kind === "command" && !cleanText(rawParams.command ?? rawParams.cmd ?? "")) {
|
|
10684
|
+
const commandText = cleanText(toolInput.command ?? toolInput.cmd ?? "");
|
|
10685
|
+
if (commandText) {
|
|
10686
|
+
rawParams.command = commandText;
|
|
10687
|
+
}
|
|
10688
|
+
}
|
|
10689
|
+
return rawParams;
|
|
10690
|
+
}
|
|
10691
|
+
|
|
10203
10692
|
function extractApprovalFileRefs(params) {
|
|
10204
10693
|
if (!isPlainObject(params)) {
|
|
10205
10694
|
return [];
|
|
@@ -12621,6 +13110,26 @@ function buildTimelineThreads(entries, config) {
|
|
|
12621
13110
|
.slice(0, config.maxTimelineThreads);
|
|
12622
13111
|
}
|
|
12623
13112
|
|
|
13113
|
+
function buildActiveTimelineActivityEntries(runtime, locale) {
|
|
13114
|
+
if (!(runtime?.activeTimelineActivitiesByKey instanceof Map)) {
|
|
13115
|
+
return [];
|
|
13116
|
+
}
|
|
13117
|
+
const now = Date.now();
|
|
13118
|
+
const entries = [];
|
|
13119
|
+
for (const [key, activity] of [...runtime.activeTimelineActivitiesByKey.entries()]) {
|
|
13120
|
+
const updatedAtMs = Number(activity?.updatedAtMs) || 0;
|
|
13121
|
+
if (!updatedAtMs || now - updatedAtMs > TIMELINE_ACTIVITY_TTL_MS) {
|
|
13122
|
+
runtime.activeTimelineActivitiesByKey.delete(key);
|
|
13123
|
+
continue;
|
|
13124
|
+
}
|
|
13125
|
+
const entry = buildTimelineActivityEntry(activity, locale);
|
|
13126
|
+
if (entry) {
|
|
13127
|
+
entries.push(entry);
|
|
13128
|
+
}
|
|
13129
|
+
}
|
|
13130
|
+
return entries;
|
|
13131
|
+
}
|
|
13132
|
+
|
|
12624
13133
|
function buildTimelineResponse(runtime, state, config, locale) {
|
|
12625
13134
|
const messageEntries = normalizeTimelineEntries(
|
|
12626
13135
|
state.recentTimelineEntries ?? runtime.recentTimelineEntries,
|
|
@@ -12628,7 +13137,11 @@ function buildTimelineResponse(runtime, state, config, locale) {
|
|
|
12628
13137
|
);
|
|
12629
13138
|
runtime.recentTimelineEntries = messageEntries;
|
|
12630
13139
|
const entries = normalizeTimelineEntries(
|
|
12631
|
-
[
|
|
13140
|
+
[
|
|
13141
|
+
...buildActiveTimelineActivityEntries(runtime, locale),
|
|
13142
|
+
...messageEntries,
|
|
13143
|
+
...buildOperationalTimelineEntries(runtime, state, config, locale),
|
|
13144
|
+
],
|
|
12632
13145
|
config.maxTimelineEntries
|
|
12633
13146
|
).map((entry) => ({
|
|
12634
13147
|
kind: entry.kind,
|
|
@@ -12646,6 +13159,9 @@ function buildTimelineResponse(runtime, state, config, locale) {
|
|
|
12646
13159
|
outcome: entry.outcome || "",
|
|
12647
13160
|
createdAtMs: entry.createdAtMs,
|
|
12648
13161
|
provider: normalizeProvider(entry.provider),
|
|
13162
|
+
...(entry.activityPhase != null ? { activityPhase: entry.activityPhase } : {}),
|
|
13163
|
+
...(entry.commandText != null ? { commandText: entry.commandText } : {}),
|
|
13164
|
+
...timelineAutoPilotProjection(entry),
|
|
12649
13165
|
...(entry.draftType != null ? { draftType: entry.draftType } : {}),
|
|
12650
13166
|
}));
|
|
12651
13167
|
|
|
@@ -12655,6 +13171,24 @@ function buildTimelineResponse(runtime, state, config, locale) {
|
|
|
12655
13171
|
};
|
|
12656
13172
|
}
|
|
12657
13173
|
|
|
13174
|
+
function timelineAutoPilotProjection(entry) {
|
|
13175
|
+
if (cleanText(entry?.kind || "") !== "approval" || cleanText(entry?.outcome || "") !== "approved") {
|
|
13176
|
+
return {};
|
|
13177
|
+
}
|
|
13178
|
+
const stableId = cleanText(entry?.stableId || "");
|
|
13179
|
+
if (stableId.includes(":autopilot-write")) {
|
|
13180
|
+
const lane = cleanText(stableId.match(/:autopilot-write:([a-z_-]+)$/u)?.[1] || "");
|
|
13181
|
+
return {
|
|
13182
|
+
autoPilotMode: "write",
|
|
13183
|
+
...(lane ? { autoPilotWriteLane: lane } : {}),
|
|
13184
|
+
};
|
|
13185
|
+
}
|
|
13186
|
+
if (stableId.endsWith(":autopilot")) {
|
|
13187
|
+
return { autoPilotMode: "read" };
|
|
13188
|
+
}
|
|
13189
|
+
return {};
|
|
13190
|
+
}
|
|
13191
|
+
|
|
12658
13192
|
function buildPendingApprovalDetail(runtime, state, approval, locale) {
|
|
12659
13193
|
const previousContext = buildPreviousApprovalContext(runtime, approval);
|
|
12660
13194
|
const approvalKind = cleanText(approval.kind || "");
|
|
@@ -13272,7 +13806,7 @@ function buildAmbientSuggestionsDetail(entry, locale) {
|
|
|
13272
13806
|
|
|
13273
13807
|
function buildTimelineFileEventDetail(entry, locale) {
|
|
13274
13808
|
const fileEventType = normalizeTimelineFileEventType(entry?.fileEventType ?? "");
|
|
13275
|
-
const commandText = ["read", "search"].includes(fileEventType)
|
|
13809
|
+
const commandText = ["read", "search", "git"].includes(fileEventType)
|
|
13276
13810
|
? firstMarkdownCodeFenceText(entry?.messageText ?? "")
|
|
13277
13811
|
: "";
|
|
13278
13812
|
const detailText = [
|
|
@@ -14525,9 +15059,26 @@ async function handleNativeApprovalDecision({ config, runtime, state, approval,
|
|
|
14525
15059
|
});
|
|
14526
15060
|
} else if (approval.resolveClaudeWaiter) {
|
|
14527
15061
|
if (approval.provider === "claude" && approval.kind === "plan") {
|
|
14528
|
-
// ExitPlanMode
|
|
14529
|
-
//
|
|
14530
|
-
//
|
|
15062
|
+
// ExitPlanMode CANNOT actually be auto-bypassed via the PreToolUse
|
|
15063
|
+
// hook. We tested both "allow" and "deny" verdicts on Claude Desktop:
|
|
15064
|
+
//
|
|
15065
|
+
// - permissionDecision: "allow"
|
|
15066
|
+
// Claude Desktop ignores the verdict and re-shows its native
|
|
15067
|
+
// plan dialog on the Mac, asking for approval a second time
|
|
15068
|
+
// even though the user already tapped on mobile.
|
|
15069
|
+
// - permissionDecision: "deny"
|
|
15070
|
+
// The native dialog is suppressed, but Claude Desktop also
|
|
15071
|
+
// does not exit plan mode internally, so Edit/Write stay
|
|
15072
|
+
// blocked — the session cannot make progress.
|
|
15073
|
+
//
|
|
15074
|
+
// Neither verdict gives us "phone tap → plan mode exits → editing
|
|
15075
|
+
// resumes" by itself. Until we can drive the Mac dialog through an
|
|
15076
|
+
// external mechanism (AppleScript / Accessibility API), the chosen
|
|
15077
|
+
// workaround is "deny + reason": Claude reads the reason as
|
|
15078
|
+
// instructions ("user already approved on mobile, proceed without
|
|
15079
|
+
// calling ExitPlanMode again"). That at least lets the desktop user
|
|
15080
|
+
// finish the flow with one extra Mac click on the native dialog
|
|
15081
|
+
// instead of getting a duplicate dialog from a stale "allow".
|
|
14531
15082
|
approval.resolveClaudeWaiter({
|
|
14532
15083
|
permissionDecision: "deny",
|
|
14533
15084
|
permissionDecisionReason:
|
|
@@ -17592,6 +18143,13 @@ if (url.pathname === "/api/hazbase/logout" && req.method === "POST") {
|
|
|
17592
18143
|
const threadId = String(body.threadId || body.sessionId || "");
|
|
17593
18144
|
console.log(`[claude-stop] threadId=${threadId} pendingTotal=${runtime.nativeApprovalsByToken.size}`);
|
|
17594
18145
|
if (threadId) {
|
|
18146
|
+
clearTimelineActivity({
|
|
18147
|
+
config,
|
|
18148
|
+
runtime,
|
|
18149
|
+
provider: "claude",
|
|
18150
|
+
threadId,
|
|
18151
|
+
source: "claude-stop",
|
|
18152
|
+
});
|
|
17595
18153
|
const pending = [];
|
|
17596
18154
|
for (const approval of runtime.nativeApprovalsByToken.values()) {
|
|
17597
18155
|
if (!approval.resolved && approval.conversationId === threadId) {
|
|
@@ -17662,6 +18220,21 @@ if (url.pathname === "/api/hazbase/logout" && req.method === "POST") {
|
|
|
17662
18220
|
// for this session/tool as accepted (so iPhone moves it to completed).
|
|
17663
18221
|
const threadId = String(body.threadId || body.sessionId || "");
|
|
17664
18222
|
const toolName = String(body.toolName || "");
|
|
18223
|
+
const eventCwd = String(body.cwd || "");
|
|
18224
|
+
const threadLabel =
|
|
18225
|
+
runtime.claudeSessionTitles.get(threadId) ||
|
|
18226
|
+
(eventCwd ? path.basename(eventCwd) : "") ||
|
|
18227
|
+
threadId.slice(0, 40);
|
|
18228
|
+
upsertTimelineActivity({
|
|
18229
|
+
config,
|
|
18230
|
+
runtime,
|
|
18231
|
+
provider: "claude",
|
|
18232
|
+
threadId,
|
|
18233
|
+
threadLabel,
|
|
18234
|
+
phase: "thinking",
|
|
18235
|
+
cwd: eventCwd,
|
|
18236
|
+
source: "claude-post-tool",
|
|
18237
|
+
});
|
|
17665
18238
|
const fingerprint = claudeToolFingerprint(toolName, body.toolInput);
|
|
17666
18239
|
console.log(`[claude-post-tool] threadId=${threadId} tool=${toolName} fp=${fingerprint.slice(0, 80)} pendingTotal=${runtime.nativeApprovalsByToken.size}`);
|
|
17667
18240
|
const match = findClaudePendingApprovalForTool(runtime, threadId, toolName, fingerprint);
|
|
@@ -17697,6 +18270,31 @@ if (url.pathname === "/api/hazbase/logout" && req.method === "POST") {
|
|
|
17697
18270
|
}
|
|
17698
18271
|
}
|
|
17699
18272
|
|
|
18273
|
+
if (eventType === "PreToolUse") {
|
|
18274
|
+
const threadId = String(body.threadId || body.sessionId || "");
|
|
18275
|
+
const eventCwd = String(body.cwd || "");
|
|
18276
|
+
const toolName = String(body.toolName || "");
|
|
18277
|
+
const toolActivity = timelineActivityFromClaudeTool(toolName, isPlainObject(body.toolInput) ? body.toolInput : {});
|
|
18278
|
+
const threadLabel =
|
|
18279
|
+
runtime.claudeSessionTitles.get(threadId) ||
|
|
18280
|
+
(eventCwd ? path.basename(eventCwd) : "") ||
|
|
18281
|
+
threadId.slice(0, 40);
|
|
18282
|
+
upsertTimelineActivity({
|
|
18283
|
+
config,
|
|
18284
|
+
runtime,
|
|
18285
|
+
provider: "claude",
|
|
18286
|
+
threadId,
|
|
18287
|
+
threadLabel,
|
|
18288
|
+
phase: toolActivity.phase,
|
|
18289
|
+
summary: toolActivity.summary,
|
|
18290
|
+
commandText: toolActivity.commandText,
|
|
18291
|
+
fileRefs: toolActivity.fileRefs,
|
|
18292
|
+
cwd: eventCwd,
|
|
18293
|
+
source: "claude-pre-tool",
|
|
18294
|
+
});
|
|
18295
|
+
return writeJson(res, 200, {});
|
|
18296
|
+
}
|
|
18297
|
+
|
|
17700
18298
|
if (eventType !== "approval_request") {
|
|
17701
18299
|
// Non-approval events (Notification, Stop, etc.) — acknowledge immediately
|
|
17702
18300
|
return writeJson(res, 200, {});
|
|
@@ -17708,6 +18306,8 @@ if (url.pathname === "/api/hazbase/logout" && req.method === "POST") {
|
|
|
17708
18306
|
const threadId = String(body.threadId || "");
|
|
17709
18307
|
const requestId = String(body.requestId || crypto.randomUUID());
|
|
17710
18308
|
const requestKey = `${threadId}:${requestId}`;
|
|
18309
|
+
const approvalKind = String(body.approvalKind || "command");
|
|
18310
|
+
const rawParams = claudeApprovalRawParams(body, approvalKind);
|
|
17711
18311
|
|
|
17712
18312
|
const approval = {
|
|
17713
18313
|
token,
|
|
@@ -17715,13 +18315,14 @@ if (url.pathname === "/api/hazbase/logout" && req.method === "POST") {
|
|
|
17715
18315
|
conversationId: threadId,
|
|
17716
18316
|
requestId,
|
|
17717
18317
|
ownerClientId: null,
|
|
17718
|
-
kind:
|
|
18318
|
+
kind: approvalKind,
|
|
17719
18319
|
threadLabel:
|
|
17720
18320
|
runtime.claudeSessionTitles.get(threadId) ||
|
|
17721
18321
|
(body.cwd ? path.basename(String(body.cwd)) : "") ||
|
|
17722
18322
|
String(body.threadId || "").slice(0, 40),
|
|
17723
18323
|
title: config.approvalTitle,
|
|
17724
18324
|
messageText: String(body.messageText || ""),
|
|
18325
|
+
rawParams,
|
|
17725
18326
|
fileRefs: Array.isArray(body.fileRefs) ? body.fileRefs : [],
|
|
17726
18327
|
previousFileRefs: [],
|
|
17727
18328
|
diffText: String(body.diffText || ""),
|
|
@@ -17744,6 +18345,20 @@ if (url.pathname === "/api/hazbase/logout" && req.method === "POST") {
|
|
|
17744
18345
|
readOnly: body.notifyOnly === true,
|
|
17745
18346
|
};
|
|
17746
18347
|
|
|
18348
|
+
upsertTimelineActivity({
|
|
18349
|
+
config,
|
|
18350
|
+
runtime,
|
|
18351
|
+
provider: "claude",
|
|
18352
|
+
threadId,
|
|
18353
|
+
threadLabel: approval.threadLabel,
|
|
18354
|
+
phase: "awaiting_approval",
|
|
18355
|
+
summary: approval.messageText,
|
|
18356
|
+
fileRefs: approval.fileRefs,
|
|
18357
|
+
cwd: approval.cwd,
|
|
18358
|
+
source: "claude-approval-request",
|
|
18359
|
+
atMs: approval.createdAtMs,
|
|
18360
|
+
});
|
|
18361
|
+
|
|
17747
18362
|
const claudeAutoPilotMatch =
|
|
17748
18363
|
approval.kind === "command"
|
|
17749
18364
|
? maybeAutoApproveTrustedRead({
|
|
@@ -17800,21 +18415,28 @@ if (url.pathname === "/api/hazbase/logout" && req.method === "POST") {
|
|
|
17800
18415
|
runtime.nativeApprovalsByToken.set(token, approval);
|
|
17801
18416
|
runtime.nativeApprovalsByRequestKey.set(requestKey, approval);
|
|
17802
18417
|
|
|
17803
|
-
|
|
17804
|
-
|
|
17805
|
-
|
|
17806
|
-
|
|
17807
|
-
|
|
17808
|
-
|
|
17809
|
-
|
|
17810
|
-
|
|
17811
|
-
|
|
17812
|
-
|
|
17813
|
-
|
|
17814
|
-
|
|
17815
|
-
|
|
17816
|
-
|
|
17817
|
-
|
|
18418
|
+
try {
|
|
18419
|
+
const pushChanged = await deliverWebPushItem({
|
|
18420
|
+
config,
|
|
18421
|
+
state,
|
|
18422
|
+
kind: "approval",
|
|
18423
|
+
tab: "inbox",
|
|
18424
|
+
subtab: "pending",
|
|
18425
|
+
token: approval.token,
|
|
18426
|
+
stableId: pendingApprovalStableId(approval),
|
|
18427
|
+
title: approval.title,
|
|
18428
|
+
body: approval.messageText,
|
|
18429
|
+
buildLocalizedContent: ({ locale }) => ({
|
|
18430
|
+
title: formatLocalizedTitle(locale, "server.title.approval", approval.threadLabel),
|
|
18431
|
+
body: formatNativeApprovalMessage(approval.kind, approval.rawParams, locale),
|
|
18432
|
+
}),
|
|
18433
|
+
});
|
|
18434
|
+
if (pushChanged) {
|
|
18435
|
+
await saveState(config.stateFile, state);
|
|
18436
|
+
}
|
|
18437
|
+
} catch (error) {
|
|
18438
|
+
console.error(`[claude-approval-push] ${approval.requestKey} | ${error.message}`);
|
|
18439
|
+
}
|
|
17818
18440
|
|
|
17819
18441
|
// Notify-only path: phone gets a read-only entry + push, but the
|
|
17820
18442
|
// hook does not block on a decision. The PostToolUse handler will
|
|
@@ -18037,6 +18659,22 @@ if (url.pathname === "/api/hazbase/logout" && req.method === "POST") {
|
|
|
18037
18659
|
if (draftType === "reply" && !postId) {
|
|
18038
18660
|
return writeJson(res, 400, { error: "missing-postId-for-reply" });
|
|
18039
18661
|
}
|
|
18662
|
+
if (draftType !== "original_post") {
|
|
18663
|
+
const scoutState = rollScoutDayIfNeeded(await readScoutState());
|
|
18664
|
+
const quota = await getMoltbookReplyQuotaState({ state: scoutState, maxDaily: 5 });
|
|
18665
|
+
if (quota.quotaReached) {
|
|
18666
|
+
await writeScoutState(scoutState);
|
|
18667
|
+
return writeJson(res, 409, {
|
|
18668
|
+
error: "moltbook-reply-quota-reached",
|
|
18669
|
+
sentToday: quota.sentToday,
|
|
18670
|
+
pendingToday: quota.pendingToday,
|
|
18671
|
+
usedToday: quota.usedToday,
|
|
18672
|
+
maxDaily: quota.maxDaily,
|
|
18673
|
+
day: quota.day,
|
|
18674
|
+
});
|
|
18675
|
+
}
|
|
18676
|
+
await writeScoutState(scoutState);
|
|
18677
|
+
}
|
|
18040
18678
|
const token = historyToken(`moltbook_draft:${sourceId}`);
|
|
18041
18679
|
const postTitle = cleanText(body.postTitle || "");
|
|
18042
18680
|
const postUrl = cleanText(body.postUrl || `https://www.moltbook.com/post/${postId}`);
|
|
@@ -18172,6 +18810,17 @@ if (url.pathname === "/api/hazbase/logout" && req.method === "POST") {
|
|
|
18172
18810
|
const action = String(body.action || "") === "approve" ? "approve" : "deny";
|
|
18173
18811
|
const editedText = cleanText(body.editedText || "");
|
|
18174
18812
|
const editedTitle = cleanText(body.editedTitle || "");
|
|
18813
|
+
if (action === "approve" && draft.draftType !== "original_post") {
|
|
18814
|
+
const quota = await reserveMoltbookReplyQuotaSlot(draft, 5);
|
|
18815
|
+
if (quota.quotaReached) {
|
|
18816
|
+
return writeJson(res, 409, {
|
|
18817
|
+
error: "moltbook-reply-quota-reached",
|
|
18818
|
+
sentToday: quota.sentToday,
|
|
18819
|
+
maxDaily: quota.maxDaily,
|
|
18820
|
+
day: quota.day,
|
|
18821
|
+
});
|
|
18822
|
+
}
|
|
18823
|
+
}
|
|
18175
18824
|
const decision = {
|
|
18176
18825
|
action,
|
|
18177
18826
|
text: action === "approve" ? editedText || draft.draftText : "",
|
|
@@ -18205,6 +18854,15 @@ if (url.pathname === "/api/hazbase/logout" && req.method === "POST") {
|
|
|
18205
18854
|
await executeMoltbookDraftPost(draft, config, runtime, state);
|
|
18206
18855
|
} catch (postError) {
|
|
18207
18856
|
console.error(`[moltbook-draft-post-error] ${postError.message}`);
|
|
18857
|
+
await releaseMoltbookReplyQuotaSlot(draft).catch((error) => console.error(`[moltbook-quota-release] ${error.message}`));
|
|
18858
|
+
if (draft.parentCommentId) {
|
|
18859
|
+
await updateInboxStatus(draft.parentCommentId, "pending", {
|
|
18860
|
+
source: "mobile-draft-approve",
|
|
18861
|
+
draftStatus: "failed",
|
|
18862
|
+
draftToken: draft.token,
|
|
18863
|
+
draftError: String(postError.message || "").slice(0, 500),
|
|
18864
|
+
}).catch((error) => console.error(`[moltbook-inbox-post-failed] ${error.message}`));
|
|
18865
|
+
}
|
|
18208
18866
|
try {
|
|
18209
18867
|
await deliverWebPushItem({
|
|
18210
18868
|
config, state, kind: "moltbook_draft", token: draft.token,
|
|
@@ -18224,6 +18882,14 @@ if (url.pathname === "/api/hazbase/logout" && req.method === "POST") {
|
|
|
18224
18882
|
setTimeout(() => runtime.moltbookDraftsByToken.delete(token), 120_000).unref?.();
|
|
18225
18883
|
})();
|
|
18226
18884
|
} else {
|
|
18885
|
+
if (draft.parentCommentId) {
|
|
18886
|
+
await updateInboxStatus(draft.parentCommentId, "skipped", {
|
|
18887
|
+
source: "mobile-draft-deny",
|
|
18888
|
+
draftStatus: "denied",
|
|
18889
|
+
draftToken: draft.token,
|
|
18890
|
+
skippedAt: new Date().toISOString(),
|
|
18891
|
+
}).catch((error) => console.error(`[moltbook-inbox-denied] ${error.message}`));
|
|
18892
|
+
}
|
|
18227
18893
|
// Deny — clean up.
|
|
18228
18894
|
await deleteDraft(token).catch(() => {});
|
|
18229
18895
|
setTimeout(() => runtime.moltbookDraftsByToken.delete(token), 120_000).unref?.();
|
|
@@ -20306,9 +20972,21 @@ async function executeMoltbookDraftPost(draft, config, runtime, state) {
|
|
|
20306
20972
|
createdPostId = draft.postId || null;
|
|
20307
20973
|
createdCommentId = comment?.id || null;
|
|
20308
20974
|
console.log(`[moltbook-draft-post] Posted reply (commentId=${comment?.id}) to post ${draft.postId}`);
|
|
20975
|
+
if (draft.parentCommentId) {
|
|
20976
|
+
await updateInboxStatus(draft.parentCommentId, "replied", {
|
|
20977
|
+
source: "mobile-draft-approve",
|
|
20978
|
+
replyText: finalText,
|
|
20979
|
+
replyCommentId: createdCommentId || "",
|
|
20980
|
+
replyVerification: verification || null,
|
|
20981
|
+
draftStatus: "posted",
|
|
20982
|
+
draftToken: draft.token,
|
|
20983
|
+
}).catch((error) => console.error(`[moltbook-inbox-replied] ${error.message}`));
|
|
20984
|
+
}
|
|
20309
20985
|
|
|
20310
20986
|
const scoutState = rollScoutDayIfNeeded(await readScoutState());
|
|
20311
|
-
|
|
20987
|
+
if (!draft.replyQuotaReserved) {
|
|
20988
|
+
scoutState.sentToday += 1;
|
|
20989
|
+
}
|
|
20312
20990
|
markPostSeen(scoutState, draft.postId, "published");
|
|
20313
20991
|
// Append the reply to `recentComposeTitles` so it shows up in the
|
|
20314
20992
|
// "最近の投稿" list with its reply badge. `recordComposeAttempt` knows
|