@codeoid/core 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/README.md ADDED
@@ -0,0 +1,39 @@
1
+ # @codeoid/core
2
+
3
+ Framework-agnostic client core for the [Codeoid](https://github.com/saucam/codeoid)
4
+ daemon — everything a frontend needs except the pixels:
5
+
6
+ - **`CodeoidClient`** — the WebSocket transport: auth handshake (token +
7
+ protocol version + capability declaration), request/response correlation,
8
+ liveness heartbeat, exponential-backoff reconnect with full jitter. Needs
9
+ only a WHATWG `WebSocket` global (browsers, React Native, Bun, Node ≥ 22).
10
+ All timing is injectable. Native hosts call `reconnectNow()` from their own
11
+ resume signal (e.g. React Native `AppState`).
12
+ - **`MessageStore`** + kernels (`mergeDeltaInto`, `dedupeReplay`) — the
13
+ single source of truth for transcript accumulation: upsert-by-messageId,
14
+ streaming delta merges, snapshot vs chunked vs incremental replay routing
15
+ (`ingest()`), per-message versions and per-session epochs.
16
+ - **`ResumeCursors`** — `replay.resume` cursor tracking so reconnects fetch
17
+ only the tail mutated since, not the whole scrollback.
18
+ - Display helpers shared across frontends: usage formatters (`formatTokens`,
19
+ `formatCostUsd`, …), identity/provenance labels (`shortSub`,
20
+ `identityLabel`, …), approval scanning, slash-command parsing.
21
+
22
+ ```ts
23
+ import { CodeoidClient, MessageStore, ResumeCursors } from "@codeoid/core";
24
+ import { CAPABILITIES } from "@codeoid/protocol";
25
+
26
+ const store = new MessageStore();
27
+ const cursors = new ResumeCursors();
28
+ const client = new CodeoidClient({
29
+ url: "ws://localhost:7400",
30
+ token,
31
+ capabilities: [CAPABILITIES.PARTS, CAPABILITIES.CHUNKED_REPLAY, CAPABILITIES.SEQ_RESUME],
32
+ clientName: "my-frontend/1.0",
33
+ });
34
+ client.onMessage((msg) => store.ingest(msg, cursors));
35
+ await client.connect();
36
+ ```
37
+
38
+ Ships TypeScript source (every consumer transpiles TS — Bun, Vite, Metro).
39
+ `@codeoid/protocol` is a peer dependency.
package/package.json ADDED
@@ -0,0 +1,28 @@
1
+ {
2
+ "name": "@codeoid/core",
3
+ "version": "0.1.0",
4
+ "description": "Framework-agnostic Codeoid client core — WebSocket transport (auth handshake, reconnect, heartbeat), message store semantics, resume cursors, and display helpers. Shared by the web UI and the mobile client.",
5
+ "license": "MIT",
6
+ "type": "module",
7
+ "repository": {
8
+ "type": "git",
9
+ "url": "git+https://github.com/saucam/codeoid.git",
10
+ "directory": "packages/core"
11
+ },
12
+ "main": "./src/index.ts",
13
+ "types": "./src/index.ts",
14
+ "exports": {
15
+ ".": {
16
+ "types": "./src/index.ts",
17
+ "default": "./src/index.ts"
18
+ }
19
+ },
20
+ "sideEffects": false,
21
+ "files": ["src", "!src/**/*.test.ts", "README.md"],
22
+ "publishConfig": {
23
+ "access": "public"
24
+ },
25
+ "peerDependencies": {
26
+ "@codeoid/protocol": "^0.1.0"
27
+ }
28
+ }
@@ -0,0 +1,60 @@
1
+ /**
2
+ * Shared pending-approval lookup for ApprovalBar + desktop notifications.
3
+ *
4
+ * The naive version scanned the ENTIRE message array from index 0 on every
5
+ * streaming delta — O(N) per delta on a 5 000-message session. Two levers
6
+ * keep this cheap while preserving user-visible behavior:
7
+ *
8
+ * 1. Status gate. Approvals can only pend mid-turn, so when the session
9
+ * is `idle` / `error` there is nothing to find and we skip the scan
10
+ * entirely. We deliberately gate on "any active status" rather than
11
+ * strictly `waiting_approval`: with parallel pending approvals (rare,
12
+ * stream-input mode) the daemon flips status to `tool_running` /
13
+ * `thinking` the moment the FIRST approval resolves even though a
14
+ * second is still waiting (session.ts resolves each canUseTool with
15
+ * `setStatus(approved ? "tool_running" : "thinking")`), and a strict
16
+ * gate would hide that second approval bar forever — deadlocking the
17
+ * turn from the user's point of view.
18
+ *
19
+ * 2. Turn-bounded backward scan. Pending approvals live in the CURRENT
20
+ * turn — every earlier turn's tool calls were finalized (executing /
21
+ * cancelled) before the next user message could be accepted. So scan
22
+ * backward from the tail and stop at the first `user` message. Within
23
+ * that window the OLDEST pending match wins, matching the previous
24
+ * forward-scan semantics (the daemon serializes approvals oldest
25
+ * first).
26
+ */
27
+
28
+ import type { SessionMessage, SessionStatus } from "@codeoid/protocol";
29
+
30
+ const APPROVAL_POSSIBLE: ReadonlySet<SessionStatus> = new Set<SessionStatus>([
31
+ "waiting_approval",
32
+ "thinking",
33
+ "tool_running",
34
+ ]);
35
+
36
+ /**
37
+ * Find the oldest tool_call in `waiting_confirmation` within the current
38
+ * turn, or null. `status` is the session's current status (undefined when
39
+ * the session record is missing).
40
+ */
41
+ export function findPendingApproval(
42
+ messages: readonly SessionMessage[],
43
+ status: SessionStatus | undefined,
44
+ ): SessionMessage | null {
45
+ if (!status || !APPROVAL_POSSIBLE.has(status)) return null;
46
+ let oldest: SessionMessage | null = null;
47
+ for (let i = messages.length - 1; i >= 0; i--) {
48
+ const m = messages[i];
49
+ if (!m) continue;
50
+ if (m.role === "user") break; // turn boundary — nothing pends earlier
51
+ if (
52
+ m.role === "tool_call" &&
53
+ m.tool &&
54
+ m.tool.state.phase === "waiting_confirmation"
55
+ ) {
56
+ oldest = m; // keep walking — an earlier pending in this turn wins
57
+ }
58
+ }
59
+ return oldest;
60
+ }