@sparkelf/dsh-group-chat 0.5.3 → 0.5.5

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
@@ -791,6 +791,36 @@ const cleanupRoomSessions = async (input) => {
791
791
  errors
792
792
  };
793
793
  };
794
+ //#endregion
795
+ //#region src/room-workspace.ts
796
+ /**
797
+ * Give a room's Session the workspace that makes it selectable.
798
+ *
799
+ * The path is the Session's own `cwd`: that is the one path `attachSession` will accept, and it
800
+ * is stable for the life of the Session (a room's directory is fixed when the Session is
801
+ * created). Reuse is by exact path, so opening the same room twice never creates a second
802
+ * workspace.
803
+ *
804
+ * @param registry - the workspace registry, absent on a minimal host.
805
+ * @param path - the room Session's directory.
806
+ * @param title - display title for a newly created workspace.
807
+ * @param sessionId - the room's canonical Session.
808
+ * @returns which workspace the room belongs to, and whether this call created it.
809
+ * @throws when the registry refuses the create or the attach; the caller decides how to report.
810
+ */
811
+ const ensureRoomWorkspace = async (registry, path, title, sessionId) => {
812
+ if (registry === void 0) return {
813
+ workspaceId: void 0,
814
+ created: false
815
+ };
816
+ const existing = await registry.resolveByPath(path);
817
+ const workspace = existing ?? await registry.create(path, title);
818
+ await workspace.attachSession(sessionId);
819
+ return {
820
+ workspaceId: workspace.id,
821
+ created: existing === void 0
822
+ };
823
+ };
794
824
  /**
795
825
  * Wrapper for built-in http.js to emulate the browser XMLHttpRequest object.
796
826
  *
@@ -9255,7 +9285,10 @@ var RoomSessions = class {
9255
9285
  */
9256
9286
  async ensure(roomId, room) {
9257
9287
  const existing = this.existing(roomId);
9258
- if (existing !== void 0) return existing;
9288
+ if (existing !== void 0) {
9289
+ this.titleRoomSession(existing, room.name);
9290
+ return existing;
9291
+ }
9259
9292
  const pending = this.pending.get(roomId);
9260
9293
  if (pending !== void 0) return pending;
9261
9294
  const opening = this.open(roomId, room).finally(() => {
@@ -9285,6 +9318,7 @@ var RoomSessions = class {
9285
9318
  const session = this.existing(roomId);
9286
9319
  if (session === void 0) throw new Error(`group-chat could not open a session for room ${roomId}`);
9287
9320
  await this.installRoomModel(session, room, true);
9321
+ this.titleRoomSession(session, room.name);
9288
9322
  return session;
9289
9323
  }
9290
9324
  /** Load a persisted room Session, or `undefined` only when the controller denies it exists. */
@@ -9331,6 +9365,24 @@ var RoomSessions = class {
9331
9365
  get archiveRegistry() {
9332
9366
  return hostService(this.ctx, "workspaceRegistry");
9333
9367
  }
9368
+ /**
9369
+ * Give the room's Session the title a reader expects, once.
9370
+ *
9371
+ * A room Session starts titled from its first message, which reads like an ordinary
9372
+ * conversation in the sidebar. Renaming happens only while the Session has never carried a
9373
+ * `session/title` event: once the plugin or the reader has named it, that choice stands.
9374
+ * @param session - the room's Session.
9375
+ * @param name - the room name, used as `群聊·<name>`.
9376
+ */
9377
+ titleRoomSession(session, name) {
9378
+ if (session.snapshotEvents().some((event) => event.type === "session/title")) return;
9379
+ const titles = hostService(this.ctx, "sessionTitle");
9380
+ try {
9381
+ titles?.rename(session, `群聊·${name}`);
9382
+ } catch (error) {
9383
+ this.ctx.logger?.warn?.("[group-chat] naming the room session failed", session.id, error);
9384
+ }
9385
+ }
9334
9386
  /** Hide a replacement the controller minted instead of honoring the room id. */
9335
9387
  async archiveReplacement(sessionId) {
9336
9388
  try {
@@ -10004,6 +10056,33 @@ const handleRoomRequest = async (route, httpMethod, payload, url, backend, runti
10004
10056
  * `purge: true` plus `confirm` equal to the room id. The canonical room Session is never a
10005
10057
  * candidate, and neither is another room's Session or a subagent child.
10006
10058
  */
10059
+ /**
10060
+ * Make one room the view: resolve its Session, give it the workspace that makes it selectable,
10061
+ * and answer with the Session id. The browser cannot do any of this itself -- it has no registry
10062
+ * and no workspace, and `openSession` refuses a Session no workspace owns.
10063
+ */
10064
+ const openMatch = /^rooms\/([^/]+)\/open$/u.exec(route);
10065
+ if (openMatch !== null && openMatch[1] !== void 0) {
10066
+ if (httpMethod !== "POST") throw new RouteRefusal("opening a room accepts POST", 405);
10067
+ const targetRoomId = decodeURIComponent(openMatch[1]);
10068
+ const room = (await backend.listRooms()).find((candidate) => candidate.id === targetRoomId);
10069
+ if (room === void 0) throw new RouteRefusal("this account is not a member of that room", 404);
10070
+ const session = await new RoomSessions(ctx).ensure(targetRoomId, {
10071
+ name: room.name,
10072
+ ...room.cwd === null ? {} : { cwd: room.cwd },
10073
+ agentPreset: room.agentPreset ?? void 0,
10074
+ modelProvider: room.modelProvider ?? void 0,
10075
+ model: room.model ?? void 0,
10076
+ reasoningEffort: room.reasoningEffort ?? void 0
10077
+ });
10078
+ const directory = session.header.cwd ?? resolveRoomDirectory(targetRoomId, room.cwd);
10079
+ const workspace = await ensureRoomWorkspace(hostService(ctx, "workspaceRegistry"), directory, `群聊·${room.name}`, session.id);
10080
+ return {
10081
+ sessionId: session.id,
10082
+ workspaceId: workspace.workspaceId,
10083
+ workspaceCreated: workspace.created
10084
+ };
10085
+ }
10007
10086
  const cleanupMatch = /^rooms\/([^/]+)\/sessions\/cleanup$/u.exec(route);
10008
10087
  if (cleanupMatch !== null && cleanupMatch[1] !== void 0) {
10009
10088
  if (httpMethod !== "POST") throw new RouteRefusal("session cleanup accepts POST", 405);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sparkelf/dsh-group-chat",
3
- "version": "0.5.3",
3
+ "version": "0.5.5",
4
4
  "description": "Human-agent group chat for DataOps-managed DSH workspaces: rooms broadcast to every member, and only a member's own @mention wakes that member's agent.",
5
5
  "type": "module",
6
6
  "main": "./lib/index.js",