bothouse-dsh 0.1.47 → 0.1.48

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/client/client.js CHANGED
@@ -1,4 +1,4 @@
1
- // dsh-client-source-sha256: c6169b4843e9e2e6d885ef37f3a5ee6a3047260c3c6abdb4e3c4c58e93175a1f
1
+ // dsh-client-source-sha256: 4cdb6b6bd9c4f3a8ef008fced3e4c011501f74d835505b158ecc38bde39ca3a8
2
2
  // dsh-style-sha256: f5d384a5b03e6c383f622d9f8cb3563fb1695e55146740801d238d6606619709
3
3
  window.__ModuleLoader__.load({
4
4
  id: "bothouse-dsh",
@@ -104,6 +104,24 @@ const INITIAL = {
104
104
  const MAX_BACKOFF_MS = 3e4;
105
105
  /** Messages requested per page, both for the first read and for `loadOlder`. */
106
106
  const PAGE_SIZE = 100;
107
+ /** Browser storage key holding the channel the owner had open before a reload. */
108
+ const ROOM_KEY = "dsh-collab.room";
109
+ /** Remember the open channel so reopening the panel returns to it instead of the first one. */
110
+ const rememberRoom = (roomId) => {
111
+ try {
112
+ globalThis.localStorage?.setItem(ROOM_KEY, String(roomId));
113
+ } catch (error) {}
114
+ };
115
+ /** Channel id remembered by an earlier visit, or undefined when absent or unreadable. */
116
+ const rememberedRoom = () => {
117
+ try {
118
+ const raw = globalThis.localStorage?.getItem(ROOM_KEY);
119
+ const id = raw === null || raw === void 0 || raw === "" ? NaN : Number(raw);
120
+ return Number.isInteger(id) ? id : void 0;
121
+ } catch (error) {
122
+ return;
123
+ }
124
+ };
107
125
  /** Messages requested per catch-up read after a reconnect. */
108
126
  const CATCH_UP_LIMIT = 200;
109
127
  /** Status-coded service failure so the panel can tell "not enabled" from "not signed in". */
@@ -319,7 +337,8 @@ function createChannelSource(transport) {
319
337
  try {
320
338
  const state = await request("collab/api/state", { signal: read.controller.signal });
321
339
  if (!current()) return;
322
- const roomId = state.rooms.some((r) => r.id === previous.roomId) ? previous.roomId : state.rooms[0]?.id;
340
+ const remembered = previous.roomId === void 0 ? rememberedRoom() : void 0;
341
+ const roomId = state.rooms.some((r) => r.id === previous.roomId) ? previous.roomId : state.rooms.some((r) => r.id === remembered) ? remembered : state.rooms[0]?.id;
323
342
  const rows = [];
324
343
  let hasMoreOlder = false;
325
344
  if (roomId !== void 0) {
@@ -391,6 +410,7 @@ function createChannelSource(transport) {
391
410
  };
392
411
  const selectRoom = async (roomId) => {
393
412
  historyEpoch += 1;
413
+ rememberRoom(roomId);
394
414
  if (roomId !== snapshot.roomId) earlyTasks.clear();
395
415
  const shown = outbox.filter((entry) => entry.roomId === roomId);
396
416
  set(roomId === snapshot.roomId ? {
@@ -52,7 +52,9 @@ export const NOTICE_PREFIXES = ['📬', '✅', '↪', '⏳', '🔄', '⏹', '⏸
52
52
  const FOOTER = /(?:^|\n+)\*[^*\n]*·[^*]*\*\s*$/s;
53
53
  const BARE_MENTION = '(只 @ 了你,没写内容:看上面的频道消息判断要你做什么;看不出来就问)';
54
54
  export const PROGRESS_PREFIX = '__cc_connect_progress_card_v1__:';
55
- const CARD_STATES = { running: '处理中', completed: '已完成', failed: '失败' };
55
+ // cancelled (#913): a card whose turn was cut without an outcome — a restarted bridge, a
56
+ // cancelled DSH job. page.html styles it like any other card state.
57
+ const CARD_STATES = { running: '处理中', completed: '已完成', failed: '失败', cancelled: '已取消' };
56
58
  const RESERVED_IDS = new Set(['user', 'system']);
57
59
  const MAX_NAME = 64;
58
60
 
@@ -245,9 +247,40 @@ export class CloudWebGateway {
245
247
  this.hub.dropDrafts('role', id);
246
248
  // ...and its pending permission requests are gone with the old process.
247
249
  this.approvals.expireWhere(p => p.meta?.gateway === this && p.meta.bot === id);
250
+ // ...and the progress cards it already posted would stay 「处理中」 forever: the old
251
+ // process held the only handle, and the turn it resumes posts a second card (#913).
252
+ this.closeStaleCards(id);
248
253
  return bot;
249
254
  }
250
255
 
256
+ /**
257
+ * #913: close the progress cards a bot left running. A restarted cc-connect keeps no
258
+ * message ids, so nothing will ever edit those rows again, while its resumed turn posts
259
+ * a fresh card; the room then shows two 「处理中」 and no way to tell which is alive.
260
+ * They are closed as 已取消 (page.html already styles that state) and keep their steps,
261
+ * plus a line saying the turn was cut, so the history stays readable.
262
+ *
263
+ * Every card row this member posted is looked at, not the last page of one room: the
264
+ * room may have carried a hundred messages since (a busy channel, a workshop), and a
265
+ * running card there must still be closed. Only this member's own rows are touched,
266
+ * and only rows still `running`: a card the resumed turn reopens by its own handle goes
267
+ * back to 处理中 on the next frame.
268
+ */
269
+ closeStaleCards(memberId) {
270
+ for (const row of this.hub.store.authorCards('role', memberId)) {
271
+ let card;
272
+ try { card = JSON.parse(row.card); } catch { continue; }
273
+ if (card?.state !== 'running') continue;
274
+ const note = '⏹ 已中断(桥重启),这一步不会再更新';
275
+ const brief = typeof card.brief === 'string' && card.brief !== '' ? `${card.brief}\n${note}` : note;
276
+ // The stored title is already rendered ("CC · 处理中"): swap only its state label,
277
+ // so the card keeps its agent name.
278
+ const label = ` · ${CARD_STATES[card.state] ?? ''}`;
279
+ const agent = typeof card.title === 'string' && card.title.endsWith(label) ? card.title.slice(0, -label.length) : (card.title ?? 'Agent');
280
+ this.hub.edit(row.id, 'role', memberId, `${row.text}\n${note}`, JSON.stringify({ ...card, state: 'cancelled', title: `${agent} · ${CARD_STATES.cancelled}`, brief }));
281
+ }
282
+ }
283
+
251
284
  /** Forward one new channel message to every connected bot except its author. */
252
285
  onNew(msg) {
253
286
  if (msg.author_kind === 'system' || msg.card !== null || this.bots.size === 0 || this.hub.teamExit?.answerFor(msg)) return;