@cjhyy/code-shell-web 0.9.44 → 0.9.47
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/mobileOutputClient.d.ts +4 -0
- package/dist/hooks/mobileOutputClient.js +83 -28
- 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/mobileOutputRecovery.d.ts +12 -0
- package/dist/lib/mobileOutputRecovery.js +155 -11
- 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
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})`);
|
|
@@ -4,6 +4,16 @@ import { initialChatState } from "./streamReducer.js";
|
|
|
4
4
|
type Reply = Extract<MobileServerEvent, {
|
|
5
5
|
type: "session.outputJournal";
|
|
6
6
|
}>;
|
|
7
|
+
type BaseReply = Extract<MobileServerEvent, {
|
|
8
|
+
type: "session.legacyBase";
|
|
9
|
+
}>;
|
|
10
|
+
export type MobileLegacyBaseOptions = {
|
|
11
|
+
throughEventId: string;
|
|
12
|
+
cursor?: never;
|
|
13
|
+
} | {
|
|
14
|
+
cursor: string;
|
|
15
|
+
throughEventId?: never;
|
|
16
|
+
};
|
|
7
17
|
/** Valid identities may arrive before their new Core Run acquires a durable anchor. */
|
|
8
18
|
export declare class MobilePendingInput extends Error {
|
|
9
19
|
readonly ids: string[];
|
|
@@ -27,6 +37,8 @@ type MobileOutputRecoveryResult = {
|
|
|
27
37
|
/** Build a private candidate, then join its frozen prefix to an actual Main snapshot. */
|
|
28
38
|
export declare function recoverMobileOutput(args: {
|
|
29
39
|
read: (options: OutputJournalOptions) => Promise<Reply>;
|
|
40
|
+
/** Supplied only after an implementing host explicitly advertises the capability. */
|
|
41
|
+
readBase?: (options: MobileLegacyBaseOptions) => Promise<BaseReply>;
|
|
30
42
|
canContinue: () => boolean;
|
|
31
43
|
latestCursor: () => string | undefined;
|
|
32
44
|
requireJournal?: boolean;
|
|
@@ -1,6 +1,87 @@
|
|
|
1
1
|
import { applyOutputJournalPage, compareOutputCursors, } from "./outputJournalRecovery.js";
|
|
2
2
|
import { assertOutputCoverage } from "./outputCoverage.js";
|
|
3
3
|
import { initialChatState, reduceStream } from "./streamReducer.js";
|
|
4
|
+
const MiB = 1024 * 1024;
|
|
5
|
+
const wireBytes = (value) => new TextEncoder().encode(JSON.stringify(value)).length;
|
|
6
|
+
function record(value) {
|
|
7
|
+
return !!value && typeof value === "object" && !Array.isArray(value);
|
|
8
|
+
}
|
|
9
|
+
function onlyKeys(value, keys) {
|
|
10
|
+
return Object.keys(value).every((key) => keys.includes(key));
|
|
11
|
+
}
|
|
12
|
+
const BASE_EVENT_KEYS = {
|
|
13
|
+
session_started: ["type", "sessionId"],
|
|
14
|
+
stream_request_start: ["type", "agentId"],
|
|
15
|
+
assistant_message: ["type", "agentId"],
|
|
16
|
+
text_delta: ["type", "text", "agentId"],
|
|
17
|
+
thinking_delta: ["type", "text", "agentId"],
|
|
18
|
+
user_message: ["type", "text", "attachments", "clientMessageId"],
|
|
19
|
+
tool_use_start: ["type", "toolCall", "agentId"],
|
|
20
|
+
tool_use_args_delta: ["type", "toolCallId", "args", "agentId"],
|
|
21
|
+
tool_result: ["type", "result", "agentId"],
|
|
22
|
+
agent_start: ["type", "agentId", "name", "description", "status"],
|
|
23
|
+
error: ["type", "error"],
|
|
24
|
+
turn_complete: ["type", "reason"],
|
|
25
|
+
};
|
|
26
|
+
/** Only the display projection's event schema is accepted on this new wire path. */
|
|
27
|
+
function baseEvent(value, sessionId) {
|
|
28
|
+
if (!record(value) ||
|
|
29
|
+
typeof value.type !== "string" ||
|
|
30
|
+
!Object.hasOwn(BASE_EVENT_KEYS, value.type) ||
|
|
31
|
+
!onlyKeys(value, BASE_EVENT_KEYS[value.type]) ||
|
|
32
|
+
(value.agentId !== undefined && typeof value.agentId !== "string"))
|
|
33
|
+
return false;
|
|
34
|
+
switch (value.type) {
|
|
35
|
+
case "session_started":
|
|
36
|
+
return value.sessionId === sessionId;
|
|
37
|
+
case "stream_request_start":
|
|
38
|
+
case "assistant_message":
|
|
39
|
+
return true;
|
|
40
|
+
case "text_delta":
|
|
41
|
+
case "thinking_delta":
|
|
42
|
+
return typeof value.text === "string";
|
|
43
|
+
case "user_message":
|
|
44
|
+
return (typeof value.text === "string" &&
|
|
45
|
+
(value.clientMessageId === undefined || typeof value.clientMessageId === "string") &&
|
|
46
|
+
(value.attachments === undefined ||
|
|
47
|
+
(Array.isArray(value.attachments) &&
|
|
48
|
+
value.attachments.every((item) => record(item) &&
|
|
49
|
+
onlyKeys(item, ["name", "size", "mime", "path"]) &&
|
|
50
|
+
typeof item.name === "string" &&
|
|
51
|
+
Number.isSafeInteger(item.size) &&
|
|
52
|
+
item.size >= 0 &&
|
|
53
|
+
["mime", "path"].every((key) => item[key] === undefined || typeof item[key] === "string")))));
|
|
54
|
+
case "tool_use_start":
|
|
55
|
+
return (record(value.toolCall) &&
|
|
56
|
+
onlyKeys(value.toolCall, ["id", "toolName", "args"]) &&
|
|
57
|
+
typeof value.toolCall.id === "string" &&
|
|
58
|
+
!!value.toolCall.id &&
|
|
59
|
+
typeof value.toolCall.toolName === "string" &&
|
|
60
|
+
record(value.toolCall.args));
|
|
61
|
+
case "tool_use_args_delta":
|
|
62
|
+
return typeof value.toolCallId === "string" && !!value.toolCallId && record(value.args);
|
|
63
|
+
case "tool_result":
|
|
64
|
+
return (record(value.result) &&
|
|
65
|
+
onlyKeys(value.result, ["id", "result", "isError", "error"]) &&
|
|
66
|
+
typeof value.result.id === "string" &&
|
|
67
|
+
!!value.result.id &&
|
|
68
|
+
typeof value.result.result === "string" &&
|
|
69
|
+
typeof value.result.isError === "boolean" &&
|
|
70
|
+
(value.result.error === undefined || typeof value.result.error === "string"));
|
|
71
|
+
case "agent_start":
|
|
72
|
+
return (typeof value.agentId === "string" &&
|
|
73
|
+
!!value.agentId &&
|
|
74
|
+
typeof value.name === "string" &&
|
|
75
|
+
typeof value.description === "string" &&
|
|
76
|
+
value.status === "recorded");
|
|
77
|
+
case "error":
|
|
78
|
+
return typeof value.error === "string";
|
|
79
|
+
case "turn_complete":
|
|
80
|
+
return value.reason === "aborted_tools";
|
|
81
|
+
default:
|
|
82
|
+
return false;
|
|
83
|
+
}
|
|
84
|
+
}
|
|
4
85
|
/** Valid identities may arrive before their new Core Run acquires a durable anchor. */
|
|
5
86
|
export class MobilePendingInput extends Error {
|
|
6
87
|
ids;
|
|
@@ -63,21 +144,84 @@ export async function recoverMobileOutput(args) {
|
|
|
63
144
|
}
|
|
64
145
|
if (reply.page.status === "unavailable")
|
|
65
146
|
return null;
|
|
66
|
-
if (reply.page.status !== "ok" ||
|
|
67
|
-
!reply.legacyBaseComplete ||
|
|
68
|
-
!reply.legacyBase ||
|
|
69
|
-
!Array.isArray(reply.legacyBase.events) ||
|
|
70
|
-
reply.legacyBase.events[0]?.type !== "session_started" ||
|
|
71
|
-
reply.legacyBase.throughEventId !== reply.page.legacyBaseThroughEventId ||
|
|
72
|
-
new TextEncoder().encode(JSON.stringify(reply.legacyBase)).length > 1024 * 1024 + 4096)
|
|
73
|
-
throw new Error("Recovery base is incomplete");
|
|
74
147
|
let chat = initialChatState();
|
|
75
|
-
|
|
76
|
-
|
|
148
|
+
let pages = 0;
|
|
149
|
+
if (reply.page.status === "ok" && reply.legacyBaseComplete === false && args.readBase) {
|
|
150
|
+
const throughEventId = reply.page.legacyBaseThroughEventId;
|
|
151
|
+
if (typeof throughEventId !== "string" || !throughEventId || throughEventId.length > 512)
|
|
152
|
+
throw new Error("Recovery base is incomplete");
|
|
153
|
+
const cursors = new Set();
|
|
154
|
+
let cursor, index = 0, bytes = 0;
|
|
155
|
+
for (let count = 0;; count++) {
|
|
156
|
+
if (!args.canContinue() || count >= 40 || ++pages > 2048)
|
|
157
|
+
throw new Error("Recovery base cancelled or bounded");
|
|
158
|
+
const base = await args.readBase(cursor ? { cursor } : { throughEventId });
|
|
159
|
+
if (!args.canContinue())
|
|
160
|
+
throw new Error("Recovery cancelled");
|
|
161
|
+
const size = wireBytes(base);
|
|
162
|
+
bytes += size;
|
|
163
|
+
if (!record(base) ||
|
|
164
|
+
!onlyKeys(base, [
|
|
165
|
+
"type",
|
|
166
|
+
"sessionId",
|
|
167
|
+
"recoveryId",
|
|
168
|
+
"requestId",
|
|
169
|
+
"ok",
|
|
170
|
+
"throughEventId",
|
|
171
|
+
"index",
|
|
172
|
+
"events",
|
|
173
|
+
"complete",
|
|
174
|
+
"next",
|
|
175
|
+
]) ||
|
|
176
|
+
base.type !== "session.legacyBase" ||
|
|
177
|
+
typeof base.requestId !== "string" ||
|
|
178
|
+
!base.requestId ||
|
|
179
|
+
base.requestId.length > 128 ||
|
|
180
|
+
base.ok !== true ||
|
|
181
|
+
base.sessionId !== reply.sessionId ||
|
|
182
|
+
base.recoveryId !== reply.recoveryId ||
|
|
183
|
+
base.throughEventId !== throughEventId ||
|
|
184
|
+
!Number.isSafeInteger(base.index) ||
|
|
185
|
+
base.index !== index ||
|
|
186
|
+
!Array.isArray(base.events) ||
|
|
187
|
+
!base.events.length ||
|
|
188
|
+
!base.events.every((event) => baseEvent(event, reply.sessionId)) ||
|
|
189
|
+
(index === 0 && base.events[0].type !== "session_started") ||
|
|
190
|
+
base.events.some((event, offset) => event.type === "session_started" && index + offset !== 0) ||
|
|
191
|
+
size > MiB + 4096 ||
|
|
192
|
+
bytes > 32 * MiB + 40 * 4096 ||
|
|
193
|
+
typeof base.complete !== "boolean" ||
|
|
194
|
+
(base.complete
|
|
195
|
+
? base.next !== undefined
|
|
196
|
+
: typeof base.next !== "string" ||
|
|
197
|
+
!base.next ||
|
|
198
|
+
base.next.length > 2048 ||
|
|
199
|
+
cursors.has(base.next)))
|
|
200
|
+
throw new Error("Recovery base is incomplete or exceeds its bound");
|
|
201
|
+
for (const event of base.events)
|
|
202
|
+
chat = reduceStream(chat, event);
|
|
203
|
+
index += base.events.length;
|
|
204
|
+
if (base.complete)
|
|
205
|
+
break;
|
|
206
|
+
cursor = base.next;
|
|
207
|
+
cursors.add(cursor);
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
else {
|
|
211
|
+
if (reply.page.status !== "ok" ||
|
|
212
|
+
!reply.legacyBaseComplete ||
|
|
213
|
+
!reply.legacyBase ||
|
|
214
|
+
!Array.isArray(reply.legacyBase.events) ||
|
|
215
|
+
reply.legacyBase.events[0]?.type !== "session_started" ||
|
|
216
|
+
reply.legacyBase.throughEventId !== reply.page.legacyBaseThroughEventId ||
|
|
217
|
+
wireBytes(reply.legacyBase) > MiB + 4096)
|
|
218
|
+
throw new Error("Recovery base is incomplete");
|
|
219
|
+
for (const event of reply.legacyBase.events)
|
|
220
|
+
chat = reduceStream(chat, event);
|
|
221
|
+
}
|
|
77
222
|
// A legacy transcript establishes display history, never live completion.
|
|
78
223
|
chat = { ...chat, run: chat.run === "error" ? "error" : "idle", liveByAgent: {} };
|
|
79
224
|
let recovery = { incomplete: false };
|
|
80
|
-
let pages = 0;
|
|
81
225
|
// A continuation retains the verified private candidate and the same total
|
|
82
226
|
// page budget. No prefix is published until the eventual frozen join passes.
|
|
83
227
|
const advance = async () => {
|
package/dist/lib/storage.js
CHANGED
|
@@ -10,30 +10,57 @@ const K = {
|
|
|
10
10
|
deviceSecret: "cs.deviceSecret",
|
|
11
11
|
deviceName: "cs.deviceName",
|
|
12
12
|
};
|
|
13
|
+
/**
|
|
14
|
+
* Values whose localStorage write failed in this page. Reads prefer them, so
|
|
15
|
+
* one page stays self-consistent even when nothing can be persisted: pairing
|
|
16
|
+
* and the authentication that follows must present the same secret.
|
|
17
|
+
*/
|
|
18
|
+
const unpersisted = new Map();
|
|
13
19
|
/**
|
|
14
20
|
* Best-effort localStorage write. On a quota-constrained mobile browser
|
|
15
21
|
* (iOS/Android default ~5MB) setItem throws QuotaExceededError; since these
|
|
16
22
|
* calls run synchronously inside ws.onopen (the auth handshake), an uncaught
|
|
17
23
|
* throw aborts onopen mid-flight → handshake never sent → blank app, no retry.
|
|
18
|
-
* Swallow the write failure and
|
|
19
|
-
*
|
|
24
|
+
* Swallow the write failure and keep the value for this page instead; losing
|
|
25
|
+
* persistence only means a fresh secret/name next session.
|
|
20
26
|
*/
|
|
21
27
|
function safeSet(key, value) {
|
|
22
28
|
try {
|
|
23
29
|
localStorage.setItem(key, value);
|
|
30
|
+
unpersisted.delete(key);
|
|
31
|
+
}
|
|
32
|
+
catch {
|
|
33
|
+
/* quota / disabled storage — keep the value for the rest of this page */
|
|
34
|
+
unpersisted.set(key, value);
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
function safeGet(key) {
|
|
38
|
+
const local = unpersisted.get(key);
|
|
39
|
+
if (local !== undefined)
|
|
40
|
+
return local;
|
|
41
|
+
try {
|
|
42
|
+
return localStorage.getItem(key);
|
|
24
43
|
}
|
|
25
44
|
catch {
|
|
26
|
-
|
|
45
|
+
return null;
|
|
27
46
|
}
|
|
28
47
|
}
|
|
29
48
|
export const deviceStore = {
|
|
30
|
-
getId: () =>
|
|
49
|
+
getId: () => safeGet(K.deviceId) ?? "",
|
|
31
50
|
setId: (v) => safeSet(K.deviceId, v),
|
|
32
|
-
clearId: () =>
|
|
51
|
+
clearId: () => {
|
|
52
|
+
unpersisted.delete(K.deviceId);
|
|
53
|
+
try {
|
|
54
|
+
localStorage.removeItem(K.deviceId);
|
|
55
|
+
}
|
|
56
|
+
catch {
|
|
57
|
+
/* disabled storage has nothing persisted to remove */
|
|
58
|
+
}
|
|
59
|
+
},
|
|
33
60
|
/** Get the stable per-browser secret, minting+persisting one on first use.
|
|
34
61
|
* Mirrors the old getSecret() so existing devices keep the same secret. */
|
|
35
62
|
getOrCreateSecret: (mint) => {
|
|
36
|
-
let s =
|
|
63
|
+
let s = safeGet(K.deviceSecret);
|
|
37
64
|
if (!s) {
|
|
38
65
|
s = mint();
|
|
39
66
|
safeSet(K.deviceSecret, s);
|
|
@@ -42,7 +69,7 @@ export const deviceStore = {
|
|
|
42
69
|
},
|
|
43
70
|
/** Device display name, defaulting to a platform-derived label on first use. */
|
|
44
71
|
getOrCreateName: () => {
|
|
45
|
-
let n =
|
|
72
|
+
let n = safeGet(K.deviceName);
|
|
46
73
|
if (!n) {
|
|
47
74
|
const platform = (typeof navigator !== "undefined" && navigator.platform) || "Phone";
|
|
48
75
|
n = translate(loadUILanguage(), "mobile.storage.browserDevice", { platform });
|
|
@@ -118,7 +118,7 @@ export function reduceStream(state, raw) {
|
|
|
118
118
|
...s2,
|
|
119
119
|
items: [...s2.items, item],
|
|
120
120
|
liveByAgent: { ...s2.liveByAgent, [key]: id },
|
|
121
|
-
run: "running",
|
|
121
|
+
run: event.agentId ? s.run : "running",
|
|
122
122
|
};
|
|
123
123
|
}
|
|
124
124
|
case "text_delta": {
|
|
@@ -129,7 +129,7 @@ export function reduceStream(state, raw) {
|
|
|
129
129
|
if (live) {
|
|
130
130
|
return {
|
|
131
131
|
...s,
|
|
132
|
-
run: "running",
|
|
132
|
+
run: event.agentId ? s.run : "running",
|
|
133
133
|
items: s.items.map((i) => i.id === liveId && i.kind === "assistant" ? { ...i, text: i.text + text } : i),
|
|
134
134
|
};
|
|
135
135
|
}
|
|
@@ -148,7 +148,7 @@ export function reduceStream(state, raw) {
|
|
|
148
148
|
...s2,
|
|
149
149
|
items: [...s2.items, item],
|
|
150
150
|
liveByAgent: { ...s2.liveByAgent, [key]: id },
|
|
151
|
-
run: "running",
|
|
151
|
+
run: event.agentId ? s.run : "running",
|
|
152
152
|
};
|
|
153
153
|
}
|
|
154
154
|
case "thinking_delta": {
|
|
@@ -158,7 +158,7 @@ export function reduceStream(state, raw) {
|
|
|
158
158
|
if (liveId && s.items.some((i) => i.id === liveId && i.kind === "assistant")) {
|
|
159
159
|
return {
|
|
160
160
|
...s,
|
|
161
|
-
run: "running",
|
|
161
|
+
run: event.agentId ? s.run : "running",
|
|
162
162
|
items: s.items.map((i) => i.id === liveId && i.kind === "assistant" ? { ...i, reasoning: i.reasoning + text } : i),
|
|
163
163
|
};
|
|
164
164
|
}
|
|
@@ -175,7 +175,7 @@ export function reduceStream(state, raw) {
|
|
|
175
175
|
...s2,
|
|
176
176
|
items: [...s2.items, item],
|
|
177
177
|
liveByAgent: { ...s2.liveByAgent, [key]: id },
|
|
178
|
-
run: "running",
|
|
178
|
+
run: event.agentId ? s.run : "running",
|
|
179
179
|
};
|
|
180
180
|
}
|
|
181
181
|
case "tool_use_start": {
|
|
@@ -205,7 +205,12 @@ export function reduceStream(state, raw) {
|
|
|
205
205
|
const { [call.id]: _used, ...restOrphans } = s.orphanResults;
|
|
206
206
|
orphanResults = Object.keys(restOrphans).length > 0 ? restOrphans : undefined;
|
|
207
207
|
}
|
|
208
|
-
return {
|
|
208
|
+
return {
|
|
209
|
+
...s,
|
|
210
|
+
run: event.agentId ? s.run : "running",
|
|
211
|
+
items: [...s.items, item],
|
|
212
|
+
orphanResults,
|
|
213
|
+
};
|
|
209
214
|
}
|
|
210
215
|
case "tool_use_args_delta": {
|
|
211
216
|
// Tool args stream incrementally (large Edit/Write/Bash payloads). Merge
|
|
@@ -238,7 +243,7 @@ export function reduceStream(state, raw) {
|
|
|
238
243
|
orphanResults: {
|
|
239
244
|
...(s.orphanResults ?? {}),
|
|
240
245
|
[result.id]: {
|
|
241
|
-
result: result.result ?? result.error ?? "",
|
|
246
|
+
result: stripSystemReminders(result.result ?? result.error ?? ""),
|
|
242
247
|
error: Boolean(result.isError || result.error),
|
|
243
248
|
},
|
|
244
249
|
},
|
|
@@ -511,6 +516,20 @@ export function reduceStream(state, raw) {
|
|
|
511
516
|
}
|
|
512
517
|
case "error": {
|
|
513
518
|
const [id, s2] = freshId("err");
|
|
519
|
+
if (event.agentId) {
|
|
520
|
+
const key = agentKey(event);
|
|
521
|
+
const { [key]: _gone, ...rest } = s2.liveByAgent;
|
|
522
|
+
return {
|
|
523
|
+
...s2,
|
|
524
|
+
liveByAgent: rest,
|
|
525
|
+
items: [
|
|
526
|
+
...s2.items.map((item) => item.kind === "assistant" && !item.done && item.agentId === key
|
|
527
|
+
? { ...item, done: true }
|
|
528
|
+
: item),
|
|
529
|
+
{ kind: "system_error", id, text: event.error || "运行出错" },
|
|
530
|
+
],
|
|
531
|
+
};
|
|
532
|
+
}
|
|
514
533
|
return {
|
|
515
534
|
...s2,
|
|
516
535
|
run: "error",
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import type { ModelTraceConfig, ModelTraceEvent, ModelTraceListQuery, ModelTracePage, ModelTraceSessionPage, ModelTraceRecord } from "@cjhyy/code-shell-core/internal";
|
|
2
|
+
export type { ModelTraceAttempt, ModelTraceCallType, ModelTraceConfig, ModelTraceContentState, ModelTraceEvent, ModelTraceListQuery, ModelTraceOutput, ModelTracePage, ModelTraceSessionPage, ModelTraceRecord, ModelTraceStatus, ModelTraceSummary, ModelTraceSessionSummary, } from "@cjhyy/code-shell-core/internal";
|
|
3
|
+
type RemoteModelTraceQuery = Omit<ModelTraceListQuery, "workspacePath" | "workspaceScopes">;
|
|
4
|
+
export interface RemoteModelTraceApi {
|
|
5
|
+
list(query?: RemoteModelTraceQuery): Promise<ModelTracePage>;
|
|
6
|
+
sessions(query?: RemoteModelTraceQuery): Promise<ModelTraceSessionPage>;
|
|
7
|
+
get(id: string): Promise<{
|
|
8
|
+
record: ModelTraceRecord;
|
|
9
|
+
events: ModelTraceEvent[];
|
|
10
|
+
} | null>;
|
|
11
|
+
getConfig(): Promise<ModelTraceConfig>;
|
|
12
|
+
}
|
|
13
|
+
/** Supply the client's existing authenticated, workspace-bound JSON requester. */
|
|
14
|
+
export declare function createRemoteModelTraceApi(options: {
|
|
15
|
+
request: (path: string, init?: RequestInit) => Promise<unknown>;
|
|
16
|
+
}): RemoteModelTraceApi;
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
/** Supply the client's existing authenticated, workspace-bound JSON requester. */
|
|
2
|
+
export function createRemoteModelTraceApi(options) {
|
|
3
|
+
const querySuffix = (query) => {
|
|
4
|
+
const params = new URLSearchParams();
|
|
5
|
+
for (const [key, value] of Object.entries(query)) {
|
|
6
|
+
if (["workspacePath", "workspaceScopes", "storageDir"].includes(key))
|
|
7
|
+
throw new Error("Model history scope belongs to the Host");
|
|
8
|
+
if (value !== undefined)
|
|
9
|
+
params.set(key, String(value));
|
|
10
|
+
}
|
|
11
|
+
return params.size ? `?${params}` : "";
|
|
12
|
+
};
|
|
13
|
+
return {
|
|
14
|
+
async list(query = {}) {
|
|
15
|
+
return (await options.request(`/api/v1/model-traces${querySuffix(query)}`));
|
|
16
|
+
},
|
|
17
|
+
async sessions(query = {}) {
|
|
18
|
+
return (await options.request(`/api/v1/model-traces/sessions${querySuffix(query)}`));
|
|
19
|
+
},
|
|
20
|
+
async get(id) {
|
|
21
|
+
if (!/^[A-Za-z0-9_-]{1,128}$/.test(id))
|
|
22
|
+
throw new Error("Invalid model trace id");
|
|
23
|
+
try {
|
|
24
|
+
return (await options.request(`/api/v1/model-traces/${encodeURIComponent(id)}`));
|
|
25
|
+
}
|
|
26
|
+
catch (error) {
|
|
27
|
+
if (error?.status === 404)
|
|
28
|
+
return null;
|
|
29
|
+
throw error;
|
|
30
|
+
}
|
|
31
|
+
},
|
|
32
|
+
async getConfig() {
|
|
33
|
+
return (await options.request("/api/v1/model-traces/config"));
|
|
34
|
+
},
|
|
35
|
+
};
|
|
36
|
+
}
|