@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.
@@ -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";
@@ -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
@@ -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 normalizePath(p) {
12
- const normalized = p.replace(/\\/g, "/").replace(/\/+$/, "");
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 isSameOrInside(cwd, root) {
16
- const target = normalizePath(cwd).toLowerCase();
17
- const base = normalizePath(root).toLowerCase();
18
- return target === base || target.startsWith(`${base}/`);
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
- const mime = file.type.toLowerCase();
48
- if (mime === "image/jpg") {
49
- return new File([await file.arrayBuffer()], file.name, {
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
- for (const event of reply.legacyBase.events)
76
- chat = reduceStream(chat, event);
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 () => {
@@ -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 let the caller proceed with the in-memory value;
19
- * losing persistence only means a fresh secret/name next session.
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
- /* quota / disabled storage — best-effort, value still returned to caller */
45
+ return null;
27
46
  }
28
47
  }
29
48
  export const deviceStore = {
30
- getId: () => localStorage.getItem(K.deviceId) ?? "",
49
+ getId: () => safeGet(K.deviceId) ?? "",
31
50
  setId: (v) => safeSet(K.deviceId, v),
32
- clearId: () => localStorage.removeItem(K.deviceId),
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 = localStorage.getItem(K.deviceSecret);
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 = localStorage.getItem(K.deviceName);
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 { ...s, run: "running", items: [...s.items, item], orphanResults };
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
+ }