@cjhyy/code-shell-server 0.9.48 → 0.9.49

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.
Files changed (34) hide show
  1. package/dist/desktop-web/http-api.d.ts +9 -0
  2. package/dist/desktop-web/http-api.js +17 -1
  3. package/dist/http-byte-range.d.ts +5 -0
  4. package/dist/http-byte-range.js +19 -0
  5. package/dist/hub/files.js +71 -9
  6. package/dist/index.panels.d.ts +1 -0
  7. package/dist/index.panels.js +1 -0
  8. package/dist/mobile-remote/access-passcode.d.ts +5 -0
  9. package/dist/mobile-remote/access-passcode.js +48 -9
  10. package/dist/mobile-remote/mobile-client-event-validator.js +30 -1
  11. package/dist/mobile-remote/remote-host-manager.d.ts +7 -1
  12. package/dist/mobile-remote/remote-host-manager.js +132 -56
  13. package/dist/mobile-remote/tunnel-manager.d.ts +1 -0
  14. package/dist/mobile-remote/tunnel-manager.js +45 -20
  15. package/dist/panels/background-service-process.d.ts +39 -0
  16. package/dist/panels/background-service-process.js +457 -0
  17. package/dist/panels/background-service-store.d.ts +67 -0
  18. package/dist/panels/background-service-store.js +539 -0
  19. package/dist/panels/background-services.d.ts +193 -0
  20. package/dist/panels/background-services.js +1159 -0
  21. package/dist/panels/bridge-contract.d.ts +1 -1
  22. package/dist/panels/execution-gate.d.ts +7 -1
  23. package/dist/panels/execution-gate.js +26 -0
  24. package/dist/panels/management.js +49 -13
  25. package/dist/panels/process-files.js +28 -3
  26. package/dist/panels/resources/library.d.ts +1 -4
  27. package/dist/panels/resources/library.js +2 -18
  28. package/dist/panels/resources/references.js +21 -8
  29. package/dist/panels/runtime.js +1 -1
  30. package/dist/panels/tool-jobs.d.ts +2 -0
  31. package/dist/panels/tool-jobs.js +1 -1
  32. package/dist/sessions-service.d.ts +4 -0
  33. package/dist/sessions-service.js +17 -0
  34. package/package.json +6 -6
@@ -11,7 +11,7 @@ export declare const panelBridgeLimits: Readonly<{
11
11
  callTimeoutMs: 15000;
12
12
  consentTimeoutMs: number;
13
13
  }>;
14
- export type PanelBridgeErrorCode = "PERMISSION_DENIED" | "REVOKED" | "SESSION_CHANGED" | "NOT_SUPPORTED" | "RATE_LIMITED" | "PARAMS_TOO_LARGE" | "RESULT_TOO_LARGE" | "TIMEOUT" | "INVALID_ARGUMENT" | "OPERATION_FAILED";
14
+ export type PanelBridgeErrorCode = "PERMISSION_DENIED" | "REVOKED" | "SESSION_CHANGED" | "NOT_SUPPORTED" | "RATE_LIMITED" | "PARAMS_TOO_LARGE" | "RESULT_TOO_LARGE" | "TIMEOUT" | "INVALID_ARGUMENT" | "OPERATION_FAILED" | "SERVICE_CAPACITY" | "SERVICE_CONFIG_CONFLICT" | "SERVICE_REVISION_CONFLICT" | "SERVICE_REQUEST_KEY_CONFLICT";
15
15
  export declare class PanelBridgeError extends Error {
16
16
  readonly code: PanelBridgeErrorCode;
17
17
  readonly retryAfterMs?: number | undefined;
@@ -1,6 +1,6 @@
1
1
  /** What holds an execution, so a refused change can name it. Host-assigned only. */
2
2
  export interface PanelExecutionHolder {
3
- kind: "panel-call" | "agent-tool" | "process" | "agent-task" | "tool-job" | "automation";
3
+ kind: "panel-call" | "agent-tool" | "process" | "agent-task" | "tool-job" | "automation" | "service";
4
4
  /** Desktop WebContents id of the Panel instance that owns this execution. */
5
5
  guestId?: number;
6
6
  /** Trusted Host description, such as a native tool entry name. */
@@ -32,6 +32,12 @@ export declare class PanelExecutionBusyError extends Error {
32
32
  export declare class PanelExecutionGate {
33
33
  private readonly executions;
34
34
  private readonly sources;
35
+ private readonly settled;
36
+ private readonly serviceDrains;
37
+ registerServiceDrain(drain: (matches: (scope: PanelExecutionScope) => boolean) => Promise<void>): () => void;
38
+ /** Call only after validating the concrete authorized catalog mutation. */
39
+ drainServices(matches: (scope: PanelExecutionScope) => boolean): Promise<void>;
40
+ onMutationSettled(listener: () => void): () => void;
35
41
  private readonly mutations;
36
42
  enter(scope: PanelExecutionScope): () => void;
37
43
  run<T>(scope: PanelExecutionScope, work: () => Promise<T>): Promise<T>;
@@ -27,6 +27,24 @@ export class PanelExecutionBusyError extends Error {
27
27
  export class PanelExecutionGate {
28
28
  executions = new Set();
29
29
  sources = new Set();
30
+ settled = new Set();
31
+ serviceDrains = new Set();
32
+ registerServiceDrain(drain) {
33
+ this.serviceDrains.add(drain);
34
+ return () => {
35
+ this.serviceDrains.delete(drain);
36
+ };
37
+ }
38
+ /** Call only after validating the concrete authorized catalog mutation. */
39
+ async drainServices(matches) {
40
+ await Promise.all([...this.serviceDrains].map((drain) => drain(matches)));
41
+ }
42
+ onMutationSettled(listener) {
43
+ this.settled.add(listener);
44
+ return () => {
45
+ this.settled.delete(listener);
46
+ };
47
+ }
30
48
  mutations = new Set();
31
49
  enter(scope) {
32
50
  const held = {
@@ -98,6 +116,14 @@ export class PanelExecutionGate {
98
116
  }
99
117
  finally {
100
118
  this.mutations.delete(matches);
119
+ for (const listener of this.settled) {
120
+ try {
121
+ listener();
122
+ }
123
+ catch {
124
+ /* observer */
125
+ }
126
+ }
101
127
  }
102
128
  }
103
129
  }
@@ -506,6 +506,13 @@ export function createPanelManagement(options) {
506
506
  catalogMutation = next.catch(() => undefined);
507
507
  return next;
508
508
  }
509
+ async function drainSelectedServices(id, expected, guard, matches) {
510
+ const current = await selectedApp(id);
511
+ if (revision(current.app, current.digest) !== expected)
512
+ throw new PanelManagementError(409, "conflict", "面板已改变,请刷新后重试。");
513
+ await guard();
514
+ await panelExecutionGate.drainServices(matches);
515
+ }
509
516
  function setBinding(app, bound, expected) {
510
517
  options.assertBinding?.();
511
518
  if (!hasProject)
@@ -658,10 +665,15 @@ export function createPanelManagement(options) {
658
665
  throw new PanelManagementError(403, "review_owner", "请在当前设备重新审阅这个版本。");
659
666
  await assertAuthorized(context, held.generation);
660
667
  const review = held.public;
661
- return mutate(context, { appId: review.appId, kind: "binding" }, async (guard) => {
662
- await assertAuthorized(context, held.generation);
668
+ const assertReviewLease = () => {
669
+ if (closed || generation(context.ownerId) !== held.generation)
670
+ throw new PanelManagementError(401, "login_required", "登录已失效,请重新登录。");
663
671
  if (restores.get(token) !== held || held.public.expiresAt <= now())
664
672
  throw new PanelManagementError(409, "review_expired", "版本审阅已失效,请重新选择。");
673
+ };
674
+ const validateReview = async (guard) => {
675
+ await assertAuthorized(context, held.generation);
676
+ assertReviewLease();
665
677
  const current = await restorationSelection(review.appId);
666
678
  if (current.revision !== review.expectedRevision)
667
679
  throw new PanelManagementError(409, "conflict", "项目面板已改变,请重新审阅。");
@@ -670,12 +682,25 @@ export function createPanelManagement(options) {
670
682
  throw new PanelManagementError(400, "unsupported", "这个版本无法在当前环境使用。");
671
683
  await guard();
672
684
  await assertAuthorized(context, held.generation);
673
- if (restores.get(token) !== held || held.public.expiresAt <= now())
674
- throw new PanelManagementError(409, "review_expired", "版本审阅已失效,请重新选择。");
685
+ assertReviewLease();
686
+ if (projectState(review.appId) !== held.projectState)
687
+ throw new PanelManagementError(409, "conflict", "项目面板配置已改变,请重新审阅。");
688
+ return target;
689
+ };
690
+ return mutate(context, { appId: review.appId, kind: "binding" }, async (guard) => {
691
+ const target = await validateReview(guard);
692
+ assertReviewLease();
675
693
  setBinding(target, true, { projectState: held.projectState });
676
694
  restores.delete(token);
677
695
  await options.onChanged?.(review.appId, "binding");
678
696
  return { id: review.appId, packageDigest: review.packageDigest };
697
+ }, async (guard, matches) => {
698
+ // A reviewed version selection changes the executor's package just like
699
+ // unbinding. Validate authority and exact retained bytes before drain,
700
+ // then revalidate them before writing. Other executions still block.
701
+ await validateReview(guard);
702
+ assertReviewLease();
703
+ await panelExecutionGate.drainServices(matches);
679
704
  });
680
705
  },
681
706
  assertAuthorized,
@@ -761,6 +786,8 @@ export function createPanelManagement(options) {
761
786
  throw new PanelManagementError(409, "already_installed", "这个面板已经安装,请确认项目更新。");
762
787
  let admitted = false;
763
788
  const assertReviewLease = () => {
789
+ if (closed || generation(context.ownerId) !== held.generation)
790
+ throw new PanelManagementError(401, "login_required", "登录已失效,请重新登录。");
764
791
  if (!admitted && (reviews.get(token) !== held || held.public.expiresAt <= now()))
765
792
  throw new PanelManagementError(409, "review_expired", "预览已失效,请重新检查来源。");
766
793
  };
@@ -783,6 +810,7 @@ export function createPanelManagement(options) {
783
810
  };
784
811
  return mutate(context, { appId: held.public.preview.id, kind: held.public.kind }, async (guard) => {
785
812
  await validateReview(guard);
813
+ assertReviewLease();
786
814
  const id = held.public.preview.id;
787
815
  const installed = await installReviewedLocalPanelApp(held.source, held.digest, new Date(now()).toISOString(), {
788
816
  overwrite: held.public.kind === "update",
@@ -798,6 +826,7 @@ export function createPanelManagement(options) {
798
826
  if (projectState(id) !== held.projectState)
799
827
  throw new PanelManagementError(409, "conflict", "项目面板配置已改变,请重新预览。");
800
828
  await guard();
829
+ assertReviewLease();
801
830
  },
802
831
  });
803
832
  reviews.delete(token);
@@ -813,7 +842,7 @@ export function createPanelManagement(options) {
813
842
  invalidateSkillCache();
814
843
  await options.onChanged?.(id, held.public.kind);
815
844
  return { id, packageDigest: installed.packageDigest };
816
- }, approval?.releaseExecutions
845
+ }, held.public.kind === "update"
817
846
  ? async (guard, matches) => {
818
847
  await validateReview(guard);
819
848
  // A local directory/ZIP may have changed since preview. Check its
@@ -823,12 +852,19 @@ export function createPanelManagement(options) {
823
852
  if (preview.id !== held.public.preview.id || preview.reviewToken !== held.digest)
824
853
  throw new PanelManagementError(409, "conflict", "来源已改变,请重新检查更新。");
825
854
  await validateReview(guard);
826
- // This admitted operation owns the review while its holders exit.
827
- // TTL expiry during cancellation must not invalidate that consent;
828
- // owner revocation and package/project changes are still checked.
829
- admitted = true;
830
- reviews.delete(token);
831
- await approval.releaseExecutions(held.public.preview.id, matches);
855
+ assertReviewLease();
856
+ if (approval?.releaseExecutions) {
857
+ // Force owns the review while its holders exit. TTL expiry during
858
+ // cancellation must not invalidate consent; revocation is checked.
859
+ admitted = true;
860
+ reviews.delete(token);
861
+ await approval.releaseExecutions(held.public.preview.id, matches);
862
+ }
863
+ else {
864
+ // Ordinary updates drain only services. Other executions still
865
+ // block, and failed publication must leave this review retryable.
866
+ await panelExecutionGate.drainServices(matches);
867
+ }
832
868
  }
833
869
  : undefined);
834
870
  },
@@ -852,7 +888,7 @@ export function createPanelManagement(options) {
852
888
  setBinding(app, bound, { ...current, revision: expected });
853
889
  await options.onChanged?.(id, "binding");
854
890
  return snapshot();
855
- });
891
+ }, (guard, matches) => drainSelectedServices(id, expected, guard, matches));
856
892
  },
857
893
  async remove(context, id, expected) {
858
894
  assertId(id);
@@ -878,7 +914,7 @@ export function createPanelManagement(options) {
878
914
  invalidateSkillCache();
879
915
  await options.onChanged?.(id, "remove");
880
916
  return { removed: true };
881
- });
917
+ }, (guard, matches) => drainSelectedServices(id, expected, guard, matches));
882
918
  },
883
919
  cancelOwner(owner) {
884
920
  generations.set(owner, generation(owner) + 1);
@@ -3,6 +3,7 @@ import { lstat, open, opendir, realpath } from "node:fs/promises";
3
3
  import { extname, isAbsolute, join, resolve } from "node:path";
4
4
  import { Readable } from "node:stream";
5
5
  import { pipeline } from "node:stream/promises";
6
+ import { rangeBounds } from "../http-byte-range.js";
6
7
  const MAX_DOWNLOAD_BYTES = 2 * 1024 * 1024 * 1024;
7
8
  const MAX_ENTRIES = 200;
8
9
  const MAX_EXAMINED = 4096;
@@ -227,20 +228,44 @@ export async function handlePanelProcessDirectory(request, response, options) {
227
228
  fail(404, "下载文件已经改变。");
228
229
  await verifyDirectory();
229
230
  await authorize();
230
- response.writeHead(200, {
231
+ // Directory outputs are mutable and have no stable representation validator.
232
+ // An If-Range condition cannot be confirmed, so return the complete file.
233
+ const range = request.headers["if-range"] === undefined ? request.headers.range : undefined;
234
+ const bounds = range === undefined
235
+ ? { start: 0, end: metadata.size - 1 }
236
+ : metadata.size > 0
237
+ ? rangeBounds(range, metadata.size)
238
+ : null;
239
+ const headers = {
231
240
  "Content-Type": "application/octet-stream",
232
241
  "Content-Disposition": disposition(name),
233
- "Content-Length": metadata.size,
242
+ "Accept-Ranges": "bytes",
234
243
  "Content-Security-Policy": "default-src 'none'; sandbox",
235
244
  "Cache-Control": "no-store",
236
245
  "X-Content-Type-Options": "nosniff",
237
246
  "Referrer-Policy": "no-referrer",
247
+ };
248
+ if (!bounds) {
249
+ response.writeHead(416, {
250
+ ...headers,
251
+ "Content-Range": `bytes */${metadata.size}`,
252
+ "Content-Length": 0,
253
+ });
254
+ response.end();
255
+ return;
256
+ }
257
+ response.writeHead(range === undefined ? 200 : 206, {
258
+ ...headers,
259
+ "Content-Length": bounds.end - bounds.start + 1,
260
+ ...(range === undefined
261
+ ? {}
262
+ : { "Content-Range": `bytes ${bounds.start}-${bounds.end}/${metadata.size}` }),
238
263
  });
239
264
  if (request.method === "HEAD" || metadata.size === 0) {
240
265
  response.end();
241
266
  return;
242
267
  }
243
- const input = file.createReadStream({ autoClose: false, start: 0, end: metadata.size - 1 });
268
+ const input = file.createReadStream({ autoClose: false, ...bounds });
244
269
  let checking = false;
245
270
  const timer = setInterval(() => {
246
271
  if (checking)
@@ -1,5 +1,6 @@
1
1
  import type { Readable } from "node:stream";
2
2
  import type { MediaAsset, MediaScope } from "./types.js";
3
+ export { rangeBounds } from "../../http-byte-range.js";
3
4
  export declare function resourceMimeType(name: string, requested?: string): string;
4
5
  export interface MediaLibraryOptions {
5
6
  rootDirectory: string;
@@ -33,10 +34,6 @@ export interface MediaReadResult {
33
34
  body: Readable | null;
34
35
  }
35
36
  export declare function resourceName(value: string): string;
36
- export declare function rangeBounds(range: string, size: number): {
37
- start: number;
38
- end: number;
39
- } | null;
40
37
  /**
41
38
  * Host-owned immutable, scope-local media. importFile accepts only paths already
42
39
  * authorized by the caller (for example native file selection); guest APIs must
@@ -3,6 +3,8 @@ import { constants } from "node:fs";
3
3
  import { lstat, mkdir, open, readdir, rename, rm } from "node:fs/promises";
4
4
  import { basename, extname, join } from "node:path";
5
5
  import { mediaDirectory, mediaScopeKey, readMediaJson, writeMediaJson } from "./storage.js";
6
+ import { rangeBounds } from "../../http-byte-range.js";
7
+ export { rangeBounds } from "../../http-byte-range.js";
6
8
  const ASSET_ID = /^asset-[a-f0-9]{64}$/;
7
9
  const MIME_BY_EXTENSION = {
8
10
  ".mp4": "video/mp4",
@@ -93,24 +95,6 @@ function assetRecord(raw, id) {
93
95
  createdAt: value.createdAt,
94
96
  };
95
97
  }
96
- export function rangeBounds(range, size) {
97
- const match = /^bytes=(\d*)-(\d*)$/.exec(range);
98
- if (!match || (!match[1] && !match[2]))
99
- return null;
100
- const first = match[1] ? Number(match[1]) : undefined;
101
- const last = match[2] ? Number(match[2]) : undefined;
102
- if ((first !== undefined && !Number.isSafeInteger(first)) ||
103
- (last !== undefined && !Number.isSafeInteger(last)))
104
- return null;
105
- if (first === undefined) {
106
- if (!last || last <= 0)
107
- return null;
108
- return { start: Math.max(0, size - last), end: size - 1 };
109
- }
110
- if (first >= size || (last !== undefined && last < first))
111
- return null;
112
- return { start: first, end: Math.min(last ?? size - 1, size - 1) };
113
- }
114
98
  /**
115
99
  * Host-owned immutable, scope-local media. importFile accepts only paths already
116
100
  * authorized by the caller (for example native file selection); guest APIs must
@@ -25,12 +25,11 @@ function referenceId(value) {
25
25
  function validIdentity(value) {
26
26
  return value && Number.isSafeInteger(value.dev) && Number.isSafeInteger(value.ino);
27
27
  }
28
- function sameSource(a, b, relocating = false) {
29
- return (a.dev === b.dev &&
30
- a.ino === b.ino &&
31
- a.size === b.size &&
32
- a.mtimeMs === b.mtimeMs &&
33
- (relocating || a.ctimeMs === b.ctimeMs));
28
+ // Like other editors, recognize an original by file, size and modification time.
29
+ // ctime is not identity: macOS changes it for tags, "last opened" and other
30
+ // extended attributes, which would otherwise turn unchanged originals "changed".
31
+ function sameSource(a, b) {
32
+ return a.dev === b.dev && a.ino === b.ino && a.size === b.size && a.mtimeMs === b.mtimeMs;
34
33
  }
35
34
  function view(record, state = "available") {
36
35
  return {
@@ -221,7 +220,21 @@ export class ExternalResourceReferences {
221
220
  if (error.code !== "ENOENT")
222
221
  throw error;
223
222
  }
224
- if ((await readdir(directory)).filter((name) => REFERENCE_ID.test(name.replace(/\.json$/, ""))).length >= MAX_REFERENCES)
223
+ const names = (await readdir(directory)).filter((name) => REFERENCE_ID.test(name.replace(/\.json$/, "")));
224
+ // IDs include the ctime seen at creation. Choosing the same unchanged
225
+ // original again must return its reference, not a duplicate asset.
226
+ for (const name of names) {
227
+ const existing = await this.record(scope, name.replace(/\.json$/, "")).catch(() => undefined);
228
+ if (existing?.root === root &&
229
+ existing.path === path &&
230
+ sameSource(existing.source, selected.initial) &&
231
+ existing.ancestors.length === selected.directory.identities.length &&
232
+ existing.ancestors.every((identity, index) => sameResourceIdentity(identity, selected.directory.identities[index]))) {
233
+ await selected.verify();
234
+ return view(existing);
235
+ }
236
+ }
237
+ if (names.length >= MAX_REFERENCES)
225
238
  throw new Error("External reference count limit exceeded");
226
239
  const record = {
227
240
  schemaVersion: 1,
@@ -302,7 +315,7 @@ export class ExternalResourceReferences {
302
315
  const selected = await this.selected(scope, root, resourceRelativePath(path), options);
303
316
  try {
304
317
  // Metadata alone cannot establish that an unrelated replacement is the original file.
305
- if (!sameSource(previous.source, selected.initial, true))
318
+ if (!sameSource(previous.source, selected.initial))
306
319
  throw new Error("This is a different file; create a new external reference instead");
307
320
  const record = {
308
321
  ...previous,
@@ -349,7 +349,7 @@ function bridgeScript(id, origin) {
349
349
  call: ask,
350
350
  callResult: (method, params) => ask(method, params).then(value => ({ ok: true, value }), error => ({ ok: false, error: { code: error.code || "OPERATION_FAILED", message: error.message, ...(typeof error.retryAfterMs === "number" ? { retryAfterMs: error.retryAfterMs } : {}) } })),
351
351
  on: (name, listener) => {
352
- if (!["context.changed", "process.output", "process.exit", "agent.task.changed", "tasks.changed", "media.job.changed"].includes(name) || typeof listener !== "function") {
352
+ if (!["context.changed", "process.output", "process.exit", "agent.task.changed", "tasks.changed", "services.changed", "media.job.changed"].includes(name) || typeof listener !== "function") {
353
353
  throw new Error("Invalid event listener");
354
354
  }
355
355
  let list = listeners.get(name);
@@ -47,6 +47,8 @@ export interface ToolJobProgress {
47
47
  export type ToolJobStatus = "queued" | "running" | "cancelling" | "succeeded" | "failed" | "cancelled" | "interrupted";
48
48
  export interface ToolJob {
49
49
  id: string;
50
+ /** Caller-owned idempotency identity, returned with its authorized receipt. */
51
+ requestKey?: string;
50
52
  scope: ToolJobScope;
51
53
  entry: ToolJobEntry;
52
54
  package?: ToolJobPackage;
@@ -100,7 +100,7 @@ function progressValue(progress) {
100
100
  }, toolJobLimits.maxProgressBytes);
101
101
  }
102
102
  function publicJob(job) {
103
- const { version: _version, requestKey: _requestKey, requestDigest: _requestDigest, ...value } = job;
103
+ const { version: _version, requestDigest: _requestDigest, ...value } = job;
104
104
  return toolJobJson(value, toolJobLimits.maxRecordBytes);
105
105
  }
106
106
  function packageValue(value) {
@@ -38,6 +38,8 @@ export interface DiskSessionMeta {
38
38
  engineSessionId: string;
39
39
  cwd: string;
40
40
  title: string;
41
+ model?: string;
42
+ provider?: string;
41
43
  updatedAt: number;
42
44
  /** Durable creation time, when supplied by the engine's state schema. */
43
45
  createdAt?: number;
@@ -69,6 +71,8 @@ export interface ListDiskSessionsResult {
69
71
  export declare function listDiskSessions(opts: {
70
72
  limit: number;
71
73
  cursor?: string;
74
+ /** Display-only project roots filter, applied before pagination. */
75
+ cwds?: readonly string[];
72
76
  includeArchived?: boolean;
73
77
  /** Host read models may include children; ordinary sidebar queries stay top-level. */
74
78
  includeSubagents?: boolean;
@@ -217,6 +217,13 @@ export async function listDiskSessions(opts, baseDir = sessionsRoot()) {
217
217
  // Preserve the public adapter's explicit relative-root form. Host receives
218
218
  // one absolute root; it never guesses a workspace for the caller.
219
219
  baseDir = path.resolve(baseDir);
220
+ const pathKey = (cwd) => {
221
+ const normalized = path.resolve(cwd);
222
+ return process.platform === "darwin" || process.platform === "win32"
223
+ ? normalized.toLocaleLowerCase("en-US")
224
+ : normalized;
225
+ };
226
+ const cwdRoots = opts.cwds?.map(pathKey);
220
227
  const dirs = (await scanSessionDirectories(baseDir)).filter(({ id }) => !isInternalEphemeralSessionId(id));
221
228
  dirs.sort(compareDiskSessionPosition);
222
229
  const decodedCursor = decodeDiskSessionCursor(opts.cursor);
@@ -288,6 +295,14 @@ export async function listDiskSessions(opts, baseDir = sessionsRoot()) {
288
295
  // is intentionally NOT filtered here — the renderer handles that via
289
296
  // isNoRepoCwd. (deleted-project resurrection)
290
297
  const cwdStr = typeof state.cwd === "string" ? state.cwd : "";
298
+ if (cwdRoots &&
299
+ (!cwdStr ||
300
+ !cwdRoots.some((root) => {
301
+ const candidate = pathKey(cwdStr);
302
+ return (candidate === root ||
303
+ candidate.startsWith(root.endsWith(path.sep) ? root : root + path.sep));
304
+ })))
305
+ continue;
291
306
  // A child transcript remains useful after its temporary worktree is gone;
292
307
  // targeted reads do not rebuild projects in the sidebar.
293
308
  const childParentId = typeof state.parentSessionId === "string" && state.parentSessionId
@@ -325,6 +340,8 @@ export async function listDiskSessions(opts, baseDir = sessionsRoot()) {
325
340
  ? { clientMessageId: state.clientMessageId }
326
341
  : {}),
327
342
  engineSessionId: id,
343
+ ...(typeof state.model === "string" ? { model: state.model } : {}),
344
+ ...(typeof state.provider === "string" ? { provider: state.provider } : {}),
328
345
  cwd: typeof state.cwd === "string" ? state.cwd : "",
329
346
  // Prefer the LLM-generated title (now persisted to state); fall back to
330
347
  // the raw first-message summary, then the id. This lets the disk rebuild
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cjhyy/code-shell-server",
3
- "version": "0.9.48",
3
+ "version": "0.9.49",
4
4
  "description": "CodeShell server transport layer — mobile remote HTTP/WS host, pairing, passcode gate, tunnel, rooms, uploads, and session/attachment services. Electron-free; precursor of the auth gateway.",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -62,14 +62,14 @@
62
62
  "clean": "node -e \"require('node:fs').rmSync('dist',{recursive:true,force:true})\""
63
63
  },
64
64
  "dependencies": {
65
- "@cjhyy/code-shell-capability-coding": "0.9.48",
66
- "@cjhyy/code-shell-core": "0.9.48",
67
- "@cjhyy/code-shell-web": "0.9.48",
65
+ "@cjhyy/code-shell-capability-coding": "0.9.49",
66
+ "@cjhyy/code-shell-core": "0.9.49",
67
+ "@cjhyy/code-shell-web": "0.9.49",
68
68
  "@modelcontextprotocol/sdk": "^1.31.0",
69
69
  "@types/ws": "^8.18.1",
70
70
  "ws": "^8.21.0",
71
- "@cjhyy/code-shell-link": "0.9.48",
72
- "@cjhyy/code-shell-host": "0.9.48"
71
+ "@cjhyy/code-shell-link": "0.9.49",
72
+ "@cjhyy/code-shell-host": "0.9.49"
73
73
  },
74
74
  "engines": {
75
75
  "node": ">=20.10"