clankerbend 0.1.0
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/DEVELOPERS.md +63 -0
- package/LICENSE +21 -0
- package/README.md +49 -0
- package/apps/README.md +11 -0
- package/apps/sticky-notes/README.md +11 -0
- package/apps/sticky-notes/onewhack.manifest.json +74 -0
- package/apps/sticky-notes/package.json +14 -0
- package/apps/sticky-notes/public/app.js +117 -0
- package/apps/sticky-notes/public/index.html +23 -0
- package/apps/sticky-notes/public/styles.css +84 -0
- package/apps/sticky-notes/src/sticky-notes-app.js +413 -0
- package/apps/vim-nav/README.md +126 -0
- package/apps/vim-nav/onewhack.manifest.json +69 -0
- package/apps/vim-nav/package.json +16 -0
- package/apps/vim-nav/public/app.js +276 -0
- package/apps/vim-nav/public/index.html +53 -0
- package/apps/vim-nav/public/styles.css +211 -0
- package/apps/vim-nav/server.mjs +10 -0
- package/apps/vim-nav/src/vim-nav-app.js +221 -0
- package/assets/onewhack.jpg +0 -0
- package/cli.mjs +91 -0
- package/docs/app-lifecycle.md +63 -0
- package/docs/app-manifest.md +130 -0
- package/docs/author-guide.md +126 -0
- package/docs/launcher-profiles.md +50 -0
- package/docs/protocol.md +1935 -0
- package/host/README.md +18 -0
- package/host/src/app-registry.js +315 -0
- package/host/src/codex-desktop-cdp-adapter.js +1826 -0
- package/host/src/codex-desktop-renderer-bridge.js +3536 -0
- package/host/src/index.js +1177 -0
- package/launch/profiles.mjs +93 -0
- package/launch/runtime-paths.mjs +21 -0
- package/package.json +66 -0
- package/scripts/release-npm.mjs +202 -0
- package/server.mjs +58 -0
package/docs/protocol.md
ADDED
|
@@ -0,0 +1,1935 @@
|
|
|
1
|
+
# OneWhack Protocol
|
|
2
|
+
|
|
3
|
+
Status: draft
|
|
4
|
+
|
|
5
|
+
Disclaimer: OneWhack is an independent OneWill project compatible with OpenAI
|
|
6
|
+
Codex Desktop. It is not affiliated with or endorsed by OpenAI.
|
|
7
|
+
|
|
8
|
+
OneWhack Protocol, short for the OneWill Extension Hacks protocol, is a
|
|
9
|
+
standalone local protocol for building companion apps around Codex Desktop
|
|
10
|
+
without requiring changes to Codex Desktop itself. OneWhack separates a shared
|
|
11
|
+
local host from individual apps: the host owns the risky integration work, and
|
|
12
|
+
apps consume stable transcript, annotation, navigation, and state primitives.
|
|
13
|
+
|
|
14
|
+
OneWhack codifies the hack paths available today:
|
|
15
|
+
|
|
16
|
+
- a local host process bound to loopback
|
|
17
|
+
- a browser side panel loaded from that local host
|
|
18
|
+
- CDP attachment to the Codex Desktop Electron renderer
|
|
19
|
+
- renderer injection for transcript discovery, annotation, scroll, and highlight
|
|
20
|
+
- `codex app-server` awareness for thread, turn, item, approval, and rollback
|
|
21
|
+
data
|
|
22
|
+
|
|
23
|
+
OneWhack is not a proposal for a privileged built-in Codex plugin API. If Codex later
|
|
24
|
+
adds such an API, it should be able to replace the adapters underneath OneWhack, but
|
|
25
|
+
the protocol here is designed to work now with external tools.
|
|
26
|
+
|
|
27
|
+
## Normative Language
|
|
28
|
+
|
|
29
|
+
The words **must**, **must not**, **should**, **should not**, and **may** are
|
|
30
|
+
normative when used in protocol sections. Examples and appendix material are
|
|
31
|
+
non-normative unless explicitly marked otherwise.
|
|
32
|
+
|
|
33
|
+
## Goals
|
|
34
|
+
|
|
35
|
+
OneWhack should give any local app a small set of reusable primitives:
|
|
36
|
+
|
|
37
|
+
1. **View**: start a local browser UI and place it in Codex Desktop's existing
|
|
38
|
+
side panel.
|
|
39
|
+
2. **Attach**: connect to a live Codex Desktop renderer through CDP.
|
|
40
|
+
3. **Observe**: read coarse Desktop context such as current URL, transcript
|
|
41
|
+
anchors, visibility, scroll state, and selected app state.
|
|
42
|
+
4. **Annotate**: add lightweight clickable markers or labels near transcript
|
|
43
|
+
anchors without taking over the transcript DOM.
|
|
44
|
+
5. **Navigate**: scroll to and highlight transcript anchors from the app panel.
|
|
45
|
+
6. **Select**: keep app-side and transcript-side selection in sync without
|
|
46
|
+
stale state overwriting newer user intent.
|
|
47
|
+
7. **Correlate**: optionally map Desktop anchors to app-server threads, turns,
|
|
48
|
+
and items when those identities are available.
|
|
49
|
+
8. **Act**: route app-specific commands through the host without granting apps
|
|
50
|
+
direct CDP or app-server control.
|
|
51
|
+
9. **Compose**: run multiple apps against one shared Desktop adapter and
|
|
52
|
+
transcript bridge.
|
|
53
|
+
10. **Extend**: optionally attach app-defined state to anchors, such as
|
|
54
|
+
reviews, traces, bookmarks, diagnostics, provenance, or timeline events.
|
|
55
|
+
|
|
56
|
+
## Scope
|
|
57
|
+
|
|
58
|
+
OneWhack `0.1` is transcript-adjacent. It covers side panels that observe, annotate,
|
|
59
|
+
navigate, and correlate Codex Desktop transcript content. Other Desktop
|
|
60
|
+
surfaces such as terminal panes, file trees, browser previews, command palette
|
|
61
|
+
commands, notifications, and settings are out of scope for this version.
|
|
62
|
+
|
|
63
|
+
## Non-Goals
|
|
64
|
+
|
|
65
|
+
- OneWhack does not require Codex Desktop changes.
|
|
66
|
+
- OneWhack does not replace `codex app-server`.
|
|
67
|
+
- OneWhack does not define a security boundary.
|
|
68
|
+
- OneWhack does not grant host filesystem, shell, network, or approval authority.
|
|
69
|
+
- OneWhack does not assume exact item-to-DOM mapping is always possible.
|
|
70
|
+
|
|
71
|
+
## Architecture
|
|
72
|
+
|
|
73
|
+
OneWhack describes a shared local host that can mount multiple apps.
|
|
74
|
+
|
|
75
|
+
```text
|
|
76
|
+
OneWhack app panel(s) <-> OneWhack host <-> Codex Desktop adapter <-> renderer bridge
|
|
77
|
+
\
|
|
78
|
+
-> app-server adapter
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
### OneWhack Host
|
|
82
|
+
|
|
83
|
+
The OneWhack host is the trusted local process for a session. It owns the
|
|
84
|
+
loopback HTTP/SSE server, app registry, app state fanout, Desktop CDP adapter,
|
|
85
|
+
renderer bridge injection, app-server adapter, lifecycle cleanup, and policy
|
|
86
|
+
checks. Apps do not talk directly to CDP or app-server unless the host grants a
|
|
87
|
+
specific capability.
|
|
88
|
+
|
|
89
|
+
One host should be enough to serve multiple apps, such as a state-diff viewer,
|
|
90
|
+
Vim-style transcript navigation, diagnostics, bookmarks, or review tools. Apps
|
|
91
|
+
share transcript anchors, selection, app-server correlation, and side-panel
|
|
92
|
+
placement through the host.
|
|
93
|
+
|
|
94
|
+
### OneWhack App
|
|
95
|
+
|
|
96
|
+
A OneWhack app is a UI and state contributor mounted by the host. It may provide
|
|
97
|
+
a side-panel route, annotations, commands, app state entries, and action
|
|
98
|
+
handlers. An app is identified by a stable `appId` and described by a manifest.
|
|
99
|
+
|
|
100
|
+
Apps should be replaceable. A panel should be able to render from host state and
|
|
101
|
+
app state without knowing whether Desktop integration is implemented through CDP,
|
|
102
|
+
a future built-in plugin API, or a mock adapter.
|
|
103
|
+
|
|
104
|
+
### Codex Desktop Adapter
|
|
105
|
+
|
|
106
|
+
The Codex Desktop adapter is the implementation-specific component that launches
|
|
107
|
+
or attaches to Codex Desktop with a loopback CDP port and evaluates the injected
|
|
108
|
+
renderer bridge. This adapter is not the protocol. It is the current way the
|
|
109
|
+
OneWhack host realizes the protocol without Codex Desktop changes.
|
|
110
|
+
|
|
111
|
+
### Injected Renderer Bridge
|
|
112
|
+
|
|
113
|
+
The JavaScript object installed in the Codex Desktop renderer. It discovers DOM
|
|
114
|
+
anchors, adds annotations, opens the native side panel through existing UI, and
|
|
115
|
+
executes scroll/highlight requests. The bridge is owned by the host, not by any
|
|
116
|
+
single app.
|
|
117
|
+
|
|
118
|
+
### App-Server Adapter
|
|
119
|
+
|
|
120
|
+
Connection to `codex app-server --stdio` or another app-server transport when
|
|
121
|
+
available. It is used for task identity and lifecycle data, not for Desktop UI
|
|
122
|
+
control. A OneWhack host may still run when app-server is unavailable, but it
|
|
123
|
+
must report that state explicitly.
|
|
124
|
+
|
|
125
|
+
## Transport
|
|
126
|
+
|
|
127
|
+
OneWhack uses ordinary local web primitives:
|
|
128
|
+
|
|
129
|
+
- HTTP JSON endpoints for commands
|
|
130
|
+
- Server-Sent Events for state updates
|
|
131
|
+
- CDP `Runtime.evaluate` for renderer bridge calls
|
|
132
|
+
- App-server JSON-RPC or JSONL streams for Codex task data when available
|
|
133
|
+
|
|
134
|
+
All host HTTP ports must bind to loopback.
|
|
135
|
+
|
|
136
|
+
```text
|
|
137
|
+
127.0.0.1:<ephemeral>
|
|
138
|
+
```
|
|
139
|
+
|
|
140
|
+
External network exposure is out of scope.
|
|
141
|
+
|
|
142
|
+
## Wire Format
|
|
143
|
+
|
|
144
|
+
All HTTP request and response bodies must be UTF-8 JSON unless the endpoint is
|
|
145
|
+
explicitly defined as SSE. JSON endpoints must use `application/json`.
|
|
146
|
+
|
|
147
|
+
Successful JSON responses use this envelope:
|
|
148
|
+
|
|
149
|
+
```ts
|
|
150
|
+
type OneWhackResponse<T> = {
|
|
151
|
+
ok: true;
|
|
152
|
+
data: T;
|
|
153
|
+
};
|
|
154
|
+
```
|
|
155
|
+
|
|
156
|
+
Failed JSON responses use this envelope:
|
|
157
|
+
|
|
158
|
+
```ts
|
|
159
|
+
type OneWhackError = {
|
|
160
|
+
ok: false;
|
|
161
|
+
error: {
|
|
162
|
+
code:
|
|
163
|
+
| "bad_request"
|
|
164
|
+
| "unauthorized"
|
|
165
|
+
| "forbidden"
|
|
166
|
+
| "not_found"
|
|
167
|
+
| "conflict"
|
|
168
|
+
| "stale_selection"
|
|
169
|
+
| "capability_unavailable"
|
|
170
|
+
| "adapter_unavailable"
|
|
171
|
+
| "app_unavailable"
|
|
172
|
+
| "action_failed"
|
|
173
|
+
| "internal_error";
|
|
174
|
+
message: string;
|
|
175
|
+
detail?: unknown;
|
|
176
|
+
};
|
|
177
|
+
};
|
|
178
|
+
```
|
|
179
|
+
|
|
180
|
+
HTTP status codes should match the envelope:
|
|
181
|
+
|
|
182
|
+
- `200`: successful read or completed command.
|
|
183
|
+
- `202`: accepted asynchronous action.
|
|
184
|
+
- `400`: invalid JSON or invalid request shape.
|
|
185
|
+
- `401`: missing or invalid session token.
|
|
186
|
+
- `403`: app lacks the requested permission.
|
|
187
|
+
- `404`: unknown endpoint, app, anchor, marker, or entry.
|
|
188
|
+
- `409`: stale selection, duplicate app id, or state conflict.
|
|
189
|
+
- `503`: required adapter or app-server capability is unavailable.
|
|
190
|
+
- `500`: unexpected host failure.
|
|
191
|
+
|
|
192
|
+
Hosts must not encode successful results as bare objects. A client can treat any
|
|
193
|
+
response without `ok: true` as failed.
|
|
194
|
+
|
|
195
|
+
### Request Validation
|
|
196
|
+
|
|
197
|
+
JSON command endpoints must reject malformed requests before dispatching to an
|
|
198
|
+
app or adapter:
|
|
199
|
+
|
|
200
|
+
1. Request bodies must parse as JSON objects. Arrays, strings, numbers, and
|
|
201
|
+
invalid JSON return `400` with code `bad_request`.
|
|
202
|
+
2. Hosts should reject bodies larger than 1 MiB for OneWhack `0.1` JSON
|
|
203
|
+
endpoints.
|
|
204
|
+
3. `POST /onewhack/apps/:appId/actions` requires an `action` object with
|
|
205
|
+
non-empty string `appId`, `actionId`, and `type` fields.
|
|
206
|
+
4. `action.appId` must match the route `:appId`. A mismatch returns `400` with
|
|
207
|
+
code `bad_request`.
|
|
208
|
+
5. `requestedAt` and `selectedAt`, when provided, must be parseable timestamps.
|
|
209
|
+
6. Transcript scroll and highlight requests require a non-empty `anchorId`.
|
|
210
|
+
7. Enum-like option fields such as scroll `behavior`, scroll `block`, and
|
|
211
|
+
selection `source` must reject unknown values with `400`.
|
|
212
|
+
|
|
213
|
+
These checks are host responsibilities. App action handlers may perform
|
|
214
|
+
additional validation for app-specific payloads.
|
|
215
|
+
|
|
216
|
+
### Session Token
|
|
217
|
+
|
|
218
|
+
A production OneWhack host must protect JSON and SSE endpoints with an
|
|
219
|
+
unguessable session token. The token is passed as:
|
|
220
|
+
|
|
221
|
+
```text
|
|
222
|
+
Authorization: Bearer <token>
|
|
223
|
+
```
|
|
224
|
+
|
|
225
|
+
Local development hosts may omit authentication only when the host manifest sets:
|
|
226
|
+
|
|
227
|
+
```json
|
|
228
|
+
{
|
|
229
|
+
"security": {
|
|
230
|
+
"localDevInsecure": true
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
```
|
|
234
|
+
|
|
235
|
+
An app must not bypass the host token by calling CDP or app-server directly.
|
|
236
|
+
|
|
237
|
+
### Bootstrap
|
|
238
|
+
|
|
239
|
+
OneWhack `0.1` defines two token bootstrap paths:
|
|
240
|
+
|
|
241
|
+
1. **Hosted panel apps**: the host serves the app entry URL and may append the
|
|
242
|
+
token in the URL fragment as `#onewhack_token=<token>`. App JavaScript reads
|
|
243
|
+
the fragment, stores the token in memory, immediately removes the fragment
|
|
244
|
+
with `history.replaceState`, and sends the token in the `Authorization`
|
|
245
|
+
header for JSON and SSE requests.
|
|
246
|
+
2. **External clients**: the token is provided out of band by the launcher, such
|
|
247
|
+
as an environment variable, config file, local IPC channel, or command output.
|
|
248
|
+
|
|
249
|
+
Hosts must not require a token in the query string. Query strings are more
|
|
250
|
+
likely to be logged or copied than URL fragments. Hosts may additionally set a
|
|
251
|
+
same-origin secure session cookie for panels they serve, but clients must still
|
|
252
|
+
support the bearer-token header.
|
|
253
|
+
|
|
254
|
+
## State Model
|
|
255
|
+
|
|
256
|
+
The host maintains one public state object and streams snapshots to the panel.
|
|
257
|
+
|
|
258
|
+
```ts
|
|
259
|
+
type OneWhackPublicState = {
|
|
260
|
+
protocolName: "onewhack";
|
|
261
|
+
protocolVersion: "0.1";
|
|
262
|
+
sequence: number;
|
|
263
|
+
generatedAt: string;
|
|
264
|
+
capabilities: OneWhackCapabilities;
|
|
265
|
+
host: HostStatus;
|
|
266
|
+
desktop: DesktopStatus;
|
|
267
|
+
panel: PanelStatus;
|
|
268
|
+
transcript: TranscriptState;
|
|
269
|
+
selection: OneWhackSelection | null;
|
|
270
|
+
selectionActions?: OneWhackSelectionAction[];
|
|
271
|
+
overlay?: OneWhackOverlay | null;
|
|
272
|
+
composer?: OneWhackComposerState;
|
|
273
|
+
apps: OneWhackAppState[];
|
|
274
|
+
appServer: AppServerStatus;
|
|
275
|
+
lastAction?: OneWhackActionResult;
|
|
276
|
+
};
|
|
277
|
+
```
|
|
278
|
+
|
|
279
|
+
```ts
|
|
280
|
+
type HostStatus = {
|
|
281
|
+
status: "starting" | "running" | "degraded" | "stopping" | "exited";
|
|
282
|
+
hostId: string;
|
|
283
|
+
url: string;
|
|
284
|
+
launchedAt?: string;
|
|
285
|
+
error?: string;
|
|
286
|
+
};
|
|
287
|
+
|
|
288
|
+
type PanelStatus = {
|
|
289
|
+
status: "closed" | "opening" | "waiting" | "open" | "focused" | "unavailable" | "error";
|
|
290
|
+
activeAppId?: string;
|
|
291
|
+
url?: string;
|
|
292
|
+
preferredWidth?: number;
|
|
293
|
+
lastOpenedAt?: string;
|
|
294
|
+
error?: string;
|
|
295
|
+
};
|
|
296
|
+
|
|
297
|
+
type TranscriptState = {
|
|
298
|
+
anchors: TranscriptAnchor[];
|
|
299
|
+
visibleCount: number;
|
|
300
|
+
annotationCount: number;
|
|
301
|
+
scroll?: TranscriptScrollState;
|
|
302
|
+
updatedAt?: string;
|
|
303
|
+
};
|
|
304
|
+
|
|
305
|
+
type TranscriptScrollState = {
|
|
306
|
+
top: number;
|
|
307
|
+
height: number;
|
|
308
|
+
clientHeight: number;
|
|
309
|
+
};
|
|
310
|
+
```
|
|
311
|
+
|
|
312
|
+
The state object is intentionally denormalized so the panel can render from a
|
|
313
|
+
single event payload.
|
|
314
|
+
|
|
315
|
+
### `GET /onewhack/state`
|
|
316
|
+
|
|
317
|
+
Returns the latest public state.
|
|
318
|
+
|
|
319
|
+
```ts
|
|
320
|
+
type GetStateResponse = OneWhackResponse<OneWhackPublicState>;
|
|
321
|
+
```
|
|
322
|
+
|
|
323
|
+
### `GET /onewhack/events`
|
|
324
|
+
|
|
325
|
+
Streams host events as SSE. The host must send a full state snapshot
|
|
326
|
+
immediately after a client connects.
|
|
327
|
+
|
|
328
|
+
```text
|
|
329
|
+
event: state
|
|
330
|
+
id: 42
|
|
331
|
+
data: {"protocolName":"onewhack","protocolVersion":"0.1","sequence":42,...}
|
|
332
|
+
```
|
|
333
|
+
|
|
334
|
+
SSE rules:
|
|
335
|
+
|
|
336
|
+
1. `id` is the decimal string form of `OneWhackPublicState.sequence`.
|
|
337
|
+
2. `state` events contain a complete `OneWhackPublicState`.
|
|
338
|
+
3. `app-state` events may contain one `OneWhackAppState`; clients must still be
|
|
339
|
+
able to recover from `state`.
|
|
340
|
+
4. `action` events contain `OneWhackActionResult`.
|
|
341
|
+
5. `heartbeat` events have empty JSON object data and should be sent at least
|
|
342
|
+
every 15 seconds.
|
|
343
|
+
6. `error` events contain `OneWhackError["error"]`.
|
|
344
|
+
7. Hosts should avoid emitting `state` when nothing semantically changed.
|
|
345
|
+
8. Clients may reconnect with `Last-Event-ID`; hosts may replay missed events
|
|
346
|
+
but must send a fresh `state` event even if replay is unavailable.
|
|
347
|
+
9. Clients must preserve active text selection and expanded rows while applying
|
|
348
|
+
updates.
|
|
349
|
+
|
|
350
|
+
### Capabilities
|
|
351
|
+
|
|
352
|
+
The host exposes negotiated capabilities in every state snapshot so apps can
|
|
353
|
+
degrade cleanly.
|
|
354
|
+
|
|
355
|
+
```ts
|
|
356
|
+
type OneWhackCapabilities = {
|
|
357
|
+
protocolVersion: "0.1";
|
|
358
|
+
host: {
|
|
359
|
+
apps: boolean;
|
|
360
|
+
actions: boolean;
|
|
361
|
+
appState: boolean;
|
|
362
|
+
sidePanel: boolean;
|
|
363
|
+
};
|
|
364
|
+
transcript: {
|
|
365
|
+
read: boolean;
|
|
366
|
+
annotate: boolean;
|
|
367
|
+
navigate: boolean;
|
|
368
|
+
select: boolean;
|
|
369
|
+
rangeSelect?: boolean;
|
|
370
|
+
rangeHighlight?: boolean;
|
|
371
|
+
};
|
|
372
|
+
overlay?: {
|
|
373
|
+
anchored: boolean;
|
|
374
|
+
forms: boolean;
|
|
375
|
+
};
|
|
376
|
+
composer?: {
|
|
377
|
+
contextItems: boolean;
|
|
378
|
+
draft: boolean;
|
|
379
|
+
submit?: boolean;
|
|
380
|
+
};
|
|
381
|
+
adapter: {
|
|
382
|
+
name: "codex-desktop-cdp" | string;
|
|
383
|
+
cdp: boolean;
|
|
384
|
+
rendererInjection: boolean;
|
|
385
|
+
rendererFetchToLoopback?: boolean;
|
|
386
|
+
providers?: {
|
|
387
|
+
transcriptSnapshot: string;
|
|
388
|
+
transcriptOrder: string;
|
|
389
|
+
transcriptNavigation: string;
|
|
390
|
+
transcriptHighlight: string;
|
|
391
|
+
transcriptTextSelection?: string;
|
|
392
|
+
composerDraft?: string;
|
|
393
|
+
composerContext?: string;
|
|
394
|
+
};
|
|
395
|
+
};
|
|
396
|
+
appServer: {
|
|
397
|
+
available: boolean;
|
|
398
|
+
correlateItems: boolean;
|
|
399
|
+
approvals: boolean;
|
|
400
|
+
rollback: boolean;
|
|
401
|
+
};
|
|
402
|
+
};
|
|
403
|
+
```
|
|
404
|
+
|
|
405
|
+
`adapter.providers` is launch-time configuration. It names which registered app
|
|
406
|
+
bridge supplies shared transcript behavior. Multiple apps may still contribute
|
|
407
|
+
annotations and app state, but only the configured provider handles each shared
|
|
408
|
+
transcript operation until a future protocol version adds dynamic arbitration.
|
|
409
|
+
|
|
410
|
+
### Forward Compatibility
|
|
411
|
+
|
|
412
|
+
OneWhack clients must treat minor-version protocol changes as additive.
|
|
413
|
+
Unknown object fields, unknown capability keys, unknown app state entry fields,
|
|
414
|
+
and unknown action result data must be ignored by default. Apps must gate
|
|
415
|
+
optional behavior on the capabilities they read from `GET /onewhack/manifest`
|
|
416
|
+
or `GET /onewhack/state`; they must not infer availability from the host name,
|
|
417
|
+
adapter name, or Codex Desktop version alone.
|
|
418
|
+
|
|
419
|
+
Hosts must keep the `ok` envelope, error envelope, endpoint names, and required
|
|
420
|
+
fields stable within a major protocol version. A host may add endpoints,
|
|
421
|
+
capabilities, app manifest fields, app state fields, action types, SSE event
|
|
422
|
+
types, or error `detail` content without breaking compatible clients. A client
|
|
423
|
+
that receives an unknown SSE event type should ignore it and recover from the
|
|
424
|
+
next `state` event.
|
|
425
|
+
|
|
426
|
+
Breaking changes require a major version bump. A host that implements a newer
|
|
427
|
+
major protocol may continue to expose a compatible older route set, but if it
|
|
428
|
+
does not, it must report the newer `protocolVersion` before app-specific
|
|
429
|
+
actions are required.
|
|
430
|
+
|
|
431
|
+
## Host API
|
|
432
|
+
|
|
433
|
+
The host API is the stable surface apps use. Codex Desktop adapter details are
|
|
434
|
+
behind the host.
|
|
435
|
+
|
|
436
|
+
Path parameters such as `:appId` and `:actionId` must be percent-encoded when
|
|
437
|
+
they contain characters outside unreserved URI characters. App ids should use
|
|
438
|
+
lowercase reverse-DNS-style names such as `onewill.vim-nav`.
|
|
439
|
+
|
|
440
|
+
### Version Negotiation
|
|
441
|
+
|
|
442
|
+
Apps must read host capabilities before assuming any optional feature exists.
|
|
443
|
+
|
|
444
|
+
```text
|
|
445
|
+
GET /onewhack/manifest
|
|
446
|
+
GET /onewhack/state
|
|
447
|
+
GET /onewhack/events
|
|
448
|
+
```
|
|
449
|
+
|
|
450
|
+
`GET /onewhack/manifest` returns host identity, protocol version, and mounted
|
|
451
|
+
app manifests.
|
|
452
|
+
|
|
453
|
+
```ts
|
|
454
|
+
type GetManifestResponse = OneWhackResponse<OneWhackHostManifest>;
|
|
455
|
+
|
|
456
|
+
type OneWhackHostManifest = {
|
|
457
|
+
oneWhackVersion: "0.1";
|
|
458
|
+
hostId: string;
|
|
459
|
+
hostName: string;
|
|
460
|
+
capabilities: OneWhackCapabilities;
|
|
461
|
+
security?: {
|
|
462
|
+
auth: "bearer" | "none";
|
|
463
|
+
localDevInsecure?: boolean;
|
|
464
|
+
};
|
|
465
|
+
apps: OneWhackAppManifest[];
|
|
466
|
+
};
|
|
467
|
+
```
|
|
468
|
+
|
|
469
|
+
If an app requires a capability that the host does not expose, the app must
|
|
470
|
+
render a degraded state rather than attempting direct CDP or app-server access.
|
|
471
|
+
|
|
472
|
+
### App Registry
|
|
473
|
+
|
|
474
|
+
The host exposes mounted apps through a registry.
|
|
475
|
+
|
|
476
|
+
```text
|
|
477
|
+
GET /onewhack/apps
|
|
478
|
+
GET /onewhack/apps/:appId/manifest
|
|
479
|
+
GET /onewhack/apps/:appId/state
|
|
480
|
+
GET /onewhack/apps/:appId/actions/:actionId
|
|
481
|
+
POST /onewhack/apps/:appId/actions
|
|
482
|
+
```
|
|
483
|
+
|
|
484
|
+
The registry lets one host serve multiple apps without each app owning a
|
|
485
|
+
separate CDP adapter or injected bridge.
|
|
486
|
+
|
|
487
|
+
```ts
|
|
488
|
+
type GetAppsResponse = OneWhackResponse<{
|
|
489
|
+
apps: OneWhackAppSummary[];
|
|
490
|
+
}>;
|
|
491
|
+
|
|
492
|
+
type OneWhackAppSummary = {
|
|
493
|
+
appId: string;
|
|
494
|
+
name: string;
|
|
495
|
+
status: OneWhackAppStatus;
|
|
496
|
+
entry?: string;
|
|
497
|
+
};
|
|
498
|
+
|
|
499
|
+
type GetAppManifestResponse = OneWhackResponse<OneWhackAppManifest>;
|
|
500
|
+
type GetAppStateResponse = OneWhackResponse<OneWhackAppState>;
|
|
501
|
+
|
|
502
|
+
type OneWhackAppStatus =
|
|
503
|
+
| "mounted"
|
|
504
|
+
| "loading"
|
|
505
|
+
| "ready"
|
|
506
|
+
| "degraded"
|
|
507
|
+
| "disabled"
|
|
508
|
+
| "error";
|
|
509
|
+
|
|
510
|
+
type OneWhackAppManifest = {
|
|
511
|
+
oneWhackVersion: "0.1";
|
|
512
|
+
appId: string;
|
|
513
|
+
version: string;
|
|
514
|
+
name: string;
|
|
515
|
+
description?: string;
|
|
516
|
+
entry: string;
|
|
517
|
+
distribution?: OneWhackAppDistribution;
|
|
518
|
+
entrypoint?: OneWhackAppEntrypoint;
|
|
519
|
+
capabilities?: {
|
|
520
|
+
panel?: boolean;
|
|
521
|
+
annotations?: boolean;
|
|
522
|
+
commands?: boolean;
|
|
523
|
+
actions?: boolean;
|
|
524
|
+
appState?: boolean;
|
|
525
|
+
selectionActions?: boolean;
|
|
526
|
+
overlays?: boolean;
|
|
527
|
+
composerContext?: boolean;
|
|
528
|
+
composerDraft?: boolean;
|
|
529
|
+
rendererBridge?: boolean;
|
|
530
|
+
};
|
|
531
|
+
contributes?: {
|
|
532
|
+
panel?: boolean;
|
|
533
|
+
annotations?: boolean;
|
|
534
|
+
commands?: boolean;
|
|
535
|
+
actions?: boolean;
|
|
536
|
+
appState?: boolean;
|
|
537
|
+
selectionActions?: boolean;
|
|
538
|
+
overlays?: boolean;
|
|
539
|
+
composerContext?: boolean;
|
|
540
|
+
composerDraft?: boolean;
|
|
541
|
+
};
|
|
542
|
+
permissions?: {
|
|
543
|
+
transcriptRead?: boolean;
|
|
544
|
+
transcriptAnnotate?: boolean;
|
|
545
|
+
transcriptNavigate?: boolean;
|
|
546
|
+
overlayWrite?: boolean;
|
|
547
|
+
composerWrite?: boolean;
|
|
548
|
+
appServerRead?: boolean;
|
|
549
|
+
appServerApprove?: boolean;
|
|
550
|
+
appServerRollback?: boolean;
|
|
551
|
+
};
|
|
552
|
+
panel?: {
|
|
553
|
+
title: string;
|
|
554
|
+
reloadPolicy: "preserve" | "reload";
|
|
555
|
+
preferredWidth?: number;
|
|
556
|
+
};
|
|
557
|
+
rendererBridge?: OneWhackRendererBridgeManifest;
|
|
558
|
+
lifecycle?: OneWhackAppLifecycleManifest;
|
|
559
|
+
};
|
|
560
|
+
|
|
561
|
+
type OneWhackAppDistribution = {
|
|
562
|
+
kind: "local" | "npm" | "tarball" | "binary";
|
|
563
|
+
source?: string;
|
|
564
|
+
integrity?: string;
|
|
565
|
+
update?: {
|
|
566
|
+
channel?: string;
|
|
567
|
+
url?: string;
|
|
568
|
+
packageName?: string;
|
|
569
|
+
};
|
|
570
|
+
};
|
|
571
|
+
|
|
572
|
+
type OneWhackAppEntrypoint = {
|
|
573
|
+
kind: "module" | "binary" | "static";
|
|
574
|
+
module?: string;
|
|
575
|
+
factory?: string;
|
|
576
|
+
command?: string;
|
|
577
|
+
args?: string[];
|
|
578
|
+
publicDir?: string;
|
|
579
|
+
};
|
|
580
|
+
|
|
581
|
+
type OneWhackRendererBridgeManifest = {
|
|
582
|
+
script: string;
|
|
583
|
+
primary?: boolean;
|
|
584
|
+
provides?: (
|
|
585
|
+
| "transcriptSnapshot"
|
|
586
|
+
| "transcriptOrder"
|
|
587
|
+
| "transcriptNavigation"
|
|
588
|
+
| "transcriptHighlight"
|
|
589
|
+
| "transcriptTextSelection"
|
|
590
|
+
| "composerDraft"
|
|
591
|
+
| "composerContext"
|
|
592
|
+
)[];
|
|
593
|
+
methods?: {
|
|
594
|
+
openPanel?: string;
|
|
595
|
+
scroll?: string;
|
|
596
|
+
highlight?: string;
|
|
597
|
+
};
|
|
598
|
+
};
|
|
599
|
+
|
|
600
|
+
type OneWhackAppLifecycleManifest = {
|
|
601
|
+
install?: { kind: string };
|
|
602
|
+
start?: { kind: string };
|
|
603
|
+
stop?: { kind: string };
|
|
604
|
+
update?: { kind: string };
|
|
605
|
+
remove?: { kind: string };
|
|
606
|
+
};
|
|
607
|
+
```
|
|
608
|
+
|
|
609
|
+
### Actions
|
|
610
|
+
|
|
611
|
+
Apps send commands through the host so the host can enforce capability and
|
|
612
|
+
approval rules.
|
|
613
|
+
|
|
614
|
+
```ts
|
|
615
|
+
type PostAppActionRequest = {
|
|
616
|
+
action: OneWhackAction;
|
|
617
|
+
};
|
|
618
|
+
|
|
619
|
+
type PostAppActionResponse = OneWhackResponse<
|
|
620
|
+
OneWhackActionResult | OneWhackActionReceipt
|
|
621
|
+
>;
|
|
622
|
+
|
|
623
|
+
type OneWhackAction = {
|
|
624
|
+
actionId: string;
|
|
625
|
+
appId: string;
|
|
626
|
+
type: string;
|
|
627
|
+
anchorId?: string;
|
|
628
|
+
entryId?: string;
|
|
629
|
+
markerId?: string;
|
|
630
|
+
payload?: unknown;
|
|
631
|
+
requestedAt: string;
|
|
632
|
+
};
|
|
633
|
+
|
|
634
|
+
type OneWhackActionReceipt = {
|
|
635
|
+
actionId: string;
|
|
636
|
+
appId: string;
|
|
637
|
+
accepted: true;
|
|
638
|
+
status: "accepted";
|
|
639
|
+
resultUrl?: string;
|
|
640
|
+
};
|
|
641
|
+
|
|
642
|
+
type OneWhackActionResult = {
|
|
643
|
+
actionId: string;
|
|
644
|
+
appId: string;
|
|
645
|
+
ok: boolean;
|
|
646
|
+
status?: "accepted" | "rejected" | "applied" | "failed";
|
|
647
|
+
error?: string;
|
|
648
|
+
data?: unknown;
|
|
649
|
+
completedAt?: string;
|
|
650
|
+
};
|
|
651
|
+
```
|
|
652
|
+
|
|
653
|
+
Action rules:
|
|
654
|
+
|
|
655
|
+
1. `actionId` must be unique per `appId`.
|
|
656
|
+
2. If the same `(appId, actionId)` is submitted again, the host must return the
|
|
657
|
+
original receipt or result without executing the action twice.
|
|
658
|
+
3. Hosts should complete fast actions synchronously with `200`.
|
|
659
|
+
4. Hosts may accept long-running actions with `202`; the final result must be
|
|
660
|
+
emitted as an `action` SSE event.
|
|
661
|
+
5. Unknown action `type` values must return `404` with code `not_found`.
|
|
662
|
+
6. Missing permissions must return `403` with code `forbidden`.
|
|
663
|
+
7. Unavailable adapters must return `503` with code `adapter_unavailable` or
|
|
664
|
+
`capability_unavailable`.
|
|
665
|
+
8. When `resultUrl` is present, it must point to
|
|
666
|
+
`/onewhack/apps/:appId/actions/:actionId`.
|
|
667
|
+
9. App handlers must return an action result with boolean `ok`. Invalid handler
|
|
668
|
+
returns must fail with `500` and code `action_failed`.
|
|
669
|
+
|
|
670
|
+
Public examples include `vim.jump`, `vim.search`, `bookmark.add`, and
|
|
671
|
+
`diagnostics.open`. App-specific action types do not need to be published in
|
|
672
|
+
this document, but they must follow the same action contract.
|
|
673
|
+
|
|
674
|
+
### Action Result Lookup
|
|
675
|
+
|
|
676
|
+
Hosts must retain completed action results for at least 60 seconds.
|
|
677
|
+
|
|
678
|
+
```text
|
|
679
|
+
GET /onewhack/apps/:appId/actions/:actionId
|
|
680
|
+
```
|
|
681
|
+
|
|
682
|
+
```ts
|
|
683
|
+
type GetAppActionResponse = OneWhackResponse<
|
|
684
|
+
OneWhackActionResult | OneWhackActionReceipt
|
|
685
|
+
>;
|
|
686
|
+
```
|
|
687
|
+
|
|
688
|
+
This endpoint lets clients recover when they miss an SSE `action` event.
|
|
689
|
+
|
|
690
|
+
### App Lifecycle
|
|
691
|
+
|
|
692
|
+
OneWhack `0.1` defines manifest-based app registration. A host may install apps
|
|
693
|
+
from local manifests, npm packages, tarballs, or binary-style bundles. Download
|
|
694
|
+
and update transports are implementation details, but every installed app must
|
|
695
|
+
resolve to a validated `OneWhackAppManifest`.
|
|
696
|
+
|
|
697
|
+
The public launcher command is intentionally small:
|
|
698
|
+
|
|
699
|
+
```text
|
|
700
|
+
clankerbend codex
|
|
701
|
+
```
|
|
702
|
+
|
|
703
|
+
App installation, enablement, update, and removal are product configuration
|
|
704
|
+
concerns owned by the running OneWhack experience. The registry model remains
|
|
705
|
+
part of the host protocol, but apps should not depend on a public app-management
|
|
706
|
+
CLI being present.
|
|
707
|
+
|
|
708
|
+
Regardless of loading mechanism, a mounted app must appear in
|
|
709
|
+
`GET /onewhack/apps`.
|
|
710
|
+
|
|
711
|
+
Lifecycle rules:
|
|
712
|
+
|
|
713
|
+
1. App ids must be unique within a host.
|
|
714
|
+
2. A manifest parse failure must produce an app summary with `status: "error"`
|
|
715
|
+
when the host can identify the intended `appId`.
|
|
716
|
+
3. Disabled apps should remain visible with `status: "disabled"` and must not
|
|
717
|
+
contribute annotations or actions.
|
|
718
|
+
4. A degraded app may contribute read-only state but must not perform actions
|
|
719
|
+
that require unavailable capabilities.
|
|
720
|
+
5. Hosts must remove an app's annotations when that app is disabled, unmounted,
|
|
721
|
+
or enters unrecoverable error.
|
|
722
|
+
6. Hosts should keep app state available after recoverable errors so panels can
|
|
723
|
+
render the failure and any last-known entries.
|
|
724
|
+
7. Hosts must reject manifests with unknown capabilities or permissions unless
|
|
725
|
+
the host advertises a newer compatible protocol version.
|
|
726
|
+
8. An app must not receive hidden APIs that are unavailable to another app
|
|
727
|
+
declaring the same manifest.
|
|
728
|
+
|
|
729
|
+
### Panel Model
|
|
730
|
+
|
|
731
|
+
OneWhack `0.1` assumes one Codex Desktop side panel controlled by the host. The
|
|
732
|
+
host may implement that side panel as:
|
|
733
|
+
|
|
734
|
+
- a shared shell that routes between mounted apps, or
|
|
735
|
+
- direct app entry pages loaded one at a time.
|
|
736
|
+
|
|
737
|
+
In either model:
|
|
738
|
+
|
|
739
|
+
1. `PanelStatus.activeAppId` identifies the app currently shown when known.
|
|
740
|
+
2. `POST /onewhack/panel/open` opens or focuses the host panel and may switch
|
|
741
|
+
the active panel app by `appId`.
|
|
742
|
+
3. `POST /onewhack/apps/:appId/actions` may switch the active app when the
|
|
743
|
+
action requires app UI.
|
|
744
|
+
4. Opening an already-loaded panel must not reload the page unless the app
|
|
745
|
+
manifest uses `reloadPolicy: "reload"`.
|
|
746
|
+
5. Hosted app routes should live under `/apps/:appId/` on the host origin.
|
|
747
|
+
|
|
748
|
+
## Codex Desktop Adapter
|
|
749
|
+
|
|
750
|
+
### Launch
|
|
751
|
+
|
|
752
|
+
OneWhack can launch Codex Desktop with CDP:
|
|
753
|
+
|
|
754
|
+
```sh
|
|
755
|
+
/Applications/Codex.app/Contents/MacOS/Codex \
|
|
756
|
+
--remote-debugging-address=127.0.0.1 \
|
|
757
|
+
--remote-debugging-port=<free> \
|
|
758
|
+
--user-data-dir=<test-or-onewhack-profile>
|
|
759
|
+
```
|
|
760
|
+
|
|
761
|
+
The host must not patch `/Applications/Codex.app`.
|
|
762
|
+
|
|
763
|
+
### Attach
|
|
764
|
+
|
|
765
|
+
The CDP adapter attaches to the `app://-/index.html` renderer target. If
|
|
766
|
+
`/json/list` is unavailable or unstable, the adapter may attach via the browser
|
|
767
|
+
WebSocket and target discovery.
|
|
768
|
+
|
|
769
|
+
### `DesktopStatus`
|
|
770
|
+
|
|
771
|
+
```ts
|
|
772
|
+
type DesktopStatus = {
|
|
773
|
+
cdpStatus: "starting" | "waiting-for-renderer" | "connected" | "disconnected" | "exited";
|
|
774
|
+
cdpPort?: number;
|
|
775
|
+
desktopPid?: number;
|
|
776
|
+
target?: {
|
|
777
|
+
type?: string;
|
|
778
|
+
title?: string;
|
|
779
|
+
url?: string;
|
|
780
|
+
};
|
|
781
|
+
error?: string;
|
|
782
|
+
};
|
|
783
|
+
```
|
|
784
|
+
|
|
785
|
+
## Renderer Runtime API
|
|
786
|
+
|
|
787
|
+
The CDP adapter initializes a single renderer runtime at:
|
|
788
|
+
|
|
789
|
+
```ts
|
|
790
|
+
window.__oneWhackRuntime
|
|
791
|
+
```
|
|
792
|
+
|
|
793
|
+
Apps register into runtime-owned app slots. The runtime is the only privileged
|
|
794
|
+
renderer global; individual apps must not require their own global bridge names.
|
|
795
|
+
|
|
796
|
+
```ts
|
|
797
|
+
type OneWhackRendererRuntime = {
|
|
798
|
+
protocolVersion: string;
|
|
799
|
+
hostUrl: string;
|
|
800
|
+
apps: Record<string, RendererAppSlot>;
|
|
801
|
+
registerApp(app: RendererAppRegistration): RendererAppSlot;
|
|
802
|
+
getApp(appId: string): RendererAppSlot | null;
|
|
803
|
+
getBridge(appId: string): RendererBridge | null;
|
|
804
|
+
getEntryUrl(appId: string): string | null;
|
|
805
|
+
placeAnnotation(anchor: HTMLElement, annotation: RendererPlacedAnnotation): HTMLElement;
|
|
806
|
+
removeAnnotations(appId: string, liveAnchorIds?: Iterable<string>): void;
|
|
807
|
+
};
|
|
808
|
+
|
|
809
|
+
type RendererAppSlot = {
|
|
810
|
+
appId: string;
|
|
811
|
+
entryUrl: string | null;
|
|
812
|
+
capabilities: Record<string, unknown>;
|
|
813
|
+
bridge: RendererBridge | null;
|
|
814
|
+
injectedAt: string | null;
|
|
815
|
+
};
|
|
816
|
+
|
|
817
|
+
type RendererAppRegistration = {
|
|
818
|
+
appId: string;
|
|
819
|
+
entryUrl?: string;
|
|
820
|
+
capabilities?: Record<string, unknown>;
|
|
821
|
+
bridge: RendererBridge;
|
|
822
|
+
injectedAt?: string;
|
|
823
|
+
};
|
|
824
|
+
|
|
825
|
+
type RendererPlacedAnnotation = {
|
|
826
|
+
appId: string;
|
|
827
|
+
anchorId: string;
|
|
828
|
+
markerId?: string;
|
|
829
|
+
priority?: number;
|
|
830
|
+
placement?: "leading-rail" | "above";
|
|
831
|
+
element: HTMLElement;
|
|
832
|
+
};
|
|
833
|
+
```
|
|
834
|
+
|
|
835
|
+
Each app bridge must be idempotent. Re-evaluating an app injection should update
|
|
836
|
+
styles and handlers without duplicating UI.
|
|
837
|
+
|
|
838
|
+
```ts
|
|
839
|
+
type RendererBridge = {
|
|
840
|
+
version: number;
|
|
841
|
+
snapshot(): RendererSnapshot;
|
|
842
|
+
openPanel(): Promise<PanelOpenResult>;
|
|
843
|
+
scrollToAnchor(anchorId: string, options?: ScrollOptions): ScrollResult;
|
|
844
|
+
highlightAnchor(anchorId: string, options?: HighlightOptions): HighlightResult;
|
|
845
|
+
setAnnotations(annotations: AnchorAnnotation[]): AnnotationResult;
|
|
846
|
+
};
|
|
847
|
+
```
|
|
848
|
+
|
|
849
|
+
`placeAnnotation()` is the renderer-side primitive for injected bridge code that
|
|
850
|
+
has already created a marker element. Apps ask the runtime to place an
|
|
851
|
+
annotation on a transcript anchor; the runtime, not the app, owns the host
|
|
852
|
+
container, slot ordering, offsets, stacking, cleanup, and collision avoidance.
|
|
853
|
+
Injected app bridges must not absolutely position transcript-adjacent markers
|
|
854
|
+
against Codex Desktop transcript rows themselves.
|
|
855
|
+
|
|
856
|
+
`removeAnnotations()` removes stale host-owned slots for one app without
|
|
857
|
+
touching markers owned by other apps. When `liveAnchorIds` is provided, the
|
|
858
|
+
runtime preserves that app's slots only for those anchors.
|
|
859
|
+
|
|
860
|
+
### `snapshot()`
|
|
861
|
+
|
|
862
|
+
Discovers the transcript state from the live DOM.
|
|
863
|
+
|
|
864
|
+
```ts
|
|
865
|
+
type RendererSnapshot = {
|
|
866
|
+
href: string;
|
|
867
|
+
title: string;
|
|
868
|
+
version: number;
|
|
869
|
+
scroll: {
|
|
870
|
+
top: number;
|
|
871
|
+
height: number;
|
|
872
|
+
clientHeight: number;
|
|
873
|
+
};
|
|
874
|
+
anchors: TranscriptAnchor[];
|
|
875
|
+
visibleCount: number;
|
|
876
|
+
annotationCount: number;
|
|
877
|
+
selection: OneWhackSelection | null;
|
|
878
|
+
};
|
|
879
|
+
```
|
|
880
|
+
|
|
881
|
+
The current DOM anchor selectors are implementation details of the adapter, but
|
|
882
|
+
the current Codex Desktop adapter probes these selectors:
|
|
883
|
+
|
|
884
|
+
```text
|
|
885
|
+
[data-content-search-unit-key]
|
|
886
|
+
[data-turn-key]
|
|
887
|
+
[data-content-search-turn-key]
|
|
888
|
+
[data-thread-user-message-navigation-item-id]
|
|
889
|
+
```
|
|
890
|
+
|
|
891
|
+
### Transcript Anchor
|
|
892
|
+
|
|
893
|
+
```ts
|
|
894
|
+
type TranscriptAnchor = {
|
|
895
|
+
anchorId: string;
|
|
896
|
+
kind:
|
|
897
|
+
| "content-search-unit"
|
|
898
|
+
| "turn"
|
|
899
|
+
| "content-search-turn"
|
|
900
|
+
| "navigation-item"
|
|
901
|
+
| "unknown";
|
|
902
|
+
visible: boolean;
|
|
903
|
+
top?: number;
|
|
904
|
+
height?: number;
|
|
905
|
+
textPreview: string;
|
|
906
|
+
order: number;
|
|
907
|
+
inferredRole?: "user" | "assistant" | "system" | "tool";
|
|
908
|
+
appServer?: {
|
|
909
|
+
threadId?: string;
|
|
910
|
+
turnId?: string;
|
|
911
|
+
itemId?: string;
|
|
912
|
+
};
|
|
913
|
+
};
|
|
914
|
+
```
|
|
915
|
+
|
|
916
|
+
`anchorId` is the DOM-derived navigation key. It is only guaranteed stable for
|
|
917
|
+
the current Desktop renderer session unless the adapter can map it to
|
|
918
|
+
app-server identity.
|
|
919
|
+
|
|
920
|
+
### Identity Tiers
|
|
921
|
+
|
|
922
|
+
OneWhack uses separate identity tiers because exact app-server-to-DOM mapping is
|
|
923
|
+
not always available.
|
|
924
|
+
|
|
925
|
+
```ts
|
|
926
|
+
type OneWhackIdentity = {
|
|
927
|
+
anchorId?: string;
|
|
928
|
+
entryId?: string;
|
|
929
|
+
markerId?: string;
|
|
930
|
+
appId?: string;
|
|
931
|
+
appServer?: {
|
|
932
|
+
threadId?: string;
|
|
933
|
+
turnId?: string;
|
|
934
|
+
itemId?: string;
|
|
935
|
+
};
|
|
936
|
+
correlationId?: string;
|
|
937
|
+
};
|
|
938
|
+
```
|
|
939
|
+
|
|
940
|
+
- `anchorId` identifies a live transcript DOM anchor for the current renderer
|
|
941
|
+
session.
|
|
942
|
+
- `entryId` identifies app-owned state, such as a timeline event, bookmark, or
|
|
943
|
+
navigation target.
|
|
944
|
+
- `markerId` identifies a clickable annotation marker contributed by an app.
|
|
945
|
+
- `appServer.threadId`, `turnId`, and `itemId` identify Codex task objects when
|
|
946
|
+
app-server correlation is available.
|
|
947
|
+
- `correlationId` is a best-effort host-generated link between two or more
|
|
948
|
+
identity tiers.
|
|
949
|
+
|
|
950
|
+
Apps must not assume `anchorId` is durable across restarts. Durable state should
|
|
951
|
+
prefer app-owned `entryId` plus app-server identity when available.
|
|
952
|
+
|
|
953
|
+
### `openPanel()`
|
|
954
|
+
|
|
955
|
+
Uses existing Codex Desktop UI to open the Browser side panel and load the host
|
|
956
|
+
URL.
|
|
957
|
+
|
|
958
|
+
```ts
|
|
959
|
+
type PanelOpenResult = {
|
|
960
|
+
ok: boolean;
|
|
961
|
+
mode?:
|
|
962
|
+
| "opened-url-input"
|
|
963
|
+
| "opened-local-server-list"
|
|
964
|
+
| "already-open"
|
|
965
|
+
| "focused";
|
|
966
|
+
error?: string;
|
|
967
|
+
};
|
|
968
|
+
```
|
|
969
|
+
|
|
970
|
+
Important rule: if the webview or iframe is already on the host URL,
|
|
971
|
+
`openPanel()` must return `already-open` and must not reload it. Reloading the
|
|
972
|
+
panel destroys selection and expansion state.
|
|
973
|
+
|
|
974
|
+
### `scrollToAnchor()`
|
|
975
|
+
|
|
976
|
+
Scrolls a transcript anchor into view.
|
|
977
|
+
|
|
978
|
+
```ts
|
|
979
|
+
type ScrollOptions = {
|
|
980
|
+
behavior?: "auto" | "smooth";
|
|
981
|
+
block?: "start" | "center" | "end" | "nearest";
|
|
982
|
+
};
|
|
983
|
+
|
|
984
|
+
type ScrollResult = {
|
|
985
|
+
ok: boolean;
|
|
986
|
+
anchorId?: string;
|
|
987
|
+
error?: string;
|
|
988
|
+
};
|
|
989
|
+
```
|
|
990
|
+
|
|
991
|
+
### `highlightAnchor()`
|
|
992
|
+
|
|
993
|
+
Temporarily highlights a transcript anchor.
|
|
994
|
+
|
|
995
|
+
```ts
|
|
996
|
+
type HighlightOptions = {
|
|
997
|
+
durationMs?: number;
|
|
998
|
+
};
|
|
999
|
+
|
|
1000
|
+
type HighlightResult = {
|
|
1001
|
+
ok: boolean;
|
|
1002
|
+
anchorId?: string;
|
|
1003
|
+
error?: string;
|
|
1004
|
+
};
|
|
1005
|
+
```
|
|
1006
|
+
|
|
1007
|
+
### `setAnnotations()`
|
|
1008
|
+
|
|
1009
|
+
Adds or replaces transcript-adjacent annotations.
|
|
1010
|
+
|
|
1011
|
+
```ts
|
|
1012
|
+
type AnchorAnnotation = {
|
|
1013
|
+
appId: string;
|
|
1014
|
+
anchorId: string;
|
|
1015
|
+
placement: "leading-rail" | "above";
|
|
1016
|
+
markers: AnchorAnnotationMarker[];
|
|
1017
|
+
priority?: number;
|
|
1018
|
+
};
|
|
1019
|
+
|
|
1020
|
+
type AnchorAnnotationMarker = {
|
|
1021
|
+
markerId: string;
|
|
1022
|
+
glyph: string;
|
|
1023
|
+
label: string;
|
|
1024
|
+
tone?: "default" | "info" | "success" | "warning" | "danger" | "accent" | "muted";
|
|
1025
|
+
shape?: "circle" | "square" | "squircle" | "diamond" | "custom";
|
|
1026
|
+
category?: string;
|
|
1027
|
+
badge?: {
|
|
1028
|
+
glyph: string;
|
|
1029
|
+
label: string;
|
|
1030
|
+
tone?: "default" | "info" | "success" | "warning" | "danger" | "accent" | "muted";
|
|
1031
|
+
};
|
|
1032
|
+
entryId?: string;
|
|
1033
|
+
action?: {
|
|
1034
|
+
type: string;
|
|
1035
|
+
payload?: unknown;
|
|
1036
|
+
};
|
|
1037
|
+
data?: unknown;
|
|
1038
|
+
};
|
|
1039
|
+
|
|
1040
|
+
type AnnotationResult = {
|
|
1041
|
+
ok: boolean;
|
|
1042
|
+
applied: number;
|
|
1043
|
+
error?: string;
|
|
1044
|
+
};
|
|
1045
|
+
```
|
|
1046
|
+
|
|
1047
|
+
The renderer bridge may infer annotations when no external data source is
|
|
1048
|
+
available. Inferred markers must be marked as inferred in host state.
|
|
1049
|
+
|
|
1050
|
+
Annotation ownership rules:
|
|
1051
|
+
|
|
1052
|
+
1. Every app-owned annotation must include `appId`.
|
|
1053
|
+
2. The host merges annotations from all apps for the same `anchorId`.
|
|
1054
|
+
3. Lower `priority` values render earlier; omitted priority defaults to `100`.
|
|
1055
|
+
Ties sort by `appId`, then `markerId`, both lexicographically.
|
|
1056
|
+
4. Marker clicks route to the contributing app through
|
|
1057
|
+
`POST /onewhack/apps/:appId/actions`.
|
|
1058
|
+
5. The renderer bridge must remove stale markers for an app without removing
|
|
1059
|
+
markers owned by other apps.
|
|
1060
|
+
6. Marker identity is scoped by `(appId, markerId)`. Two apps may use the same
|
|
1061
|
+
`markerId` without collision.
|
|
1062
|
+
7. If a placement cannot fit all markers, the host should preserve sorted order
|
|
1063
|
+
and may collapse overflow into a host-owned overflow marker.
|
|
1064
|
+
8. Apps request placement; the host decides actual layout. Apps may style the
|
|
1065
|
+
marker contents, but must not rely on fixed offsets, negative margins, or
|
|
1066
|
+
transcript-row absolute positioning for placement.
|
|
1067
|
+
|
|
1068
|
+
## Panel API
|
|
1069
|
+
|
|
1070
|
+
The panel talks only to the host server. It does not need direct CDP access.
|
|
1071
|
+
|
|
1072
|
+
### `POST /onewhack/panel/open`
|
|
1073
|
+
|
|
1074
|
+
Asks the host to open/focus the panel in Codex Desktop. The host calls
|
|
1075
|
+
`window.__oneWhackRuntime.getBridge(activeAppId).openPanel()` or an equivalent
|
|
1076
|
+
app-id-scoped bridge call via CDP.
|
|
1077
|
+
|
|
1078
|
+
```ts
|
|
1079
|
+
type OpenPanelRequest = {
|
|
1080
|
+
appId?: string;
|
|
1081
|
+
};
|
|
1082
|
+
```
|
|
1083
|
+
|
|
1084
|
+
When `appId` is provided, the host must set `PanelStatus.activeAppId` to that
|
|
1085
|
+
registered app before loading the panel URL. Unknown app ids return `404`.
|
|
1086
|
+
|
|
1087
|
+
```ts
|
|
1088
|
+
type OpenPanelResponse = OneWhackResponse<PanelOpenResult>;
|
|
1089
|
+
```
|
|
1090
|
+
|
|
1091
|
+
### `POST /onewhack/transcript/scroll`
|
|
1092
|
+
|
|
1093
|
+
```json
|
|
1094
|
+
{
|
|
1095
|
+
"anchorId": "opaque-anchor",
|
|
1096
|
+
"behavior": "smooth",
|
|
1097
|
+
"block": "center"
|
|
1098
|
+
}
|
|
1099
|
+
```
|
|
1100
|
+
|
|
1101
|
+
```ts
|
|
1102
|
+
type ScrollToAnchorRequest = {
|
|
1103
|
+
anchorId: string;
|
|
1104
|
+
behavior?: "auto" | "smooth";
|
|
1105
|
+
block?: "start" | "center" | "end" | "nearest";
|
|
1106
|
+
};
|
|
1107
|
+
|
|
1108
|
+
type ScrollToAnchorResponse = OneWhackResponse<ScrollResult>;
|
|
1109
|
+
```
|
|
1110
|
+
|
|
1111
|
+
### `POST /onewhack/transcript/highlight`
|
|
1112
|
+
|
|
1113
|
+
```json
|
|
1114
|
+
{
|
|
1115
|
+
"anchorId": "opaque-anchor",
|
|
1116
|
+
"durationMs": 2200
|
|
1117
|
+
}
|
|
1118
|
+
```
|
|
1119
|
+
|
|
1120
|
+
```ts
|
|
1121
|
+
type HighlightAnchorRequest = {
|
|
1122
|
+
anchorId: string;
|
|
1123
|
+
durationMs?: number;
|
|
1124
|
+
};
|
|
1125
|
+
|
|
1126
|
+
type HighlightAnchorResponse = OneWhackResponse<HighlightResult>;
|
|
1127
|
+
```
|
|
1128
|
+
|
|
1129
|
+
### `POST /onewhack/selection`
|
|
1130
|
+
|
|
1131
|
+
Updates global host selection.
|
|
1132
|
+
|
|
1133
|
+
```json
|
|
1134
|
+
{
|
|
1135
|
+
"selection": {
|
|
1136
|
+
"selectionId": "sel_123",
|
|
1137
|
+
"source": "panel",
|
|
1138
|
+
"appId": "example.app",
|
|
1139
|
+
"anchorId": "opaque-anchor",
|
|
1140
|
+
"entryId": "app-entry-1",
|
|
1141
|
+
"selectedAt": "2026-06-17T19:21:00.000Z"
|
|
1142
|
+
}
|
|
1143
|
+
}
|
|
1144
|
+
```
|
|
1145
|
+
|
|
1146
|
+
```ts
|
|
1147
|
+
type PostSelectionRequest = {
|
|
1148
|
+
selection: OneWhackSelection;
|
|
1149
|
+
};
|
|
1150
|
+
|
|
1151
|
+
type PostSelectionResponse = OneWhackResponse<{
|
|
1152
|
+
selection: OneWhackSelection;
|
|
1153
|
+
stale?: boolean;
|
|
1154
|
+
}>;
|
|
1155
|
+
```
|
|
1156
|
+
|
|
1157
|
+
The host must ignore stale selections. A selection is stale when its `selectedAt`
|
|
1158
|
+
is older than the current accepted selection. If two selections have the same
|
|
1159
|
+
`selectedAt`, host receive order wins. On every accepted selection, the host must
|
|
1160
|
+
assign a monotonically increasing `sequence` and `acceptedAt` timestamp.
|
|
1161
|
+
|
|
1162
|
+
```ts
|
|
1163
|
+
type OneWhackSelection = {
|
|
1164
|
+
selectionId: string;
|
|
1165
|
+
sequence?: number;
|
|
1166
|
+
source: "panel" | "transcript" | "adapter";
|
|
1167
|
+
appId?: string;
|
|
1168
|
+
anchorId?: string;
|
|
1169
|
+
quote?: string;
|
|
1170
|
+
range?: OneWhackTextRange;
|
|
1171
|
+
markerId?: string;
|
|
1172
|
+
entryId?: string;
|
|
1173
|
+
appServer?: {
|
|
1174
|
+
threadId?: string;
|
|
1175
|
+
turnId?: string;
|
|
1176
|
+
itemId?: string;
|
|
1177
|
+
};
|
|
1178
|
+
correlationId?: string;
|
|
1179
|
+
selectedAt: string;
|
|
1180
|
+
acceptedAt?: string;
|
|
1181
|
+
};
|
|
1182
|
+
```
|
|
1183
|
+
|
|
1184
|
+
This monotonic rule prevents stale renderer polling from restoring an
|
|
1185
|
+
older transcript click after the user selected a newer side-panel entry.
|
|
1186
|
+
|
|
1187
|
+
### Text Ranges
|
|
1188
|
+
|
|
1189
|
+
Apps must not inspect Codex Desktop DOM to interpret selected text. When the
|
|
1190
|
+
adapter observes a transcript text selection, it normalizes that selection into
|
|
1191
|
+
a `OneWhackTextRange` and updates host `selection`.
|
|
1192
|
+
|
|
1193
|
+
```ts
|
|
1194
|
+
type OneWhackTextRange = {
|
|
1195
|
+
anchorId: string;
|
|
1196
|
+
text: string;
|
|
1197
|
+
quote?: string;
|
|
1198
|
+
prefix?: string;
|
|
1199
|
+
suffix?: string;
|
|
1200
|
+
startOffset?: number;
|
|
1201
|
+
endOffset?: number;
|
|
1202
|
+
fingerprint?: string;
|
|
1203
|
+
};
|
|
1204
|
+
```
|
|
1205
|
+
|
|
1206
|
+
`prefix`, `suffix`, offsets, and `fingerprint` are best-effort adapter hints.
|
|
1207
|
+
Apps may store them for later source matching, but they must treat `anchorId`
|
|
1208
|
+
plus selected `text` as the only required fields.
|
|
1209
|
+
|
|
1210
|
+
```text
|
|
1211
|
+
POST /onewhack/transcript/highlight-range
|
|
1212
|
+
```
|
|
1213
|
+
|
|
1214
|
+
```ts
|
|
1215
|
+
type PostHighlightRangeRequest = {
|
|
1216
|
+
range: OneWhackTextRange;
|
|
1217
|
+
durationMs?: number;
|
|
1218
|
+
behavior?: "auto" | "smooth";
|
|
1219
|
+
block?: "start" | "center" | "end" | "nearest";
|
|
1220
|
+
};
|
|
1221
|
+
```
|
|
1222
|
+
|
|
1223
|
+
The host delegates range mapping to the adapter. If precise text-range
|
|
1224
|
+
highlighting is unavailable, the adapter may highlight the containing anchor.
|
|
1225
|
+
|
|
1226
|
+
### Selection Actions
|
|
1227
|
+
|
|
1228
|
+
Apps can contribute actions for host-rendered transcript selection menus through
|
|
1229
|
+
app state. The host or renderer runtime owns menu placement and click routing.
|
|
1230
|
+
|
|
1231
|
+
```ts
|
|
1232
|
+
type OneWhackSelectionAction = {
|
|
1233
|
+
actionId: string;
|
|
1234
|
+
appId: string;
|
|
1235
|
+
type: string;
|
|
1236
|
+
label: string;
|
|
1237
|
+
icon?: string;
|
|
1238
|
+
appliesTo: "text-selection" | "anchor-selection";
|
|
1239
|
+
enabled?: boolean;
|
|
1240
|
+
payload?: unknown;
|
|
1241
|
+
};
|
|
1242
|
+
```
|
|
1243
|
+
|
|
1244
|
+
When the user chooses a selection action, the host sends a normal
|
|
1245
|
+
`POST /onewhack/apps/:appId/actions` request with the current selection in the
|
|
1246
|
+
action payload or app context.
|
|
1247
|
+
|
|
1248
|
+
### Anchored Overlays
|
|
1249
|
+
|
|
1250
|
+
Apps can request a host-rendered overlay anchored to an anchor or text range.
|
|
1251
|
+
Apps provide fields and actions; the host/adapter owns DOM placement.
|
|
1252
|
+
|
|
1253
|
+
```text
|
|
1254
|
+
POST /onewhack/overlay/open
|
|
1255
|
+
POST /onewhack/overlay/close
|
|
1256
|
+
```
|
|
1257
|
+
|
|
1258
|
+
```ts
|
|
1259
|
+
type OneWhackOverlay = {
|
|
1260
|
+
overlayId: string;
|
|
1261
|
+
appId: string;
|
|
1262
|
+
kind: "form" | "menu" | "notice";
|
|
1263
|
+
title?: string;
|
|
1264
|
+
anchorId?: string;
|
|
1265
|
+
range?: OneWhackTextRange;
|
|
1266
|
+
fields?: OneWhackOverlayField[];
|
|
1267
|
+
actions?: OneWhackOverlayAction[];
|
|
1268
|
+
openedAt?: string;
|
|
1269
|
+
};
|
|
1270
|
+
|
|
1271
|
+
type OneWhackOverlayField = {
|
|
1272
|
+
fieldId: string;
|
|
1273
|
+
kind: "text" | "textarea" | "hidden";
|
|
1274
|
+
label?: string;
|
|
1275
|
+
value?: string;
|
|
1276
|
+
};
|
|
1277
|
+
|
|
1278
|
+
type OneWhackOverlayAction = {
|
|
1279
|
+
label: string;
|
|
1280
|
+
type: string;
|
|
1281
|
+
payload?: unknown;
|
|
1282
|
+
};
|
|
1283
|
+
```
|
|
1284
|
+
|
|
1285
|
+
### Composer Context and Drafts
|
|
1286
|
+
|
|
1287
|
+
Composer context items are app-contributed chips or context blocks owned by the
|
|
1288
|
+
host. Apps add/remove items and ask the host to serialize them into a draft; the
|
|
1289
|
+
adapter owns Codex composer DOM details.
|
|
1290
|
+
|
|
1291
|
+
```ts
|
|
1292
|
+
type OneWhackComposerState = {
|
|
1293
|
+
contextItems: OneWhackComposerContextItem[];
|
|
1294
|
+
draft: {
|
|
1295
|
+
text: string;
|
|
1296
|
+
mode: "replace" | "append" | "prepend";
|
|
1297
|
+
contextItemIds: string[];
|
|
1298
|
+
updatedAt?: string;
|
|
1299
|
+
};
|
|
1300
|
+
lastSubmittedAt?: string;
|
|
1301
|
+
};
|
|
1302
|
+
|
|
1303
|
+
type OneWhackComposerContextItem = {
|
|
1304
|
+
itemId: string;
|
|
1305
|
+
appId: string;
|
|
1306
|
+
label: string;
|
|
1307
|
+
body: string;
|
|
1308
|
+
anchorId?: string;
|
|
1309
|
+
range?: OneWhackTextRange;
|
|
1310
|
+
status: "queued" | "sent" | "resolved";
|
|
1311
|
+
createdAt?: string;
|
|
1312
|
+
updatedAt?: string;
|
|
1313
|
+
};
|
|
1314
|
+
```
|
|
1315
|
+
|
|
1316
|
+
```text
|
|
1317
|
+
POST /onewhack/composer/context
|
|
1318
|
+
POST /onewhack/composer/context/remove
|
|
1319
|
+
POST /onewhack/composer/draft
|
|
1320
|
+
POST /onewhack/composer/submit
|
|
1321
|
+
```
|
|
1322
|
+
|
|
1323
|
+
`/composer/draft` accepts `{ text, mode, contextItemIds }`. `/composer/submit`
|
|
1324
|
+
is optional and must fail gracefully when the adapter cannot safely submit.
|
|
1325
|
+
|
|
1326
|
+
## Renderer Events
|
|
1327
|
+
|
|
1328
|
+
The injected bridge cannot rely on `fetch()` from `app://` to loopback because
|
|
1329
|
+
renderer-to-localhost fetch may fail in this integration path. Therefore the
|
|
1330
|
+
bridge must be able to communicate through CDP-polled renderer state.
|
|
1331
|
+
|
|
1332
|
+
The bridge records events in renderer globals:
|
|
1333
|
+
|
|
1334
|
+
```ts
|
|
1335
|
+
type RendererEventBuffer = {
|
|
1336
|
+
selection?: OneWhackSelection;
|
|
1337
|
+
panelOpen?: PanelOpenResult & { at: string };
|
|
1338
|
+
hostEvents?: OneWhackRendererHostEvent[];
|
|
1339
|
+
};
|
|
1340
|
+
```
|
|
1341
|
+
|
|
1342
|
+
The CDP adapter polls `snapshot()` and accepts only newer renderer selections.
|
|
1343
|
+
For host-rendered UI such as selection menus, overlays, and composer context
|
|
1344
|
+
chips, the renderer queues typed host events. The CDP adapter drains those
|
|
1345
|
+
events and invokes the same host APIs that app panels use.
|
|
1346
|
+
|
|
1347
|
+
```ts
|
|
1348
|
+
type OneWhackRendererHostEvent =
|
|
1349
|
+
| {
|
|
1350
|
+
kind: "selection";
|
|
1351
|
+
selection: OneWhackSelection;
|
|
1352
|
+
}
|
|
1353
|
+
| {
|
|
1354
|
+
kind: "appAction";
|
|
1355
|
+
appId: string;
|
|
1356
|
+
type: string;
|
|
1357
|
+
payload?: unknown;
|
|
1358
|
+
requestedAt?: string;
|
|
1359
|
+
}
|
|
1360
|
+
| {
|
|
1361
|
+
kind: "overlayClose";
|
|
1362
|
+
overlayId?: string;
|
|
1363
|
+
}
|
|
1364
|
+
| {
|
|
1365
|
+
kind: "composerContextRemove";
|
|
1366
|
+
itemId: string;
|
|
1367
|
+
}
|
|
1368
|
+
| {
|
|
1369
|
+
kind: "highlightRange";
|
|
1370
|
+
range: OneWhackTextRange;
|
|
1371
|
+
}
|
|
1372
|
+
| {
|
|
1373
|
+
kind: "highlightAnchor";
|
|
1374
|
+
anchorId: string;
|
|
1375
|
+
};
|
|
1376
|
+
```
|
|
1377
|
+
|
|
1378
|
+
If renderer-to-loopback networking is available, the bridge may also call the
|
|
1379
|
+
host HTTP endpoints directly as an optimization.
|
|
1380
|
+
|
|
1381
|
+
## App State
|
|
1382
|
+
|
|
1383
|
+
OneWhack does not prescribe an app state provider. App state can come from
|
|
1384
|
+
app-server, a local audit system, an MCP server, a test runner, a diagnostics
|
|
1385
|
+
collector, a bookmark store, an editor-navigation model, or simple DOM
|
|
1386
|
+
inference.
|
|
1387
|
+
|
|
1388
|
+
```ts
|
|
1389
|
+
type OneWhackAppState = {
|
|
1390
|
+
appId: string;
|
|
1391
|
+
status: OneWhackAppStatus;
|
|
1392
|
+
source: string;
|
|
1393
|
+
connected: boolean;
|
|
1394
|
+
entries: OneWhackAppEntry[];
|
|
1395
|
+
annotations?: AnchorAnnotation[];
|
|
1396
|
+
commands?: OneWhackCommand[];
|
|
1397
|
+
selectionActions?: OneWhackSelectionAction[];
|
|
1398
|
+
updatedAt?: string;
|
|
1399
|
+
};
|
|
1400
|
+
|
|
1401
|
+
type OneWhackAppEntry = {
|
|
1402
|
+
entryId: string;
|
|
1403
|
+
appId: string;
|
|
1404
|
+
anchorId?: string;
|
|
1405
|
+
markerId?: string;
|
|
1406
|
+
correlationId?: string;
|
|
1407
|
+
title: string;
|
|
1408
|
+
summary?: string;
|
|
1409
|
+
category?: string;
|
|
1410
|
+
tone?: AnchorAnnotationMarker["tone"];
|
|
1411
|
+
status?: string;
|
|
1412
|
+
target?: {
|
|
1413
|
+
path?: string;
|
|
1414
|
+
url?: string;
|
|
1415
|
+
label?: string;
|
|
1416
|
+
};
|
|
1417
|
+
detail?: unknown;
|
|
1418
|
+
occurredAt?: string;
|
|
1419
|
+
};
|
|
1420
|
+
|
|
1421
|
+
type OneWhackCommand = {
|
|
1422
|
+
commandId: string;
|
|
1423
|
+
appId: string;
|
|
1424
|
+
label: string;
|
|
1425
|
+
type: string;
|
|
1426
|
+
shortcut?: string;
|
|
1427
|
+
enabled?: boolean;
|
|
1428
|
+
payload?: unknown;
|
|
1429
|
+
};
|
|
1430
|
+
```
|
|
1431
|
+
|
|
1432
|
+
Panel UIs should render `OneWhackAppEntry[]` without knowing the provider. A
|
|
1433
|
+
timeline, review list, trace viewer, bookmark list, or diagnostics stream can
|
|
1434
|
+
all be represented as entries bound optionally to transcript anchors.
|
|
1435
|
+
|
|
1436
|
+
## App-Server Adapter
|
|
1437
|
+
|
|
1438
|
+
The app-server adapter is separate from Desktop control. OneWhack hosts are
|
|
1439
|
+
app-server-aware and report app-server status even when they cannot connect.
|
|
1440
|
+
|
|
1441
|
+
Useful app-server methods include:
|
|
1442
|
+
|
|
1443
|
+
- `thread/start`
|
|
1444
|
+
- `thread/resume`
|
|
1445
|
+
- `turn/start`
|
|
1446
|
+
- `turn/steer`
|
|
1447
|
+
- `turn/interrupt`
|
|
1448
|
+
- `thread/read`
|
|
1449
|
+
- `thread/turns/items/list`
|
|
1450
|
+
- approval request/response methods
|
|
1451
|
+
- `thread/rollback`
|
|
1452
|
+
|
|
1453
|
+
OneWhack should expose app-server data as correlation metadata, not as a replacement
|
|
1454
|
+
transport.
|
|
1455
|
+
|
|
1456
|
+
```ts
|
|
1457
|
+
type AppServerStatus = {
|
|
1458
|
+
status: "disabled" | "starting" | "connected" | "error" | "exited";
|
|
1459
|
+
pid?: number;
|
|
1460
|
+
version?: string;
|
|
1461
|
+
error?: string;
|
|
1462
|
+
};
|
|
1463
|
+
```
|
|
1464
|
+
|
|
1465
|
+
If an app sends approval responses through app-server, that path must be
|
|
1466
|
+
explicit and user-driven. OneWhack must not auto-approve or auto-deny actions.
|
|
1467
|
+
|
|
1468
|
+
## Minimal Endpoint Set
|
|
1469
|
+
|
|
1470
|
+
A OneWhack host must implement these endpoints:
|
|
1471
|
+
|
|
1472
|
+
```text
|
|
1473
|
+
GET /onewhack/manifest
|
|
1474
|
+
GET /onewhack/state
|
|
1475
|
+
GET /onewhack/events
|
|
1476
|
+
GET /onewhack/apps
|
|
1477
|
+
GET /onewhack/apps/:appId/manifest
|
|
1478
|
+
GET /onewhack/apps/:appId/state
|
|
1479
|
+
GET /onewhack/apps/:appId/actions/:actionId
|
|
1480
|
+
POST /onewhack/apps/:appId/actions
|
|
1481
|
+
POST /onewhack/panel/open
|
|
1482
|
+
POST /onewhack/transcript/scroll
|
|
1483
|
+
POST /onewhack/transcript/highlight
|
|
1484
|
+
POST /onewhack/selection
|
|
1485
|
+
```
|
|
1486
|
+
|
|
1487
|
+
## Manifest
|
|
1488
|
+
|
|
1489
|
+
OneWhack hosts and apps describe themselves with manifests. Manifests are
|
|
1490
|
+
normative for the host launcher, app registry, and panel UI; Codex Desktop does
|
|
1491
|
+
not need to understand them. A packaged app should keep its manifest on disk,
|
|
1492
|
+
and a running host must expose resolved manifests through the host API.
|
|
1493
|
+
|
|
1494
|
+
```json
|
|
1495
|
+
{
|
|
1496
|
+
"oneWhackVersion": "0.1",
|
|
1497
|
+
"hostId": "onewill.onewhack.local",
|
|
1498
|
+
"hostName": "OneWhack Host",
|
|
1499
|
+
"security": {
|
|
1500
|
+
"auth": "bearer",
|
|
1501
|
+
"localDevInsecure": false
|
|
1502
|
+
},
|
|
1503
|
+
"capabilities": {
|
|
1504
|
+
"host": {
|
|
1505
|
+
"apps": true,
|
|
1506
|
+
"actions": true,
|
|
1507
|
+
"appState": true,
|
|
1508
|
+
"sidePanel": true
|
|
1509
|
+
},
|
|
1510
|
+
"transcript": {
|
|
1511
|
+
"read": true,
|
|
1512
|
+
"annotate": true,
|
|
1513
|
+
"navigate": true,
|
|
1514
|
+
"select": true
|
|
1515
|
+
},
|
|
1516
|
+
"adapter": {
|
|
1517
|
+
"name": "codex-desktop-cdp",
|
|
1518
|
+
"cdp": true,
|
|
1519
|
+
"rendererInjection": true,
|
|
1520
|
+
"providers": {
|
|
1521
|
+
"transcriptSnapshot": "onewill.vim-nav",
|
|
1522
|
+
"transcriptOrder": "onewill.vim-nav",
|
|
1523
|
+
"transcriptNavigation": "onewill.vim-nav",
|
|
1524
|
+
"transcriptHighlight": "onewill.vim-nav"
|
|
1525
|
+
}
|
|
1526
|
+
},
|
|
1527
|
+
"appServer": {
|
|
1528
|
+
"available": true,
|
|
1529
|
+
"correlateItems": true,
|
|
1530
|
+
"approvals": false,
|
|
1531
|
+
"rollback": false
|
|
1532
|
+
}
|
|
1533
|
+
},
|
|
1534
|
+
"apps": [
|
|
1535
|
+
{
|
|
1536
|
+
"oneWhackVersion": "0.1",
|
|
1537
|
+
"appId": "example.app",
|
|
1538
|
+
"version": "0.1.0",
|
|
1539
|
+
"name": "Example App",
|
|
1540
|
+
"entry": "http://127.0.0.1:0/apps/example/",
|
|
1541
|
+
"distribution": {
|
|
1542
|
+
"kind": "local",
|
|
1543
|
+
"source": ".",
|
|
1544
|
+
"integrity": "dev-local"
|
|
1545
|
+
},
|
|
1546
|
+
"entrypoint": {
|
|
1547
|
+
"kind": "module",
|
|
1548
|
+
"module": "./src/example-app.js",
|
|
1549
|
+
"factory": "createExampleApp",
|
|
1550
|
+
"publicDir": "./public"
|
|
1551
|
+
},
|
|
1552
|
+
"capabilities": {
|
|
1553
|
+
"panel": true,
|
|
1554
|
+
"annotations": true,
|
|
1555
|
+
"commands": true,
|
|
1556
|
+
"actions": true,
|
|
1557
|
+
"appState": true,
|
|
1558
|
+
"rendererBridge": false
|
|
1559
|
+
},
|
|
1560
|
+
"contributes": {
|
|
1561
|
+
"panel": true,
|
|
1562
|
+
"annotations": true,
|
|
1563
|
+
"commands": true,
|
|
1564
|
+
"actions": true,
|
|
1565
|
+
"appState": true
|
|
1566
|
+
},
|
|
1567
|
+
"permissions": {
|
|
1568
|
+
"transcriptRead": true,
|
|
1569
|
+
"transcriptAnnotate": true,
|
|
1570
|
+
"transcriptNavigate": true,
|
|
1571
|
+
"appServerRead": true,
|
|
1572
|
+
"appServerApprove": false,
|
|
1573
|
+
"appServerRollback": false
|
|
1574
|
+
},
|
|
1575
|
+
"panel": {
|
|
1576
|
+
"title": "Example",
|
|
1577
|
+
"reloadPolicy": "preserve",
|
|
1578
|
+
"preferredWidth": 420
|
|
1579
|
+
}
|
|
1580
|
+
}
|
|
1581
|
+
]
|
|
1582
|
+
}
|
|
1583
|
+
```
|
|
1584
|
+
|
|
1585
|
+
## Public Example: Vim Navigator App
|
|
1586
|
+
|
|
1587
|
+
The public reference app for OneWhack `0.1` is a Vim-style transcript navigator.
|
|
1588
|
+
Other apps should use the same host/app contract and document any app-specific
|
|
1589
|
+
state or action vocabulary they expose.
|
|
1590
|
+
|
|
1591
|
+
### App Manifest
|
|
1592
|
+
|
|
1593
|
+
```json
|
|
1594
|
+
{
|
|
1595
|
+
"oneWhackVersion": "0.1",
|
|
1596
|
+
"appId": "onewill.vim-nav",
|
|
1597
|
+
"version": "0.1.0",
|
|
1598
|
+
"name": "OneWill VimNav",
|
|
1599
|
+
"entry": "http://127.0.0.1:49152/apps/onewill.vim-nav/",
|
|
1600
|
+
"distribution": {
|
|
1601
|
+
"kind": "local",
|
|
1602
|
+
"source": ".",
|
|
1603
|
+
"integrity": "dev-local"
|
|
1604
|
+
},
|
|
1605
|
+
"entrypoint": {
|
|
1606
|
+
"kind": "module",
|
|
1607
|
+
"module": "./src/vim-nav-app.js",
|
|
1608
|
+
"factory": "createVimNavigatorApp",
|
|
1609
|
+
"publicDir": "./public"
|
|
1610
|
+
},
|
|
1611
|
+
"capabilities": {
|
|
1612
|
+
"panel": true,
|
|
1613
|
+
"annotations": true,
|
|
1614
|
+
"commands": true,
|
|
1615
|
+
"actions": true,
|
|
1616
|
+
"appState": true,
|
|
1617
|
+
"rendererBridge": false
|
|
1618
|
+
},
|
|
1619
|
+
"contributes": {
|
|
1620
|
+
"panel": true,
|
|
1621
|
+
"annotations": true,
|
|
1622
|
+
"commands": true,
|
|
1623
|
+
"actions": true,
|
|
1624
|
+
"appState": true
|
|
1625
|
+
},
|
|
1626
|
+
"permissions": {
|
|
1627
|
+
"transcriptRead": true,
|
|
1628
|
+
"transcriptAnnotate": true,
|
|
1629
|
+
"transcriptNavigate": true,
|
|
1630
|
+
"appServerRead": false,
|
|
1631
|
+
"appServerApprove": false,
|
|
1632
|
+
"appServerRollback": false
|
|
1633
|
+
},
|
|
1634
|
+
"panel": {
|
|
1635
|
+
"title": "Vim Nav",
|
|
1636
|
+
"reloadPolicy": "preserve",
|
|
1637
|
+
"preferredWidth": 360
|
|
1638
|
+
}
|
|
1639
|
+
}
|
|
1640
|
+
```
|
|
1641
|
+
|
|
1642
|
+
### App State
|
|
1643
|
+
|
|
1644
|
+
```json
|
|
1645
|
+
{
|
|
1646
|
+
"ok": true,
|
|
1647
|
+
"data": {
|
|
1648
|
+
"appId": "onewill.vim-nav",
|
|
1649
|
+
"status": "ready",
|
|
1650
|
+
"source": "dom",
|
|
1651
|
+
"connected": true,
|
|
1652
|
+
"entries": [
|
|
1653
|
+
{
|
|
1654
|
+
"entryId": "nav:anchor-2",
|
|
1655
|
+
"appId": "onewill.vim-nav",
|
|
1656
|
+
"anchorId": "anchor-2",
|
|
1657
|
+
"title": "02 assistant",
|
|
1658
|
+
"summary": "Latest assistant response",
|
|
1659
|
+
"category": "navigation",
|
|
1660
|
+
"status": "visible"
|
|
1661
|
+
}
|
|
1662
|
+
],
|
|
1663
|
+
"annotations": [
|
|
1664
|
+
{
|
|
1665
|
+
"appId": "onewill.vim-nav",
|
|
1666
|
+
"anchorId": "anchor-2",
|
|
1667
|
+
"placement": "leading-rail",
|
|
1668
|
+
"priority": 50,
|
|
1669
|
+
"markers": [
|
|
1670
|
+
{
|
|
1671
|
+
"markerId": "ruler-2",
|
|
1672
|
+
"glyph": "02",
|
|
1673
|
+
"label": "Jump to transcript item 2",
|
|
1674
|
+
"tone": "accent",
|
|
1675
|
+
"shape": "circle",
|
|
1676
|
+
"entryId": "nav:anchor-2",
|
|
1677
|
+
"action": {
|
|
1678
|
+
"type": "vim.jump",
|
|
1679
|
+
"payload": {
|
|
1680
|
+
"anchorId": "anchor-2"
|
|
1681
|
+
}
|
|
1682
|
+
}
|
|
1683
|
+
}
|
|
1684
|
+
]
|
|
1685
|
+
}
|
|
1686
|
+
],
|
|
1687
|
+
"commands": [
|
|
1688
|
+
{
|
|
1689
|
+
"commandId": "vim.next",
|
|
1690
|
+
"appId": "onewill.vim-nav",
|
|
1691
|
+
"label": "Next transcript item",
|
|
1692
|
+
"type": "vim.jumpRelative",
|
|
1693
|
+
"shortcut": "j",
|
|
1694
|
+
"enabled": true,
|
|
1695
|
+
"payload": {
|
|
1696
|
+
"delta": 1
|
|
1697
|
+
}
|
|
1698
|
+
}
|
|
1699
|
+
],
|
|
1700
|
+
"updatedAt": "2026-06-17T19:21:00.000Z"
|
|
1701
|
+
}
|
|
1702
|
+
}
|
|
1703
|
+
```
|
|
1704
|
+
|
|
1705
|
+
### Action Request
|
|
1706
|
+
|
|
1707
|
+
```json
|
|
1708
|
+
{
|
|
1709
|
+
"action": {
|
|
1710
|
+
"actionId": "act_001",
|
|
1711
|
+
"appId": "onewill.vim-nav",
|
|
1712
|
+
"type": "vim.jump",
|
|
1713
|
+
"anchorId": "anchor-2",
|
|
1714
|
+
"entryId": "nav:anchor-2",
|
|
1715
|
+
"payload": {
|
|
1716
|
+
"behavior": "smooth",
|
|
1717
|
+
"block": "center"
|
|
1718
|
+
},
|
|
1719
|
+
"requestedAt": "2026-06-17T19:21:01.000Z"
|
|
1720
|
+
}
|
|
1721
|
+
}
|
|
1722
|
+
```
|
|
1723
|
+
|
|
1724
|
+
### Action Result
|
|
1725
|
+
|
|
1726
|
+
```json
|
|
1727
|
+
{
|
|
1728
|
+
"ok": true,
|
|
1729
|
+
"data": {
|
|
1730
|
+
"actionId": "act_001",
|
|
1731
|
+
"appId": "onewill.vim-nav",
|
|
1732
|
+
"ok": true,
|
|
1733
|
+
"status": "applied",
|
|
1734
|
+
"completedAt": "2026-06-17T19:21:01.050Z"
|
|
1735
|
+
}
|
|
1736
|
+
}
|
|
1737
|
+
```
|
|
1738
|
+
|
|
1739
|
+
## Security And Lifecycle
|
|
1740
|
+
|
|
1741
|
+
OneWhack is local integration glue, not a sandbox or security boundary. The
|
|
1742
|
+
host still needs explicit safety rules because it can reach CDP, app-server,
|
|
1743
|
+
and user-visible UI.
|
|
1744
|
+
|
|
1745
|
+
### Loopback And Session Access
|
|
1746
|
+
|
|
1747
|
+
- Bind host HTTP servers to loopback only.
|
|
1748
|
+
- Prefer ephemeral ports.
|
|
1749
|
+
- Use an unguessable session token for panel requests once multiple apps or
|
|
1750
|
+
long-lived hosts are supported.
|
|
1751
|
+
- Do not expose the host server on external interfaces.
|
|
1752
|
+
|
|
1753
|
+
### App Permissions
|
|
1754
|
+
|
|
1755
|
+
App manifests declare requested permissions. The host must enforce them.
|
|
1756
|
+
|
|
1757
|
+
```ts
|
|
1758
|
+
type OneWhackAppPermissions = {
|
|
1759
|
+
transcriptRead?: boolean;
|
|
1760
|
+
transcriptAnnotate?: boolean;
|
|
1761
|
+
transcriptNavigate?: boolean;
|
|
1762
|
+
appServerRead?: boolean;
|
|
1763
|
+
appServerApprove?: boolean;
|
|
1764
|
+
appServerRollback?: boolean;
|
|
1765
|
+
};
|
|
1766
|
+
```
|
|
1767
|
+
|
|
1768
|
+
For OneWhack `0.1`, an explicit `false` means the host must deny that
|
|
1769
|
+
capability. Omitted permissions are interpreted by host policy, but reference
|
|
1770
|
+
hosts should treat omitted transcript permissions as allowed for local
|
|
1771
|
+
development apps. A host that denies `transcriptRead` must provide an empty
|
|
1772
|
+
transcript view to that app. A host that denies `transcriptAnnotate` must ignore
|
|
1773
|
+
or remove that app's annotations. A host that denies `transcriptNavigate` must
|
|
1774
|
+
return `403` for action paths that try to scroll or highlight transcript
|
|
1775
|
+
anchors through host context helpers.
|
|
1776
|
+
|
|
1777
|
+
No app receives approval, rollback, filesystem, shell, or network authority just
|
|
1778
|
+
because it is mounted. Approval and rollback actions must be explicit,
|
|
1779
|
+
user-driven, and visible in app state.
|
|
1780
|
+
|
|
1781
|
+
### Desktop Lifecycle
|
|
1782
|
+
|
|
1783
|
+
- Do not patch the Codex app bundle.
|
|
1784
|
+
- Prefer launching through the host so CDP is enabled at process startup.
|
|
1785
|
+
- Attach to an already-running Desktop session only when CDP is already
|
|
1786
|
+
available.
|
|
1787
|
+
- Use a test or OneWhack profile when launching Desktop unless the user
|
|
1788
|
+
explicitly requests their normal profile.
|
|
1789
|
+
- Clean up host-launched Codex and Node processes on exit.
|
|
1790
|
+
|
|
1791
|
+
### UI Lifecycle
|
|
1792
|
+
|
|
1793
|
+
- Do not reload an already-open side panel as part of open/focus.
|
|
1794
|
+
- Do not replace panel DOM while the user has active text selection.
|
|
1795
|
+
- Do not remove annotations owned by another app.
|
|
1796
|
+
- Treat DOM selectors as adapter configuration, not protocol fields.
|
|
1797
|
+
|
|
1798
|
+
## Appendix: Non-Normative Reference Mapping
|
|
1799
|
+
|
|
1800
|
+
The public Vim Navigator example maps implementation files to OneWhack terms as
|
|
1801
|
+
follows:
|
|
1802
|
+
|
|
1803
|
+
| Vim Navigator implementation | OneWhack term |
|
|
1804
|
+
| --- | --- |
|
|
1805
|
+
| `onewhack/host/src/index.js` | Reusable OneWhack Host |
|
|
1806
|
+
| `onewhack/apps/vim-nav/server.mjs` | Host launcher registering one app |
|
|
1807
|
+
| `onewhack/apps/vim-nav/src/vim-nav-app.js` | Vim Navigator app manifest, state, and actions |
|
|
1808
|
+
| `onewhack/apps/vim-nav/public/index.html` | Hosted app panel |
|
|
1809
|
+
| `onewhack/apps/vim-nav/public/app.js` | App client |
|
|
1810
|
+
| `onewhack/apps/vim-nav/public/styles.css` | App panel styling |
|
|
1811
|
+
| `onewhack/host/src/codex-desktop-renderer-bridge.js` | Host-owned injected renderer bridge |
|
|
1812
|
+
| CDP `Runtime.evaluate` | Desktop CDP Adapter call, not used by mock tests |
|
|
1813
|
+
| `window.__oneWhackRuntime.getBridge("onewill.vim-nav").snapshot()` | `RendererBridge.snapshot()` |
|
|
1814
|
+
| `window.__oneWhackRuntime.getBridge("onewill.vim-nav").openPanel()` | `RendererBridge.openPanel()` |
|
|
1815
|
+
| `window.__oneWhackRuntime.getBridge("onewill.vim-nav").scrollToAnchor(anchorId)` | `scrollToAnchor(anchorId)` |
|
|
1816
|
+
| `window.__oneWhackRuntime.getBridge("onewill.vim-nav").highlightAnchor(anchorId)` | `highlightAnchor(anchorId)` |
|
|
1817
|
+
| numbered transcript ruler pills | `AnchorAnnotation.markers` |
|
|
1818
|
+
| `/onewhack/apps/onewill.vim-nav/state` | App state endpoint |
|
|
1819
|
+
| `/onewhack/apps/onewill.vim-nav/actions` | App action endpoint |
|
|
1820
|
+
| `onewhack/apps/vim-nav/test-onewhack.mjs` | Fast host/app behavior e2e |
|
|
1821
|
+
|
|
1822
|
+
## Operational Rules
|
|
1823
|
+
|
|
1824
|
+
1. A host may mount multiple apps, but only one renderer bridge should own
|
|
1825
|
+
transcript discovery and annotation placement.
|
|
1826
|
+
2. Apps must go through the host for transcript navigation, annotation updates,
|
|
1827
|
+
action execution, and app-server correlation.
|
|
1828
|
+
3. Treat exact app-server item mapping as optional unless proven.
|
|
1829
|
+
4. Preserve user selection and expanded panel state during host event updates.
|
|
1830
|
+
5. Prefer degraded, read-only behavior when CDP, app-server, or requested app
|
|
1831
|
+
permissions are unavailable.
|
|
1832
|
+
|
|
1833
|
+
## Behavior Profiles
|
|
1834
|
+
|
|
1835
|
+
OneWhack `0.1` describes behavior in four profiles. Implementations should state
|
|
1836
|
+
which profiles they support and cover those behaviors with e2e tests.
|
|
1837
|
+
|
|
1838
|
+
### Core Host
|
|
1839
|
+
|
|
1840
|
+
A Core Host must implement:
|
|
1841
|
+
|
|
1842
|
+
- JSON envelope and error model.
|
|
1843
|
+
- Session token behavior or explicit `localDevInsecure`.
|
|
1844
|
+
- `GET /onewhack/manifest`.
|
|
1845
|
+
- `GET /onewhack/state`.
|
|
1846
|
+
- `GET /onewhack/events`.
|
|
1847
|
+
- `GET /onewhack/apps`.
|
|
1848
|
+
- `GET /onewhack/apps/:appId/manifest`.
|
|
1849
|
+
- `GET /onewhack/apps/:appId/state`.
|
|
1850
|
+
- `GET /onewhack/apps/:appId/actions/:actionId`.
|
|
1851
|
+
- `POST /onewhack/apps/:appId/actions`.
|
|
1852
|
+
- App lifecycle states and permission enforcement.
|
|
1853
|
+
|
|
1854
|
+
### Transcript Host
|
|
1855
|
+
|
|
1856
|
+
A Transcript Host is a Core Host that also implements:
|
|
1857
|
+
|
|
1858
|
+
- Transcript anchors in `OneWhackPublicState`.
|
|
1859
|
+
- `POST /onewhack/panel/open`.
|
|
1860
|
+
- `POST /onewhack/transcript/scroll`.
|
|
1861
|
+
- `POST /onewhack/transcript/highlight`.
|
|
1862
|
+
- `POST /onewhack/selection`.
|
|
1863
|
+
- Annotation ownership and merge rules.
|
|
1864
|
+
|
|
1865
|
+
### Codex Desktop Adapter
|
|
1866
|
+
|
|
1867
|
+
A Codex Desktop Adapter is a Transcript Host implementation that realizes the
|
|
1868
|
+
transcript surface through Codex Desktop CDP and renderer injection. It must
|
|
1869
|
+
follow the lifecycle rules for CDP launch/attach, idempotent injection, and
|
|
1870
|
+
process cleanup.
|
|
1871
|
+
|
|
1872
|
+
### App Client
|
|
1873
|
+
|
|
1874
|
+
An App Client must:
|
|
1875
|
+
|
|
1876
|
+
- Read `GET /onewhack/manifest` and verify required capabilities.
|
|
1877
|
+
- Use `GET /onewhack/apps/:appId/state` or `GET /onewhack/state` as its source
|
|
1878
|
+
of truth.
|
|
1879
|
+
- Use `POST /onewhack/apps/:appId/actions` for app commands.
|
|
1880
|
+
- Use `GET /onewhack/apps/:appId/actions/:actionId` to recover action results
|
|
1881
|
+
when needed.
|
|
1882
|
+
- Treat `OneWhackError` as the only failure contract.
|
|
1883
|
+
- Degrade cleanly when capabilities or permissions are unavailable.
|
|
1884
|
+
|
|
1885
|
+
## Required E2E Coverage
|
|
1886
|
+
|
|
1887
|
+
A OneWhack `0.1` implementation should include fast e2e coverage for these
|
|
1888
|
+
observable behaviors:
|
|
1889
|
+
|
|
1890
|
+
1. `GET /onewhack/manifest` returns `ok: true`, `oneWhackVersion: "0.1"`,
|
|
1891
|
+
host capabilities, security posture, and app manifests.
|
|
1892
|
+
2. `GET /onewhack/state` returns `ok: true` with `protocolName: "onewhack"`,
|
|
1893
|
+
`protocolVersion: "0.1"`, numeric `sequence`, ISO `generatedAt`, host
|
|
1894
|
+
status, transcript state, app states, and app-server status.
|
|
1895
|
+
3. `GET /onewhack/events` emits an initial `state` event, monotonically
|
|
1896
|
+
increasing event ids, and `heartbeat` events.
|
|
1897
|
+
4. Unknown apps return `404` with `OneWhackError.error.code: "not_found"`.
|
|
1898
|
+
5. Missing or invalid bearer tokens return `401` unless `localDevInsecure` is
|
|
1899
|
+
explicitly enabled.
|
|
1900
|
+
6. Duplicate `(appId, actionId)` submissions return the same receipt or result
|
|
1901
|
+
without executing twice.
|
|
1902
|
+
7. Async action completion is observable through both SSE `action` events and
|
|
1903
|
+
`GET /onewhack/apps/:appId/actions/:actionId`.
|
|
1904
|
+
8. Stale selections return the current accepted selection with `stale: true`
|
|
1905
|
+
and do not change host `selection`.
|
|
1906
|
+
9. Annotation merge order is stable by `priority`, then `appId`, then
|
|
1907
|
+
`markerId`.
|
|
1908
|
+
10. Disabling or unmounting an app removes only that app's annotations.
|
|
1909
|
+
11. `POST /onewhack/transcript/scroll` and
|
|
1910
|
+
`POST /onewhack/transcript/highlight` return `404` for unknown anchors and do
|
|
1911
|
+
not mutate selection.
|
|
1912
|
+
12. Opening an already-loaded panel does not reload it when
|
|
1913
|
+
`reloadPolicy: "preserve"`.
|
|
1914
|
+
|
|
1915
|
+
## Versioning
|
|
1916
|
+
|
|
1917
|
+
OneWhack version `0.1` is the minimal hack-path protocol:
|
|
1918
|
+
|
|
1919
|
+
- local host state/event endpoints
|
|
1920
|
+
- host/app manifest negotiation
|
|
1921
|
+
- app registry and app-scoped state
|
|
1922
|
+
- app action routing
|
|
1923
|
+
- CDP renderer bridge
|
|
1924
|
+
- transcript anchor discovery
|
|
1925
|
+
- transcript annotation
|
|
1926
|
+
- panel open/focus
|
|
1927
|
+
- selection synchronization
|
|
1928
|
+
- app-server correlation
|
|
1929
|
+
|
|
1930
|
+
Future versions may add alternate adapters, but they should preserve the same
|
|
1931
|
+
host-facing concepts. Minor versions should add optional capabilities or fields
|
|
1932
|
+
behind capability checks. Major versions may change required fields or endpoint
|
|
1933
|
+
semantics, but a host must make that visible through `protocolVersion`,
|
|
1934
|
+
`oneWhackVersion`, and `capabilities.protocolVersion` before app actions are
|
|
1935
|
+
required.
|