@cjhyy/code-shell-web 0.9.43 → 0.9.46
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/dist/components/ModelTraceExplorer.d.ts +10 -0
- package/dist/components/ModelTraceExplorer.js +490 -0
- package/dist/components/model-trace-presentation.d.ts +11 -0
- package/dist/components/model-trace-presentation.js +73 -0
- package/dist/hooks/useRemoteApp.d.ts +23 -7
- package/dist/hooks/useRemoteApp.js +124 -39
- package/dist/i18n/mobile.d.ts +4 -0
- package/dist/i18n/mobile.js +4 -0
- package/dist/i18n/translate.d.ts +4 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +2 -0
- package/dist/lib/format.d.ts +3 -0
- package/dist/lib/format.js +24 -8
- package/dist/lib/mobileAttachments.d.ts +2 -0
- package/dist/lib/mobileAttachments.js +6 -3
- package/dist/lib/storage.js +34 -7
- package/dist/lib/streamReducer.js +26 -7
- package/dist/model-trace-api.d.ts +16 -0
- package/dist/model-trace-api.js +36 -0
- package/dist-app/assets/index-B0Ob4zoq.css +1 -0
- package/dist-app/assets/index-DcljaCB6.js +75 -0
- package/dist-app/index.html +2 -2
- package/package.json +20 -4
- package/dist-app/assets/index-DFx6OGDf.js +0 -57
- package/dist-app/assets/index-DSlekIOe.css +0 -1
|
@@ -19,6 +19,14 @@ export interface PendingApproval {
|
|
|
19
19
|
options?: string[];
|
|
20
20
|
/** When true, the user must pick an option (no free text). */
|
|
21
21
|
optionsOnly?: boolean;
|
|
22
|
+
/** CLI AskUserQuestion asking several questions in one request. They are
|
|
23
|
+
* answered together: the request accepts exactly one response. */
|
|
24
|
+
questions?: Array<{
|
|
25
|
+
question: string;
|
|
26
|
+
header?: string;
|
|
27
|
+
options: string[];
|
|
28
|
+
multiSelect: boolean;
|
|
29
|
+
}>;
|
|
22
30
|
/** A nonblocking question remains answerable after the run ends. */
|
|
23
31
|
asynchronous?: boolean;
|
|
24
32
|
/** True for path-scoped tools (Read/Edit/Write/…) → show file/dir scope. */
|
|
@@ -36,6 +44,8 @@ export interface RemoteApp {
|
|
|
36
44
|
/** The bound room — for the user this is always an external CC (Claude Code)
|
|
37
45
|
* session. The room is internal transport; there is no user-facing room list. */
|
|
38
46
|
activeRoom?: RoomPublic;
|
|
47
|
+
/** Set while a CLI room is bound, even before its metadata has loaded. */
|
|
48
|
+
activeRoomId?: string;
|
|
39
49
|
approvals: PendingApproval[];
|
|
40
50
|
permissionMode: PermissionMode;
|
|
41
51
|
loading: {
|
|
@@ -50,9 +60,17 @@ export interface RemoteApp {
|
|
|
50
60
|
};
|
|
51
61
|
notice?: string;
|
|
52
62
|
logout: () => void;
|
|
63
|
+
/** Resolves true once the host accepted the message. */
|
|
53
64
|
sendChat: (input: {
|
|
54
65
|
text: string;
|
|
55
66
|
attachments: MobileComposerAttachment[];
|
|
67
|
+
modelTraceSource?: string;
|
|
68
|
+
/** Retry an earlier intent whose delivery could not be confirmed. The host
|
|
69
|
+
* ignores an ID it already recorded, so the task never runs twice. */
|
|
70
|
+
clientMessageId?: string;
|
|
71
|
+
/** The frame reached the socket but neither acceptance nor rejection
|
|
72
|
+
* arrived (lost connection or timeout): the host may have run it. */
|
|
73
|
+
onUncertain?: (clientMessageId: string) => void;
|
|
56
74
|
}) => Promise<boolean>;
|
|
57
75
|
stopRun: () => void;
|
|
58
76
|
selectSession: (id: string) => void;
|
|
@@ -81,14 +99,11 @@ export interface RemoteApp {
|
|
|
81
99
|
ccCliKind: CcCliKind;
|
|
82
100
|
setCcCliKind: (kind: CcCliKind) => void;
|
|
83
101
|
openCcSession: (sessionId: string, cwd: string, mode: PermissionMode) => void;
|
|
84
|
-
respondCcApproval: (roomId: string, requestId: string, decision:
|
|
85
|
-
behavior: "allow";
|
|
86
|
-
updatedInput?: unknown;
|
|
87
|
-
} | {
|
|
88
|
-
behavior: "deny";
|
|
89
|
-
message: string;
|
|
90
|
-
}) => void;
|
|
102
|
+
respondCcApproval: (roomId: string, requestId: string, decision: CcApprovalDecision) => void;
|
|
91
103
|
}
|
|
104
|
+
type CcApprovalDecision = Extract<import("@cjhyy/code-shell-core").MobileClientEvent, {
|
|
105
|
+
type: "ccRoom.respondApproval";
|
|
106
|
+
}>["decision"];
|
|
92
107
|
export type MobileSessionCreateTarget = {
|
|
93
108
|
projectId: string;
|
|
94
109
|
rootId?: string;
|
|
@@ -101,3 +116,4 @@ export interface RemoteAppOptions {
|
|
|
101
116
|
onNotification?: (method: string, params: Record<string, unknown>) => void;
|
|
102
117
|
}
|
|
103
118
|
export declare function useRemoteApp(options?: RemoteAppOptions): RemoteApp;
|
|
119
|
+
export {};
|
|
@@ -9,6 +9,9 @@ import { MobileOutputClient } from "./mobileOutputClient.js";
|
|
|
9
9
|
import { useRemoteSocket } from "./useRemoteSocket.js";
|
|
10
10
|
import { prepareMobileAttachments, } from "../lib/mobileAttachments.js";
|
|
11
11
|
import { filterNewRoomMessages, clearUnreadSession, markSessionUnread, markRoomSeqApplied, maxRoomSeq, alignSessionEpoch, pruneUnreadSessions, rawApprovalResolvedRequestId, removeResolvedApproval, roomMessageSeq, selectSessionReplayEntries, } from "./remoteAppSync.js";
|
|
12
|
+
/** The host explicitly refused a message, so it was not admitted. */
|
|
13
|
+
class MessageRejectedError extends Error {
|
|
14
|
+
}
|
|
12
15
|
/** Tools whose grants can be remembered with a path scope (file/dir). */
|
|
13
16
|
const PATH_SCOPED = new Set([
|
|
14
17
|
"Read",
|
|
@@ -80,6 +83,10 @@ export function useRemoteApp(options = {}) {
|
|
|
80
83
|
const approvalsRef = useRef(approvals);
|
|
81
84
|
approvalsRef.current = approvals;
|
|
82
85
|
const submittingAsyncApprovalsRef = useRef(new Set());
|
|
86
|
+
/** Synchronous approvals whose card was consumed on send. A correlated
|
|
87
|
+
* delivery error asks Main to replay them if Core still holds the request. */
|
|
88
|
+
const submittedApprovalsRef = useRef(new Set());
|
|
89
|
+
const requestActiveResyncRef = useRef(() => undefined);
|
|
83
90
|
// The SELECTED project (one-true-source for "what am I looking at"), distinct
|
|
84
91
|
// from activeSessionCwd which is derived from the bound session. Drives session
|
|
85
92
|
// list filtering AND ccRoom.listSessions.
|
|
@@ -101,6 +108,8 @@ export function useRemoteApp(options = {}) {
|
|
|
101
108
|
* socket's callback), so it can't close over `socket` directly. */
|
|
102
109
|
const sendRef = useRef(null);
|
|
103
110
|
const connectionGenerationRef = useRef(0);
|
|
111
|
+
/** Image PUTs in flight; a lost connection aborts them so sending recovers. */
|
|
112
|
+
const uploadAbortsRef = useRef(new Set());
|
|
104
113
|
const uploadWaitersRef = useRef(new Map());
|
|
105
114
|
const conversationRevisionRef = useRef(0);
|
|
106
115
|
const pendingCreateRef = useRef(null);
|
|
@@ -185,6 +194,9 @@ export function useRemoteApp(options = {}) {
|
|
|
185
194
|
waiter.resolve();
|
|
186
195
|
}, []);
|
|
187
196
|
useEffect(() => () => {
|
|
197
|
+
for (const upload of uploadAbortsRef.current)
|
|
198
|
+
upload.abort(new Error("Remote app closed"));
|
|
199
|
+
uploadAbortsRef.current.clear();
|
|
188
200
|
for (const waiter of uploadWaitersRef.current.values()) {
|
|
189
201
|
clearTimeout(waiter.timer);
|
|
190
202
|
waiter.reject(new Error("Remote app closed"));
|
|
@@ -226,8 +238,10 @@ export function useRemoteApp(options = {}) {
|
|
|
226
238
|
* subscription cwd from activeProjectCwd. */
|
|
227
239
|
const ccHistoryCwdRef = useRef(undefined);
|
|
228
240
|
/** openSession replies do not echo cwd. Keep it keyed by the requested
|
|
229
|
-
* session until ccRoom.opened confirms which room acquired it.
|
|
230
|
-
|
|
241
|
+
* session until ccRoom.opened confirms which room acquired it. The
|
|
242
|
+
* conversation revision of the request lets a reply that arrives after the
|
|
243
|
+
* user navigated elsewhere be discarded instead of navigating back. */
|
|
244
|
+
const pendingCcOpensRef = useRef(new Map());
|
|
231
245
|
const ccBacklogLoadedRef = useRef(false);
|
|
232
246
|
/** Which CLI the currently-open cc session belongs to — selects the on-disk
|
|
233
247
|
* history reader (claude vs codex rollout) for ccRoom.readHistory. Captured
|
|
@@ -247,6 +261,7 @@ export function useRemoteApp(options = {}) {
|
|
|
247
261
|
}, []);
|
|
248
262
|
const clearApproval = useCallback((requestId) => {
|
|
249
263
|
submittingAsyncApprovalsRef.current.delete(requestId);
|
|
264
|
+
submittedApprovalsRef.current.delete(requestId);
|
|
250
265
|
approvalsRef.current = removeResolvedApproval(approvalsRef.current, requestId);
|
|
251
266
|
setApprovals((prev) => removeResolvedApproval(prev, requestId));
|
|
252
267
|
}, []);
|
|
@@ -317,6 +332,9 @@ export function useRemoteApp(options = {}) {
|
|
|
317
332
|
if (a.sessionId) {
|
|
318
333
|
if (boundSessionRef.current && a.sessionId !== boundSessionRef.current)
|
|
319
334
|
return;
|
|
335
|
+
// A pending creation owns the unbound view; only its acceptance binds it.
|
|
336
|
+
if (!boundSessionRef.current && pendingCreateRef.current)
|
|
337
|
+
return;
|
|
320
338
|
if (!boundSessionRef.current) {
|
|
321
339
|
boundSessionRef.current = a.sessionId;
|
|
322
340
|
setActiveSessionId(a.sessionId);
|
|
@@ -558,6 +576,12 @@ export function useRemoteApp(options = {}) {
|
|
|
558
576
|
break;
|
|
559
577
|
}
|
|
560
578
|
}
|
|
579
|
+
else if (pendingCreateRef.current) {
|
|
580
|
+
// Another task's output while a new session awaits its acceptance:
|
|
581
|
+
// binding it would move the new conversation (and its draft) there.
|
|
582
|
+
setUnreadSessionIds((prev) => markSessionUnread(prev, event.sessionId));
|
|
583
|
+
break;
|
|
584
|
+
}
|
|
561
585
|
else {
|
|
562
586
|
boundSessionRef.current = event.sessionId;
|
|
563
587
|
setActiveSessionId(event.sessionId);
|
|
@@ -753,6 +777,11 @@ export function useRemoteApp(options = {}) {
|
|
|
753
777
|
case "room.closed":
|
|
754
778
|
// Keep the internal room snapshot in sync (resolves activeRoom metadata).
|
|
755
779
|
setRooms((prev) => prev.map((r) => (r.id === event.roomId ? { ...r, open: false } : r)));
|
|
780
|
+
// The room's process is gone, so no turn is running any more. Its
|
|
781
|
+
// stopped process cannot report its own turn end.
|
|
782
|
+
if (event.roomId === activeRoomIdRef.current) {
|
|
783
|
+
dispatchChat({ kind: "raw", raw: { type: "turn_complete", reason: "aborted_tools" } });
|
|
784
|
+
}
|
|
756
785
|
break;
|
|
757
786
|
case "room.recovery":
|
|
758
787
|
if (event.roomId === activeRoomIdRef.current) {
|
|
@@ -772,16 +801,21 @@ export function useRemoteApp(options = {}) {
|
|
|
772
801
|
setLoadingKey("roomHistory", false);
|
|
773
802
|
}
|
|
774
803
|
if (event.clientMessageId) {
|
|
775
|
-
settleMessageAck(event.clientMessageId, new
|
|
804
|
+
settleMessageAck(event.clientMessageId, new MessageRejectedError(event.message));
|
|
776
805
|
}
|
|
777
806
|
setNotice(event.message || t("mobile.notice.roomError"));
|
|
778
807
|
break;
|
|
779
808
|
case "error":
|
|
780
809
|
if (event.approvalId) {
|
|
781
810
|
submittingAsyncApprovalsRef.current.delete(event.approvalId);
|
|
811
|
+
// The decision did not reach Core. The request may still be
|
|
812
|
+
// pending there; an authoritative resync replays its card.
|
|
813
|
+
if (submittedApprovalsRef.current.delete(event.approvalId)) {
|
|
814
|
+
requestActiveResyncRef.current();
|
|
815
|
+
}
|
|
782
816
|
}
|
|
783
817
|
if (event.clientMessageId) {
|
|
784
|
-
settleMessageAck(event.clientMessageId, new
|
|
818
|
+
settleMessageAck(event.clientMessageId, new MessageRejectedError(event.message));
|
|
785
819
|
}
|
|
786
820
|
setNotice(event.message);
|
|
787
821
|
setLoading({
|
|
@@ -829,9 +863,13 @@ export function useRemoteApp(options = {}) {
|
|
|
829
863
|
setLoadingKey("ccSessions", false);
|
|
830
864
|
}
|
|
831
865
|
break;
|
|
832
|
-
case "ccRoom.opened":
|
|
866
|
+
case "ccRoom.opened": {
|
|
867
|
+
const request = pendingCcOpensRef.current.get(event.sessionId);
|
|
868
|
+
pendingCcOpensRef.current.delete(event.sessionId);
|
|
869
|
+
// Only this browser's latest open request may change navigation.
|
|
870
|
+
if (!request || request.revision !== conversationRevisionRef.current)
|
|
871
|
+
break;
|
|
833
872
|
if (event.status === "missing") {
|
|
834
|
-
pendingCcOpenCwdsRef.current.delete(event.sessionId);
|
|
835
873
|
setNotice(t("mobile.notice.ccRoomOpenFailed"));
|
|
836
874
|
break;
|
|
837
875
|
}
|
|
@@ -854,17 +892,14 @@ export function useRemoteApp(options = {}) {
|
|
|
854
892
|
...room,
|
|
855
893
|
open: event.status === "running",
|
|
856
894
|
observing: event.status === "observing",
|
|
895
|
+
...(event.permissionMode ? { permissionMode: event.permissionMode } : {}),
|
|
857
896
|
}
|
|
858
897
|
: room));
|
|
859
898
|
// Refresh metadata even when this room was created after the last
|
|
860
899
|
// room.list snapshot, so the mobile view can display read-only state.
|
|
861
900
|
sendRef.current?.({ type: "room.list" });
|
|
862
901
|
ccHistorySessionRef.current = event.sessionId;
|
|
863
|
-
ccHistoryCwdRef.current =
|
|
864
|
-
pendingCcOpenCwdsRef.current.get(event.sessionId) ??
|
|
865
|
-
activeProjectCwdRef.current ??
|
|
866
|
-
undefined;
|
|
867
|
-
pendingCcOpenCwdsRef.current.delete(event.sessionId);
|
|
902
|
+
ccHistoryCwdRef.current = request.cwd;
|
|
868
903
|
ccBacklogLoadedRef.current = false;
|
|
869
904
|
boundSessionRef.current = undefined;
|
|
870
905
|
setApprovals([]);
|
|
@@ -887,6 +922,7 @@ export function useRemoteApp(options = {}) {
|
|
|
887
922
|
requestRoomCatchup(event.roomId, 0);
|
|
888
923
|
}
|
|
889
924
|
break;
|
|
925
|
+
}
|
|
890
926
|
case "ccRoom.transcriptSubscribed":
|
|
891
927
|
// Atomic snapshot + live subscription. roomCursor identifies exactly
|
|
892
928
|
// where the snapshot ended in the shared room log; the incremental
|
|
@@ -948,6 +984,9 @@ export function useRemoteApp(options = {}) {
|
|
|
948
984
|
risk,
|
|
949
985
|
options: askUser?.options,
|
|
950
986
|
optionsOnly: false,
|
|
987
|
+
...(askUser?.questions && askUser.questions.length > 1
|
|
988
|
+
? { questions: askUser.questions }
|
|
989
|
+
: {}),
|
|
951
990
|
pathScoped: PATH_SCOPED.has(event.req.toolName),
|
|
952
991
|
},
|
|
953
992
|
]);
|
|
@@ -1024,6 +1063,9 @@ export function useRemoteApp(options = {}) {
|
|
|
1024
1063
|
if (sid && sid !== boundSessionRef.current)
|
|
1025
1064
|
return;
|
|
1026
1065
|
}
|
|
1066
|
+
else if (pendingCreateRef.current) {
|
|
1067
|
+
return; // the pending creation's acceptance decides the binding
|
|
1068
|
+
}
|
|
1027
1069
|
else if (sid) {
|
|
1028
1070
|
// Auto-bind to the first session we see so the feed is coherent.
|
|
1029
1071
|
boundSessionRef.current = sid;
|
|
@@ -1084,6 +1126,7 @@ export function useRemoteApp(options = {}) {
|
|
|
1084
1126
|
...(cursor.epoch ? { epoch: cursor.epoch } : {}),
|
|
1085
1127
|
});
|
|
1086
1128
|
}, [setLoadingKey, sessionCursor, requestRoomCatchup]);
|
|
1129
|
+
requestActiveResyncRef.current = requestActiveResync;
|
|
1087
1130
|
const socket = useRemoteSocket({
|
|
1088
1131
|
onServerEvent,
|
|
1089
1132
|
onRawLine,
|
|
@@ -1096,7 +1139,11 @@ export function useRemoteApp(options = {}) {
|
|
|
1096
1139
|
return;
|
|
1097
1140
|
outputClient.cancel(false);
|
|
1098
1141
|
submittingAsyncApprovalsRef.current.clear();
|
|
1142
|
+
submittedApprovalsRef.current.clear();
|
|
1099
1143
|
const error = new Error("Remote connection was lost");
|
|
1144
|
+
for (const upload of uploadAbortsRef.current)
|
|
1145
|
+
upload.abort(error);
|
|
1146
|
+
uploadAbortsRef.current.clear();
|
|
1100
1147
|
for (const waiter of uploadWaitersRef.current.values()) {
|
|
1101
1148
|
clearTimeout(waiter.timer);
|
|
1102
1149
|
waiter.reject(error);
|
|
@@ -1119,17 +1166,21 @@ export function useRemoteApp(options = {}) {
|
|
|
1119
1166
|
const conversationRevision = conversationRevisionRef.current;
|
|
1120
1167
|
const roomId = activeRoomIdRef.current;
|
|
1121
1168
|
const sessionId = boundSessionRef.current;
|
|
1122
|
-
const clientMessageId =
|
|
1169
|
+
const clientMessageId = input.clientMessageId ??
|
|
1170
|
+
globalThis.crypto?.randomUUID?.() ??
|
|
1123
1171
|
`mobile-message-${Date.now()}-${Math.random().toString(36).slice(2)}`;
|
|
1124
1172
|
let attachments;
|
|
1125
1173
|
if (roomId && roomsRef.current.find((room) => room.id === roomId)?.observing) {
|
|
1126
1174
|
setNotice(t("mobile.notice.ccRoomObserving"));
|
|
1127
1175
|
return false;
|
|
1128
1176
|
}
|
|
1177
|
+
const upload = new AbortController();
|
|
1178
|
+
uploadAbortsRef.current.add(upload);
|
|
1129
1179
|
try {
|
|
1130
1180
|
attachments = await prepareMobileAttachments(input.attachments, {
|
|
1131
1181
|
beginUpload: (metadata) => beginMobileUpload(metadata, connectionGeneration),
|
|
1132
1182
|
fetch: window.fetch.bind(window),
|
|
1183
|
+
signal: upload.signal,
|
|
1133
1184
|
});
|
|
1134
1185
|
}
|
|
1135
1186
|
catch (error) {
|
|
@@ -1138,6 +1189,9 @@ export function useRemoteApp(options = {}) {
|
|
|
1138
1189
|
}));
|
|
1139
1190
|
return false;
|
|
1140
1191
|
}
|
|
1192
|
+
finally {
|
|
1193
|
+
uploadAbortsRef.current.delete(upload);
|
|
1194
|
+
}
|
|
1141
1195
|
if (conversationRevision !== conversationRevisionRef.current)
|
|
1142
1196
|
return false;
|
|
1143
1197
|
const summaries = attachments.map(({ name, mime, size }) => ({ name, mime, size }));
|
|
@@ -1159,6 +1213,7 @@ export function useRemoteApp(options = {}) {
|
|
|
1159
1213
|
else {
|
|
1160
1214
|
queued = socket.send({
|
|
1161
1215
|
type: "chat.send",
|
|
1216
|
+
...(input.modelTraceSource ? { modelTraceSource: input.modelTraceSource } : {}),
|
|
1162
1217
|
text,
|
|
1163
1218
|
sessionId,
|
|
1164
1219
|
clientMessageId,
|
|
@@ -1174,6 +1229,9 @@ export function useRemoteApp(options = {}) {
|
|
|
1174
1229
|
await acknowledged;
|
|
1175
1230
|
}
|
|
1176
1231
|
catch (error) {
|
|
1232
|
+
// Only an explicit host rejection proves the message was not admitted.
|
|
1233
|
+
if (!(error instanceof MessageRejectedError))
|
|
1234
|
+
input.onUncertain?.(clientMessageId);
|
|
1177
1235
|
setNotice(t("mobile.notice.attachmentSendFailed", {
|
|
1178
1236
|
error: error instanceof Error ? error.message : String(error),
|
|
1179
1237
|
}));
|
|
@@ -1190,6 +1248,14 @@ export function useRemoteApp(options = {}) {
|
|
|
1190
1248
|
return true;
|
|
1191
1249
|
}, [beginMobileUpload, setNotice, settleMessageAck, socket, waitForMessageAck]);
|
|
1192
1250
|
const stopRun = useCallback(() => {
|
|
1251
|
+
const roomId = activeRoomIdRef.current;
|
|
1252
|
+
if (roomId) {
|
|
1253
|
+
// A CLI room runs in its own resident process, not a native session.
|
|
1254
|
+
// Closing it stops the process; the next message resumes the same CLI
|
|
1255
|
+
// conversation.
|
|
1256
|
+
socket.send({ type: "room.close", roomId });
|
|
1257
|
+
return;
|
|
1258
|
+
}
|
|
1193
1259
|
socket.send({ type: "run.stop", sessionId: boundSessionRef.current });
|
|
1194
1260
|
}, [socket]);
|
|
1195
1261
|
const refreshSessions = useCallback(() => {
|
|
@@ -1246,22 +1312,51 @@ export function useRemoteApp(options = {}) {
|
|
|
1246
1312
|
// Pin the CLI this session belongs to so its on-disk backlog is read with
|
|
1247
1313
|
// the right reader even if the user switches the pane's CLI afterward.
|
|
1248
1314
|
ccHistoryKindRef.current = ccCliKindRef.current;
|
|
1249
|
-
|
|
1315
|
+
pendingCcOpensRef.current.set(sessionId, {
|
|
1316
|
+
cwd,
|
|
1317
|
+
revision: conversationRevisionRef.current,
|
|
1318
|
+
});
|
|
1250
1319
|
socket.send({ type: "ccRoom.openSession", sessionId, cwd, mode, kind: ccCliKindRef.current });
|
|
1251
1320
|
}, [socket]);
|
|
1252
1321
|
const respondCcApproval = useCallback((roomId, requestId, decision) => {
|
|
1253
1322
|
const a = approvalsRef.current.find((p) => p.requestId === requestId);
|
|
1254
1323
|
if (!a)
|
|
1255
1324
|
return; // approve-once: already resolved
|
|
1256
|
-
|
|
1325
|
+
// An unsent response never reaches the CLI, which is still waiting.
|
|
1326
|
+
// Keep the card so the user can retry once the connection recovers.
|
|
1327
|
+
if (!socket.send({ type: "ccRoom.respondApproval", roomId, requestId, decision })) {
|
|
1328
|
+
setNotice(t("mobile.notice.approvalSendFailed"));
|
|
1329
|
+
return;
|
|
1330
|
+
}
|
|
1257
1331
|
approvalsRef.current = approvalsRef.current.filter((p) => p.requestId !== requestId);
|
|
1258
1332
|
setApprovals((prev) => prev.filter((p) => p.requestId !== requestId));
|
|
1259
|
-
}, [socket]);
|
|
1333
|
+
}, [setNotice, socket]);
|
|
1260
1334
|
const newSession = useCallback((target, name) => {
|
|
1261
|
-
outputClient.cancel();
|
|
1262
|
-
conversationRevisionRef.current += 1;
|
|
1263
1335
|
const clientRequestId = globalThis.crypto?.randomUUID?.() ??
|
|
1264
1336
|
`mobile-create-${Date.now()}-${Math.random().toString(36).slice(2)}`;
|
|
1337
|
+
const stableTarget = target === undefined && activeProjectIdRef.current
|
|
1338
|
+
? { projectId: activeProjectIdRef.current }
|
|
1339
|
+
: target;
|
|
1340
|
+
// Creation needs the host's acceptance. Send it before switching the
|
|
1341
|
+
// view: offline, a dropped frame used to leave the view on the new target
|
|
1342
|
+
// while a later first send fell back to the previous (possibly another
|
|
1343
|
+
// project's) session.
|
|
1344
|
+
const created = socket.send(stableTarget && typeof stableTarget === "object"
|
|
1345
|
+
? { type: "session.create", clientRequestId, ...stableTarget, ...(name ? { name } : {}) }
|
|
1346
|
+
: stableTarget === undefined
|
|
1347
|
+
? { type: "session.create", clientRequestId, ...(name ? { name } : {}) }
|
|
1348
|
+
: {
|
|
1349
|
+
type: "session.create",
|
|
1350
|
+
clientRequestId,
|
|
1351
|
+
cwd: stableTarget,
|
|
1352
|
+
...(name ? { name } : {}),
|
|
1353
|
+
});
|
|
1354
|
+
if (!created) {
|
|
1355
|
+
setNotice(t("mobile.notice.createOffline"));
|
|
1356
|
+
return;
|
|
1357
|
+
}
|
|
1358
|
+
outputClient.cancel();
|
|
1359
|
+
conversationRevisionRef.current += 1;
|
|
1265
1360
|
pendingCreateRef.current = {
|
|
1266
1361
|
clientRequestId,
|
|
1267
1362
|
conversationRevision: conversationRevisionRef.current,
|
|
@@ -1273,9 +1368,6 @@ export function useRemoteApp(options = {}) {
|
|
|
1273
1368
|
roomId: activeRoomIdRef.current,
|
|
1274
1369
|
});
|
|
1275
1370
|
}
|
|
1276
|
-
const stableTarget = target === undefined && activeProjectIdRef.current
|
|
1277
|
-
? { projectId: activeProjectIdRef.current }
|
|
1278
|
-
: target;
|
|
1279
1371
|
const selectedProject = stableTarget && typeof stableTarget === "object" && stableTarget.projectId
|
|
1280
1372
|
? projectsRef.current.find((project) => project.id === stableTarget.projectId)
|
|
1281
1373
|
: undefined;
|
|
@@ -1315,25 +1407,7 @@ export function useRemoteApp(options = {}) {
|
|
|
1315
1407
|
setApprovals([]);
|
|
1316
1408
|
setLoadingKey("sessionHistory", false);
|
|
1317
1409
|
dispatchChat({ kind: "reset" });
|
|
1318
|
-
|
|
1319
|
-
socket.send({
|
|
1320
|
-
type: "session.create",
|
|
1321
|
-
clientRequestId,
|
|
1322
|
-
...stableTarget,
|
|
1323
|
-
...(name ? { name } : {}),
|
|
1324
|
-
});
|
|
1325
|
-
}
|
|
1326
|
-
else {
|
|
1327
|
-
socket.send(stableTarget === undefined
|
|
1328
|
-
? { type: "session.create", clientRequestId, ...(name ? { name } : {}) }
|
|
1329
|
-
: {
|
|
1330
|
-
type: "session.create",
|
|
1331
|
-
clientRequestId,
|
|
1332
|
-
cwd: stableTarget,
|
|
1333
|
-
...(name ? { name } : {}),
|
|
1334
|
-
});
|
|
1335
|
-
}
|
|
1336
|
-
}, [socket, setLoadingKey]);
|
|
1410
|
+
}, [socket, setLoadingKey, setNotice]);
|
|
1337
1411
|
const respondApproval = useCallback((requestId, decision, opts) => {
|
|
1338
1412
|
const a = approvalsRef.current.find((p) => p.requestId === requestId);
|
|
1339
1413
|
// Dedup at the source: if this approval is already gone from the ref it was
|
|
@@ -1361,12 +1435,17 @@ export function useRemoteApp(options = {}) {
|
|
|
1361
1435
|
submittingAsyncApprovalsRef.current.add(requestId);
|
|
1362
1436
|
return;
|
|
1363
1437
|
}
|
|
1438
|
+
submittedApprovalsRef.current.add(requestId);
|
|
1364
1439
|
// Update the ref synchronously too so a second tap in the same tick (before
|
|
1365
1440
|
// the async setApprovals commits) sees it gone and no-ops above.
|
|
1366
1441
|
approvalsRef.current = approvalsRef.current.filter((p) => p.requestId !== requestId);
|
|
1367
1442
|
setApprovals((prev) => prev.filter((p) => p.requestId !== requestId));
|
|
1368
1443
|
}, [socket]);
|
|
1369
1444
|
const setPermissionMode = useCallback((mode) => {
|
|
1445
|
+
// A CLI room's mode is a spawn-time argument of its process; the native
|
|
1446
|
+
// session mode does not govern it. Never pretend to change it here.
|
|
1447
|
+
if (activeRoomIdRef.current)
|
|
1448
|
+
return;
|
|
1370
1449
|
setPermissionModeState(mode);
|
|
1371
1450
|
socket.send({ type: "permission.setMode", sessionId: boundSessionRef.current, mode });
|
|
1372
1451
|
}, [socket]);
|
|
@@ -1388,6 +1467,11 @@ export function useRemoteApp(options = {}) {
|
|
|
1388
1467
|
ccHistorySessionRef.current = undefined;
|
|
1389
1468
|
ccHistoryCwdRef.current = undefined;
|
|
1390
1469
|
ccBacklogLoadedRef.current = false;
|
|
1470
|
+
// The room's cards belong to the conversation being left and must not
|
|
1471
|
+
// block other conversations. The host keeps the requests pending and
|
|
1472
|
+
// replays them when the room is opened again.
|
|
1473
|
+
approvalsRef.current = approvalsRef.current.filter((approval) => !approval.roomId);
|
|
1474
|
+
setApprovals((prev) => prev.filter((approval) => !approval.roomId));
|
|
1391
1475
|
setLoadingKey("roomHistory", false);
|
|
1392
1476
|
dispatchChat({ kind: "reset" });
|
|
1393
1477
|
}, [setLoadingKey, socket]);
|
|
@@ -1439,6 +1523,7 @@ export function useRemoteApp(options = {}) {
|
|
|
1439
1523
|
activeCwd,
|
|
1440
1524
|
projects,
|
|
1441
1525
|
activeRoom,
|
|
1526
|
+
activeRoomId,
|
|
1442
1527
|
approvals,
|
|
1443
1528
|
permissionMode,
|
|
1444
1529
|
loading,
|
package/dist/i18n/mobile.d.ts
CHANGED
|
@@ -97,6 +97,8 @@ export declare const mobile: {
|
|
|
97
97
|
readonly ccRoomOpenFailed: "cc 会话无法打开";
|
|
98
98
|
readonly ccRoomObserving: "此 CC 会话当前为只读查看,消息未发送";
|
|
99
99
|
readonly attachmentSendFailed: "图片发送失败:{error}";
|
|
100
|
+
readonly createOffline: "连接已断开,重新连接后才能新建任务。";
|
|
101
|
+
readonly approvalSendFailed: "审批未送达,请检查连接后重试";
|
|
100
102
|
};
|
|
101
103
|
readonly permissionMode: {
|
|
102
104
|
readonly default: "默认";
|
|
@@ -261,6 +263,8 @@ export declare const mobile: {
|
|
|
261
263
|
readonly ccRoomOpenFailed: "Could not open cc session";
|
|
262
264
|
readonly ccRoomObserving: "This CC session is read-only; the message was not sent";
|
|
263
265
|
readonly attachmentSendFailed: "Could not send images: {error}";
|
|
266
|
+
readonly createOffline: "Disconnected. Reconnect before starting a new task.";
|
|
267
|
+
readonly approvalSendFailed: "The approval was not delivered. Check the connection and try again.";
|
|
264
268
|
};
|
|
265
269
|
readonly permissionMode: {
|
|
266
270
|
readonly default: "Default";
|
package/dist/i18n/mobile.js
CHANGED
|
@@ -97,6 +97,8 @@ export const mobile = {
|
|
|
97
97
|
ccRoomOpenFailed: "cc 会话无法打开",
|
|
98
98
|
ccRoomObserving: "此 CC 会话当前为只读查看,消息未发送",
|
|
99
99
|
attachmentSendFailed: "图片发送失败:{error}",
|
|
100
|
+
createOffline: "连接已断开,重新连接后才能新建任务。",
|
|
101
|
+
approvalSendFailed: "审批未送达,请检查连接后重试",
|
|
100
102
|
},
|
|
101
103
|
permissionMode: {
|
|
102
104
|
default: "默认",
|
|
@@ -261,6 +263,8 @@ export const mobile = {
|
|
|
261
263
|
ccRoomOpenFailed: "Could not open cc session",
|
|
262
264
|
ccRoomObserving: "This CC session is read-only; the message was not sent",
|
|
263
265
|
attachmentSendFailed: "Could not send images: {error}",
|
|
266
|
+
createOffline: "Disconnected. Reconnect before starting a new task.",
|
|
267
|
+
approvalSendFailed: "The approval was not delivered. Check the connection and try again.",
|
|
264
268
|
},
|
|
265
269
|
permissionMode: {
|
|
266
270
|
default: "Default",
|
package/dist/i18n/translate.d.ts
CHANGED
|
@@ -107,6 +107,8 @@ export declare const webMessages: {
|
|
|
107
107
|
readonly ccRoomOpenFailed: "cc 会话无法打开";
|
|
108
108
|
readonly ccRoomObserving: "此 CC 会话当前为只读查看,消息未发送";
|
|
109
109
|
readonly attachmentSendFailed: "图片发送失败:{error}";
|
|
110
|
+
readonly createOffline: "连接已断开,重新连接后才能新建任务。";
|
|
111
|
+
readonly approvalSendFailed: "审批未送达,请检查连接后重试";
|
|
110
112
|
};
|
|
111
113
|
readonly permissionMode: {
|
|
112
114
|
readonly default: "默认";
|
|
@@ -271,6 +273,8 @@ export declare const webMessages: {
|
|
|
271
273
|
readonly ccRoomOpenFailed: "Could not open cc session";
|
|
272
274
|
readonly ccRoomObserving: "This CC session is read-only; the message was not sent";
|
|
273
275
|
readonly attachmentSendFailed: "Could not send images: {error}";
|
|
276
|
+
readonly createOffline: "Disconnected. Reconnect before starting a new task.";
|
|
277
|
+
readonly approvalSendFailed: "The approval was not delivered. Check the connection and try again.";
|
|
274
278
|
};
|
|
275
279
|
readonly permissionMode: {
|
|
276
280
|
readonly default: "Default";
|
package/dist/index.d.ts
CHANGED
|
@@ -25,7 +25,9 @@ export * from "./lib/mobileAttachments.js";
|
|
|
25
25
|
export * from "./lib/uiLanguage.js";
|
|
26
26
|
export * from "./link-authorization.js";
|
|
27
27
|
export * from "./link-authorization-view.js";
|
|
28
|
+
export * from "./model-trace-api.js";
|
|
28
29
|
export { mobile } from "./i18n/mobile.js";
|
|
29
30
|
export { translate, t, webMessages, type WebTranslationKey, type WebTranslateParams, } from "./i18n/translate.js";
|
|
30
31
|
export { isEnvironmentDescriptor, projectReferenceKey } from "./lib/environment.js";
|
|
31
32
|
export type { EnvironmentDescriptor, ProjectReference } from "./lib/environment.js";
|
|
33
|
+
export { ModelTraceExplorer } from "./components/ModelTraceExplorer.js";
|
package/dist/index.js
CHANGED
|
@@ -28,7 +28,9 @@ export * from "./lib/mobileAttachments.js";
|
|
|
28
28
|
export * from "./lib/uiLanguage.js";
|
|
29
29
|
export * from "./link-authorization.js";
|
|
30
30
|
export * from "./link-authorization-view.js";
|
|
31
|
+
export * from "./model-trace-api.js";
|
|
31
32
|
// ── i18n: mobile namespace dict + zh-fallback translate ─────────────────────
|
|
32
33
|
export { mobile } from "./i18n/mobile.js";
|
|
33
34
|
export { translate, t, webMessages, } from "./i18n/translate.js";
|
|
34
35
|
export { isEnvironmentDescriptor, projectReferenceKey } from "./lib/environment.js";
|
|
36
|
+
export { ModelTraceExplorer } from "./components/ModelTraceExplorer.js";
|
package/dist/lib/format.d.ts
CHANGED
|
@@ -5,10 +5,13 @@ interface ProjectPathLike {
|
|
|
5
5
|
id?: string;
|
|
6
6
|
path: string;
|
|
7
7
|
name: string;
|
|
8
|
+
pathCaseSensitive?: boolean;
|
|
8
9
|
roots?: Array<{
|
|
9
10
|
path: string;
|
|
10
11
|
}>;
|
|
11
12
|
}
|
|
13
|
+
/** Compare Host paths using that project's declared canonical-key policy. */
|
|
14
|
+
export declare function sameProjectPath(left: string | null | undefined, right: string | null | undefined, project?: ProjectPathLike): boolean;
|
|
12
15
|
/** Return the desktop project whose V2 root owns `cwd` (longest prefix wins). */
|
|
13
16
|
export declare function projectForCwd<T extends ProjectPathLike>(cwd: string | null | undefined, projects?: T[]): T | undefined;
|
|
14
17
|
/** Group items by their cwd (project). When desktop project order is provided
|
package/dist/lib/format.js
CHANGED
|
@@ -8,14 +8,30 @@ export function basename(p) {
|
|
|
8
8
|
const parts = p.replace(/[/\\]+$/, "").split(/[/\\]/);
|
|
9
9
|
return parts[parts.length - 1] ?? "";
|
|
10
10
|
}
|
|
11
|
-
function
|
|
12
|
-
|
|
11
|
+
function isWindowsPath(p) {
|
|
12
|
+
return /^[a-z]:[/\\]/i.test(p) || /^\\\\[^\\]+\\[^\\]+/.test(p);
|
|
13
|
+
}
|
|
14
|
+
function normalizePath(p, project) {
|
|
15
|
+
const windows = isWindowsPath(project.path);
|
|
16
|
+
const normalized = (windows ? p.replace(/\\/g, "/") : p).replace(/\/+$/, "");
|
|
13
17
|
return normalized === "" ? "/" : normalized;
|
|
14
18
|
}
|
|
15
|
-
function
|
|
16
|
-
const
|
|
17
|
-
const
|
|
18
|
-
return
|
|
19
|
+
function comparisonPath(path, project) {
|
|
20
|
+
const normalized = normalizePath(path, project);
|
|
21
|
+
const sensitive = project.pathCaseSensitive ?? !isWindowsPath(project.path);
|
|
22
|
+
return sensitive ? normalized : normalized.toLocaleLowerCase("en-US");
|
|
23
|
+
}
|
|
24
|
+
function isSameOrInside(cwd, root, project) {
|
|
25
|
+
const target = comparisonPath(cwd, project);
|
|
26
|
+
const base = comparisonPath(root, project);
|
|
27
|
+
return target === base || target.startsWith(base.endsWith("/") ? base : `${base}/`);
|
|
28
|
+
}
|
|
29
|
+
/** Compare Host paths using that project's declared canonical-key policy. */
|
|
30
|
+
export function sameProjectPath(left, right, project) {
|
|
31
|
+
if (!left || !right)
|
|
32
|
+
return left === right;
|
|
33
|
+
const policy = project ?? { path: left, name: "" };
|
|
34
|
+
return comparisonPath(left, policy) === comparisonPath(right, policy);
|
|
19
35
|
}
|
|
20
36
|
/** Return the desktop project whose V2 root owns `cwd` (longest prefix wins). */
|
|
21
37
|
export function projectForCwd(cwd, projects = []) {
|
|
@@ -26,9 +42,9 @@ export function projectForCwd(cwd, projects = []) {
|
|
|
26
42
|
for (const p of projects) {
|
|
27
43
|
const roots = p.roots?.length ? p.roots.map((root) => root.path) : [p.path];
|
|
28
44
|
for (const root of roots) {
|
|
29
|
-
if (!root || !isSameOrInside(cwd, root))
|
|
45
|
+
if (!root || !isSameOrInside(cwd, root, p))
|
|
30
46
|
continue;
|
|
31
|
-
const len = normalizePath(root).length;
|
|
47
|
+
const len = normalizePath(root, p).length;
|
|
32
48
|
if (len > bestLen) {
|
|
33
49
|
best = p;
|
|
34
50
|
bestLen = len;
|
|
@@ -17,6 +17,8 @@ export interface MobileUploadTicket {
|
|
|
17
17
|
interface PrepareOptions {
|
|
18
18
|
beginUpload: (metadata: MobileImageBase) => Promise<MobileUploadTicket>;
|
|
19
19
|
fetch: typeof fetch;
|
|
20
|
+
/** Aborts an upload already in flight, e.g. when its connection is lost. */
|
|
21
|
+
signal?: AbortSignal;
|
|
20
22
|
}
|
|
21
23
|
export declare function normalizeMobileImage(file: File): Promise<File>;
|
|
22
24
|
export declare function prepareMobileAttachments(attachments: MobileComposerAttachment[], options: PrepareOptions): Promise<MobileImageAttachment[]>;
|
|
@@ -44,13 +44,15 @@ async function canvasJpeg(file) {
|
|
|
44
44
|
}
|
|
45
45
|
}
|
|
46
46
|
export async function normalizeMobileImage(file) {
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
47
|
+
// An accepted MIME alias still follows the ordinary conversion/size rules.
|
|
48
|
+
// Wrap the Blob directly to preserve bytes without an extra full-buffer copy.
|
|
49
|
+
if (file.type.toLowerCase() === "image/jpg") {
|
|
50
|
+
file = new File([file], file.name, {
|
|
50
51
|
type: "image/jpeg",
|
|
51
52
|
lastModified: file.lastModified,
|
|
52
53
|
});
|
|
53
54
|
}
|
|
55
|
+
const mime = file.type.toLowerCase();
|
|
54
56
|
if (SUPPORTED_MIMES.has(mime) && file.size <= MOBILE_MAX_IMAGE_BYTES) {
|
|
55
57
|
return file;
|
|
56
58
|
}
|
|
@@ -112,6 +114,7 @@ export async function prepareMobileAttachments(attachments, options) {
|
|
|
112
114
|
headers: { "Content-Type": mime },
|
|
113
115
|
body: file,
|
|
114
116
|
credentials: "same-origin",
|
|
117
|
+
...(options.signal ? { signal: options.signal } : {}),
|
|
115
118
|
});
|
|
116
119
|
if (!response.ok)
|
|
117
120
|
throw new Error(`Image upload failed (${response.status})`);
|