@sparkelf/dsh-group-chat 0.5.4 → 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,48 +791,35 @@ const cleanupRoomSessions = async (input) => {
791
791
  errors
792
792
  };
793
793
  };
794
+ //#endregion
795
+ //#region src/room-workspace.ts
794
796
  /**
795
- * Register a room's Session with the workspace that owns its directory.
797
+ * Give a room's Session the workspace that makes it selectable.
796
798
  *
797
- * The shell's `openSession` selects a Session that belongs to a Workspace, so a room created
798
- * with a `cwd` and no workspace id is not selectable until this runs. Registration is idempotent
799
- * on the registry (`attachSession` checks membership before writing), which is what lets the
800
- * discovery poll run it on every boot without duplicating rows.
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.
801
803
  *
802
- * Never throws: a room that cannot be registered still reads and writes through its Host, only
803
- * the main view cannot follow it, and that is a state worth reporting rather than a failed open.
804
- * @param registry - the workspace registry surface, absent on a minimal host.
805
- * @param directory - the room directory the workspace is resolved by.
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.
806
807
  * @param sessionId - the room's canonical Session.
807
- * @param log - optional warning sink; the plugin passes its Cordis logger.
808
- * @returns whether the Session is now attached.
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.
809
810
  */
810
- const attachRoomSession = async (registry, directory, sessionId, log) => {
811
- if (registry === void 0) {
812
- log?.warn?.("[group-chat] the workspace registry is unavailable; the room session stays unlisted", sessionId);
813
- return {
814
- attached: false,
815
- reason: "registry-unavailable"
816
- };
817
- }
818
- try {
819
- const workspace = await registry.resolveByPath(directory);
820
- if (workspace === void 0) {
821
- log?.warn?.("[group-chat] no workspace owns the room directory; the room session stays unlisted", sessionId, directory);
822
- return {
823
- attached: false,
824
- reason: "workspace-not-found"
825
- };
826
- }
827
- await workspace.attachSession(sessionId);
828
- return { attached: true };
829
- } catch (error) {
830
- log?.warn?.("[group-chat] attaching the room session to its workspace failed", sessionId, error);
831
- return {
832
- attached: false,
833
- reason: "attach-failed"
834
- };
835
- }
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
+ };
836
823
  };
837
824
  /**
838
825
  * Wrapper for built-in http.js to emulate the browser XMLHttpRequest object.
@@ -9298,7 +9285,10 @@ var RoomSessions = class {
9298
9285
  */
9299
9286
  async ensure(roomId, room) {
9300
9287
  const existing = this.existing(roomId);
9301
- if (existing !== void 0) return existing;
9288
+ if (existing !== void 0) {
9289
+ this.titleRoomSession(existing, room.name);
9290
+ return existing;
9291
+ }
9302
9292
  const pending = this.pending.get(roomId);
9303
9293
  if (pending !== void 0) return pending;
9304
9294
  const opening = this.open(roomId, room).finally(() => {
@@ -9314,7 +9304,6 @@ var RoomSessions = class {
9314
9304
  const resumed = await this.resolvePersisted(sessionId, directory);
9315
9305
  if (resumed !== void 0) {
9316
9306
  await this.installRoomModel(resumed, room, false);
9317
- await this.attachToWorkspace(sessionId, directory);
9318
9307
  return resumed;
9319
9308
  }
9320
9309
  const created = await this.ctx.sessionController.create({
@@ -9329,7 +9318,7 @@ var RoomSessions = class {
9329
9318
  const session = this.existing(roomId);
9330
9319
  if (session === void 0) throw new Error(`group-chat could not open a session for room ${roomId}`);
9331
9320
  await this.installRoomModel(session, room, true);
9332
- await this.attachToWorkspace(sessionId, directory);
9321
+ this.titleRoomSession(session, room.name);
9333
9322
  return session;
9334
9323
  }
9335
9324
  /** Load a persisted room Session, or `undefined` only when the controller denies it exists. */
@@ -9377,18 +9366,22 @@ var RoomSessions = class {
9377
9366
  return hostService(this.ctx, "workspaceRegistry");
9378
9367
  }
9379
9368
  /**
9380
- * Attach the room's Session to the workspace that owns its directory.
9369
+ * Give the room's Session the title a reader expects, once.
9381
9370
  *
9382
- * The shell's `openSession` selects a Session that belongs to a Workspace, so a room created
9383
- * with a `cwd` alone is not selectable and clicking it moves the highlight only. Attaching is
9384
- * idempotent on the registry side, and every failure here is logged rather than fatal: a room
9385
- * whose registration failed still reads and writes through its Host, only the main view
9386
- * cannot follow it.
9387
- * @param sessionId - the room's canonical Session.
9388
- * @param directory - the room directory the workspace is resolved by.
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>`.
9389
9376
  */
9390
- async attachToWorkspace(sessionId, directory) {
9391
- await attachRoomSession(hostService(this.ctx, "workspaceRegistry"), directory, sessionId, this.ctx.logger);
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
+ }
9392
9385
  }
9393
9386
  /** Hide a replacement the controller minted instead of honoring the room id. */
9394
9387
  async archiveReplacement(sessionId) {
@@ -10063,6 +10056,33 @@ const handleRoomRequest = async (route, httpMethod, payload, url, backend, runti
10063
10056
  * `purge: true` plus `confirm` equal to the room id. The canonical room Session is never a
10064
10057
  * candidate, and neither is another room's Session or a subagent child.
10065
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
+ }
10066
10086
  const cleanupMatch = /^rooms\/([^/]+)\/sessions\/cleanup$/u.exec(route);
10067
10087
  if (cleanupMatch !== null && cleanupMatch[1] !== void 0) {
10068
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.4",
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",