relay-companion 0.1.21 → 0.1.22
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 +86 -63
package/package.json
CHANGED
package/src/codex-desktop.js
CHANGED
|
@@ -78,87 +78,110 @@ export async function notifyCodexDesktopThreads({
|
|
|
78
78
|
return { attempted: true, ok: results.some((result) => result.ok), results };
|
|
79
79
|
}
|
|
80
80
|
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
81
|
+
// Runs INSIDE each Codex renderer: it is serialized to a string via toString()
|
|
82
|
+
// and evaluated in the window's page context, so it must be self-contained — no
|
|
83
|
+
// imports, only renderer globals (window, location). Exported at module scope so
|
|
84
|
+
// the thread-routing decision can be unit-tested directly.
|
|
85
|
+
export async function relayRefreshCodexRenderer(payload) {
|
|
86
|
+
const hostId = "local";
|
|
87
|
+
const bridge = window.electronBridge;
|
|
88
|
+
if (!bridge?.sendMessageFromView) return { ok: false, reason: "missing-electron-bridge" };
|
|
86
89
|
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
}
|
|
90
|
+
const sent = [];
|
|
91
|
+
async function send(message) {
|
|
92
|
+
try {
|
|
93
|
+
await bridge.sendMessageFromView(message);
|
|
94
|
+
sent.push({ type: message.type, ok: true });
|
|
95
|
+
} catch (error) {
|
|
96
|
+
sent.push({ type: message.type, ok: false, error: String(error?.message || error) });
|
|
95
97
|
}
|
|
98
|
+
}
|
|
96
99
|
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
100
|
+
if (Array.isArray(payload.pinnedThreadIds)) {
|
|
101
|
+
await send({ type: "set-pinned-thread-ids-for-host", hostId, threadIds: payload.pinnedThreadIds });
|
|
102
|
+
}
|
|
103
|
+
if (payload.threadIds.length) {
|
|
104
|
+
await send({ type: "hydrate-pinned-threads", hostId, threadIds: payload.threadIds });
|
|
105
|
+
}
|
|
106
|
+
await send({ type: "refresh-recent-conversations-for-host", hostId });
|
|
107
|
+
for (const threadId of payload.threadIds) {
|
|
108
|
+
// Codex Desktop (>=0.142) serves thread views from its in-memory core and
|
|
109
|
+
// never cold-loads externally created threads — the view spins forever
|
|
110
|
+
// until an app restart. Adopt the thread the way the app itself opens one:
|
|
111
|
+
// drop any stale cache entry, ask the tail-hydration path to load history
|
|
112
|
+
// (feature-gated; harmless when off — dependentConversationIds is REQUIRED
|
|
113
|
+
// or the handler throws), then maybe-resume-conversation, which activates
|
|
114
|
+
// the thread summary and resumes the conversation from the rollout on disk.
|
|
115
|
+
await send({ type: "discard-conversation-from-cache", conversationId: threadId });
|
|
116
|
+
await send({ type: "ensure-conversation-history-loaded", conversationId: threadId, dependentConversationIds: [] });
|
|
117
|
+
await send({
|
|
118
|
+
type: "maybe-resume-conversation",
|
|
119
|
+
hostId,
|
|
120
|
+
conversationId: threadId,
|
|
121
|
+
model: null,
|
|
122
|
+
serviceTier: null,
|
|
123
|
+
reasoningEffort: null,
|
|
124
|
+
workspaceRoots: [],
|
|
125
|
+
collaborationMode: null,
|
|
126
|
+
});
|
|
127
|
+
await send({ type: "broadcast-conversation-snapshot-for-host", hostId, conversationId: threadId });
|
|
128
|
+
}
|
|
129
|
+
if (payload.openThreadId) {
|
|
130
|
+
try {
|
|
131
|
+
const encoded = encodeURIComponent(String(payload.openThreadId));
|
|
132
|
+
// Codex renders a thread through two separate router surfaces: the main
|
|
133
|
+
// window (sidebar + recents rail) at /local/<id>, and the borderless hotkey
|
|
134
|
+
// "quick chat" overlay at /hotkey-window/thread/<id>. The app's own
|
|
135
|
+
// navigation chooses between them by whether the current window is the
|
|
136
|
+
// hotkey one (its QH() keys off location.pathname). Mirror that here so the
|
|
137
|
+
// thread opens in whichever surface the user is actually in — otherwise
|
|
138
|
+
// every open is forced into the overlay even when the main window is up.
|
|
139
|
+
const path = location.pathname.startsWith("/hotkey-window")
|
|
140
|
+
? `/hotkey-window/thread/${encoded}`
|
|
141
|
+
: `/local/${encoded}`;
|
|
142
|
+
window.postMessage({ type: "navigate-to-route", path }, location.origin);
|
|
143
|
+
sent.push({ type: "navigate-to-route", ok: true, threadId: payload.openThreadId, path });
|
|
144
|
+
} catch (error) {
|
|
145
|
+
sent.push({ type: "navigate-to-route", ok: false, error: String(error?.message || error) });
|
|
139
146
|
}
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
return { ok: sent.some((item) => item.ok), href: location.href, openThreadId: payload.openThreadId || null, sent };
|
|
150
|
+
}
|
|
140
151
|
|
|
141
|
-
|
|
142
|
-
|
|
152
|
+
export function buildCodexDesktopRefreshExpression({ threadIds = [], pinnedThreadIds = null, openThreadId = null } = {}) {
|
|
153
|
+
const rendererCode = `(${relayRefreshCodexRenderer.toString()})(${JSON.stringify({
|
|
143
154
|
threadIds: uniqueStrings(threadIds),
|
|
144
155
|
pinnedThreadIds,
|
|
145
156
|
openThreadId: String(openThreadId || "").trim() || null,
|
|
146
157
|
})})`;
|
|
158
|
+
// Only bring a window forward when we're actually opening a thread.
|
|
159
|
+
const wantsFocus = Boolean(String(openThreadId || "").trim());
|
|
147
160
|
|
|
148
161
|
return `(async () => {
|
|
149
162
|
const req = process.mainModule?.require || process.getBuiltinModule("module").createRequire(process.cwd() + "/");
|
|
150
163
|
const { BrowserWindow } = req("electron");
|
|
151
164
|
const code = ${JSON.stringify(rendererCode)};
|
|
165
|
+
const wantsFocus = ${JSON.stringify(wantsFocus)};
|
|
152
166
|
const windows = [];
|
|
153
167
|
for (const win of BrowserWindow.getAllWindows()) {
|
|
154
168
|
if (win.isDestroyed()) continue;
|
|
155
169
|
try {
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
170
|
+
const url = win.webContents.getURL();
|
|
171
|
+
const result = await win.webContents.executeJavaScript(code, true);
|
|
172
|
+
// Bring the main window forward after navigating it to the thread, the way
|
|
173
|
+
// Codex's own restoreShowAndFocusWindow does. The hotkey overlay manages
|
|
174
|
+
// its own visibility, so leave it alone and only surface the main window.
|
|
175
|
+
let focused = false;
|
|
176
|
+
if (wantsFocus && !String(url || "").includes("/hotkey-window")) {
|
|
177
|
+
try {
|
|
178
|
+
if (win.isMinimized()) win.restore();
|
|
179
|
+
win.show();
|
|
180
|
+
win.focus();
|
|
181
|
+
focused = true;
|
|
182
|
+
} catch {}
|
|
183
|
+
}
|
|
184
|
+
windows.push({ id: win.id, visible: win.isVisible(), url, focused, result });
|
|
162
185
|
} catch (error) {
|
|
163
186
|
windows.push({ id: win.id, ok: false, error: String(error?.message || error) });
|
|
164
187
|
}
|