viveworker 0.1.11 → 0.2.1
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 +27 -8
- package/package.json +7 -2
- package/scripts/viveworker-bridge.mjs +794 -43
- package/scripts/viveworker-claude-hook.mjs +839 -0
- package/scripts/viveworker.mjs +71 -0
- package/web/app.css +171 -0
- package/web/app.js +523 -59
- package/web/i18n.js +95 -57
|
@@ -64,6 +64,12 @@ const runtime = {
|
|
|
64
64
|
threadOwnerClientIds: new Map(),
|
|
65
65
|
nativeApprovalsByToken: new Map(),
|
|
66
66
|
nativeApprovalsByRequestKey: new Map(),
|
|
67
|
+
claudeApprovalWaiters: new Map(),
|
|
68
|
+
claudeKnownFiles: [],
|
|
69
|
+
lastClaudeScanAt: 0,
|
|
70
|
+
claudeFileStates: new Map(),
|
|
71
|
+
claudeSessionTitles: new Map(),
|
|
72
|
+
lastClaudeSessionTitleScanAt: 0,
|
|
67
73
|
fileApprovalDeltasById: new Map(),
|
|
68
74
|
planRequestsByToken: new Map(),
|
|
69
75
|
planRequestsByRequestKey: new Map(),
|
|
@@ -82,6 +88,7 @@ const runtime = {
|
|
|
82
88
|
stopping: false,
|
|
83
89
|
};
|
|
84
90
|
const state = await loadState(config.stateFile);
|
|
91
|
+
await syncClaudeAwayModeSentinel(config, state.claudeAwayMode === true);
|
|
85
92
|
const migratedPairedDevicesStateChanged = migratePairedDevicesState({ config, state });
|
|
86
93
|
const restoredPendingPlanStateChanged = restorePendingPlanRequests({ config, runtime, state });
|
|
87
94
|
const restoredPendingUserInputStateChanged = restorePendingUserInputRequests({ config, runtime, state });
|
|
@@ -204,6 +211,7 @@ function buildSessionLocalePayload(config, state, deviceId) {
|
|
|
204
211
|
defaultLocale: defaultLocale(config),
|
|
205
212
|
deviceDetectedLocale: resolved.detectedLocale || null,
|
|
206
213
|
deviceOverrideLocale: resolved.overrideLocale || null,
|
|
214
|
+
claudeAwayMode: state?.claudeAwayMode === true,
|
|
207
215
|
};
|
|
208
216
|
}
|
|
209
217
|
|
|
@@ -311,18 +319,19 @@ function fileEventTitle(locale, fileEventType) {
|
|
|
311
319
|
}
|
|
312
320
|
}
|
|
313
321
|
|
|
314
|
-
function fileEventDetailCopy(locale, fileEventType) {
|
|
322
|
+
function fileEventDetailCopy(locale, fileEventType, provider) {
|
|
323
|
+
const vars = { provider: providerDisplayName(locale, provider) };
|
|
315
324
|
switch (normalizeTimelineFileEventType(fileEventType)) {
|
|
316
325
|
case "read":
|
|
317
|
-
return t(locale, "detail.fileEvent.read");
|
|
326
|
+
return t(locale, "detail.fileEvent.read", vars);
|
|
318
327
|
case "write":
|
|
319
|
-
return t(locale, "detail.fileEvent.write");
|
|
328
|
+
return t(locale, "detail.fileEvent.write", vars);
|
|
320
329
|
case "create":
|
|
321
|
-
return t(locale, "detail.fileEvent.create");
|
|
330
|
+
return t(locale, "detail.fileEvent.create", vars);
|
|
322
331
|
case "delete":
|
|
323
|
-
return t(locale, "detail.fileEvent.delete");
|
|
332
|
+
return t(locale, "detail.fileEvent.delete", vars);
|
|
324
333
|
case "rename":
|
|
325
|
-
return t(locale, "detail.fileEvent.rename");
|
|
334
|
+
return t(locale, "detail.fileEvent.rename", vars);
|
|
326
335
|
default:
|
|
327
336
|
return t(locale, "detail.detailUnavailable");
|
|
328
337
|
}
|
|
@@ -1978,6 +1987,15 @@ function handleSignal() {
|
|
|
1978
1987
|
runtime.stopping = true;
|
|
1979
1988
|
}
|
|
1980
1989
|
|
|
1990
|
+
function normalizeProvider(value) {
|
|
1991
|
+
const normalized = String(value || "").toLowerCase();
|
|
1992
|
+
return normalized === "claude" ? "claude" : "codex";
|
|
1993
|
+
}
|
|
1994
|
+
|
|
1995
|
+
function providerDisplayName(locale, provider) {
|
|
1996
|
+
return t(locale, normalizeProvider(provider) === "claude" ? "common.claude" : "common.codex");
|
|
1997
|
+
}
|
|
1998
|
+
|
|
1981
1999
|
function normalizeHistoryItems(rawItems, maxItems) {
|
|
1982
2000
|
if (!Array.isArray(rawItems)) {
|
|
1983
2001
|
return [];
|
|
@@ -2044,6 +2062,7 @@ function normalizeHistoryItem(raw) {
|
|
|
2044
2062
|
readOnly: raw.readOnly !== false,
|
|
2045
2063
|
primaryLabel: cleanText(raw.primaryLabel ?? "") || "詳細",
|
|
2046
2064
|
tone: cleanText(raw.tone ?? "") || "secondary",
|
|
2065
|
+
provider: normalizeProvider(raw.provider),
|
|
2047
2066
|
};
|
|
2048
2067
|
}
|
|
2049
2068
|
|
|
@@ -2196,6 +2215,7 @@ function normalizeTimelineEntry(raw) {
|
|
|
2196
2215
|
primaryLabel: cleanText(raw.primaryLabel ?? "") || "詳細",
|
|
2197
2216
|
tone: cleanText(raw.tone ?? "") || "secondary",
|
|
2198
2217
|
cwd: resolvePath(cleanText(raw.cwd || "")),
|
|
2218
|
+
provider: normalizeProvider(raw.provider),
|
|
2199
2219
|
};
|
|
2200
2220
|
}
|
|
2201
2221
|
|
|
@@ -2421,6 +2441,7 @@ function recordActionHistoryItem({
|
|
|
2421
2441
|
diffAddedLines = 0,
|
|
2422
2442
|
diffRemovedLines = 0,
|
|
2423
2443
|
outcome = "",
|
|
2444
|
+
provider = "codex",
|
|
2424
2445
|
}) {
|
|
2425
2446
|
const item = {
|
|
2426
2447
|
stableId,
|
|
@@ -2438,6 +2459,7 @@ function recordActionHistoryItem({
|
|
|
2438
2459
|
diffAddedLines,
|
|
2439
2460
|
diffRemovedLines,
|
|
2440
2461
|
outcome,
|
|
2462
|
+
provider: normalizeProvider(provider),
|
|
2441
2463
|
createdAtMs: Date.now(),
|
|
2442
2464
|
readOnly: true,
|
|
2443
2465
|
primaryLabel: "詳細",
|
|
@@ -2723,6 +2745,21 @@ async function scanOnce({ config, runtime, state }) {
|
|
|
2723
2745
|
dirty = dirty || changed;
|
|
2724
2746
|
}
|
|
2725
2747
|
|
|
2748
|
+
if (config.webUiEnabled) {
|
|
2749
|
+
if (now - runtime.lastClaudeScanAt >= config.directoryScanIntervalMs) {
|
|
2750
|
+
runtime.claudeKnownFiles = await listClaudeTranscriptFiles(config.claudeProjectsDir);
|
|
2751
|
+
runtime.lastClaudeScanAt = now;
|
|
2752
|
+
}
|
|
2753
|
+
if (now - runtime.lastClaudeSessionTitleScanAt >= config.directoryScanIntervalMs) {
|
|
2754
|
+
await refreshClaudeSessionTitles(runtime);
|
|
2755
|
+
runtime.lastClaudeSessionTitleScanAt = now;
|
|
2756
|
+
}
|
|
2757
|
+
for (const filePath of runtime.claudeKnownFiles) {
|
|
2758
|
+
const changed = await processClaudeTranscriptFile({ filePath, config, runtime, state, now });
|
|
2759
|
+
dirty = dirty || changed;
|
|
2760
|
+
}
|
|
2761
|
+
}
|
|
2762
|
+
|
|
2726
2763
|
if (config.webUiEnabled) {
|
|
2727
2764
|
const sqliteTimelineChanged = await processSqliteTimelineLog({
|
|
2728
2765
|
config,
|
|
@@ -2945,6 +2982,230 @@ function fileEventCallIdFromStableId(stableId) {
|
|
|
2945
2982
|
return match ? cleanText(match[1]) : "";
|
|
2946
2983
|
}
|
|
2947
2984
|
|
|
2985
|
+
async function refreshClaudeSessionTitles(runtime) {
|
|
2986
|
+
// Read ~/Library/Application Support/Claude/claude-code-sessions/*/*/local_*.json
|
|
2987
|
+
// Each file maps cliSessionId → title (auto-generated by Claude Desktop).
|
|
2988
|
+
const baseDir = path.join(
|
|
2989
|
+
os.homedir(),
|
|
2990
|
+
"Library",
|
|
2991
|
+
"Application Support",
|
|
2992
|
+
"Claude",
|
|
2993
|
+
"claude-code-sessions"
|
|
2994
|
+
);
|
|
2995
|
+
try {
|
|
2996
|
+
const topEntries = await fs.readdir(baseDir, { withFileTypes: true });
|
|
2997
|
+
for (const top of topEntries) {
|
|
2998
|
+
if (!top.isDirectory()) continue;
|
|
2999
|
+
const topPath = path.join(baseDir, top.name);
|
|
3000
|
+
let midEntries;
|
|
3001
|
+
try {
|
|
3002
|
+
midEntries = await fs.readdir(topPath, { withFileTypes: true });
|
|
3003
|
+
} catch {
|
|
3004
|
+
continue;
|
|
3005
|
+
}
|
|
3006
|
+
for (const mid of midEntries) {
|
|
3007
|
+
if (!mid.isDirectory()) continue;
|
|
3008
|
+
const midPath = path.join(topPath, mid.name);
|
|
3009
|
+
let files;
|
|
3010
|
+
try {
|
|
3011
|
+
files = await fs.readdir(midPath, { withFileTypes: true });
|
|
3012
|
+
} catch {
|
|
3013
|
+
continue;
|
|
3014
|
+
}
|
|
3015
|
+
for (const file of files) {
|
|
3016
|
+
if (!file.isFile() || !file.name.startsWith("local_") || !file.name.endsWith(".json")) continue;
|
|
3017
|
+
try {
|
|
3018
|
+
const raw = await fs.readFile(path.join(midPath, file.name), "utf8");
|
|
3019
|
+
const data = JSON.parse(raw);
|
|
3020
|
+
const cliSessionId = cleanText(data?.cliSessionId || "");
|
|
3021
|
+
const title = cleanText(data?.title || "");
|
|
3022
|
+
if (cliSessionId && title) {
|
|
3023
|
+
runtime.claudeSessionTitles.set(cliSessionId, title);
|
|
3024
|
+
}
|
|
3025
|
+
} catch {
|
|
3026
|
+
// skip unreadable/invalid
|
|
3027
|
+
}
|
|
3028
|
+
}
|
|
3029
|
+
}
|
|
3030
|
+
}
|
|
3031
|
+
} catch {
|
|
3032
|
+
// base dir missing — Claude Desktop not installed
|
|
3033
|
+
}
|
|
3034
|
+
}
|
|
3035
|
+
|
|
3036
|
+
async function listClaudeTranscriptFiles(claudeProjectsDir) {
|
|
3037
|
+
try {
|
|
3038
|
+
const result = [];
|
|
3039
|
+
const entries = await fs.readdir(claudeProjectsDir, { withFileTypes: true });
|
|
3040
|
+
for (const entry of entries) {
|
|
3041
|
+
if (!entry.isDirectory()) continue;
|
|
3042
|
+
const projectPath = path.join(claudeProjectsDir, entry.name);
|
|
3043
|
+
try {
|
|
3044
|
+
const files = await fs.readdir(projectPath, { withFileTypes: true });
|
|
3045
|
+
for (const file of files) {
|
|
3046
|
+
if (!file.isFile() || !file.name.endsWith(".jsonl")) continue;
|
|
3047
|
+
result.push(path.join(projectPath, file.name));
|
|
3048
|
+
}
|
|
3049
|
+
} catch {
|
|
3050
|
+
// skip unreadable project dirs
|
|
3051
|
+
}
|
|
3052
|
+
}
|
|
3053
|
+
return result;
|
|
3054
|
+
} catch {
|
|
3055
|
+
return [];
|
|
3056
|
+
}
|
|
3057
|
+
}
|
|
3058
|
+
|
|
3059
|
+
async function processClaudeTranscriptFile({ filePath, config, runtime, state, now }) {
|
|
3060
|
+
let fileState = runtime.claudeFileStates.get(filePath);
|
|
3061
|
+
if (!fileState) {
|
|
3062
|
+
// Derive a default threadLabel from the project directory path
|
|
3063
|
+
// e.g. ~/.claude/projects/-Users-y-hoshino-Work-kanade/session.jsonl → "kanade"
|
|
3064
|
+
const projectDirName = path.basename(path.dirname(filePath));
|
|
3065
|
+
const decodedProjectDir = projectDirName.replace(/^-/u, "").replace(/-/gu, "/");
|
|
3066
|
+
const defaultThreadLabel = path.basename(decodedProjectDir) || "";
|
|
3067
|
+
|
|
3068
|
+
// On startup, use the same "recent only" strategy as processRolloutFile to avoid
|
|
3069
|
+
// re-processing old entries that would be immediately discarded by the 250-entry limit.
|
|
3070
|
+
let initialOffset = 0;
|
|
3071
|
+
let startupCutoffMs = 0;
|
|
3072
|
+
try {
|
|
3073
|
+
const stat = await fs.stat(filePath);
|
|
3074
|
+
const shouldReplayRecent = stat.mtimeMs >= now - config.replaySeconds * 1000;
|
|
3075
|
+
if (shouldReplayRecent) {
|
|
3076
|
+
// Recently active transcript: start from near the end and apply a time cutoff
|
|
3077
|
+
initialOffset = Math.max(0, stat.size - config.maxReadBytes);
|
|
3078
|
+
startupCutoffMs = now - config.replaySeconds * 1000;
|
|
3079
|
+
} else {
|
|
3080
|
+
// Inactive transcript: skip to end, only process future appends
|
|
3081
|
+
initialOffset = stat.size;
|
|
3082
|
+
}
|
|
3083
|
+
} catch {
|
|
3084
|
+
// stat failed — start from beginning with no cutoff
|
|
3085
|
+
}
|
|
3086
|
+
|
|
3087
|
+
fileState = { offset: initialOffset, threadId: "", threadLabel: defaultThreadLabel, cwd: "", startupCutoffMs, remainder: "" };
|
|
3088
|
+
runtime.claudeFileStates.set(filePath, fileState);
|
|
3089
|
+
}
|
|
3090
|
+
|
|
3091
|
+
let fileContent;
|
|
3092
|
+
let newOffset;
|
|
3093
|
+
try {
|
|
3094
|
+
const stat = await fs.stat(filePath);
|
|
3095
|
+
if (stat.size <= fileState.offset) return false;
|
|
3096
|
+
if (stat.size - fileState.offset > config.maxReadBytes) {
|
|
3097
|
+
fileState.offset = Math.max(0, stat.size - config.maxReadBytes);
|
|
3098
|
+
fileState.remainder = "";
|
|
3099
|
+
}
|
|
3100
|
+
const readLen = stat.size - fileState.offset;
|
|
3101
|
+
const buf = Buffer.alloc(readLen);
|
|
3102
|
+
const fh = await fs.open(filePath, "r");
|
|
3103
|
+
try {
|
|
3104
|
+
const { bytesRead } = await fh.read(buf, 0, readLen, fileState.offset);
|
|
3105
|
+
fileContent = buf.slice(0, bytesRead).toString("utf8");
|
|
3106
|
+
} finally {
|
|
3107
|
+
await fh.close();
|
|
3108
|
+
}
|
|
3109
|
+
newOffset = stat.size;
|
|
3110
|
+
} catch {
|
|
3111
|
+
return false;
|
|
3112
|
+
}
|
|
3113
|
+
|
|
3114
|
+
const merged = (fileState.remainder || "") + fileContent;
|
|
3115
|
+
const lines = merged.split("\n");
|
|
3116
|
+
// Last element is the in-progress partial line (or "" if file ends with \n).
|
|
3117
|
+
// Stash it as the next remainder so a future scan can complete it.
|
|
3118
|
+
fileState.remainder = lines.pop() ?? "";
|
|
3119
|
+
fileState.offset = newOffset;
|
|
3120
|
+
let dirty = false;
|
|
3121
|
+
|
|
3122
|
+
for (const line of lines) {
|
|
3123
|
+
const trimmed = line.trim();
|
|
3124
|
+
if (!trimmed) continue;
|
|
3125
|
+
|
|
3126
|
+
let record;
|
|
3127
|
+
try {
|
|
3128
|
+
record = JSON.parse(trimmed);
|
|
3129
|
+
} catch {
|
|
3130
|
+
continue;
|
|
3131
|
+
}
|
|
3132
|
+
|
|
3133
|
+
// Apply startup cutoff: skip entries older than replaySeconds on first scan
|
|
3134
|
+
if (fileState.startupCutoffMs && record.timestamp) {
|
|
3135
|
+
const recordMs = Date.parse(record.timestamp);
|
|
3136
|
+
if (recordMs && recordMs < fileState.startupCutoffMs) {
|
|
3137
|
+
// Still update threadId/cwd state so later entries inherit correct context
|
|
3138
|
+
if (record.sessionId) fileState.threadId = record.sessionId;
|
|
3139
|
+
if (record.cwd) {
|
|
3140
|
+
fileState.cwd = record.cwd;
|
|
3141
|
+
fileState.threadLabel = path.basename(record.cwd);
|
|
3142
|
+
}
|
|
3143
|
+
continue;
|
|
3144
|
+
}
|
|
3145
|
+
}
|
|
3146
|
+
|
|
3147
|
+
if (record.sessionId) fileState.threadId = record.sessionId;
|
|
3148
|
+
if (record.cwd) {
|
|
3149
|
+
fileState.cwd = record.cwd;
|
|
3150
|
+
fileState.threadLabel = path.basename(record.cwd);
|
|
3151
|
+
}
|
|
3152
|
+
|
|
3153
|
+
// Only process Claude Desktop sessions
|
|
3154
|
+
if (record.entrypoint !== "claude-desktop") continue;
|
|
3155
|
+
|
|
3156
|
+
const type = record.type;
|
|
3157
|
+
if (type !== "user" && type !== "assistant") continue;
|
|
3158
|
+
// Skip Claude Code's meta records (e.g. Stop hook feedback injected as a
|
|
3159
|
+
// synthetic user message). They are not real user input.
|
|
3160
|
+
if (record.isMeta === true) continue;
|
|
3161
|
+
|
|
3162
|
+
const msg = record.message || {};
|
|
3163
|
+
const content = msg.content;
|
|
3164
|
+
const uuid = cleanText(record.uuid || "");
|
|
3165
|
+
const createdAtMs = record.timestamp ? Date.parse(record.timestamp) : now;
|
|
3166
|
+
const threadId = cleanText(fileState.threadId || record.sessionId || "");
|
|
3167
|
+
// Prefer Claude Desktop's auto-generated session title (e.g. "Sync Codex app from Mac to iPhone")
|
|
3168
|
+
// when available; fall back to the cwd-derived basename (e.g. "viveworker").
|
|
3169
|
+
const claudeTitle = threadId ? runtime.claudeSessionTitles.get(threadId) || "" : "";
|
|
3170
|
+
const threadLabel = claudeTitle || fileState.threadLabel || "";
|
|
3171
|
+
|
|
3172
|
+
let text = "";
|
|
3173
|
+
if (typeof content === "string") {
|
|
3174
|
+
text = content;
|
|
3175
|
+
} else if (Array.isArray(content)) {
|
|
3176
|
+
for (const block of content) {
|
|
3177
|
+
if (block?.type === "text" && block.text) text += block.text;
|
|
3178
|
+
}
|
|
3179
|
+
}
|
|
3180
|
+
text = cleanText(text);
|
|
3181
|
+
if (!text) continue;
|
|
3182
|
+
|
|
3183
|
+
const kind = type === "user" ? "user_message" : "assistant_final";
|
|
3184
|
+
const stableId = `${kind}:${threadId}:${uuid}`;
|
|
3185
|
+
const entry = normalizeTimelineEntry({
|
|
3186
|
+
stableId,
|
|
3187
|
+
token: historyToken(stableId),
|
|
3188
|
+
kind,
|
|
3189
|
+
threadId,
|
|
3190
|
+
threadLabel,
|
|
3191
|
+
title: threadLabel || "Claude",
|
|
3192
|
+
summary: truncate(singleLine(text), 180),
|
|
3193
|
+
messageText: text,
|
|
3194
|
+
createdAtMs,
|
|
3195
|
+
readOnly: true,
|
|
3196
|
+
cwd: fileState.cwd,
|
|
3197
|
+
provider: "claude",
|
|
3198
|
+
});
|
|
3199
|
+
if (entry) {
|
|
3200
|
+
dirty = recordTimelineEntry({ config, runtime, state, entry }) || dirty;
|
|
3201
|
+
}
|
|
3202
|
+
}
|
|
3203
|
+
|
|
3204
|
+
// Clear startup cutoff after first scan so subsequent incremental reads are unfiltered
|
|
3205
|
+
fileState.startupCutoffMs = 0;
|
|
3206
|
+
return dirty;
|
|
3207
|
+
}
|
|
3208
|
+
|
|
2948
3209
|
async function processSqliteCompletionLog({ config, runtime, state, now }) {
|
|
2949
3210
|
const logsDbFile = cleanText(runtime.logsDbFile || config.codexLogsDbFile || "");
|
|
2950
3211
|
if (!logsDbFile) {
|
|
@@ -3702,8 +3963,13 @@ async function copyTimelineAttachmentToPersistentDir(config, sourcePath) {
|
|
|
3702
3963
|
config.timelineAttachmentsDir,
|
|
3703
3964
|
`${Date.now()}-${crypto.randomUUID()}${extension}`
|
|
3704
3965
|
);
|
|
3705
|
-
|
|
3706
|
-
|
|
3966
|
+
try {
|
|
3967
|
+
await fs.copyFile(normalizedSourcePath, destinationPath);
|
|
3968
|
+
return destinationPath;
|
|
3969
|
+
} catch (error) {
|
|
3970
|
+
console.warn(`[timeline-image-copy-skipped] ${error?.message || error}`);
|
|
3971
|
+
return "";
|
|
3972
|
+
}
|
|
3707
3973
|
}
|
|
3708
3974
|
|
|
3709
3975
|
async function normalizePersistedTimelineImagePaths({ config, state, imagePaths = [] }) {
|
|
@@ -3746,7 +4012,7 @@ async function normalizePersistedTimelineImagePaths({ config, state, imagePaths
|
|
|
3746
4012
|
|
|
3747
4013
|
let persistentPath = existingSourcePath;
|
|
3748
4014
|
if (!existingSourcePath.startsWith(`${config.timelineAttachmentsDir}${path.sep}`)) {
|
|
3749
|
-
persistentPath = await copyTimelineAttachmentToPersistentDir(config, existingSourcePath);
|
|
4015
|
+
persistentPath = await copyTimelineAttachmentToPersistentDir(config, existingSourcePath) || existingSourcePath;
|
|
3750
4016
|
}
|
|
3751
4017
|
|
|
3752
4018
|
aliases[normalizedPath] = persistentPath;
|
|
@@ -4287,7 +4553,7 @@ function buildRolloutEvent({ record, filePath, fileState, sessionIndex, config,
|
|
|
4287
4553
|
const message = formatMessage(
|
|
4288
4554
|
[
|
|
4289
4555
|
t(config.defaultLocale, "server.message.approveOnMac"),
|
|
4290
|
-
justification || t(config.defaultLocale, "server.message.approvalNeededInCodex"),
|
|
4556
|
+
justification || t(config.defaultLocale, "server.message.approvalNeededInCodex", { provider: providerDisplayName(config.defaultLocale, "codex") }),
|
|
4291
4557
|
cmd ? t(config.defaultLocale, "server.message.commandPrefix", { command: cmd }) : null,
|
|
4292
4558
|
approval.extraCount > 0 ? t(config.defaultLocale, "server.message.extraApprovals", { count: approval.extraCount }) : null,
|
|
4293
4559
|
],
|
|
@@ -4804,6 +5070,7 @@ async function createNativeApproval({ config, runtime, conversationId, request,
|
|
|
4804
5070
|
createdAtMs: now,
|
|
4805
5071
|
resolved: false,
|
|
4806
5072
|
resolving: false,
|
|
5073
|
+
provider: "codex",
|
|
4807
5074
|
};
|
|
4808
5075
|
}
|
|
4809
5076
|
|
|
@@ -6979,7 +7246,7 @@ function getNativeThreadLabel({ runtime, conversationId, cwd }) {
|
|
|
6979
7246
|
if (cwd) {
|
|
6980
7247
|
return truncate(cleanText(path.basename(cwd)), 90) || shortId(normalizedConversationId);
|
|
6981
7248
|
}
|
|
6982
|
-
return shortId(normalizedConversationId) || "
|
|
7249
|
+
return shortId(normalizedConversationId) || t(DEFAULT_LOCALE, "server.fallback.codexTask", { provider: providerDisplayName(DEFAULT_LOCALE, "codex") });
|
|
6983
7250
|
}
|
|
6984
7251
|
|
|
6985
7252
|
function threadStateArchiveStatus(threadState) {
|
|
@@ -8667,6 +8934,7 @@ function buildPendingInboxItems(runtime, state, config, locale) {
|
|
|
8667
8934
|
summary: formatNotificationBody(approval.messageText, 100) || approval.messageText,
|
|
8668
8935
|
primaryLabel: t(locale, "server.action.review"),
|
|
8669
8936
|
createdAtMs: Number(approval.createdAtMs) || now,
|
|
8937
|
+
provider: normalizeProvider(approval.provider),
|
|
8670
8938
|
});
|
|
8671
8939
|
}
|
|
8672
8940
|
|
|
@@ -8686,6 +8954,7 @@ function buildPendingInboxItems(runtime, state, config, locale) {
|
|
|
8686
8954
|
summary: formatNotificationBody(planRequest.messageText, 100) || planRequest.messageText,
|
|
8687
8955
|
primaryLabel: t(locale, "server.action.review"),
|
|
8688
8956
|
createdAtMs: Number(planRequest.createdAtMs) || now,
|
|
8957
|
+
provider: "codex",
|
|
8689
8958
|
});
|
|
8690
8959
|
}
|
|
8691
8960
|
|
|
@@ -8709,6 +8978,7 @@ function buildPendingInboxItems(runtime, state, config, locale) {
|
|
|
8709
8978
|
summary: userInputRequest.notificationText || formatNotificationBody(userInputRequest.messageText, 100),
|
|
8710
8979
|
primaryLabel: t(locale, userInputRequest.supported ? "server.action.select" : "server.action.detail"),
|
|
8711
8980
|
createdAtMs: Number(userInputRequest.createdAtMs) || now,
|
|
8981
|
+
provider: "codex",
|
|
8712
8982
|
});
|
|
8713
8983
|
}
|
|
8714
8984
|
|
|
@@ -8732,6 +9002,7 @@ function buildCompletedInboxItems(runtime, state, config, locale) {
|
|
|
8732
9002
|
fileRefs: normalizeTimelineFileRefs(item.fileRefs ?? []),
|
|
8733
9003
|
primaryLabel: t(locale, "server.action.detail"),
|
|
8734
9004
|
createdAtMs: item.createdAtMs,
|
|
9005
|
+
provider: normalizeProvider(item.provider),
|
|
8735
9006
|
}));
|
|
8736
9007
|
}
|
|
8737
9008
|
|
|
@@ -8923,6 +9194,7 @@ function buildOperationalTimelineEntries(runtime, state, config, locale) {
|
|
|
8923
9194
|
messageText: approval.messageText,
|
|
8924
9195
|
outcome: "pending",
|
|
8925
9196
|
createdAtMs: Number(approval.createdAtMs) || now,
|
|
9197
|
+
provider: normalizeProvider(approval.provider),
|
|
8926
9198
|
})
|
|
8927
9199
|
);
|
|
8928
9200
|
}
|
|
@@ -8946,6 +9218,7 @@ function buildOperationalTimelineEntries(runtime, state, config, locale) {
|
|
|
8946
9218
|
messageText: planRequest.messageText,
|
|
8947
9219
|
outcome: "pending",
|
|
8948
9220
|
createdAtMs: Number(planRequest.createdAtMs) || now,
|
|
9221
|
+
provider: "codex",
|
|
8949
9222
|
})
|
|
8950
9223
|
);
|
|
8951
9224
|
}
|
|
@@ -8973,6 +9246,7 @@ function buildOperationalTimelineEntries(runtime, state, config, locale) {
|
|
|
8973
9246
|
messageText: userInputRequest.messageText,
|
|
8974
9247
|
outcome: "pending",
|
|
8975
9248
|
createdAtMs: Number(userInputRequest.createdAtMs) || now,
|
|
9249
|
+
provider: "codex",
|
|
8976
9250
|
})
|
|
8977
9251
|
);
|
|
8978
9252
|
}
|
|
@@ -8993,6 +9267,7 @@ function buildOperationalTimelineEntries(runtime, state, config, locale) {
|
|
|
8993
9267
|
messageText: historyItem.messageText,
|
|
8994
9268
|
outcome: historyItem.outcome,
|
|
8995
9269
|
createdAtMs: historyItem.createdAtMs,
|
|
9270
|
+
provider: normalizeProvider(historyItem.provider),
|
|
8996
9271
|
})
|
|
8997
9272
|
);
|
|
8998
9273
|
}
|
|
@@ -9031,7 +9306,7 @@ function buildTimelineThreads(entries, config) {
|
|
|
9031
9306
|
}
|
|
9032
9307
|
const preferredLabel =
|
|
9033
9308
|
sanitizeTimelineThreadFilterLabel(entry.threadLabel || "", threadId) ||
|
|
9034
|
-
t(DEFAULT_LOCALE, "server.fallback.codexTask");
|
|
9309
|
+
t(DEFAULT_LOCALE, "server.fallback.codexTask", { provider: providerDisplayName(DEFAULT_LOCALE, entry?.provider) });
|
|
9035
9310
|
const existing = byThread.get(threadId);
|
|
9036
9311
|
if (!existing) {
|
|
9037
9312
|
byThread.set(threadId, {
|
|
@@ -9080,6 +9355,7 @@ function buildTimelineResponse(runtime, state, config, locale) {
|
|
|
9080
9355
|
diffRemovedLines: Math.max(0, Number(entry.diffRemovedLines) || 0),
|
|
9081
9356
|
outcome: entry.outcome || "",
|
|
9082
9357
|
createdAtMs: entry.createdAtMs,
|
|
9358
|
+
provider: normalizeProvider(entry.provider),
|
|
9083
9359
|
}));
|
|
9084
9360
|
|
|
9085
9361
|
return {
|
|
@@ -9090,9 +9366,11 @@ function buildTimelineResponse(runtime, state, config, locale) {
|
|
|
9090
9366
|
|
|
9091
9367
|
function buildPendingApprovalDetail(runtime, approval, locale) {
|
|
9092
9368
|
const previousContext = buildPreviousApprovalContext(runtime, approval);
|
|
9093
|
-
|
|
9369
|
+
const approvalKind = cleanText(approval.kind || "");
|
|
9370
|
+
const detail = {
|
|
9094
9371
|
kind: "approval",
|
|
9095
|
-
approvalKind
|
|
9372
|
+
approvalKind,
|
|
9373
|
+
provider: normalizeProvider(approval.provider),
|
|
9096
9374
|
token: approval.token,
|
|
9097
9375
|
title: formatLocalizedTitle(locale, "server.title.approval", approval.threadLabel),
|
|
9098
9376
|
threadLabel: approval.threadLabel || "",
|
|
@@ -9105,12 +9383,28 @@ function buildPendingApprovalDetail(runtime, approval, locale) {
|
|
|
9105
9383
|
diffAddedLines: Math.max(0, Number(approval.diffAddedLines) || 0),
|
|
9106
9384
|
diffRemovedLines: Math.max(0, Number(approval.diffRemovedLines) || 0),
|
|
9107
9385
|
previousContext,
|
|
9108
|
-
readOnly:
|
|
9109
|
-
actions:
|
|
9110
|
-
|
|
9111
|
-
|
|
9112
|
-
|
|
9386
|
+
readOnly: approval.readOnly === true,
|
|
9387
|
+
actions: approval.readOnly === true
|
|
9388
|
+
? []
|
|
9389
|
+
: [
|
|
9390
|
+
{ label: t(locale, "server.action.approve"), tone: "primary", url: `/api/items/approval/${encodeURIComponent(approval.token)}/accept`, body: {} },
|
|
9391
|
+
{ label: t(locale, "server.action.reject"), tone: "danger", url: `/api/items/approval/${encodeURIComponent(approval.token)}/decline`, body: {} },
|
|
9392
|
+
],
|
|
9113
9393
|
};
|
|
9394
|
+
if (approvalKind === "plan") {
|
|
9395
|
+
const planText = String(approval.planText || "");
|
|
9396
|
+
detail.planText = planText;
|
|
9397
|
+
detail.planHtml = planText
|
|
9398
|
+
? renderMessageHtml(planText, `<p>${escapeHtml(t(locale, "detail.planReady"))}</p>`)
|
|
9399
|
+
: "";
|
|
9400
|
+
}
|
|
9401
|
+
if (approvalKind === "question") {
|
|
9402
|
+
detail.questions = Array.isArray(approval.questions) ? approval.questions : [];
|
|
9403
|
+
detail.answerUrl = `/api/items/question/${encodeURIComponent(approval.token)}/answer`;
|
|
9404
|
+
// Question answers are submitted via answerUrl; remove approve/reject actions.
|
|
9405
|
+
detail.actions = [];
|
|
9406
|
+
}
|
|
9407
|
+
return detail;
|
|
9114
9408
|
}
|
|
9115
9409
|
|
|
9116
9410
|
function buildPreviousApprovalContext(runtime, approval) {
|
|
@@ -9393,7 +9687,7 @@ function buildTimelineFileEventDetail(entry, locale) {
|
|
|
9393
9687
|
threadLabel: entry.threadLabel || "",
|
|
9394
9688
|
fileEventType,
|
|
9395
9689
|
createdAtMs: Number(entry.createdAtMs) || 0,
|
|
9396
|
-
messageHtml: renderMessageHtml(fileEventDetailCopy(locale, fileEventType), `<p>${escapeHtml(t(locale, "detail.detailUnavailable"))}</p>`),
|
|
9690
|
+
messageHtml: renderMessageHtml(fileEventDetailCopy(locale, fileEventType, entry?.provider), `<p>${escapeHtml(t(locale, "detail.detailUnavailable"))}</p>`),
|
|
9397
9691
|
fileRefs: normalizeTimelineFileRefs(entry.fileRefs ?? []),
|
|
9398
9692
|
previousFileRefs: normalizeTimelineFileRefs(entry.previousFileRefs ?? []),
|
|
9399
9693
|
diffAvailable: Boolean(entry.diffAvailable),
|
|
@@ -9522,7 +9816,7 @@ async function submitGenericUserInputDecision({ config, runtime, state, userInpu
|
|
|
9522
9816
|
title: userInputRequest.title,
|
|
9523
9817
|
messageText: userInputRequest.testRequest
|
|
9524
9818
|
? `${t(config.defaultLocale, "server.message.choiceSubmittedTest")}\n\n${buildChoiceHistoryText(userInputRequest, submittedAnswers)}`
|
|
9525
|
-
: `${t(config.defaultLocale, "server.message.choiceSubmitted")}\n\n${buildChoiceHistoryText(userInputRequest, submittedAnswers)}`,
|
|
9819
|
+
: `${t(config.defaultLocale, "server.message.choiceSubmitted", { provider: providerDisplayName(config.defaultLocale, "codex") })}\n\n${buildChoiceHistoryText(userInputRequest, submittedAnswers)}`,
|
|
9526
9820
|
summary: userInputRequest.testRequest
|
|
9527
9821
|
? t(config.defaultLocale, "server.message.choiceSummaryReceivedTest")
|
|
9528
9822
|
: t(config.defaultLocale, "server.message.choiceSummarySubmitted"),
|
|
@@ -9960,8 +10254,98 @@ async function handlePlanDecision({ config, runtime, state, planRequest, decisio
|
|
|
9960
10254
|
console.log(`[plan-decision] ${planRequest.requestKey} | ${decision} | ${decisionTransport}`);
|
|
9961
10255
|
}
|
|
9962
10256
|
|
|
10257
|
+
function claudeToolFingerprint(toolName, toolInput) {
|
|
10258
|
+
if (!toolInput || typeof toolInput !== "object") return "";
|
|
10259
|
+
if (toolName === "Bash") {
|
|
10260
|
+
return String(toolInput.command || "").slice(0, 500);
|
|
10261
|
+
}
|
|
10262
|
+
if (toolName === "Write" || toolName === "Edit" || toolName === "MultiEdit") {
|
|
10263
|
+
return String(toolInput.file_path || toolInput.path || "");
|
|
10264
|
+
}
|
|
10265
|
+
if (toolName === "AskUserQuestion") {
|
|
10266
|
+
// PostToolUse may include the user's `answers` in toolInput, which would
|
|
10267
|
+
// change a naive JSON.stringify fingerprint. Hash only the question texts.
|
|
10268
|
+
try {
|
|
10269
|
+
const qs = Array.isArray(toolInput.questions) ? toolInput.questions : [];
|
|
10270
|
+
return JSON.stringify(qs.map((q) => String(q?.question || q?.header || ""))).slice(0, 500);
|
|
10271
|
+
} catch {
|
|
10272
|
+
return "";
|
|
10273
|
+
}
|
|
10274
|
+
}
|
|
10275
|
+
if (toolName === "ExitPlanMode") {
|
|
10276
|
+
return String(toolInput.plan || "").slice(0, 500);
|
|
10277
|
+
}
|
|
10278
|
+
try {
|
|
10279
|
+
return JSON.stringify(toolInput).slice(0, 500);
|
|
10280
|
+
} catch {
|
|
10281
|
+
return "";
|
|
10282
|
+
}
|
|
10283
|
+
}
|
|
10284
|
+
|
|
10285
|
+
function formatClaudeQuestionAnswers(questions, answers) {
|
|
10286
|
+
if (!Array.isArray(questions) || !Array.isArray(answers)) return "";
|
|
10287
|
+
const lines = [];
|
|
10288
|
+
for (let i = 0; i < questions.length; i++) {
|
|
10289
|
+
const q = questions[i] || {};
|
|
10290
|
+
const ans = answers[i] || {};
|
|
10291
|
+
const questionText = String(q.question || q.header || "").trim();
|
|
10292
|
+
if (!questionText) continue;
|
|
10293
|
+
const options = Array.isArray(q.options) ? q.options : [];
|
|
10294
|
+
const optionIndices = Array.isArray(ans.optionIndices) ? ans.optionIndices : [];
|
|
10295
|
+
const labels = optionIndices
|
|
10296
|
+
.map((idx) => options[idx]?.label)
|
|
10297
|
+
.filter((label) => typeof label === "string" && label.length > 0);
|
|
10298
|
+
const note = typeof ans.note === "string" ? ans.note.trim() : "";
|
|
10299
|
+
let answerLine = "";
|
|
10300
|
+
if (labels.length > 0) {
|
|
10301
|
+
answerLine = labels.join(", ");
|
|
10302
|
+
}
|
|
10303
|
+
if (note) {
|
|
10304
|
+
answerLine = answerLine ? `${answerLine} (note: ${note})` : note;
|
|
10305
|
+
}
|
|
10306
|
+
if (!answerLine) {
|
|
10307
|
+
answerLine = "(no answer)";
|
|
10308
|
+
}
|
|
10309
|
+
lines.push(`Q${i + 1}: ${questionText}`);
|
|
10310
|
+
lines.push(`A: ${answerLine}`);
|
|
10311
|
+
}
|
|
10312
|
+
return lines.join("\n");
|
|
10313
|
+
}
|
|
10314
|
+
|
|
10315
|
+
function findClaudePendingApprovalForTool(runtime, threadId, toolName, fingerprint) {
|
|
10316
|
+
let match = null;
|
|
10317
|
+
for (const approval of runtime.nativeApprovalsByToken.values()) {
|
|
10318
|
+
if (approval.resolved) continue;
|
|
10319
|
+
if (approval.conversationId !== threadId) continue;
|
|
10320
|
+
if (approval.claudeToolName && toolName && approval.claudeToolName !== toolName) continue;
|
|
10321
|
+
if (approval.claudeToolFingerprint && fingerprint && approval.claudeToolFingerprint !== fingerprint) continue;
|
|
10322
|
+
if (!match || approval.createdAtMs > match.createdAtMs) match = approval;
|
|
10323
|
+
}
|
|
10324
|
+
return match;
|
|
10325
|
+
}
|
|
10326
|
+
|
|
9963
10327
|
async function handleNativeApprovalDecision({ config, runtime, state, approval, decision }) {
|
|
9964
|
-
|
|
10328
|
+
if (approval.resolveClaudeWaiter) {
|
|
10329
|
+
if (approval.provider === "claude" && approval.kind === "plan") {
|
|
10330
|
+
// ExitPlanMode cannot be truly auto-approved via permissionDecision: "allow"
|
|
10331
|
+
// (Claude still shows the native PC plan dialog). Instead, deny the tool
|
|
10332
|
+
// call with a reason that tells Claude the user already decided on mobile.
|
|
10333
|
+
approval.resolveClaudeWaiter({
|
|
10334
|
+
permissionDecision: "deny",
|
|
10335
|
+
permissionDecisionReason:
|
|
10336
|
+
decision === "accept"
|
|
10337
|
+
? "User approved the plan via the viveworker mobile app. Proceed with implementing the plan exactly as proposed and do not call ExitPlanMode again."
|
|
10338
|
+
: "User rejected the plan via the viveworker mobile app. Revise the plan based on the conversation context before calling ExitPlanMode again.",
|
|
10339
|
+
});
|
|
10340
|
+
} else {
|
|
10341
|
+
approval.resolveClaudeWaiter(decision);
|
|
10342
|
+
}
|
|
10343
|
+
} else if (approval.provider === "claude") {
|
|
10344
|
+
// notifyOnly Claude approvals have no long-poll waiter and no ipcClient
|
|
10345
|
+
// back-channel — the desktop user already answered. Nothing to send.
|
|
10346
|
+
} else {
|
|
10347
|
+
await runtime.ipcClient?.sendApprovalDecision(approval, decision);
|
|
10348
|
+
}
|
|
9965
10349
|
approval.resolved = true;
|
|
9966
10350
|
approval.resolving = false;
|
|
9967
10351
|
runtime.nativeApprovalsByToken.delete(approval.token);
|
|
@@ -9975,8 +10359,8 @@ async function handleNativeApprovalDecision({ config, runtime, state, approval,
|
|
|
9975
10359
|
token: approval.token,
|
|
9976
10360
|
title: approval.title,
|
|
9977
10361
|
threadLabel: approval.threadLabel || "",
|
|
9978
|
-
messageText: `${approvalDecisionMessage(decision, config.defaultLocale)}\n\n${approval.messageText}`,
|
|
9979
|
-
summary: approvalDecisionMessage(decision, config.defaultLocale),
|
|
10362
|
+
messageText: `${approvalDecisionMessage(decision, config.defaultLocale, approval.provider)}\n\n${approval.messageText}`,
|
|
10363
|
+
summary: approvalDecisionMessage(decision, config.defaultLocale, approval.provider),
|
|
9980
10364
|
fileRefs: normalizeTimelineFileRefs(approval.fileRefs ?? []),
|
|
9981
10365
|
diffText: normalizeTimelineDiffText(approval.diffText ?? ""),
|
|
9982
10366
|
diffSource: normalizeTimelineDiffSource(approval.diffSource ?? ""),
|
|
@@ -9984,6 +10368,7 @@ async function handleNativeApprovalDecision({ config, runtime, state, approval,
|
|
|
9984
10368
|
diffAddedLines: Math.max(0, Number(approval.diffAddedLines) || 0),
|
|
9985
10369
|
diffRemovedLines: Math.max(0, Number(approval.diffRemovedLines) || 0),
|
|
9986
10370
|
outcome: decision === "accept" ? "approved" : "rejected",
|
|
10371
|
+
provider: approval.provider || "codex",
|
|
9987
10372
|
});
|
|
9988
10373
|
if (stateChanged) {
|
|
9989
10374
|
await saveState(config.stateFile, state);
|
|
@@ -10394,6 +10779,26 @@ function createNativeApprovalServer({ config, runtime, state }) {
|
|
|
10394
10779
|
});
|
|
10395
10780
|
}
|
|
10396
10781
|
|
|
10782
|
+
if (url.pathname === "/api/settings/claude-away-mode" && req.method === "POST") {
|
|
10783
|
+
const session = requireMutatingApiSession(req, res, config, state);
|
|
10784
|
+
if (!session) {
|
|
10785
|
+
return;
|
|
10786
|
+
}
|
|
10787
|
+
let payload;
|
|
10788
|
+
try {
|
|
10789
|
+
payload = await parseJsonBody(req);
|
|
10790
|
+
} catch {
|
|
10791
|
+
return writeJson(res, 400, { error: "invalid-json-body" });
|
|
10792
|
+
}
|
|
10793
|
+
const enabled = payload?.enabled === true;
|
|
10794
|
+
if (state.claudeAwayMode !== enabled) {
|
|
10795
|
+
state.claudeAwayMode = enabled;
|
|
10796
|
+
await saveState(config.stateFile, state);
|
|
10797
|
+
}
|
|
10798
|
+
await syncClaudeAwayModeSentinel(config, enabled);
|
|
10799
|
+
return writeJson(res, 200, { ok: true, enabled });
|
|
10800
|
+
}
|
|
10801
|
+
|
|
10397
10802
|
if (url.pathname === "/api/push/status" && req.method === "GET") {
|
|
10398
10803
|
const session = requireApiSession(req, res, config, state);
|
|
10399
10804
|
if (!session) {
|
|
@@ -10515,6 +10920,221 @@ function createNativeApprovalServer({ config, runtime, state }) {
|
|
|
10515
10920
|
});
|
|
10516
10921
|
}
|
|
10517
10922
|
|
|
10923
|
+
if (url.pathname === "/api/providers/claude/events" && req.method === "POST") {
|
|
10924
|
+
const hookSecret = req.headers["x-viveworker-hook-secret"] || "";
|
|
10925
|
+
if (!config.sessionSecret || hookSecret !== config.sessionSecret) {
|
|
10926
|
+
return writeJson(res, 401, { error: "unauthorized" });
|
|
10927
|
+
}
|
|
10928
|
+
|
|
10929
|
+
let body;
|
|
10930
|
+
try {
|
|
10931
|
+
body = await parseJsonBody(req);
|
|
10932
|
+
} catch {
|
|
10933
|
+
return writeJson(res, 400, { error: "invalid-json-body" });
|
|
10934
|
+
}
|
|
10935
|
+
|
|
10936
|
+
const eventType = String(body.eventType || body.hookEventName || "");
|
|
10937
|
+
|
|
10938
|
+
if (eventType === "Stop" || eventType === "SessionEnd") {
|
|
10939
|
+
// Agent turn ended — any still-pending native approvals for this
|
|
10940
|
+
// session were not resolved by PostToolUse, meaning the user
|
|
10941
|
+
// denied them on PC. Resolve as deny so iPhone clears them.
|
|
10942
|
+
const threadId = String(body.threadId || body.sessionId || "");
|
|
10943
|
+
console.log(`[claude-stop] threadId=${threadId} pendingTotal=${runtime.nativeApprovalsByToken.size}`);
|
|
10944
|
+
if (threadId) {
|
|
10945
|
+
const pending = [];
|
|
10946
|
+
for (const approval of runtime.nativeApprovalsByToken.values()) {
|
|
10947
|
+
if (!approval.resolved && approval.conversationId === threadId) {
|
|
10948
|
+
pending.push(approval);
|
|
10949
|
+
}
|
|
10950
|
+
}
|
|
10951
|
+
console.log(`[claude-stop] matched=${pending.length}`);
|
|
10952
|
+
for (const approval of pending) {
|
|
10953
|
+
try {
|
|
10954
|
+
await handleNativeApprovalDecision({ config, runtime, state, approval, decision: "reject" });
|
|
10955
|
+
} catch (error) {
|
|
10956
|
+
console.error(`[claude-stop-resolve-error] ${approval.requestKey} | ${error.message}`);
|
|
10957
|
+
}
|
|
10958
|
+
}
|
|
10959
|
+
}
|
|
10960
|
+
return writeJson(res, 200, {});
|
|
10961
|
+
}
|
|
10962
|
+
|
|
10963
|
+
if (eventType === "file_event") {
|
|
10964
|
+
const threadId = String(body.threadId || body.sessionId || "");
|
|
10965
|
+
const filePath = String(body.filePath || "");
|
|
10966
|
+
const fileEventType = String(body.fileEventType || "");
|
|
10967
|
+
if (!threadId || !filePath || !fileEventType) {
|
|
10968
|
+
return writeJson(res, 200, {});
|
|
10969
|
+
}
|
|
10970
|
+
const eventCwd = String(body.cwd || "");
|
|
10971
|
+
const createdAtMs = Number(body.createdAtMs) || Date.now();
|
|
10972
|
+
const threadLabel =
|
|
10973
|
+
runtime.claudeSessionTitles.get(threadId) ||
|
|
10974
|
+
getNativeThreadLabel({ runtime, conversationId: threadId, cwd: eventCwd }) ||
|
|
10975
|
+
(eventCwd ? path.basename(eventCwd) : "") ||
|
|
10976
|
+
threadId.slice(0, 40);
|
|
10977
|
+
const stableId = `file_event:${fileEventType}:${threadId}:${historyToken(`${threadId}:${createdAtMs}:${filePath}`)}`;
|
|
10978
|
+
const token = historyToken(`file_event:${fileEventType}:${threadId}:${createdAtMs}:${filePath}`);
|
|
10979
|
+
const entry = {
|
|
10980
|
+
stableId,
|
|
10981
|
+
token,
|
|
10982
|
+
kind: "file_event",
|
|
10983
|
+
fileEventType,
|
|
10984
|
+
threadId,
|
|
10985
|
+
threadLabel,
|
|
10986
|
+
title: fileEventTitle(DEFAULT_LOCALE, fileEventType),
|
|
10987
|
+
summary: "",
|
|
10988
|
+
fileRefs: [filePath],
|
|
10989
|
+
diffText: String(body.diffText || ""),
|
|
10990
|
+
diffSource: "claude-hook",
|
|
10991
|
+
diffAvailable: body.diffAvailable === true || Boolean(body.diffText),
|
|
10992
|
+
diffAddedLines: Math.max(0, Number(body.diffAddedLines) || 0),
|
|
10993
|
+
diffRemovedLines: Math.max(0, Number(body.diffRemovedLines) || 0),
|
|
10994
|
+
createdAtMs,
|
|
10995
|
+
cwd: eventCwd,
|
|
10996
|
+
readOnly: true,
|
|
10997
|
+
provider: "claude",
|
|
10998
|
+
};
|
|
10999
|
+
const changed = recordTimelineEntry({ config, runtime, state, entry });
|
|
11000
|
+
if (changed) {
|
|
11001
|
+
try {
|
|
11002
|
+
await saveState(config.stateFile, state);
|
|
11003
|
+
} catch (error) {
|
|
11004
|
+
console.error(`[claude-file-event-save] ${error.message}`);
|
|
11005
|
+
}
|
|
11006
|
+
}
|
|
11007
|
+
return writeJson(res, 200, { ok: true });
|
|
11008
|
+
}
|
|
11009
|
+
|
|
11010
|
+
if (eventType === "PostToolUse" || eventType === "PostToolUseFailure") {
|
|
11011
|
+
// The tool actually ran on PC — resolve any pending native approval
|
|
11012
|
+
// for this session/tool as accepted (so iPhone moves it to completed).
|
|
11013
|
+
const threadId = String(body.threadId || body.sessionId || "");
|
|
11014
|
+
const toolName = String(body.toolName || "");
|
|
11015
|
+
const fingerprint = claudeToolFingerprint(toolName, body.toolInput);
|
|
11016
|
+
console.log(`[claude-post-tool] threadId=${threadId} tool=${toolName} fp=${fingerprint.slice(0, 80)} pendingTotal=${runtime.nativeApprovalsByToken.size}`);
|
|
11017
|
+
const match = findClaudePendingApprovalForTool(runtime, threadId, toolName, fingerprint);
|
|
11018
|
+
console.log(`[claude-post-tool] match=${match ? match.requestKey : "none"}`);
|
|
11019
|
+
if (match) {
|
|
11020
|
+
try {
|
|
11021
|
+
await handleNativeApprovalDecision({ config, runtime, state, approval: match, decision: "accept" });
|
|
11022
|
+
} catch (error) {
|
|
11023
|
+
console.error(`[claude-post-tool-resolve-error] ${match.requestKey} | ${error.message}`);
|
|
11024
|
+
}
|
|
11025
|
+
}
|
|
11026
|
+
return writeJson(res, 200, {});
|
|
11027
|
+
}
|
|
11028
|
+
|
|
11029
|
+
if (eventType !== "approval_request") {
|
|
11030
|
+
// Non-approval events (Notification, Stop, etc.) — acknowledge immediately
|
|
11031
|
+
return writeJson(res, 200, {});
|
|
11032
|
+
}
|
|
11033
|
+
|
|
11034
|
+
// Approval request — register in shared map so UI can display it,
|
|
11035
|
+
// then long-poll until user decides (or 10 min timeout)
|
|
11036
|
+
const token = crypto.randomBytes(18).toString("hex");
|
|
11037
|
+
const threadId = String(body.threadId || "");
|
|
11038
|
+
const requestId = String(body.requestId || crypto.randomUUID());
|
|
11039
|
+
const requestKey = `${threadId}:${requestId}`;
|
|
11040
|
+
|
|
11041
|
+
const approval = {
|
|
11042
|
+
token,
|
|
11043
|
+
requestKey,
|
|
11044
|
+
conversationId: threadId,
|
|
11045
|
+
requestId,
|
|
11046
|
+
ownerClientId: null,
|
|
11047
|
+
kind: String(body.approvalKind || "command"),
|
|
11048
|
+
threadLabel:
|
|
11049
|
+
runtime.claudeSessionTitles.get(threadId) ||
|
|
11050
|
+
(body.cwd ? path.basename(String(body.cwd)) : "") ||
|
|
11051
|
+
String(body.threadId || "").slice(0, 40),
|
|
11052
|
+
title: config.approvalTitle,
|
|
11053
|
+
messageText: String(body.messageText || ""),
|
|
11054
|
+
fileRefs: Array.isArray(body.fileRefs) ? body.fileRefs : [],
|
|
11055
|
+
previousFileRefs: [],
|
|
11056
|
+
diffText: String(body.diffText || ""),
|
|
11057
|
+
diffAvailable: Boolean(body.diffAvailable),
|
|
11058
|
+
diffSource: String(body.diffSource || "claude_permission_request"),
|
|
11059
|
+
diffAddedLines: Math.max(0, Number(body.diffAddedLines) || 0),
|
|
11060
|
+
diffRemovedLines: Math.max(0, Number(body.diffRemovedLines) || 0),
|
|
11061
|
+
createdAtMs: Number(body.createdAtMs) || Date.now(),
|
|
11062
|
+
resolved: false,
|
|
11063
|
+
resolving: false,
|
|
11064
|
+
resolveClaudeWaiter: null,
|
|
11065
|
+
claudeToolName: String(body.toolName || ""),
|
|
11066
|
+
claudeToolFingerprint: claudeToolFingerprint(String(body.toolName || ""), body.toolInput),
|
|
11067
|
+
provider: "claude",
|
|
11068
|
+
planText: String(body.planText || ""),
|
|
11069
|
+
questions: Array.isArray(body.questions) ? body.questions : [],
|
|
11070
|
+
notifyOnly: body.notifyOnly === true,
|
|
11071
|
+
readOnly: body.notifyOnly === true,
|
|
11072
|
+
};
|
|
11073
|
+
|
|
11074
|
+
runtime.nativeApprovalsByToken.set(token, approval);
|
|
11075
|
+
runtime.nativeApprovalsByRequestKey.set(requestKey, approval);
|
|
11076
|
+
|
|
11077
|
+
deliverWebPushItem({
|
|
11078
|
+
config,
|
|
11079
|
+
state,
|
|
11080
|
+
kind: "approval",
|
|
11081
|
+
token: approval.token,
|
|
11082
|
+
stableId: pendingApprovalStableId(approval),
|
|
11083
|
+
title: approval.title,
|
|
11084
|
+
body: approval.messageText,
|
|
11085
|
+
buildLocalizedContent: ({ locale }) => ({
|
|
11086
|
+
title: formatLocalizedTitle(locale, "server.title.approval", approval.threadLabel),
|
|
11087
|
+
body: approval.messageText,
|
|
11088
|
+
}),
|
|
11089
|
+
}).catch(() => {});
|
|
11090
|
+
|
|
11091
|
+
// Notify-only path: phone gets a read-only entry + push, but the
|
|
11092
|
+
// hook does not block on a decision. The PostToolUse handler will
|
|
11093
|
+
// clean it up once the desktop user finishes the tool call.
|
|
11094
|
+
if (approval.notifyOnly) {
|
|
11095
|
+
return writeJson(res, 200, { ok: true, notifyOnly: true });
|
|
11096
|
+
}
|
|
11097
|
+
|
|
11098
|
+
const CLAUDE_APPROVAL_WAIT_MS = 600_000; // 10 min
|
|
11099
|
+
|
|
11100
|
+
const decisionPromise = new Promise((resolve) => {
|
|
11101
|
+
approval.resolveClaudeWaiter = resolve;
|
|
11102
|
+
});
|
|
11103
|
+
|
|
11104
|
+
const timeoutPromise = new Promise((resolve) => {
|
|
11105
|
+
setTimeout(() => resolve("deny"), CLAUDE_APPROVAL_WAIT_MS);
|
|
11106
|
+
});
|
|
11107
|
+
|
|
11108
|
+
const decision = await Promise.race([decisionPromise, timeoutPromise]);
|
|
11109
|
+
|
|
11110
|
+
// Clean up if timed out (waiter was never called)
|
|
11111
|
+
if (!approval.resolved) {
|
|
11112
|
+
approval.resolved = true;
|
|
11113
|
+
approval.resolving = false;
|
|
11114
|
+
runtime.nativeApprovalsByToken.delete(token);
|
|
11115
|
+
runtime.nativeApprovalsByRequestKey.delete(requestKey);
|
|
11116
|
+
}
|
|
11117
|
+
|
|
11118
|
+
// The waiter may resolve with either a string ("accept" / "reject" /
|
|
11119
|
+
// "deny") or an object { permissionDecision, permissionDecisionReason }.
|
|
11120
|
+
// The latter is used by the AskUserQuestion answer flow so the user's
|
|
11121
|
+
// chosen text is fed back to Claude.
|
|
11122
|
+
if (decision && typeof decision === "object") {
|
|
11123
|
+
const responseBody = {
|
|
11124
|
+
permissionDecision:
|
|
11125
|
+
decision.permissionDecision === "allow" ? "allow" : "deny",
|
|
11126
|
+
};
|
|
11127
|
+
if (typeof decision.permissionDecisionReason === "string" && decision.permissionDecisionReason) {
|
|
11128
|
+
responseBody.permissionDecisionReason = decision.permissionDecisionReason;
|
|
11129
|
+
}
|
|
11130
|
+
return writeJson(res, 200, responseBody);
|
|
11131
|
+
}
|
|
11132
|
+
|
|
11133
|
+
return writeJson(res, 200, {
|
|
11134
|
+
permissionDecision: decision === "accept" ? "allow" : "deny",
|
|
11135
|
+
});
|
|
11136
|
+
}
|
|
11137
|
+
|
|
10518
11138
|
if (url.pathname === "/api/inbox" && req.method === "GET") {
|
|
10519
11139
|
const session = requireApiSession(req, res, config, state);
|
|
10520
11140
|
if (!session) {
|
|
@@ -10658,6 +11278,9 @@ function createNativeApprovalServer({ config, runtime, state }) {
|
|
|
10658
11278
|
approval.resolving = true;
|
|
10659
11279
|
try {
|
|
10660
11280
|
await handleNativeApprovalDecision({ config, runtime, state, approval, decision });
|
|
11281
|
+
if (approval.provider === "claude") {
|
|
11282
|
+
activateClaudeDesktopIfMac(req);
|
|
11283
|
+
}
|
|
10661
11284
|
return writeJson(res, 200, { ok: true, decision });
|
|
10662
11285
|
} catch (error) {
|
|
10663
11286
|
approval.resolving = false;
|
|
@@ -10665,6 +11288,85 @@ function createNativeApprovalServer({ config, runtime, state }) {
|
|
|
10665
11288
|
}
|
|
10666
11289
|
}
|
|
10667
11290
|
|
|
11291
|
+
const apiQuestionAnswerMatch = url.pathname.match(/^\/api\/items\/question\/([^/]+)\/answer$/u);
|
|
11292
|
+
if (apiQuestionAnswerMatch && req.method === "POST") {
|
|
11293
|
+
const session = requireMutatingApiSession(req, res, config, state);
|
|
11294
|
+
if (!session) {
|
|
11295
|
+
return;
|
|
11296
|
+
}
|
|
11297
|
+
|
|
11298
|
+
const token = decodeURIComponent(apiQuestionAnswerMatch[1]);
|
|
11299
|
+
const approval = runtime.nativeApprovalsByToken.get(token);
|
|
11300
|
+
if (!approval) {
|
|
11301
|
+
return writeJson(res, 404, { error: "question-not-found" });
|
|
11302
|
+
}
|
|
11303
|
+
if (approval.kind !== "question") {
|
|
11304
|
+
return writeJson(res, 409, { error: "not-a-question" });
|
|
11305
|
+
}
|
|
11306
|
+
if (approval.resolved || approval.resolving) {
|
|
11307
|
+
return writeJson(res, 409, { error: "question-already-handled" });
|
|
11308
|
+
}
|
|
11309
|
+
|
|
11310
|
+
let payload;
|
|
11311
|
+
try {
|
|
11312
|
+
payload = await parseJsonBody(req);
|
|
11313
|
+
} catch {
|
|
11314
|
+
return writeJson(res, 400, { error: "invalid-json-body" });
|
|
11315
|
+
}
|
|
11316
|
+
|
|
11317
|
+
const answers = Array.isArray(payload?.answers) ? payload.answers : [];
|
|
11318
|
+
const reasonText = formatClaudeQuestionAnswers(approval.questions, answers);
|
|
11319
|
+
if (!reasonText) {
|
|
11320
|
+
return writeJson(res, 400, { error: "no-answers" });
|
|
11321
|
+
}
|
|
11322
|
+
|
|
11323
|
+
approval.resolving = true;
|
|
11324
|
+
try {
|
|
11325
|
+
if (approval.resolveClaudeWaiter) {
|
|
11326
|
+
approval.resolveClaudeWaiter({
|
|
11327
|
+
permissionDecision: "deny",
|
|
11328
|
+
permissionDecisionReason: reasonText,
|
|
11329
|
+
});
|
|
11330
|
+
}
|
|
11331
|
+
approval.resolved = true;
|
|
11332
|
+
approval.resolving = false;
|
|
11333
|
+
runtime.nativeApprovalsByToken.delete(approval.token);
|
|
11334
|
+
runtime.nativeApprovalsByRequestKey.delete(approval.requestKey);
|
|
11335
|
+
|
|
11336
|
+
const stateChanged = recordActionHistoryItem({
|
|
11337
|
+
config,
|
|
11338
|
+
runtime,
|
|
11339
|
+
state,
|
|
11340
|
+
kind: "approval",
|
|
11341
|
+
stableId: `approval:${approval.requestKey}:${Date.now()}`,
|
|
11342
|
+
token: approval.token,
|
|
11343
|
+
title: approval.title,
|
|
11344
|
+
threadLabel: approval.threadLabel || "",
|
|
11345
|
+
messageText: reasonText,
|
|
11346
|
+
summary: t(config.defaultLocale, "server.message.questionAnswered", {
|
|
11347
|
+
provider: providerDisplayName(config.defaultLocale, approval.provider),
|
|
11348
|
+
}),
|
|
11349
|
+
fileRefs: [],
|
|
11350
|
+
diffText: "",
|
|
11351
|
+
diffSource: "",
|
|
11352
|
+
diffAvailable: false,
|
|
11353
|
+
diffAddedLines: 0,
|
|
11354
|
+
diffRemovedLines: 0,
|
|
11355
|
+
outcome: "answered",
|
|
11356
|
+
provider: approval.provider || "claude",
|
|
11357
|
+
});
|
|
11358
|
+
if (stateChanged) {
|
|
11359
|
+
await saveState(config.stateFile, state);
|
|
11360
|
+
}
|
|
11361
|
+
console.log(`[claude-question-answer] ${approval.requestKey}`);
|
|
11362
|
+
activateClaudeDesktopIfMac(req);
|
|
11363
|
+
return writeJson(res, 200, { ok: true });
|
|
11364
|
+
} catch (error) {
|
|
11365
|
+
approval.resolving = false;
|
|
11366
|
+
return writeJson(res, 500, { error: error.message });
|
|
11367
|
+
}
|
|
11368
|
+
}
|
|
11369
|
+
|
|
10668
11370
|
const apiPlanDecisionMatch = url.pathname.match(/^\/api\/items\/plan\/([^/]+)\/(implement|decline)$/u);
|
|
10669
11371
|
if (apiPlanDecisionMatch && req.method === "POST") {
|
|
10670
11372
|
const session = requireMutatingApiSession(req, res, config, state);
|
|
@@ -10942,7 +11644,7 @@ function createNativeApprovalServer({ config, runtime, state }) {
|
|
|
10942
11644
|
title: userInputRequest.title,
|
|
10943
11645
|
body: userInputRequest.testRequest
|
|
10944
11646
|
? `${t(config.defaultLocale, "server.message.choiceSubmittedTest")}\n\n${formatSubmittedTestAnswers(userInputRequest, userInputRequest.draftAnswers)}`
|
|
10945
|
-
: t(config.defaultLocale, "server.message.choiceSubmitted"),
|
|
11647
|
+
: t(config.defaultLocale, "server.message.choiceSubmitted", { provider: providerDisplayName(config.defaultLocale, "codex") }),
|
|
10946
11648
|
tone: "ok",
|
|
10947
11649
|
})
|
|
10948
11650
|
);
|
|
@@ -11455,6 +12157,28 @@ function writeJson(res, statusCode, body) {
|
|
|
11455
12157
|
res.end(`${JSON.stringify(body)}\n`);
|
|
11456
12158
|
}
|
|
11457
12159
|
|
|
12160
|
+
// Return focus to the Claude Desktop app when a Mac client just submitted a
|
|
12161
|
+
// plan / question / approval decision, so the user can resume in Claude
|
|
12162
|
+
// without manually switching apps. iOS / Android paired devices are skipped
|
|
12163
|
+
// (UA match). If Claude is not running, we do nothing — the AppleScript
|
|
12164
|
+
// `exists (process "Claude")` guard prevents an unwanted launch.
|
|
12165
|
+
function activateClaudeDesktopIfMac(req) {
|
|
12166
|
+
try {
|
|
12167
|
+
const ua = String(req?.headers?.["user-agent"] || "");
|
|
12168
|
+
if (/iPhone|iPad|iPod|Android/iu.test(ua)) return;
|
|
12169
|
+
if (!/Macintosh|Mac OS X/iu.test(ua)) return;
|
|
12170
|
+
const script = `tell application "System Events"
|
|
12171
|
+
if exists (processes where name is "Claude") then
|
|
12172
|
+
tell application "Claude" to activate
|
|
12173
|
+
end if
|
|
12174
|
+
end tell`;
|
|
12175
|
+
const child = spawn("osascript", ["-e", script], { stdio: "ignore" });
|
|
12176
|
+
child.unref();
|
|
12177
|
+
} catch {
|
|
12178
|
+
// Best effort.
|
|
12179
|
+
}
|
|
12180
|
+
}
|
|
12181
|
+
|
|
11458
12182
|
function writeHtml(res, statusCode, html) {
|
|
11459
12183
|
res.statusCode = statusCode;
|
|
11460
12184
|
res.setHeader("Content-Type", "text/html; charset=utf-8");
|
|
@@ -11477,7 +12201,7 @@ function stopHttpServer(server) {
|
|
|
11477
12201
|
});
|
|
11478
12202
|
}
|
|
11479
12203
|
|
|
11480
|
-
function renderApprovalPage({ eyebrow, title, messageText, token, basePath, actions, locale = DEFAULT_LOCALE }) {
|
|
12204
|
+
function renderApprovalPage({ eyebrow, title, messageText, token, basePath, actions, locale = DEFAULT_LOCALE, provider = "codex" }) {
|
|
11481
12205
|
const messageHtml = renderMessageHtml(messageText, `<p>${escapeHtml(t(locale, "detail.approvalRequested"))}</p>`);
|
|
11482
12206
|
const actionsHtml = actions
|
|
11483
12207
|
.map(
|
|
@@ -11582,7 +12306,7 @@ function renderApprovalPage({ eyebrow, title, messageText, token, basePath, acti
|
|
|
11582
12306
|
<div class="message">
|
|
11583
12307
|
${messageHtml}
|
|
11584
12308
|
</div>
|
|
11585
|
-
<p class="hint">${escapeHtml(t(locale, "summary.approval"))}</p>
|
|
12309
|
+
<p class="hint">${escapeHtml(t(locale, "summary.approval", { provider: providerDisplayName(locale, provider) }))}</p>
|
|
11586
12310
|
<div class="actions">
|
|
11587
12311
|
${actionsHtml}
|
|
11588
12312
|
<a class="button link" href="#" onclick="history.back(); return false;">${escapeHtml(t(locale, "common.back"))}</a>
|
|
@@ -11965,7 +12689,7 @@ function renderMessagePage({ eyebrow, title, messageText, locale = DEFAULT_LOCAL
|
|
|
11965
12689
|
</html>`;
|
|
11966
12690
|
}
|
|
11967
12691
|
|
|
11968
|
-
function renderStatusPage({ title, body, tone }) {
|
|
12692
|
+
function renderStatusPage({ title, body, tone, provider = "codex", locale = DEFAULT_LOCALE }) {
|
|
11969
12693
|
return `<!doctype html>
|
|
11970
12694
|
<html lang="en">
|
|
11971
12695
|
<head>
|
|
@@ -12028,7 +12752,7 @@ function renderStatusPage({ title, body, tone }) {
|
|
|
12028
12752
|
</head>
|
|
12029
12753
|
<body>
|
|
12030
12754
|
<section class="card">
|
|
12031
|
-
<span class="chip"
|
|
12755
|
+
<span class="chip">${escapeHtml(providerDisplayName(locale, provider))}</span>
|
|
12032
12756
|
<h1>${escapeHtml(title)}</h1>
|
|
12033
12757
|
<p>${escapeHtml(body)}</p>
|
|
12034
12758
|
<p class="muted">このページは閉じて大丈夫です。</p>
|
|
@@ -12115,16 +12839,18 @@ function markdownMessageStyles() {
|
|
|
12115
12839
|
}`;
|
|
12116
12840
|
}
|
|
12117
12841
|
|
|
12118
|
-
function approvalDecisionMessage(decision, locale = config?.defaultLocale || DEFAULT_LOCALE) {
|
|
12842
|
+
function approvalDecisionMessage(decision, locale = config?.defaultLocale || DEFAULT_LOCALE, provider = "codex") {
|
|
12843
|
+
const vars = { provider: providerDisplayName(locale, provider) };
|
|
12119
12844
|
if (decision === "accept") {
|
|
12120
|
-
return t(locale, "server.message.approvalAccepted");
|
|
12845
|
+
return t(locale, "server.message.approvalAccepted", vars);
|
|
12121
12846
|
}
|
|
12122
|
-
return t(locale, "server.message.approvalRejected");
|
|
12847
|
+
return t(locale, "server.message.approvalRejected", vars);
|
|
12123
12848
|
}
|
|
12124
12849
|
|
|
12125
|
-
function planDecisionMessage(decision, locale = config?.defaultLocale || DEFAULT_LOCALE) {
|
|
12850
|
+
function planDecisionMessage(decision, locale = config?.defaultLocale || DEFAULT_LOCALE, provider = "codex") {
|
|
12851
|
+
const vars = { provider: providerDisplayName(locale, provider) };
|
|
12126
12852
|
if (decision === "implement") {
|
|
12127
|
-
return t(locale, "server.message.planImplemented");
|
|
12853
|
+
return t(locale, "server.message.planImplemented", vars);
|
|
12128
12854
|
}
|
|
12129
12855
|
return t(locale, "server.message.planDismissed");
|
|
12130
12856
|
}
|
|
@@ -12140,14 +12866,15 @@ function decisionLabel(decision, locale = config?.defaultLocale || DEFAULT_LOCAL
|
|
|
12140
12866
|
return decision === "accept" ? t(locale, "server.action.approve") : t(locale, "server.action.reject");
|
|
12141
12867
|
}
|
|
12142
12868
|
|
|
12143
|
-
function approvalDecisionConfirm(decision, locale = config?.defaultLocale || DEFAULT_LOCALE) {
|
|
12869
|
+
function approvalDecisionConfirm(decision, locale = config?.defaultLocale || DEFAULT_LOCALE, provider = "codex") {
|
|
12870
|
+
const vars = { provider: providerDisplayName(locale, provider) };
|
|
12144
12871
|
if (decision === "implement") {
|
|
12145
12872
|
return t(locale, "server.confirm.planImplement");
|
|
12146
12873
|
}
|
|
12147
12874
|
if (decision === "accept") {
|
|
12148
|
-
return t(locale, "server.confirm.approve");
|
|
12875
|
+
return t(locale, "server.confirm.approve", vars);
|
|
12149
12876
|
}
|
|
12150
|
-
return t(locale, "server.confirm.reject");
|
|
12877
|
+
return t(locale, "server.confirm.reject", vars);
|
|
12151
12878
|
}
|
|
12152
12879
|
|
|
12153
12880
|
function resolvePlanDecisionAnswer(planRequest, decision) {
|
|
@@ -12406,6 +13133,7 @@ function buildConfig(cli) {
|
|
|
12406
13133
|
pairingToken: process.env.PAIRING_TOKEN || "",
|
|
12407
13134
|
pairingExpiresAtMs: numberEnv("PAIRING_EXPIRES_AT_MS", 0),
|
|
12408
13135
|
sessionSecret: process.env.SESSION_SECRET || "",
|
|
13136
|
+
claudeProjectsDir: resolvePath(process.env.CLAUDE_PROJECTS_DIR || path.join(os.homedir(), ".claude", "projects")),
|
|
12409
13137
|
};
|
|
12410
13138
|
}
|
|
12411
13139
|
|
|
@@ -12588,6 +13316,7 @@ async function loadState(stateFile) {
|
|
|
12588
13316
|
historyFileSourceFile: cleanText(parsed.historyFileSourceFile ?? ""),
|
|
12589
13317
|
pairingConsumedAt: Number(parsed.pairingConsumedAt) || 0,
|
|
12590
13318
|
pairingConsumedCredential: cleanText(parsed.pairingConsumedCredential ?? ""),
|
|
13319
|
+
claudeAwayMode: parsed.claudeAwayMode === true,
|
|
12591
13320
|
};
|
|
12592
13321
|
} catch {
|
|
12593
13322
|
return {
|
|
@@ -12614,10 +13343,30 @@ async function loadState(stateFile) {
|
|
|
12614
13343
|
historyFileSourceFile: "",
|
|
12615
13344
|
pairingConsumedAt: 0,
|
|
12616
13345
|
pairingConsumedCredential: "",
|
|
13346
|
+
claudeAwayMode: false,
|
|
12617
13347
|
};
|
|
12618
13348
|
}
|
|
12619
13349
|
}
|
|
12620
13350
|
|
|
13351
|
+
function claudeAwayModeSentinelPath(config) {
|
|
13352
|
+
const dir = config?.configDir || path.join(os.homedir(), ".viveworker");
|
|
13353
|
+
return path.join(dir, "claude-away-mode");
|
|
13354
|
+
}
|
|
13355
|
+
|
|
13356
|
+
async function syncClaudeAwayModeSentinel(config, enabled) {
|
|
13357
|
+
const sentinel = claudeAwayModeSentinelPath(config);
|
|
13358
|
+
try {
|
|
13359
|
+
if (enabled) {
|
|
13360
|
+
await fs.mkdir(path.dirname(sentinel), { recursive: true });
|
|
13361
|
+
await fs.writeFile(sentinel, "on\n", "utf8");
|
|
13362
|
+
} else {
|
|
13363
|
+
await fs.unlink(sentinel).catch(() => {});
|
|
13364
|
+
}
|
|
13365
|
+
} catch (error) {
|
|
13366
|
+
console.error(`[claude-away-mode-sentinel-error] ${error.message}`);
|
|
13367
|
+
}
|
|
13368
|
+
}
|
|
13369
|
+
|
|
12621
13370
|
async function saveState(stateFile, state) {
|
|
12622
13371
|
const output = JSON.stringify(state, null, 2);
|
|
12623
13372
|
await fs.mkdir(path.dirname(stateFile), { recursive: true });
|
|
@@ -13016,7 +13765,7 @@ function getThreadName(sessionIndex, rolloutThreadLabels, threadId, cwd, filePat
|
|
|
13016
13765
|
}
|
|
13017
13766
|
|
|
13018
13767
|
function describeContext({ threadName }) {
|
|
13019
|
-
const threadLabel = truncate(cleanText(threadName || ""), 90) || "
|
|
13768
|
+
const threadLabel = truncate(cleanText(threadName || ""), 90) || t(DEFAULT_LOCALE, "server.fallback.codexTask", { provider: providerDisplayName(DEFAULT_LOCALE, "codex") });
|
|
13020
13769
|
return { threadLabel };
|
|
13021
13770
|
}
|
|
13022
13771
|
|
|
@@ -13099,10 +13848,11 @@ function refreshResolvedThreadLabels({ config, runtime, state }) {
|
|
|
13099
13848
|
if (!threadId) {
|
|
13100
13849
|
return entry;
|
|
13101
13850
|
}
|
|
13102
|
-
const
|
|
13851
|
+
const claudeTitle = runtime.claudeSessionTitles.get(threadId) || "";
|
|
13852
|
+
const nativeThreadLabel = claudeTitle || getNativeThreadLabel({
|
|
13103
13853
|
runtime,
|
|
13104
13854
|
conversationId: threadId,
|
|
13105
|
-
cwd: "",
|
|
13855
|
+
cwd: cleanText(entry.cwd || ""),
|
|
13106
13856
|
});
|
|
13107
13857
|
const threadLabel = preferTitleOnlyJsonThreadLabel(nativeThreadLabel, threadId, entry.messageText, entry.summary);
|
|
13108
13858
|
const title = threadLabel || kindTitle(config.defaultLocale, entry.kind);
|
|
@@ -13134,10 +13884,11 @@ function refreshResolvedThreadLabels({ config, runtime, state }) {
|
|
|
13134
13884
|
if (!threadId) {
|
|
13135
13885
|
return entry;
|
|
13136
13886
|
}
|
|
13137
|
-
const
|
|
13887
|
+
const claudeTitle = runtime.claudeSessionTitles.get(threadId) || "";
|
|
13888
|
+
const nativeThreadLabel = claudeTitle || getNativeThreadLabel({
|
|
13138
13889
|
runtime,
|
|
13139
13890
|
conversationId: threadId,
|
|
13140
|
-
cwd: "",
|
|
13891
|
+
cwd: cleanText(entry.cwd || ""),
|
|
13141
13892
|
});
|
|
13142
13893
|
const threadLabel = preferTitleOnlyJsonThreadLabel(nativeThreadLabel, threadId, entry.messageText, entry.summary);
|
|
13143
13894
|
const title = threadLabel || kindTitle(config.defaultLocale, entry.kind);
|
|
@@ -13545,7 +14296,7 @@ async function main() {
|
|
|
13545
14296
|
|
|
13546
14297
|
console.log(
|
|
13547
14298
|
[
|
|
13548
|
-
"
|
|
14299
|
+
"viveworker bridge",
|
|
13549
14300
|
`dryRun=${config.dryRun}`,
|
|
13550
14301
|
`once=${config.once}`,
|
|
13551
14302
|
`codexHome=${config.codexHome}`,
|