dsh-remote-local-plugin 0.8.8 → 0.8.9

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/lib/index.js CHANGED
@@ -1,4 +1,4 @@
1
- /* dsh-remote-local-plugin@0.8.8 — bundled host entry */
1
+ /* dsh-remote-local-plugin@0.8.9 — bundled host entry */
2
2
 
3
3
  // src/index.ts
4
4
  import { credentialRef } from "@deepseek-ai/dsh-credentials";
@@ -269,7 +269,7 @@ async function handleHostApi(ctx, service, mirrors, request) {
269
269
  async function dispatch(ctx, service, mirrors, endpoint, payload, signal) {
270
270
  switch (endpoint) {
271
271
  case "state":
272
- return service.state();
272
+ return { ...service.state(), mirroredWorkspaceIds: mirrors?.localWorkspaceIds() ?? [] };
273
273
  case "target/validate":
274
274
  return service.validateTarget(requireStringField(payload, "sshTarget"));
275
275
  case "host/add": {
@@ -504,12 +504,83 @@ var SURFACE_TYPES = /* @__PURE__ */ new Set([
504
504
  "assistant/message",
505
505
  "tool/result"
506
506
  ]);
507
+ function messageIdentity(message) {
508
+ return typeof message === "object" && message !== null ? message.id : void 0;
509
+ }
510
+ function createMirrorInbox(session) {
511
+ const nextTurn = [];
512
+ const nextStep = [];
513
+ const locate = (id) => {
514
+ for (const target of ["next-turn", "next-step"]) {
515
+ const list = target === "next-turn" ? nextTurn : nextStep;
516
+ const index = list.findIndex((message) => messageIdentity(message) === id);
517
+ if (index >= 0) return { target, index };
518
+ }
519
+ return void 0;
520
+ };
521
+ const append = (target, start, inserted, removedCount = 0) => {
522
+ try {
523
+ ;
524
+ session.append("agent/inbox/spliced", {
525
+ target,
526
+ start,
527
+ ...removedCount === 0 ? {} : { removedCount },
528
+ inserted
529
+ });
530
+ } catch {
531
+ }
532
+ };
533
+ const inbox = {
534
+ "next-turn": nextTurn,
535
+ "next-step": nextStep,
536
+ nextTurn,
537
+ nextStep,
538
+ get hasPending() {
539
+ return nextTurn.length > 0 || nextStep.length > 0;
540
+ },
541
+ remove(id) {
542
+ const found = locate(id);
543
+ if (found === void 0) return false;
544
+ const list = found.target === "next-turn" ? nextTurn : nextStep;
545
+ list.splice(found.index, 1);
546
+ append(found.target, found.index, [], 1);
547
+ return true;
548
+ },
549
+ replace(id, message) {
550
+ const found = locate(id);
551
+ if (found === void 0) return false;
552
+ const list = found.target === "next-turn" ? nextTurn : nextStep;
553
+ list.splice(found.index, 1, message);
554
+ append(found.target, found.index, [message], 1);
555
+ return true;
556
+ },
557
+ splice(target, start, deleteCount, inserted) {
558
+ const list = target === "next-turn" ? nextTurn : nextStep;
559
+ const removed = list.splice(start, deleteCount, ...inserted);
560
+ append(target === "next-turn" ? "next-turn" : "next-step", start, inserted, removed.length);
561
+ return removed;
562
+ },
563
+ clear() {
564
+ if (nextStep.length > 0) {
565
+ const count = nextStep.length;
566
+ nextStep.length = 0;
567
+ append("next-step", 0, [], count);
568
+ }
569
+ if (nextTurn.length > 0) {
570
+ const count = nextTurn.length;
571
+ nextTurn.length = 0;
572
+ append("next-turn", 0, [], count);
573
+ }
574
+ }
575
+ };
576
+ return inbox;
577
+ }
507
578
  var RemoteMirrorAgent = class {
508
579
  id;
509
580
  session;
510
581
  options;
511
582
  ctx;
512
- inbox = { "next-turn": [], "next-step": [], nextTurn: [], nextStep: [] };
583
+ inbox;
513
584
  #runtime;
514
585
  #mirror;
515
586
  constructor(mirror, runtime, ctx, options) {
@@ -519,11 +590,13 @@ var RemoteMirrorAgent = class {
519
590
  this.session = runtime.session;
520
591
  this.ctx = ctx;
521
592
  this.options = options;
593
+ this.inbox = createMirrorInbox(runtime.session);
522
594
  }
523
595
  get status() {
524
596
  return this.#runtime.running ? "running" : "idle";
525
597
  }
526
598
  cancel() {
599
+ this.inbox.clear();
527
600
  this.#mirror.cancel(this.#runtime);
528
601
  }
529
602
  whenIdle() {
@@ -536,10 +609,14 @@ var RemoteMirrorAgent = class {
536
609
  return task(new AbortController().signal);
537
610
  }
538
611
  send(message, _target, wakeup = false) {
539
- if (wakeup) this.followup(message);
612
+ if (wakeup || this.#runtime.running) this.followup(message);
540
613
  else this.#mirror.prompt(this.#runtime, message, "queue");
541
614
  }
542
615
  followup(message) {
616
+ if (this.#runtime.running) {
617
+ this.inbox.splice("next-turn", this.inbox.nextTurn.length, 0, [message]);
618
+ return;
619
+ }
543
620
  this.#mirror.prompt(this.#runtime, message, "queue");
544
621
  }
545
622
  steer(message) {
@@ -643,6 +720,10 @@ var RemoteMirrorService = class {
643
720
  }
644
721
  return void 0;
645
722
  }
723
+ /** Local workspace ids that represent remote workspaces. */
724
+ localWorkspaceIds() {
725
+ return Object.values(this.#state.workspaces).map((workspace) => workspace.localWorkspaceId).filter((id) => typeof id === "string" && id !== "");
726
+ }
646
727
  /** Whether the test-only manifest excludes this remote workspace from UI listings. */
647
728
  isTestHidden(remoteWorkspaceId, remotePath) {
648
729
  return isTestHidden(remotePath, remoteWorkspaceId, this.#testVisibility);
@@ -680,9 +761,10 @@ var RemoteMirrorService = class {
680
761
  }
681
762
  /** Route one prompt from the local Agent façade to the remote session. */
682
763
  prompt(runtime, message, mode) {
683
- const text = contentText(message);
684
- if (text === "") return;
685
- runtime.pendingUsers.push(message);
764
+ const parts = promptParts(message);
765
+ if (parts.length === 0) return;
766
+ const pending = { message, localSeq: -1 };
767
+ runtime.pendingUsers.push(pending);
686
768
  if (!runtime.running) {
687
769
  runtime.awaitingTurn = runtime.latestTurn + 1;
688
770
  runtime.awaitingStarted = false;
@@ -697,19 +779,63 @@ var RemoteMirrorService = class {
697
779
  }
698
780
  const selection = this.#selectionFor(runtime);
699
781
  if (selection !== void 0) Object.assign(runtime.agent.options, selection);
700
- void this.#hosts.promptSession(
782
+ void this.#promptContent(message).then(({ text, content }) => this.#hosts.promptSession(
701
783
  runtime.record.hostId,
702
784
  runtime.record.remoteSessionId,
703
785
  text,
704
786
  mode,
705
- selection
706
- ).catch((error) => {
787
+ selection,
788
+ content
789
+ )).then(() => {
790
+ if (!runtime.pendingUsers.includes(pending)) return;
791
+ const before = runtime.session.seq;
792
+ try {
793
+ runtime.session.append("user/message", messageData(message), { surfaceOp: "append" });
794
+ pending.localSeq = runtime.session.seq > before ? runtime.session.seq - 1 : -1;
795
+ } catch (error) {
796
+ this.#ctx.logger.warn("dsh-remote-local: could not append accepted user message: %s", String(error));
797
+ }
798
+ }).catch((error) => {
707
799
  runtime.lastError = error instanceof Error ? error.message : String(error);
708
- runtime.pendingUsers.shift();
800
+ const index = runtime.pendingUsers.indexOf(pending);
801
+ if (index >= 0) runtime.pendingUsers.splice(index, 1);
709
802
  this.#ctx.logger.warn("dsh-remote-local: remote prompt failed: %s", runtime.lastError);
803
+ try {
804
+ const emit = this.#ctx.emit;
805
+ emit.call(this.#ctx, "agent/error", {
806
+ agent: runtime.agent,
807
+ error: error instanceof Error ? error : new Error(String(error))
808
+ });
809
+ } catch (emitError) {
810
+ this.#ctx.logger.warn("dsh-remote-local: prompt error notification failed: %s", String(emitError));
811
+ }
710
812
  this.#setRunning(runtime, false);
711
813
  });
712
814
  }
815
+ async #promptContent(message) {
816
+ const content = [];
817
+ for (const part of promptParts(message)) {
818
+ if (part.type === "text") {
819
+ content.push({ type: "text", text: part.text });
820
+ continue;
821
+ }
822
+ const attachments = this.#ctx.get("attachments");
823
+ if (attachments === void 0) throw new Error("\u672C\u5730\u9644\u4EF6\u670D\u52A1\u4E0D\u53EF\u7528");
824
+ const stored = await attachments.readImage(part.attachment);
825
+ const mediaType = typeof stored.mediaType === "string" ? stored.mediaType : typeof stored.ref?.mediaType === "string" ? stored.ref.mediaType : void 0;
826
+ if (mediaType === void 0) throw new Error("\u56FE\u7247\u7F3A\u5C11 mediaType");
827
+ const name2 = part.name ?? (typeof stored.ref?.name === "string" ? stored.ref.name : void 0);
828
+ content.push({
829
+ type: "image",
830
+ data: Buffer.from(stored.data).toString("base64"),
831
+ mediaType,
832
+ ...name2 === void 0 ? {} : { name: name2 }
833
+ });
834
+ }
835
+ const text = content.filter((part) => part.type === "text").map((part) => part.text).join("\n").trim();
836
+ if (content.length === 0) throw new Error("\u6D88\u606F\u5185\u5BB9\u4E3A\u7A7A");
837
+ return { text, content };
838
+ }
713
839
  /** Route cancellation to the remote session only after the remote accepts it. */
714
840
  cancel(runtime) {
715
841
  void this.#hosts.cancelSession(runtime.record.hostId, runtime.record.remoteSessionId).then(() => {
@@ -1044,9 +1170,13 @@ var RemoteMirrorService = class {
1044
1170
  let session;
1045
1171
  try {
1046
1172
  const seed = filtered.map((event, index) => toSeedEvent(event, index, seqMap));
1173
+ const remoteCreatedAt = Date.parse(remote.createdAt);
1047
1174
  session = this.#sessions.create(localId, {
1048
1175
  ...seed.length === 0 ? {} : { seed },
1049
- meta: { cwd: anchor }
1176
+ meta: {
1177
+ cwd: anchor,
1178
+ ...Number.isFinite(remoteCreatedAt) ? { createdAt: remoteCreatedAt } : {}
1179
+ }
1050
1180
  });
1051
1181
  } catch (error) {
1052
1182
  this.#ctx.logger.warn("dsh-remote-local: could not seed mirror for %s: %s", remote.sessionId, String(error));
@@ -1221,15 +1351,19 @@ var RemoteMirrorService = class {
1221
1351
  #appendEvent(runtime, event) {
1222
1352
  const before = runtime.session.seq;
1223
1353
  runtime.applyingRemote = true;
1354
+ let mappedSeq;
1224
1355
  try {
1225
1356
  if (!isMirrorNoise(event.type)) {
1226
- appendRemoteEvent(runtime.session, remapRemoteEvent(event, runtime.remoteToLocalSeq), runtime);
1357
+ mappedSeq = appendRemoteEvent(runtime.session, remapRemoteEvent(event, runtime.remoteToLocalSeq), runtime);
1227
1358
  }
1228
1359
  } finally {
1229
1360
  runtime.applyingRemote = false;
1230
1361
  }
1231
1362
  const after = runtime.session.seq;
1232
- if (after > before) {
1363
+ if (mappedSeq !== void 0) {
1364
+ runtime.remoteToLocalSeq.set(event.seq, mappedSeq);
1365
+ this.#flushAssistantEnd(runtime, event.seq);
1366
+ } else if (after > before) {
1233
1367
  runtime.remoteToLocalSeq.set(event.seq, after - 1);
1234
1368
  this.#flushAssistantEnd(runtime, event.seq);
1235
1369
  }
@@ -1237,6 +1371,7 @@ var RemoteMirrorService = class {
1237
1371
  if (typeof turn !== "number") return;
1238
1372
  if (event.type === "turn/start") {
1239
1373
  runtime.latestTurn = Math.max(runtime.latestTurn, turn);
1374
+ this.#setRunning(runtime, true);
1240
1375
  return;
1241
1376
  }
1242
1377
  if (event.type !== "turn/end") return;
@@ -1245,6 +1380,16 @@ var RemoteMirrorService = class {
1245
1380
  runtime.awaitingTurn = null;
1246
1381
  runtime.awaitingStarted = false;
1247
1382
  this.#setRunning(runtime, false);
1383
+ const reason = typeof event.data === "object" && event.data !== null ? event.data.reason?.kind : void 0;
1384
+ if (reason === "aborted") {
1385
+ runtime.agent.inbox.clear();
1386
+ } else if (runtime.agent.inbox.hasPending) {
1387
+ const next = runtime.agent.inbox.nextTurn[0];
1388
+ if (next !== void 0) {
1389
+ runtime.agent.inbox.remove(messageIdentity(next));
1390
+ this.prompt(runtime, next, "queue");
1391
+ }
1392
+ }
1248
1393
  }
1249
1394
  }
1250
1395
  #onLocalSessionEvent(session, event) {
@@ -1459,11 +1604,8 @@ function appendRemoteEvent(session, event, runtime) {
1459
1604
  if (event.type === "user/message" && runtime.pendingUsers.length > 0) {
1460
1605
  const source = event.data.source;
1461
1606
  if (source?.kind === "user") {
1462
- const message = runtime.pendingUsers.shift();
1463
- if (message !== void 0) {
1464
- session.append("user/message", messageData(message), { surfaceOp: "append" });
1465
- return;
1466
- }
1607
+ const pending = runtime.pendingUsers.shift();
1608
+ if (pending !== void 0 && pending.localSeq >= 0) return pending.localSeq;
1467
1609
  }
1468
1610
  }
1469
1611
  const append = session.append.bind(session);
@@ -1476,6 +1618,7 @@ function appendRemoteEvent(session, event, runtime) {
1476
1618
  } else {
1477
1619
  append(event.type, event.data);
1478
1620
  }
1621
+ return void 0;
1479
1622
  }
1480
1623
  function messageData(message) {
1481
1624
  if (typeof message !== "object" || message === null) return message;
@@ -1544,20 +1687,25 @@ function latestRemoteSandboxMode(events) {
1544
1687
  }
1545
1688
  return void 0;
1546
1689
  }
1547
- function contentText(message) {
1548
- if (typeof message === "string") return message;
1549
- if (typeof message !== "object" || message === null) return "";
1690
+ function promptParts(message) {
1691
+ if (typeof message !== "object" || message === null) return [];
1550
1692
  const content = message.content;
1551
- if (!Array.isArray(content)) return "";
1552
- const texts = [];
1693
+ if (!Array.isArray(content)) return [];
1694
+ const parts = [];
1553
1695
  for (const part of content) {
1554
- if (typeof part === "string") texts.push(part);
1555
- else if (typeof part === "object" && part !== null && part.type === "text") {
1696
+ if (typeof part === "object" && part !== null && part.type === "text") {
1556
1697
  const text = part.text;
1557
- if (typeof text === "string") texts.push(text);
1698
+ if (typeof text === "string" && text.trim() !== "") parts.push({ type: "text", text });
1699
+ continue;
1700
+ }
1701
+ if (typeof part === "object" && part !== null && part.type === "image") {
1702
+ const attachment = part.attachment;
1703
+ if (attachment === void 0) continue;
1704
+ const name2 = attachment.name;
1705
+ parts.push({ type: "image", attachment, ...typeof name2 === "string" ? { name: name2 } : {} });
1558
1706
  }
1559
1707
  }
1560
- return texts.join("\n").trim();
1708
+ return parts;
1561
1709
  }
1562
1710
  function canonicalPath(path) {
1563
1711
  try {
@@ -1762,6 +1910,15 @@ function targetOf(mirrors, workspaceRoot, path) {
1762
1910
  workspaceRoot: anchor
1763
1911
  };
1764
1912
  }
1913
+ if (isAbsolute2(path)) {
1914
+ return {
1915
+ hostId: context.hostId,
1916
+ localPath: path,
1917
+ remotePath: path,
1918
+ relativePath: path,
1919
+ workspaceRoot: anchor
1920
+ };
1921
+ }
1765
1922
  throw Object.assign(
1766
1923
  new Error(`remote file path is outside the mirrored workspace: ${path}`),
1767
1924
  { code: "workspace-file/not-found" }
@@ -4601,12 +4758,13 @@ var RemoteHostsService = class {
4601
4758
  }
4602
4759
  }
4603
4760
  /** Send a prompt to a remote session. */
4604
- async promptSession(id, sessionId, text, mode, selection) {
4761
+ async promptSession(id, sessionId, text, mode, selection, content) {
4605
4762
  const client = this.#requireClient(id);
4606
4763
  try {
4607
4764
  await client.promptSession(sessionId, {
4608
4765
  text,
4609
4766
  ...mode === void 0 ? {} : { mode },
4767
+ ...content === void 0 ? {} : { content },
4610
4768
  ...selection === void 0 ? {} : selection
4611
4769
  });
4612
4770
  return { accepted: true };
@@ -17,6 +17,8 @@ export type HostApiEndpoint = 'state' | 'target/validate' | 'host/add' | 'host/r
17
17
  export interface RemoteHostsState {
18
18
  readonly protocolVersion: number;
19
19
  readonly hosts: readonly HostView[];
20
+ /** Local workspace ids whose Session creation is owned by a remote host. */
21
+ readonly mirroredWorkspaceIds: readonly string[];
20
22
  }
21
23
  /** Result of parsing a destination. */
22
24
  export interface ValidatedTarget {
@@ -25,6 +25,10 @@ interface MirrorRecord {
25
25
  lastSeq: number;
26
26
  updatedAt: string;
27
27
  }
28
+ interface PendingUser {
29
+ readonly message: unknown;
30
+ localSeq: number;
31
+ }
28
32
  interface MirrorRuntime {
29
33
  readonly record: MirrorRecord;
30
34
  readonly agent: MirrorAgent;
@@ -41,7 +45,7 @@ interface MirrorRuntime {
41
45
  running: boolean;
42
46
  stopped: boolean;
43
47
  lastError: string | null;
44
- pendingUsers: unknown[];
48
+ pendingUsers: PendingUser[];
45
49
  waiters: Array<() => void>;
46
50
  }
47
51
  interface MirrorAgent {
@@ -53,7 +57,7 @@ interface MirrorAgent {
53
57
  reasoningEffort?: string;
54
58
  };
55
59
  readonly ctx: Context;
56
- readonly inbox: Record<string, unknown[]>;
60
+ readonly inbox: MirrorInbox;
57
61
  readonly status: 'idle' | 'running';
58
62
  cancel(cause?: unknown): void;
59
63
  whenIdle(): Promise<void>;
@@ -63,6 +67,17 @@ interface MirrorAgent {
63
67
  steer(message: unknown): void;
64
68
  inject(message: unknown): void;
65
69
  }
70
+ interface MirrorInbox {
71
+ readonly 'next-turn': unknown[];
72
+ readonly 'next-step': unknown[];
73
+ readonly nextTurn: unknown[];
74
+ readonly nextStep: unknown[];
75
+ readonly hasPending: boolean;
76
+ remove(id: unknown): boolean;
77
+ replace(id: unknown, message: unknown): boolean;
78
+ splice(target: string, start: number, deleteCount: number, inserted: unknown[]): unknown[];
79
+ clear(): void;
80
+ }
66
81
  /** Mirror remote sessions into local Sessions and custom Agents. */
67
82
  export declare class RemoteMirrorService {
68
83
  #private;
@@ -79,6 +94,8 @@ export declare class RemoteMirrorService {
79
94
  remoteRoot: string;
80
95
  anchor: string;
81
96
  } | undefined;
97
+ /** Local workspace ids that represent remote workspaces. */
98
+ localWorkspaceIds(): string[];
82
99
  /** Whether the test-only manifest excludes this remote workspace from UI listings. */
83
100
  isTestHidden(remoteWorkspaceId: string, remotePath: string): boolean;
84
101
  /** Find the local mirror workspace for one remote workspace. */
@@ -84,7 +84,7 @@ export declare class RemoteHostsService {
84
84
  provider: string;
85
85
  model: string;
86
86
  reasoningEffort?: string;
87
- }): Promise<AcceptedResult>;
87
+ }, content?: readonly import('@dsh-remote/protocol').PromptContentPart[]): Promise<AcceptedResult>;
88
88
  /** Cancel a remote session's active turn. */
89
89
  cancelSession(id: string, sessionId: string): Promise<AcceptedResult>;
90
90
  /** SSH invocation facts for auxiliary remote file/terminal adapters. */
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-remote-local-plugin",
3
- "version": "0.8.8",
3
+ "version": "0.8.9",
4
4
  "description": "DSH remote-development local plugin: host registry, SSH tunnels, remote workspace/session discovery, an authenticated Connection RPC surface, agent tools, and a Settings page.",
5
5
  "keywords": [
6
6
  "dsh",