Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ Windows-only and Mac-only mode have no relay, no second computer dependency, and
## Features

- Six dynamic agent keys using the source and assignments selected in **Codex Settings > Codex Micro**.
- Optional macOS focus on local agent presses, so Codex receives subsequent keyboard shortcuts even when another app was active.
- Live idle, working, unread completion, approval/input, error, and empty states.
- Codex-aligned light and dark rendering with restrained status animation.
- Native key-down/key-up handling for Micro slots `ACT06` through `ACT12`.
Expand Down Expand Up @@ -94,6 +95,10 @@ The action names describe the default Codex Micro setup. The keys always follow

The page-navigation and profile-switch keys are built-in Stream Deck actions. All other named controls come from Codex Deck. Every official Codex Micro keycap is also exposed as a standalone action, so extra pages can be customized without changing the six synchronized Micro action slots.

### Optional macOS app focus

The **Agent Display** inspector includes a global **Focus Codex** setting for the six agent keys. It defaults to off and Stream Deck persists the choice for the plugin. On a local Mac, enabling it activates Codex before the agent action, so keyboard shortcuts such as approval target Codex rather than the previously active app. It never focuses the local app for an agent routed to a remote host.

### Usage and reset controls

![Usage limit, overview, and reset-credit controls](docs/assets/usage-controls-preview.svg)
Expand Down
4 changes: 4 additions & 0 deletions docs/MACOS.md
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,10 @@ Tailscale remote access remains a separate optional profile; see
**Start Codex Deck.command** is the double-clickable equivalent of `start`.
5. Open **Codex Settings > Codex Micro**, configure the native slots, and add the actions from the [recommended layout](../README.md#recommended-15-key-layout). Leave the Windows/Mac target position empty or replace it with another action.

### Focus Codex when opening an agent

Each agent key's **Agent Display** inspector has a global **Focus Codex** option. It is off by default, applies to all six agent keys, and is persisted by Stream Deck. When enabled, a local Mac agent press activates Codex before its native Micro event is sent, so approval and other keyboard shortcuts target Codex instead of the previously active app. Remote agents never activate the local app.

If an archive tool removed executable permissions, restore only the two launcher files:

```zsh
Expand Down
32 changes: 32 additions & 0 deletions docs/RELEASE_0.7.0.5.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
# Codex Deck v0.7.0.5

This focused macOS hotfix adds an optional, persistent way to bring Codex to
the foreground before dispatching a local agent press.

## Changes

- Adds a global **Focus Codex** option to the Agent Display property inspector.
- Clearly identifies the focus option as macOS-only while keeping it harmless
on Windows.
- Persists the option with Stream Deck so it survives app and computer
restarts and applies consistently to all six agent keys.
- Activates Codex through its macOS bundle identifier before sending a local
agent press, allowing subsequent keyboard shortcuts to target Codex.
- Leaves remote-agent routing and Windows behavior unchanged.
- Prevents the property inspector from overwriting unrelated global settings
while its initial settings are loading.

## Downloads

- Stream Deck: `com.simeo.codex-deck.streamDeckPlugin`
- Windows launcher: `codex-deck-launcher-windows-v0.7.0.5.zip`
- macOS launcher: `codex-deck-launcher-macos-v0.7.0.5.zip`
- iPhone source: use the Source code archive or clone tag `v0.7.0.5`.
- Checksums: `SHA256SUMS.txt`

Existing v0.7.0 launcher and watcher installations remain compatible. Stream
Deck users only need to install the updated plugin. Codex itself does not need
to restart.

Codex Deck is an independent community project and is not made, supported, or
endorsed by OpenAI or Elgato.
4 changes: 2 additions & 2 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "codex-stream-deck",
"version": "0.7.0-hotfix.2",
"version": "0.7.0-hotfix.5",
"private": false,
"type": "module",
"description": "Unofficial Codex Micro bridge for Stream Deck on Windows and macOS, with optional multi-host relay",
Expand Down
18 changes: 18 additions & 0 deletions src/codex-focus.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import { execFile } from "node:child_process";
import { promisify } from "node:util";

const execFileAsync = promisify(execFile);

export function codexFocusSpec(targetPlatform = process.platform): { executable: string; args: string[] } | null {
if (targetPlatform !== "darwin") return null;
return {
executable: "/usr/bin/open",
args: ["-b", "com.openai.codex"]
};
}

export async function focusCodexApp(): Promise<void> {
const spec = codexFocusSpec();
if (!spec) return;
await execFileAsync(spec.executable, spec.args, { windowsHide: true });
}
20 changes: 17 additions & 3 deletions src/controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import {
renderRateLimitResetKey, renderUsageLimitKey, renderUsageOverviewKey, type BuiltinIconName
} from "./render.js";
import { openCodexThread } from "./codex-open.js";
import { focusCodexApp } from "./codex-focus.js";
import { visualStatusFromMicro } from "./status.js";
import type {
CodexHost, HostHealth, MicroActionSlot, MicroDirection, MicroSnapshot, ReasoningAdjustment,
Expand All @@ -33,7 +34,10 @@ type AgentRegistration = { action: KeyAction; slot: number };
type MicroActionRegistration = { action: KeyAction; slot: MicroActionSlot };
type UsageLimitRegistration = { action: KeyAction; mode: UsageLimitMode };
type ActionIdentity = { id: string };
type ContextRingSettings = { showContextRings?: boolean };
export type DeckSettings = {
showContextRings?: boolean;
focusCodexOnAgentPress?: boolean;
};

const USER_ICON_ROOT = join(codexDeckStateRoot(), "icons");
const LOCAL_MOBILE_CONFIG = "mobile-local-relay-server.json";
Expand Down Expand Up @@ -75,14 +79,16 @@ export class DeckController {
private lastAgentSourceSignature = "";
private lastHostHealthSignature = "";
private showContextRings = true;
private focusCodexOnAgentPress = false;

async start(): Promise<void> {
this.stopped = false;
try {
const settings = await streamDeck.settings.getGlobalSettings<ContextRingSettings>();
const settings = await streamDeck.settings.getGlobalSettings<DeckSettings>();
this.showContextRings = settings.showContextRings !== false;
this.focusCodexOnAgentPress = settings.focusCodexOnAgentPress === true;
} catch (error) {
streamDeck.logger.warn(`Context-ring settings were unavailable; using enabled by default: ${String(error)}`);
streamDeck.logger.warn(`Global settings were unavailable; using defaults: ${String(error)}`);
}
this.localHost = await getOrCreateHostIdentity();
const persistedTarget = await readControlTarget(undefined, this.localHost.platform);
Expand Down Expand Up @@ -189,6 +195,10 @@ export class DeckController {
void Promise.all([...this.agents.values()].map((registration) => this.renderAgent(registration)));
}

setFocusCodexOnAgentPress(enabled: boolean): void {
this.focusCodexOnAgentPress = enabled;
}

registerMicroAction(slot: MicroActionSlot, action: KeyAction): void {
this.microActions.set(action.id, { action, slot });
void this.renderMicroAction({ action, slot });
Expand Down Expand Up @@ -295,6 +305,10 @@ export class DeckController {
else this.pressedAgents.delete(slot);
if (!assignment.threadKey) throw new Error("The selected Codex task has no stable thread identity.");
if (assignment.host.hostId === this.localHost?.hostId) {
if (act === 1 && this.focusCodexOnAgentPress) {
try { await focusCodexApp(); }
catch (error) { streamDeck.logger.warn(`Could not focus Codex before opening agent ${slot + 1}: ${String(error)}`); }
}
await this.microBridge.sendAgent(assignment.sourceSlot, act, assignment.threadKey);
} else await this.sendRemote({ kind: "agent", slot: assignment.sourceSlot, threadKey: assignment.threadKey, act });
if (act === 0) void this.refresh();
Expand Down
3 changes: 2 additions & 1 deletion src/plugin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,9 @@ import {

const controller = new DeckController();

streamDeck.settings.onDidReceiveGlobalSettings<{ showContextRings?: boolean }>((event) => {
streamDeck.settings.onDidReceiveGlobalSettings<{ showContextRings?: boolean; focusCodexOnAgentPress?: boolean }>((event) => {
controller.setContextRingVisibility(event.settings.showContextRings !== false);
controller.setFocusCodexOnAgentPress(event.settings.focusCodexOnAgentPress === true);
});

for (const pluginAction of [
Expand Down
2 changes: 1 addition & 1 deletion static/manifest.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"$schema": "https://schemas.elgato.com/streamdeck/plugins/manifest.json",
"Name": "Codex Deck",
"Version": "0.7.0.2",
"Version": "0.7.0.5",
"Author": "Dazer",
"Description": "Unofficial Codex Micro bridge for Stream Deck on Windows and macOS, with optional multi-host relay.",
"UUID": "com.simeo.codex-deck",
Expand Down
51 changes: 40 additions & 11 deletions static/property-inspector/agent.html
Original file line number Diff line number Diff line change
Expand Up @@ -8,20 +8,46 @@
:root { color-scheme: dark; font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif; }
body { margin: 0; padding: 14px; color: #d8d8d8; background: #2d2d2d; font-size: 12px; }
label { display: grid; grid-template-columns: 92px 1fr; align-items: center; gap: 8px; cursor: pointer; }
label + label { margin-top: 10px; }
input { width: 17px; height: 17px; margin: 0; accent-color: #1683ff; }
p { margin: 12px 0 0 100px; color: #a9a9a9; line-height: 1.35; }
</style>
</head>
<body>
<label>
<span>Context ring</span>
<input id="show-context-rings" type="checkbox" checked>
<input id="show-context-rings" type="checkbox" checked disabled>
</label>
<p>This global option applies to all six Codex agent keys on this computer.</p>
<label>
<span>Focus Codex*</span>
<input id="focus-codex-on-agent-press" type="checkbox" disabled>
</label>
<p>These global options apply to all six Codex agent keys on this computer. *Focus Codex is available on macOS only and activates the local app before opening an agent.</p>
<script>
let websocket;
let pluginContext;
let globalSettings = {};
let globalSettings;
const showContextRingsInput = document.getElementById("show-context-rings");
const focusCodexOnAgentPressInput = document.getElementById("focus-codex-on-agent-press");

function renderGlobalSettings() {
if (!globalSettings) return;
showContextRingsInput.checked = globalSettings.showContextRings !== false;
focusCodexOnAgentPressInput.checked = globalSettings.focusCodexOnAgentPress === true;
showContextRingsInput.disabled = false;
focusCodexOnAgentPressInput.disabled = false;
}

function setGlobalSetting(key, value) {
if (!globalSettings || websocket?.readyState !== WebSocket.OPEN) {
renderGlobalSettings();
return;
}
globalSettings = { ...globalSettings, [key]: value };
websocket.send(JSON.stringify({
event: "setGlobalSettings", context: pluginContext, payload: globalSettings
}));
}

window.connectElgatoStreamDeckSocket = (port, uuid, registerEvent) => {
pluginContext = uuid;
Expand All @@ -30,21 +56,24 @@
websocket.send(JSON.stringify({ event: registerEvent, uuid }));
websocket.send(JSON.stringify({ event: "getGlobalSettings", context: pluginContext }));
});
websocket.addEventListener("close", () => {
showContextRingsInput.disabled = true;
focusCodexOnAgentPressInput.disabled = true;
});
websocket.addEventListener("message", (message) => {
const event = JSON.parse(message.data);
if (event.event !== "didReceiveGlobalSettings") return;
globalSettings = event.payload?.settings ?? {};
document.getElementById("show-context-rings").checked = globalSettings.showContextRings !== false;
renderGlobalSettings();
});
};

document.getElementById("show-context-rings").addEventListener("change", (event) => {
globalSettings = { ...globalSettings, showContextRings: event.target.checked };
if (websocket?.readyState === WebSocket.OPEN) {
websocket.send(JSON.stringify({
event: "setGlobalSettings", context: pluginContext, payload: globalSettings
}));
}
showContextRingsInput.addEventListener("change", (event) => {
setGlobalSetting("showContextRings", event.target.checked);
});

focusCodexOnAgentPressInput.addEventListener("change", (event) => {
setGlobalSetting("focusCodexOnAgentPress", event.target.checked);
});
</script>
</body>
Expand Down
9 changes: 9 additions & 0 deletions test/codex-open.test.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import assert from "node:assert/strict";
import test from "node:test";
import { codexOpenSpec, codexThreadUrl } from "../src/codex-open.js";
import { codexFocusSpec } from "../src/codex-focus.js";

test("Codex task deep links only accept task UUIDs or new", () => {
assert.equal(codexThreadUrl("new"), "codex://threads/new");
Expand All @@ -16,3 +17,11 @@ test("Codex links use native launchers on Windows and macOS", () => {
assert.deepEqual(mac, { executable: "/usr/bin/open", args: ["codex://threads/new"], windowsHide: false });
assert.throws(() => codexOpenSpec("new", "linux"), /unsupported/);
});

test("agent presses can opt into focusing the local macOS Codex app", () => {
assert.deepEqual(codexFocusSpec("darwin"), {
executable: "/usr/bin/open",
args: ["-b", "com.openai.codex"]
});
assert.equal(codexFocusSpec("win32"), null);
});
4 changes: 4 additions & 0 deletions test/ios-project.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -207,7 +207,11 @@ test("context rings are optional in both Stream Deck and the native iPhone app",
assert.match(inspector, /getGlobalSettings/);
assert.match(inspector, /setGlobalSettings/);
assert.match(inspector, /showContextRings/);
assert.match(inspector, /focusCodexOnAgentPress/);
assert.match(inspector, /Focus Codex\*/);
assert.match(inspector, /available on macOS only/);
assert.match(plugin, /onDidReceiveGlobalSettings/);
assert.match(plugin, /setFocusCodexOnAgentPress\(event\.settings\.focusCodexOnAgentPress === true\)/);
assert.match(render, /data-context-used/);
assert.match(settings, /Toggle\([\s\S]*"Context rings"/);
assert.match(device, /ContextUsageIndicator/);
Expand Down
Loading