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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "relay-companion",
3
- "version": "0.1.21",
3
+ "version": "0.1.22",
4
4
  "description": "Relay companion: connects local coding agents to Relay tasks, approvals, and connector tools.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -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
- export function buildCodexDesktopRefreshExpression({ threadIds = [], pinnedThreadIds = null, openThreadId = null } = {}) {
82
- const rendererCode = `(${async function relayRefreshCodexRenderer(payload) {
83
- const hostId = "local";
84
- const bridge = window.electronBridge;
85
- if (!bridge?.sendMessageFromView) return { ok: false, reason: "missing-electron-bridge" };
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
- const sent = [];
88
- async function send(message) {
89
- try {
90
- await bridge.sendMessageFromView(message);
91
- sent.push({ type: message.type, ok: true });
92
- } catch (error) {
93
- sent.push({ type: message.type, ok: false, error: String(error?.message || error) });
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
- if (Array.isArray(payload.pinnedThreadIds)) {
98
- await send({ type: "set-pinned-thread-ids-for-host", hostId, threadIds: payload.pinnedThreadIds });
99
- }
100
- if (payload.threadIds.length) {
101
- await send({ type: "hydrate-pinned-threads", hostId, threadIds: payload.threadIds });
102
- }
103
- await send({ type: "refresh-recent-conversations-for-host", hostId });
104
- for (const threadId of payload.threadIds) {
105
- // Codex Desktop (>=0.142) serves thread views from its in-memory core and
106
- // never cold-loads externally created threads — the view spins forever
107
- // until an app restart. Adopt the thread the way the app itself opens one:
108
- // drop any stale cache entry, ask the tail-hydration path to load history
109
- // (feature-gated; harmless when off — dependentConversationIds is REQUIRED
110
- // or the handler throws), then maybe-resume-conversation, which activates
111
- // the thread summary and resumes the conversation from the rollout on disk.
112
- await send({ type: "discard-conversation-from-cache", conversationId: threadId });
113
- await send({ type: "ensure-conversation-history-loaded", conversationId: threadId, dependentConversationIds: [] });
114
- await send({
115
- type: "maybe-resume-conversation",
116
- hostId,
117
- conversationId: threadId,
118
- model: null,
119
- serviceTier: null,
120
- reasoningEffort: null,
121
- workspaceRoots: [],
122
- collaborationMode: null,
123
- });
124
- await send({ type: "broadcast-conversation-snapshot-for-host", hostId, conversationId: threadId });
125
- }
126
- if (payload.openThreadId) {
127
- try {
128
- window.postMessage(
129
- {
130
- type: "navigate-to-route",
131
- path: `/hotkey-window/thread/${encodeURIComponent(String(payload.openThreadId))}`,
132
- },
133
- location.origin,
134
- );
135
- sent.push({ type: "navigate-to-route", ok: true, threadId: payload.openThreadId });
136
- } catch (error) {
137
- sent.push({ type: "navigate-to-route", ok: false, error: String(error?.message || error) });
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
- return { ok: sent.some((item) => item.ok), href: location.href, openThreadId: payload.openThreadId || null, sent };
142
- }.toString()})(${JSON.stringify({
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
- windows.push({
157
- id: win.id,
158
- visible: win.isVisible(),
159
- url: win.webContents.getURL(),
160
- result: await win.webContents.executeJavaScript(code, true),
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
  }