relay-companion 0.1.22 → 0.1.23
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/src/codex-desktop.js +49 -21
- package/src/materializer.js +11 -4
package/package.json
CHANGED
package/src/codex-desktop.js
CHANGED
|
@@ -46,7 +46,10 @@ export async function notifyCodexDesktopThreads({
|
|
|
46
46
|
threadIds = [],
|
|
47
47
|
pinnedThreadIds = null,
|
|
48
48
|
openThreadId = null,
|
|
49
|
-
|
|
49
|
+
primeOpen = true,
|
|
50
|
+
// The open path navigates hotkey -> (prime wait) -> /local inside the renderer,
|
|
51
|
+
// so the inspector eval must outlast RELAY_CODEX_OPEN_PRIME_MS; default higher.
|
|
52
|
+
timeoutMs = Number(process.env.RELAY_CODEX_DESKTOP_TIMEOUT_MS || 8000),
|
|
50
53
|
} = {}) {
|
|
51
54
|
if (process.env.RELAY_CODEX_DESKTOP_REFRESH === "0") return { attempted: false, reason: "disabled" };
|
|
52
55
|
if (process.platform !== "darwin") return { attempted: false, reason: "not-darwin" };
|
|
@@ -59,7 +62,7 @@ export async function notifyCodexDesktopThreads({
|
|
|
59
62
|
const pids = await findCodexMainPids();
|
|
60
63
|
if (!pids.length) return { attempted: true, ok: false, reason: "codex-not-running", results: [] };
|
|
61
64
|
|
|
62
|
-
const expression = buildCodexDesktopRefreshExpression({ threadIds: ids, pinnedThreadIds: pinnedIds, openThreadId: openId });
|
|
65
|
+
const expression = buildCodexDesktopRefreshExpression({ threadIds: ids, pinnedThreadIds: pinnedIds, openThreadId: openId, primeOpen });
|
|
63
66
|
const results = [];
|
|
64
67
|
for (const pid of pids) {
|
|
65
68
|
try {
|
|
@@ -88,9 +91,15 @@ export async function relayRefreshCodexRenderer(payload) {
|
|
|
88
91
|
if (!bridge?.sendMessageFromView) return { ok: false, reason: "missing-electron-bridge" };
|
|
89
92
|
|
|
90
93
|
const sent = [];
|
|
91
|
-
|
|
94
|
+
// Bound every bridge send. Some host messages (notably maybe-resume-conversation)
|
|
95
|
+
// can hang indefinitely; the original serial awaits then block the navigation
|
|
96
|
+
// that follows, leaving the thread unopened. Cap each send so the open proceeds.
|
|
97
|
+
async function send(message, timeoutMs = 2000) {
|
|
92
98
|
try {
|
|
93
|
-
await
|
|
99
|
+
await Promise.race([
|
|
100
|
+
bridge.sendMessageFromView(message),
|
|
101
|
+
new Promise((_, reject) => setTimeout(() => reject(new Error("send-timeout")), timeoutMs)),
|
|
102
|
+
]);
|
|
94
103
|
sent.push({ type: message.type, ok: true });
|
|
95
104
|
} catch (error) {
|
|
96
105
|
sent.push({ type: message.type, ok: false, error: String(error?.message || error) });
|
|
@@ -127,33 +136,52 @@ export async function relayRefreshCodexRenderer(payload) {
|
|
|
127
136
|
await send({ type: "broadcast-conversation-snapshot-for-host", hostId, conversationId: threadId });
|
|
128
137
|
}
|
|
129
138
|
if (payload.openThreadId) {
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
139
|
+
const encoded = encodeURIComponent(String(payload.openThreadId));
|
|
140
|
+
const navTo = (path) => {
|
|
141
|
+
try {
|
|
142
|
+
window.postMessage({ type: "navigate-to-route", path }, location.origin);
|
|
143
|
+
sent.push({ type: "navigate-to-route", ok: true, path });
|
|
144
|
+
} catch (error) {
|
|
145
|
+
sent.push({ type: "navigate-to-route", ok: false, path, error: String(error?.message || error) });
|
|
146
|
+
}
|
|
147
|
+
};
|
|
148
|
+
// Codex renders a thread through two router surfaces: the main window
|
|
149
|
+
// (sidebar + recents rail) at /local/<id>, and the borderless hotkey
|
|
150
|
+
// "quick chat" view at /hotkey-window/thread/<id>. Two facts drive the logic:
|
|
151
|
+
// 1. The app uses an in-memory router, so location.pathname is ALWAYS
|
|
152
|
+
// /index.html — it never reveals the current route or window kind.
|
|
153
|
+
// 2. The /local/<id> route CANNOT cold-load an externally-created relay
|
|
154
|
+
// thread: it renders the empty "New chat" state and never recovers.
|
|
155
|
+
// Only /hotkey-window/thread/<id> loads such a thread from disk into the
|
|
156
|
+
// renderer store. Once loaded, /local/<id> renders it fine.
|
|
157
|
+
// So to land the user in the main window WITH content, prime the load via the
|
|
158
|
+
// hotkey route, then switch to /local/<id> once it is warm. The hotkey view
|
|
159
|
+
// shows the thread during the prime, so the transition is content -> content,
|
|
160
|
+
// never a blank flash. The second materializer pass runs with primeOpen=false
|
|
161
|
+
// and just re-asserts /local (the thread is warm by then).
|
|
162
|
+
const inHotkeyWindow = /hotkey-window/.test(location.href) || /hotkey-window/.test(location.search || "");
|
|
163
|
+
const primeMs = Number.isFinite(payload.primeMs) ? payload.primeMs : 1200;
|
|
164
|
+
if (inHotkeyWindow) {
|
|
165
|
+
navTo(`/hotkey-window/thread/${encoded}`);
|
|
166
|
+
} else if (payload.primeOpen === false) {
|
|
167
|
+
navTo(`/local/${encoded}`);
|
|
168
|
+
} else {
|
|
169
|
+
navTo(`/hotkey-window/thread/${encoded}`);
|
|
170
|
+
await new Promise((resolve) => setTimeout(resolve, primeMs));
|
|
171
|
+
navTo(`/local/${encoded}`);
|
|
146
172
|
}
|
|
147
173
|
}
|
|
148
174
|
|
|
149
175
|
return { ok: sent.some((item) => item.ok), href: location.href, openThreadId: payload.openThreadId || null, sent };
|
|
150
176
|
}
|
|
151
177
|
|
|
152
|
-
export function buildCodexDesktopRefreshExpression({ threadIds = [], pinnedThreadIds = null, openThreadId = null } = {}) {
|
|
178
|
+
export function buildCodexDesktopRefreshExpression({ threadIds = [], pinnedThreadIds = null, openThreadId = null, primeOpen = true } = {}) {
|
|
153
179
|
const rendererCode = `(${relayRefreshCodexRenderer.toString()})(${JSON.stringify({
|
|
154
180
|
threadIds: uniqueStrings(threadIds),
|
|
155
181
|
pinnedThreadIds,
|
|
156
182
|
openThreadId: String(openThreadId || "").trim() || null,
|
|
183
|
+
primeOpen: primeOpen !== false,
|
|
184
|
+
primeMs: Number(process.env.RELAY_CODEX_OPEN_PRIME_MS) || 1200,
|
|
157
185
|
})})`;
|
|
158
186
|
// Only bring a window forward when we're actually opening a thread.
|
|
159
187
|
const wantsFocus = Boolean(String(openThreadId || "").trim());
|
package/src/materializer.js
CHANGED
|
@@ -257,13 +257,19 @@ async function materializeRowInHost({ id, row, rowState = {}, host = "claude", l
|
|
|
257
257
|
// works. The refresh therefore discards the stale cache entry and navigates
|
|
258
258
|
// through the bridge, which forces the view to reload the now-complete file.
|
|
259
259
|
// The codex:// deep link is only the fallback when the bridge is unreachable.
|
|
260
|
+
// First pass PRIMES the open: it navigates to /hotkey-window/thread/<id>
|
|
261
|
+
// (the only route that cold-loads an externally-created thread from disk),
|
|
262
|
+
// waits, then switches to /local/<id> so the thread shows in the main window
|
|
263
|
+
// with the sidebar. The /local route alone renders an empty "New chat" pane
|
|
264
|
+
// for a fresh relay thread and never recovers.
|
|
260
265
|
const desktopOpenResult = await refreshCodexDesktopForThreads([threadId], { force: true, openThreadId: threadId });
|
|
261
266
|
// The desktop occasionally re-caches the conversation between our discard
|
|
262
267
|
// and the view's load, so a single refresh still races on some first opens.
|
|
263
|
-
//
|
|
264
|
-
//
|
|
268
|
+
// The second pass runs with primeOpen=false — the thread is warm from the
|
|
269
|
+
// first pass, so a plain /local navigate re-asserts it deterministically
|
|
270
|
+
// without a second hotkey->local visual transition.
|
|
265
271
|
await sleep(Number(process.env.RELAY_CODEX_OPEN_SECOND_PASS_MS || 1200));
|
|
266
|
-
const secondPassResult = await refreshCodexDesktopForThreads([threadId], { force: true, openThreadId: threadId });
|
|
272
|
+
const secondPassResult = await refreshCodexDesktopForThreads([threadId], { force: true, openThreadId: threadId, primeOpen: false });
|
|
267
273
|
openedInHost = Boolean(desktopOpenResult?.ok || secondPassResult?.ok);
|
|
268
274
|
skipExternalOpen = openedInHost;
|
|
269
275
|
url = `codex://threads/${encodeURIComponent(threadId)}`;
|
|
@@ -393,7 +399,7 @@ function ensureRelayCodexIndexMarker({ threadId, sessionPath, packetId }) {
|
|
|
393
399
|
}
|
|
394
400
|
}
|
|
395
401
|
|
|
396
|
-
async function refreshCodexDesktopForThreads(threadIds, { force = false, openThreadId = null } = {}) {
|
|
402
|
+
async function refreshCodexDesktopForThreads(threadIds, { force = false, openThreadId = null, primeOpen = true } = {}) {
|
|
397
403
|
const uniqueThreadIds = Array.from(new Set(threadIds.filter(Boolean)));
|
|
398
404
|
if (!uniqueThreadIds.length && !force) return null;
|
|
399
405
|
try {
|
|
@@ -401,6 +407,7 @@ async function refreshCodexDesktopForThreads(threadIds, { force = false, openThr
|
|
|
401
407
|
threadIds: uniqueThreadIds,
|
|
402
408
|
pinnedThreadIds: readPinnedThreadIds(),
|
|
403
409
|
openThreadId,
|
|
410
|
+
primeOpen,
|
|
404
411
|
});
|
|
405
412
|
if (process.env.RELAY_DEBUG && result.attempted && !result.ok) {
|
|
406
413
|
console.error(`Relay Codex desktop refresh did not reach a running window: ${JSON.stringify(result)}`);
|