Skip to content
Merged
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
44 changes: 30 additions & 14 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,18 +1,32 @@
# T3 Code Status for OpenDeck

T3 Code Status adds one OpenDeck key that shows how many open T3 Code threads are working. `4/7` means that four of seven open threads currently have T3's `Starting` or `Working` state. The ring advances toward the next cache read. Press the key to refresh immediately.
T3 Code Status adds an OpenDeck key that shows how many open T3 Code threads are working. `4/7` means that four of seven open threads currently have T3's `Starting` or `Working` state. Status arrives through a live stream; a full ring means the stream is connected. Press the key to retry an offline connection.

This is an unofficial community plugin. It is not part of T3 Code or OpenDeck.

## Install

The plugin needs Node.js 20 or newer installed on the host system. OpenDeck starts JavaScript plugins with this system-wide `node` executable. You do not need pnpm for a normal installation.

1. Start T3 Code Alpha at least once, then leave it open so the cache stays current.
1. Start T3 Code and leave its server running.
2. Install `com.beastyrabbit.t3-code-status.streamDeckPlugin` through OpenDeck's plugin manager.
3. Drag `Thread status` from the `T3 Code Status` category onto a free key.
4. In T3 Code, open **Settings → Connections**. Enable **Network access** if the pairing controls are hidden, then create a **Read only** pairing link.
5. Paste the complete link into the key's **Pair an environment** field and choose **Pair / replace authorization**. Use the link within five minutes, without opening it elsewhere first.

The first cache read starts immediately. Further reads run every 60 seconds by default. If T3 Code exits or its cache cannot be read, the key retries every five seconds until it recovers, then returns to the configured interval.
Repeat pairing for each environment you want to monitor (up to 16). Connections are shared by all keys. Direct links and hosted `app.t3.codes` pairing links are accepted. Use HTTPS for remote connections; plain HTTP beyond loopback requires explicitly allowing it on your trusted private network.
Comment thread
beastyrabbit marked this conversation as resolved.

Choose **Key display** in the key's settings:

- **Threads + questions** is the default. It shows the working/open thread count and adds a large blinking question mark while input, approval, or plan review is pending.
- **Threads only** shows the working/open thread count without question alerts.
- **Questions only** shows a faded question mark when nothing needs a response. When a thread needs input, approval, or plan review, the whole key flashes amber once per second and shows how many threads need you.

For two separate keys, drag `Thread status` onto two keys and select **Threads only** on one and **Questions only** on the other. Each key keeps its own display mode. Waiting threads, background monitoring, and errors alone do not trigger the question alert. Loading, offline, and connection errors still appear in every display mode.

Questions appear or clear when the stream delivers an update. There is no HTTP polling interval. Pressing a key does not answer or dismiss a question.

Transient connection failures retry with backoff up to 30 seconds. If any paired environment is disconnected, the key shows the connection state instead of presenting incomplete totals as live. When expired or revoked authorization prevents a connection, the key shows **LINK**: create a fresh read-only pairing link and paste it into the same settings panel. Re-pairing the same environment preserves your keys and replaces its credential. Current T3 bearer sessions expire after about 30 days; automatic authorization renewal is not available through this pairing flow. An existing stream may remain connected beyond that date because T3 checks authorization when the connection opens. Its next reconnect requires a fresh pairing link.

The normal plugin package declares these OpenDeck platforms:

Expand All @@ -22,9 +36,9 @@ The normal plugin package declares these OpenDeck platforms:
| macOS | 10.15 |
| Windows | 10 |

Linux is the only platform tested end to end with OpenDeck so far. That test used OpenDeck 2.14.0, T3 Code Alpha 0.0.36, and a physical Stream Deck. The same bundled T3 client and cache reader also connected successfully to live T3 Code Alpha 0.0.36 data on Windows 11 and macOS 26 on Apple silicon. OpenDeck was not installed on either probe machine, so those checks do not cover its plugin process, WebSocket connection, Property Inspector, key rendering, installation, or restart behavior.
Earlier releases were tested with OpenDeck 2.14.0 and a physical Stream Deck on Linux. The new pairing/stream connection is covered by local HTTP/WebSocket integration tests; those earlier hardware checks do not validate this new connection flow on Windows or macOS.

OpenDeck and T3 Code must run as the same desktop user so the plugin can see that user's T3 runtime and cache. For a Flatpak installation of OpenDeck on Linux, Node.js must be installed outside Flatpak and available on the system `PATH`. A Node.js Flatpak is not enough. Flatpak has not been tested yet.
OpenDeck needs network access to each paired T3 server. For a Flatpak installation of OpenDeck on Linux, Node.js must be installed outside Flatpak and available on the system `PATH`. A Node.js Flatpak is not enough. Flatpak has not been tested yet.

## What the key shows

Expand All @@ -33,11 +47,11 @@ OpenDeck and T3 Code must run as the same desktop user so the plugin can see tha
- Red at `0/7`: none of the open threads is working.
- Gray at `0/0`: there are no open threads.

Intermediate values move from red through yellow to green. The plugin counts open top-level threads from every environment in T3 Code's local cache. Child agents, archived threads, and settled threads do not count. Snoozed threads normally do not count; they reappear when they request approval or input, or when a fresh failure or completed turn after the snooze needs attention, provided they are otherwise still open. T3's `Starting` and `Working` states count as work. `Monitoring` counts as waiting.
Intermediate values move from red through yellow to green. The plugin counts open threads from every paired environment's shell stream. Archived and settled threads do not count. Snoozed threads normally do not count; they reappear when they request approval or input, or when a fresh failure or completed turn after the snooze needs attention, provided they are otherwise still open. T3's `Starting` and `Working` states count as work. `Monitoring` counts as waiting.

OpenDeck also exposes the same changing status as the key's accessible label.

The Property Inspector accepts refresh intervals from 5 to 300 seconds. Its default is 60 seconds.
The former refresh-interval setting is no longer used. Each environment has one subscription regardless of how many keys use it.

## Optional automatic key placement on Linux

Expand Down Expand Up @@ -98,17 +112,19 @@ gh attestation verify com.beastyrabbit.t3-code-status.streamDeckPlugin \

Run the same `gh attestation verify` command for the setup archive if you use it. GitHub Actions signs the build provenance with a short-lived Sigstore certificate. Protected version tags bind each immutable release to the source commit that the workflow scanned.

## How local cache access works
## Connection and credential storage

The plugin exchanges the one-time pairing token for a bearer session with exactly `orchestration:read`, obtains short-lived WebSocket tickets, and subscribes to `orchestration.subscribeShell`. It does not read Chromium caches or import T3 account credentials. The read permission is broader than status-only access: it also permits reading files and conversations, though this plugin does not request them.
Comment thread
beastyrabbit marked this conversation as resolved.

T3 Code stores a compact shell snapshot for every connected environment in its local Chromium cache. The plugin calls T3 Code's loopback environment endpoint to select the active T3 profile, then reads those snapshots directly. It does not need a T3 account, pairing code, access token, or separate server.
Credentials are stored separately from OpenDeck profiles and the plugin installation in `~/.config/opendeck-t3-code-status/connections.json`. The directory and file use owner-only permissions on Unix; Windows relies on the user's profile ACLs. This is a private credential file, not an encrypted vault. Do not share it. Pairing tokens are discarded after exchange, and credentials are never sent back to the settings panel or included in logs.
Comment thread
beastyrabbit marked this conversation as resolved.
Comment thread
beastyrabbit marked this conversation as resolved.

The plugin discovers the current `t3code` user-data directory as well as the legacy `T3 Code`, `T3 Code (Alpha)`, `T3 Code (Beta)`, and other `T3 Code (<channel>)` directories. If T3 Code uses a custom Electron data directory, set `T3CODE_CACHE_DIR` to the exact `IndexedDB/t3code_app_0.indexeddb.leveldb` directory.
**Forget selected environment** removes its saved credential from this plugin. Revoke the corresponding session in **T3 Settings → Connections** to remove its server authorization too.

The plugin does not interpret message, title, or prompt records. Chromium stores several object stores in the same LevelDB files, so the reader briefly holds bounded file data in memory before it filters out every non-shell record. Large shell snapshots may live in Chromium's matching external blob store; the reader resolves only the blob metadata for the exact shell key. It does not retain, log, or transmit cache contents, and it does not copy thread IDs into its own files or logs. If T3 Code is not running or has not created a cache yet, the key shows `OFF` or `ERR`, and the Property Inspector explains the state.
On reconnect the plugin checks the server's environment identity, gets a fresh ticket, and loads a fresh shell snapshot before continuing live updates. This also works with servers that lack optional replay-completion markers. Stream state stays in memory. Messages and retained thread counts are bounded.

To keep malformed cache data from exhausting the OpenDeck plugin process, the reader limits files, profiles, records, table blocks, decompressed blocks, external blobs, shell keys, serialized snapshots, and estimated parser allocations. A single LevelDB file may not exceed 64 MB, and a shell snapshot or external blob may not exceed 8 MB. The key shows `ERR` when a limit is exceeded.
This uses T3's existing first-party interface, which is not a versioned public SDK. Protocol reference: T3 Code commit `e16b8b059c9f5ff6dfed1addecffb831c6aee043`, following [the maintainer's guidance](https://github.com/pingdotgg/t3code/issues/10929#issuecomment-5601670045). Future T3 changes may require a plugin update.

Pull requests are one known limitation. T3 Code keeps a pull request's open, closed, or merged state in its live renderer but does not include it in the local shell snapshot. This plugin therefore uses only the settlement state available in the cache. A thread whose automatic settlement depends only on a pull request may remain in the denominator for a while. The plugin deliberately does not request GitHub or T3 credentials to close that gap.
The plugin uses a three-day age-based settlement default and does not import per-client sidebar preferences or pull-request state. Counts can differ from a T3 sidebar configured with other settlement rules.

## Build from source

Expand All @@ -127,7 +143,7 @@ pnpm package
- `release/com.beastyrabbit.t3-code-status.streamDeckPlugin`
- `release/com.beastyrabbit.t3-code-status-opendeck-setup.tar.gz`

The package check builds each archive twice and fails unless both copies are byte-for-byte identical. It also verifies the manifest, version, required files, archive contents, bundled dependency notices, and the isolated setup path. It rejects markers from the discarded pairing implementation and never touches the real OpenDeck configuration.
The package check builds each archive twice and fails unless both copies are byte-for-byte identical. It also verifies the manifest, version, required files, archive contents, bundled dependency notices, and the isolated setup path. It rejects the old cache reader in the runtime bundle and never touches the real OpenDeck configuration.

For a local Linux development update, OpenDeck may remain open:

Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,6 @@
"packageManager": "pnpm@11.24.0",
"type": "module",
"dependencies": {
"snappyjs": "^0.7.0",
"ws": "^8.21.3"
},
"devDependencies": {
Expand All @@ -46,6 +45,7 @@
"@types/ws": "^8.18.1",
"esbuild": "^0.28.2",
"lefthook": "^2.1.12",
"snappyjs": "^0.7.0",
"tsx": "^4.23.13",
"typescript": "^7.0.2"
}
Expand Down
59 changes: 31 additions & 28 deletions plugin/property-inspector/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -27,45 +27,48 @@ <h1>Thread status</h1>

<section class="status-card" aria-labelledby="connection-heading">
<div class="status-card__top">
<p class="section-label" id="connection-heading">Local T3 cache</p>
<p class="section-label" id="connection-heading">Live T3 connection</p>
<span class="status-light" id="status-light" data-state="loading" aria-hidden="true"></span>
</div>
<div id="connection-announcement" role="status" aria-live="polite" aria-atomic="true">
<p class="connection-state" id="connection-state">Reading cache</p>
<p class="connection-state" id="connection-state">Connecting</p>
<p class="connection-detail" id="connection-detail">
Looking for T3 Code. No sign-in required.
Checking your paired environments.
</p>
<p class="error-message" id="error-message" hidden></p>
</div>
</section>

<section class="control-section" aria-labelledby="refresh-heading">
<div class="section-heading">
<div>
<p class="section-label" id="refresh-heading">Refresh interval</p>
<p class="section-copy">The ring on the key runs until the next cache read.</p>
</div>
<span class="section-code">TIMER</span>
</div>
<label class="number-control" for="refresh-seconds">
<input
id="refresh-seconds"
type="number"
min="5"
max="300"
step="1"
inputmode="numeric"
value="60"
disabled
aria-label="Refresh interval in seconds"
aria-describedby="refresh-note"
/>
<span>seconds</span>
</label>
<p class="field-note" id="refresh-note" aria-live="polite">
Choose 5 to 300 seconds. Press the key to refresh immediately.
<section class="control-section" aria-labelledby="pair-heading">
<label class="section-label" id="pair-heading" for="pairing-link">Pair an environment</label>
<p class="field-note">In T3 Code, open Settings → Connections, enable Network access if needed, and create a Read only pairing link. Paste the complete link below.</p>
<input id="pairing-link" type="password" autocomplete="off" spellcheck="false" maxlength="16384" placeholder="Paste read-only pairing link" disabled />
Comment thread
beastyrabbit marked this conversation as resolved.
<label class="field-note http-option"><input id="allow-http" type="checkbox" disabled /> Allow unencrypted HTTP on my trusted private network</label>
Comment thread
beastyrabbit marked this conversation as resolved.
<button id="pair-button" type="button" disabled>Pair / replace authorization</button>
<p id="pairing-result" class="field-note" role="status" aria-live="polite"></p>
<p class="field-note">Pair once per environment for all keys. A new link for the same environment replaces its saved authorization. After T3's 30-day authorization expires, reconnecting requires a fresh link. An existing stream may stay connected longer.</p>
<p class="field-note">Read only also permits access to files and conversations. This plugin requests only the thread status stream.</p>
<label class="section-label" for="environment-list">Paired environments</label>
<select id="environment-list" disabled><option value="">No paired environments</option></select>
<p id="environment-detail" class="field-note"></p>
<button id="remove-connection" type="button" disabled>Forget selected environment</button>
<p class="field-note">Forgetting removes the credential from this plugin. Revoke its access in T3 Settings → Connections.</p>
</section>

<section class="control-section" aria-labelledby="display-heading">
<label class="section-label" id="display-heading" for="display-mode">Key display</label>
<select id="display-mode" aria-describedby="display-note" disabled>
<option value="combined">Threads + questions</option>
<option value="threads">Threads only</option>
<option value="questions">Questions only</option>
</select>
<p class="field-note" id="display-note" aria-live="polite">
Shows thread counts and a blinking question mark when input, approval, or plan review is pending.
</p>
<p class="field-note">For separate keys, add Thread status twice and choose a different display on each.</p>
</section>

<p class="field-note">Status updates arrive live. Press a key to retry an offline connection. A full ring means the stream is connected.</p>
</main>
</body>
</html>
Loading
Loading