@cjhyy/code-shell-web 0.8.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,42 @@
1
+ # @cjhyy/code-shell-web
2
+
3
+ Browser-only state and UI helpers for CodeShell's remote client. The package
4
+ contains no Electron code and imports CodeShell core contracts as types only.
5
+
6
+ ## Public surface
7
+
8
+ The root entry exports:
9
+
10
+ - the reconnecting WebSocket hook and remote-app state machine;
11
+ - protocol stream reducers and message mappers;
12
+ - pairing, device-credential, attachment, risk and storage helpers;
13
+ - the mobile i18n namespace and translation helpers.
14
+
15
+ ```ts
16
+ import { useRemoteApp, useRemoteSocket, classifyRisk, translate } from "@cjhyy/code-shell-web";
17
+ ```
18
+
19
+ The reusable browser logic is emitted to `dist/`. The package also ships the
20
+ standalone Vite application in `dist-app/`; `@cjhyy/code-shell-server/serve`
21
+ uses those assets for the ready-made headless web host.
22
+
23
+ ## Commands
24
+
25
+ ```bash
26
+ bun run --cwd packages/web build
27
+ bun run --cwd packages/web build:app
28
+ bun run --cwd packages/web dev:app
29
+ ```
30
+
31
+ `build` cleans both output directories before compiling the library and
32
+ building the application, so published assets cannot contain stale files.
33
+
34
+ ## Boundary
35
+
36
+ This package may use browser APIs and type-only core protocol imports. It must
37
+ not import Electron, Desktop preload globals, Node-only runtime modules or core
38
+ runtime implementations.
39
+
40
+ ## License
41
+
42
+ MIT.
@@ -0,0 +1,21 @@
1
+ export interface SessionReplayEntry {
2
+ seq: number;
3
+ event: unknown;
4
+ }
5
+ export interface SessionReplaySelection {
6
+ events: unknown[];
7
+ cursor: number;
8
+ }
9
+ export declare function selectSessionReplayEntries(entries: SessionReplayEntry[], appliedSeq: number): SessionReplaySelection;
10
+ export declare function rawApprovalResolvedRequestId(raw: unknown): string | null;
11
+ export declare function removeResolvedApproval<T extends {
12
+ requestId: string;
13
+ }>(approvals: T[], requestId: string): T[];
14
+ export declare function roomMessageSeq(msg: unknown): number | undefined;
15
+ export declare function markRoomSeqApplied(appliedSeqs: Map<string, Set<number>>, roomId: string, seq: number): void;
16
+ export declare function filterNewRoomMessages(roomId: string, messages: unknown[], appliedSeqs: Map<string, Set<number>>): unknown[];
17
+ export declare function maxRoomSeq(current: number, messages: unknown[], latestSeq?: number): number;
18
+ export declare function noteSessionSeq(latestSeqs: Map<string, number>, sessionId: string, seq: number): boolean;
19
+ export declare function markSessionUnread(unreadSessionIds: Set<string>, sessionId: string, activeSessionId?: string): Set<string>;
20
+ export declare function clearUnreadSession(unreadSessionIds: Set<string>, sessionId?: string): Set<string>;
21
+ export declare function pruneUnreadSessions(unreadSessionIds: Set<string>, sessionIds: Iterable<string>): Set<string>;
@@ -0,0 +1,102 @@
1
+ export function selectSessionReplayEntries(entries, appliedSeq) {
2
+ let cursor = appliedSeq;
3
+ const events = [];
4
+ for (const entry of entries) {
5
+ if (entry.seq > appliedSeq) {
6
+ events.push(entry.event);
7
+ if (entry.seq > cursor)
8
+ cursor = entry.seq;
9
+ }
10
+ }
11
+ return { events, cursor };
12
+ }
13
+ export function rawApprovalResolvedRequestId(raw) {
14
+ const obj = raw;
15
+ if (!obj || obj.method !== "agent/approvalResolved")
16
+ return null;
17
+ const params = obj.params;
18
+ const requestId = params?.requestId;
19
+ return typeof requestId === "string" && requestId ? requestId : null;
20
+ }
21
+ export function removeResolvedApproval(approvals, requestId) {
22
+ return approvals.filter((approval) => approval.requestId !== requestId);
23
+ }
24
+ export function roomMessageSeq(msg) {
25
+ const seq = msg?.seq;
26
+ return typeof seq === "number" && Number.isFinite(seq) ? seq : undefined;
27
+ }
28
+ export function markRoomSeqApplied(appliedSeqs, roomId, seq) {
29
+ let set = appliedSeqs.get(roomId);
30
+ if (!set) {
31
+ set = new Set();
32
+ appliedSeqs.set(roomId, set);
33
+ }
34
+ set.add(seq);
35
+ if (set.size > 1000) {
36
+ const sorted = [...set].sort((a, b) => a - b);
37
+ for (const old of sorted.slice(0, set.size - 1000))
38
+ set.delete(old);
39
+ }
40
+ }
41
+ export function filterNewRoomMessages(roomId, messages, appliedSeqs) {
42
+ const seen = appliedSeqs.get(roomId);
43
+ const next = [];
44
+ for (const msg of messages) {
45
+ const seq = roomMessageSeq(msg);
46
+ if (seq !== undefined && seen?.has(seq))
47
+ continue;
48
+ next.push(msg);
49
+ }
50
+ return next;
51
+ }
52
+ export function maxRoomSeq(current, messages, latestSeq) {
53
+ let next = typeof latestSeq === "number" && Number.isFinite(latestSeq) ? latestSeq : current;
54
+ for (const msg of messages) {
55
+ const seq = roomMessageSeq(msg);
56
+ if (seq !== undefined && seq > next)
57
+ next = seq;
58
+ }
59
+ return next;
60
+ }
61
+ export function noteSessionSeq(latestSeqs, sessionId, seq) {
62
+ if (!Number.isFinite(seq))
63
+ return false;
64
+ const current = latestSeqs.get(sessionId) ?? 0;
65
+ if (seq <= current)
66
+ return false;
67
+ latestSeqs.set(sessionId, seq);
68
+ return true;
69
+ }
70
+ export function markSessionUnread(unreadSessionIds, sessionId, activeSessionId) {
71
+ if (!sessionId || sessionId === activeSessionId || unreadSessionIds.has(sessionId)) {
72
+ return unreadSessionIds;
73
+ }
74
+ const next = new Set(unreadSessionIds);
75
+ next.add(sessionId);
76
+ return next;
77
+ }
78
+ export function clearUnreadSession(unreadSessionIds, sessionId) {
79
+ if (!sessionId || !unreadSessionIds.has(sessionId))
80
+ return unreadSessionIds;
81
+ const next = new Set(unreadSessionIds);
82
+ next.delete(sessionId);
83
+ return next;
84
+ }
85
+ export function pruneUnreadSessions(unreadSessionIds, sessionIds) {
86
+ const known = new Set(sessionIds);
87
+ let changed = false;
88
+ for (const id of unreadSessionIds) {
89
+ if (!known.has(id)) {
90
+ changed = true;
91
+ break;
92
+ }
93
+ }
94
+ if (!changed)
95
+ return unreadSessionIds;
96
+ const next = new Set();
97
+ for (const id of unreadSessionIds) {
98
+ if (known.has(id))
99
+ next.add(id);
100
+ }
101
+ return next;
102
+ }
@@ -0,0 +1,89 @@
1
+ import type { MobileSessionMeta, MobileProjectMeta, RoomPublic, MobileRemotePermissionMode as PermissionMode, ApprovalScope, ApprovalPathScope, CcDiscoveredSession } from "@cjhyy/code-shell-core";
2
+ import { type ChatState } from "../lib/streamReducer.js";
3
+ import { type Risk } from "../lib/riskClassify.js";
4
+ import { type ConnStatus } from "./useRemoteSocket.js";
5
+ import { type MobileComposerAttachment } from "../lib/mobileAttachments.js";
6
+ /** Which external coding-CLI the CC pane drives. Mirrors desktop CCRoomView. */
7
+ export type CcCliKind = "claude-code" | "codex";
8
+ export interface PendingApproval {
9
+ requestId: string;
10
+ sessionId?: string;
11
+ /** Set for cc-room (external claude CLI) approvals — routes the response via
12
+ * respondCcApproval(roomId, …) instead of the session approval path. */
13
+ roomId?: string;
14
+ toolName: string;
15
+ description: string;
16
+ summary: string;
17
+ risk: Risk;
18
+ /** AskUser options, when this is an AskUser approval. */
19
+ options?: string[];
20
+ /** When true, the user must pick an option (no free text). */
21
+ optionsOnly?: boolean;
22
+ /** True for path-scoped tools (Read/Edit/Write/…) → show file/dir scope. */
23
+ pathScoped: boolean;
24
+ }
25
+ export interface RemoteApp {
26
+ status: ConnStatus;
27
+ deviceName: string;
28
+ chat: ChatState;
29
+ sessions: MobileSessionMeta[];
30
+ unreadSessionIds: ReadonlySet<string>;
31
+ activeSessionId?: string;
32
+ activeCwd?: string | null;
33
+ projects: MobileProjectMeta[];
34
+ /** The bound room — for the user this is always an external CC (Claude Code)
35
+ * session. The room is internal transport; there is no user-facing room list. */
36
+ activeRoom?: RoomPublic;
37
+ approvals: PendingApproval[];
38
+ permissionMode: PermissionMode;
39
+ loading: {
40
+ sessions: boolean;
41
+ sessionHistory: boolean;
42
+ /** Internal: the room.list fetch that resolves `activeRoom`'s metadata for a
43
+ * bound CC session. No user-facing room list consumes this. */
44
+ rooms: boolean;
45
+ projects: boolean;
46
+ roomHistory: boolean;
47
+ ccSessions: boolean;
48
+ };
49
+ notice?: string;
50
+ logout: () => void;
51
+ sendChat: (input: {
52
+ text: string;
53
+ attachments: MobileComposerAttachment[];
54
+ }) => Promise<boolean>;
55
+ stopRun: () => void;
56
+ selectSession: (id: string) => void;
57
+ newSession: (cwd?: string | null, name?: string) => void;
58
+ refreshSessions: () => void;
59
+ respondApproval: (requestId: string, decision: "approve" | "reject", opts?: {
60
+ reason?: string;
61
+ answer?: string;
62
+ scope?: ApprovalScope;
63
+ pathScope?: ApprovalPathScope;
64
+ }) => void;
65
+ setPermissionMode: (mode: PermissionMode) => void;
66
+ extendGoal: (sessionId: string) => void;
67
+ clearGoal: (sessionId: string) => void;
68
+ /** Leave the bound CC session (drops the room binding, clears the feed). */
69
+ leaveRoom: () => void;
70
+ activeProjectCwd: string | null;
71
+ selectProject: (cwd: string) => void;
72
+ ccSessions: CcDiscoveredSession[];
73
+ ccProbe: {
74
+ available: boolean;
75
+ reason?: string;
76
+ } | null;
77
+ /** Selected CC CLI (Claude Code / Codex). Switching re-probes + re-lists. */
78
+ ccCliKind: CcCliKind;
79
+ setCcCliKind: (kind: CcCliKind) => void;
80
+ openCcSession: (sessionId: string, cwd: string, mode: PermissionMode) => void;
81
+ respondCcApproval: (roomId: string, requestId: string, decision: {
82
+ behavior: "allow";
83
+ updatedInput?: unknown;
84
+ } | {
85
+ behavior: "deny";
86
+ message: string;
87
+ }) => void;
88
+ }
89
+ export declare function useRemoteApp(): RemoteApp;