@kubitnodes/stellaris 0.3.19 → 0.3.20

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 (59) hide show
  1. package/dist/cli/index.js +213 -105
  2. package/dist/server/index.js +201 -63
  3. package/package.json +1 -1
  4. package/web/dist/assets/BackLink-BjfX2KZl.js +1 -0
  5. package/web/dist/assets/{ChannelForms-BjD5Lp_u.js → ChannelForms-BYrcUldk.js} +1 -1
  6. package/web/dist/assets/ChannelView-GJ_DusLQ.js +2 -0
  7. package/web/dist/assets/{CitizenView-tvC6dNUB.js → CitizenView-Cx7IYSRr.js} +2 -2
  8. package/web/dist/assets/{CitizensView-DsUhIYtl.js → CitizensView-IKH7_yNQ.js} +1 -1
  9. package/web/dist/assets/{CliIcon-TlyIqh7V.js → CliIcon-vSuwLv2K.js} +1 -1
  10. package/web/dist/assets/CronsView-DJbi8Qhx.js +1 -0
  11. package/web/dist/assets/KnowledgeView-CoYOG9tI.js +1 -0
  12. package/web/dist/assets/{Markdown-CePsfcUS.js → Markdown-YC-fbHiE.js} +4 -4
  13. package/web/dist/assets/MessageItem-D7MUCA_T.js +1 -0
  14. package/web/dist/assets/MetricsView-CE94VW0I.js +1 -0
  15. package/web/dist/assets/NeedsYouView-Otnm3lcG.js +1 -0
  16. package/web/dist/assets/Overview-B8f6rLb2.js +1 -0
  17. package/web/dist/assets/{Pane-DGtHsANK.js → Pane-_D7n5uKH.js} +1 -1
  18. package/web/dist/assets/{Picker-Dq5Vvxod.js → Picker-DUiiQMFN.js} +1 -1
  19. package/web/dist/assets/ProjectView-D_ACOmEd.js +1 -0
  20. package/web/dist/assets/ProposalView-4d0U3nEb.js +2 -0
  21. package/web/dist/assets/{ProposalsView-BjEfHBtx.js → ProposalsView-BovF6CMD.js} +1 -1
  22. package/web/dist/assets/{RunnersView-CM-q_AX9.js → RunnersView-B2_uCVHC.js} +1 -1
  23. package/web/dist/assets/SocietyView-Bf0iX8AY.js +1 -0
  24. package/web/dist/assets/SubjectThread-B_Vmgs3d.js +1 -0
  25. package/web/dist/assets/{TaskFileView-BMAkbh-e.js → TaskFileView-BzO0gjE6.js} +2 -2
  26. package/web/dist/assets/{TaskView-HDwqXuYy.js → TaskView-DB_Nji99.js} +2 -2
  27. package/web/dist/assets/{TasksView-Dt8M_z96.js → TasksView-Bz36YYWq.js} +1 -1
  28. package/web/dist/assets/{ThreadForms-DbV3kPCi.js → ThreadForms-1mbMZsbv.js} +1 -1
  29. package/web/dist/assets/ThreadItem-QJSA92C4.js +1 -0
  30. package/web/dist/assets/ThreadView-BECLqt5B.js +1 -0
  31. package/web/dist/assets/{citizen-DQ0Bmk-r.js → citizen-DVFr5T6R.js} +1 -1
  32. package/web/dist/assets/{compose-Cx9c9k90.js → compose-_6feqP1t.js} +1 -1
  33. package/web/dist/assets/{format-SP8GnzAX.js → format-DxXtbdNM.js} +1 -1
  34. package/web/dist/assets/{governance-DX2ph-qr.js → governance-C9xH4QTm.js} +1 -1
  35. package/web/dist/assets/index-BEVCPm4C.js +23 -0
  36. package/web/dist/assets/index-BkOq4PSU.css +2 -0
  37. package/web/dist/assets/{link-DxSeFzFf.js → link-CTjFMNoY.js} +1 -1
  38. package/web/dist/assets/{tasks-BeWF0Xy2.js → tasks-B6I9DJ8O.js} +1 -1
  39. package/web/dist/assets/useMatch-B3RzAe8y.js +1 -0
  40. package/web/dist/assets/useStickyScroll-Ca7sWbGH.js +1 -0
  41. package/web/dist/index.html +9 -9
  42. package/web/dist/assets/BackLink-CDrsO9Ls.js +0 -1
  43. package/web/dist/assets/ChannelView-dI5EHpof.js +0 -2
  44. package/web/dist/assets/CronsView-BPlSyoAe.js +0 -1
  45. package/web/dist/assets/KnowledgeView-CgRGZrOK.js +0 -1
  46. package/web/dist/assets/MessageItem-kg5zWMDe.js +0 -1
  47. package/web/dist/assets/MetricsView-cJeDTfBE.js +0 -1
  48. package/web/dist/assets/NeedsYouView-Cd1TnY7W.js +0 -1
  49. package/web/dist/assets/Overview-B6aBmove.js +0 -1
  50. package/web/dist/assets/ProjectView-uskZHP8l.js +0 -1
  51. package/web/dist/assets/ProposalView-DmjIL53_.js +0 -2
  52. package/web/dist/assets/SocietyView-DZuGEqIl.js +0 -1
  53. package/web/dist/assets/SubjectThread-pCRDn42X.js +0 -1
  54. package/web/dist/assets/ThreadItem-BVlz2w6q.js +0 -1
  55. package/web/dist/assets/ThreadView-CpBytSH7.js +0 -1
  56. package/web/dist/assets/index-BDKI0Rje.js +0 -23
  57. package/web/dist/assets/index-BPVeX6tV.css +0 -2
  58. package/web/dist/assets/useMatch-HNekgJrF.js +0 -1
  59. package/web/dist/assets/useStickyScroll-BmqddNx5.js +0 -1
@@ -6,7 +6,7 @@ import path9 from "node:path";
6
6
  import { serve } from "@hono/node-server";
7
7
 
8
8
  // packages/board-core/dist/board.js
9
- import { chmod as chmod2, cp, mkdir as mkdir4, readdir as readdir3, readFile as readFile5, rename as rename3, rm as rm2, stat as stat2, writeFile as writeFile4 } from "node:fs/promises";
9
+ import { chmod as chmod2, cp, mkdir as mkdir4, readdir as readdir3, readFile as readFile4, rename as rename3, rm as rm2, stat as stat3, writeFile as writeFile4 } from "node:fs/promises";
10
10
  import path6 from "node:path";
11
11
  import { decodeTime, monotonicFactory } from "ulid";
12
12
  import { z as z13 } from "zod";
@@ -437,6 +437,14 @@ var MessageFrontmatterSchema = z4.object({
437
437
  ts: IsoDateTimeSchema,
438
438
  mentions: z4.array(NameSchema)
439
439
  });
440
+ var MessageRefSchema = MessageFrontmatterSchema.pick({
441
+ id: true,
442
+ author: true,
443
+ channel: true,
444
+ thread: true,
445
+ ts: true
446
+ });
447
+ var MESSAGE_LOOKUP_LIMIT = 50;
440
448
  var TaskStatusSchema = z4.enum(["open", "claimed", "done", "abandoned"]);
441
449
  var ThreadStateSchema = z4.enum(["open", "closed"]);
442
450
  var ThreadSubjectSchema = z4.object({
@@ -1861,7 +1869,7 @@ var RunnerConfigSchema = z11.object({
1861
1869
  });
1862
1870
 
1863
1871
  // packages/shared/dist/version.js
1864
- var STELLARIS_VERSION = "0.3.19";
1872
+ var STELLARIS_VERSION = "0.3.20";
1865
1873
 
1866
1874
  // packages/board-core/dist/errors.js
1867
1875
  var BoardError = class extends Error {
@@ -1877,7 +1885,7 @@ function isBoardError(error, code) {
1877
1885
  }
1878
1886
 
1879
1887
  // packages/board-core/dist/events.js
1880
- import { appendFile, readFile as readFile2 } from "node:fs/promises";
1888
+ import { appendFile, open, stat } from "node:fs/promises";
1881
1889
  import path2 from "node:path";
1882
1890
 
1883
1891
  // packages/board-core/dist/fs.js
@@ -1962,10 +1970,19 @@ async function listDirs(dir) {
1962
1970
 
1963
1971
  // packages/board-core/dist/events.js
1964
1972
  var LEADING_ID = /^\{"id":"([0-9A-HJKMNP-TV-Z]{26})"/;
1973
+ var ID_PREFIX_BYTES = 64;
1974
+ var NEWLINE = 10;
1965
1975
  var EventLog = class {
1966
1976
  file;
1967
1977
  newId;
1968
1978
  now;
1979
+ lines = [];
1980
+ /** Bytes the index covers: always the end of a complete line. */
1981
+ indexed = 0;
1982
+ /** The indexed file, to tell a replaced log from a grown one. */
1983
+ inode = null;
1984
+ scanning = Promise.resolve();
1985
+ listeners = /* @__PURE__ */ new Set();
1969
1986
  constructor(file, newId, now) {
1970
1987
  this.file = file;
1971
1988
  this.newId = newId;
@@ -1982,48 +1999,97 @@ var EventLog = class {
1982
1999
  await ensureDir(path2.dirname(this.file));
1983
2000
  await appendFile(this.file, `${JSON.stringify(event)}
1984
2001
  `, "utf8");
2002
+ for (const listener of this.listeners) {
2003
+ try {
2004
+ listener(event);
2005
+ } catch {
2006
+ }
2007
+ }
1985
2008
  return event;
1986
2009
  }
2010
+ /**
2011
+ * Calls `listener` after each event this process appends, until the returned function is
2012
+ * called. Another process's appends, such as the admin CLI's, are seen only by the next read.
2013
+ */
2014
+ onAppend(listener) {
2015
+ this.listeners.add(listener);
2016
+ return () => {
2017
+ this.listeners.delete(listener);
2018
+ };
2019
+ }
1987
2020
  /** Events with an id greater than `since`, oldest first. `null` reads from the beginning. */
1988
2021
  async readSince(since, limit2 = 1e3) {
1989
- if (!await exists(this.file)) {
1990
- return [];
1991
- }
1992
- const raw = await readFile2(this.file, "utf8");
1993
- const events = [];
1994
- for (const line of raw.split("\n")) {
1995
- if (line.trim().length === 0) {
1996
- continue;
1997
- }
1998
- const leading = LEADING_ID.exec(line)?.[1];
1999
- if (since !== null && leading !== void 0 && leading <= since) {
2022
+ await this.refresh();
2023
+ const chosen = [];
2024
+ for (const line of this.lines) {
2025
+ if (since !== null && line.id <= since) {
2000
2026
  continue;
2001
2027
  }
2002
- const event = BoardEventSchema.parse(JSON.parse(line));
2003
- if (since !== null && event.id <= since) {
2004
- continue;
2005
- }
2006
- events.push(event);
2007
- if (events.length >= limit2) {
2028
+ chosen.push(line);
2029
+ if (chosen.length >= limit2) {
2008
2030
  break;
2009
2031
  }
2010
2032
  }
2011
- return events;
2033
+ const first = chosen[0];
2034
+ const last = chosen.at(-1);
2035
+ if (first === void 0 || last === void 0) {
2036
+ return [];
2037
+ }
2038
+ const span = await this.readBytes(first.start, last.end);
2039
+ return chosen.map((line) => BoardEventSchema.parse(JSON.parse(span.toString("utf8", line.start - first.start, line.end - first.start))));
2012
2040
  }
2013
2041
  /** The id of the newest event, or null for an empty log. */
2014
2042
  async lastId() {
2015
- if (!await exists(this.file)) {
2016
- return null;
2043
+ await this.refresh();
2044
+ return this.lines.at(-1)?.id ?? null;
2045
+ }
2046
+ /** Brings the index up to the file's end, one scan at a time. */
2047
+ refresh() {
2048
+ this.scanning = this.scanning.then(() => this.scan(), () => this.scan());
2049
+ return this.scanning;
2050
+ }
2051
+ async scan() {
2052
+ const info = await stat(this.file).catch((error) => {
2053
+ if (error instanceof Error && "code" in error && error.code === "ENOENT") {
2054
+ return null;
2055
+ }
2056
+ throw error;
2057
+ });
2058
+ if (info === null || info.ino !== this.inode || info.size < this.indexed) {
2059
+ this.lines = [];
2060
+ this.indexed = 0;
2061
+ this.inode = info?.ino ?? null;
2062
+ }
2063
+ if (info === null || info.size === this.indexed) {
2064
+ return;
2065
+ }
2066
+ const added = await this.readBytes(this.indexed, info.size);
2067
+ let start = 0;
2068
+ for (let at = added.indexOf(NEWLINE); at >= 0; at = added.indexOf(NEWLINE, start)) {
2069
+ const prefix = added.toString("utf8", start, Math.min(at, start + ID_PREFIX_BYTES));
2070
+ if (prefix.trim().length > 0) {
2071
+ const id = LEADING_ID.exec(prefix)?.[1] ?? BoardEventSchema.parse(JSON.parse(added.toString("utf8", start, at))).id;
2072
+ this.lines.push({ id, start: this.indexed + start, end: this.indexed + at });
2073
+ }
2074
+ start = at + 1;
2075
+ }
2076
+ this.indexed += start;
2077
+ }
2078
+ async readBytes(from, to) {
2079
+ const handle = await open(this.file, "r");
2080
+ try {
2081
+ const buffer = Buffer.alloc(to - from);
2082
+ const { bytesRead } = await handle.read(buffer, 0, buffer.length, from);
2083
+ return buffer.subarray(0, bytesRead);
2084
+ } finally {
2085
+ await handle.close();
2017
2086
  }
2018
- const lines = (await readFile2(this.file, "utf8")).trimEnd().split("\n");
2019
- const last = lines.at(-1) ?? "";
2020
- return last.length === 0 ? null : BoardEventSchema.parse(JSON.parse(last)).id;
2021
2087
  }
2022
2088
  };
2023
2089
 
2024
2090
  // packages/board-core/dist/files.js
2025
2091
  import { createHash } from "node:crypto";
2026
- import { mkdir as mkdir2, readdir as readdir2, readFile as readFile3, rename as rename2, rm, stat, writeFile as writeFile2 } from "node:fs/promises";
2092
+ import { mkdir as mkdir2, readdir as readdir2, readFile as readFile2, rename as rename2, rm, stat as stat2, writeFile as writeFile2 } from "node:fs/promises";
2027
2093
  import path3 from "node:path";
2028
2094
  var MAX_FILE_BYTES = 8 * 1024 * 1024;
2029
2095
  function isMissing2(error) {
@@ -2045,9 +2111,9 @@ var FileTree = class {
2045
2111
  }
2046
2112
  try {
2047
2113
  const absolute = path3.join(root, ...relative.split("/"));
2048
- const info = await stat(absolute);
2114
+ const info = await stat2(absolute);
2049
2115
  if (info.isFile() && info.size <= MAX_FILE_BYTES) {
2050
- files[relative] = (await readFile3(absolute)).toString("base64");
2116
+ files[relative] = (await readFile2(absolute)).toString("base64");
2051
2117
  }
2052
2118
  } catch (error) {
2053
2119
  if (!isMissing2(error)) {
@@ -2103,7 +2169,7 @@ var FileTree = class {
2103
2169
  }
2104
2170
  async hash(absolute) {
2105
2171
  try {
2106
- const info = await stat(absolute);
2172
+ const info = await stat2(absolute);
2107
2173
  if (info.size > MAX_FILE_BYTES) {
2108
2174
  return null;
2109
2175
  }
@@ -2112,7 +2178,7 @@ var FileTree = class {
2112
2178
  if (known !== void 0 && known.mtimeMs === info.mtimeMs && known.size === info.size) {
2113
2179
  return fileVersion(known.sha, executable);
2114
2180
  }
2115
- const sha = createHash("sha256").update(await readFile3(absolute)).digest("hex");
2181
+ const sha = createHash("sha256").update(await readFile2(absolute)).digest("hex");
2116
2182
  this.hashes.set(absolute, { mtimeMs: info.mtimeMs, size: info.size, sha });
2117
2183
  return fileVersion(sha, executable);
2118
2184
  } catch (error) {
@@ -2125,7 +2191,7 @@ var FileTree = class {
2125
2191
  };
2126
2192
 
2127
2193
  // packages/board-core/dist/homes.js
2128
- import { chmod, mkdir as mkdir3, readFile as readFile4, writeFile as writeFile3 } from "node:fs/promises";
2194
+ import { chmod, mkdir as mkdir3, readFile as readFile3, writeFile as writeFile3 } from "node:fs/promises";
2129
2195
  import path4 from "node:path";
2130
2196
  import { execa } from "execa";
2131
2197
  var HOME_GITIGNORE = [
@@ -2269,7 +2335,7 @@ var HomeRepos = class {
2269
2335
  await writeFile3(ignore, IGNORE_FILE, "utf8");
2270
2336
  await git(["add", "--all"], home);
2271
2337
  await git(["commit", "--quiet", "--allow-empty", "-m", "home: created by the board"], home);
2272
- } else if (await readFile4(ignore, "utf8").catch(() => "") !== IGNORE_FILE) {
2338
+ } else if (await readFile3(ignore, "utf8").catch(() => "") !== IGNORE_FILE) {
2273
2339
  await writeFile3(ignore, IGNORE_FILE, "utf8");
2274
2340
  await git(["add", ".gitignore"], home);
2275
2341
  await git(["commit", "--quiet", "-m", "home: the board's ignore file"], home);
@@ -3188,8 +3254,8 @@ function restage(existing, edit) {
3188
3254
  };
3189
3255
  }
3190
3256
  function reopenStage(stage) {
3191
- const { completedBy: _by, completedAt: _at, ...open } = stage;
3192
- return open;
3257
+ const { completedBy: _by, completedAt: _at, ...open2 } = stage;
3258
+ return open2;
3193
3259
  }
3194
3260
  function gateChanges(before, after) {
3195
3261
  const changes = [];
@@ -3584,7 +3650,7 @@ var Board = class _Board {
3584
3650
  const task = { ...TaskFrontmatterSchema.parse(raw.data), body: raw.body };
3585
3651
  if ((legacy === "open" || legacy === "closed") && !await exists(this.paths.threadFile(threads, task.id))) {
3586
3652
  const ended = task.status === "done" || task.status === "abandoned";
3587
- const open = legacy === "open" && !ended;
3653
+ const open2 = legacy === "open" && !ended;
3588
3654
  const openedBy = opened.get(task.id);
3589
3655
  const closedBy = closed.get(task.id);
3590
3656
  await this.writeThread(threads, {
@@ -3592,10 +3658,10 @@ var Board = class _Board {
3592
3658
  channel: channelRef(slug, "general"),
3593
3659
  title: task.title,
3594
3660
  subject: { kind: "task", id: task.id },
3595
- state: open ? "open" : "closed",
3661
+ state: open2 ? "open" : "closed",
3596
3662
  openedBy: openedBy?.actor ?? task.createdBy,
3597
3663
  openedAt: openedBy?.ts ?? task.createdAt,
3598
- ...open ? {} : {
3664
+ ...open2 ? {} : {
3599
3665
  closedBy: closedBy?.actor ?? SYSTEM_ACTOR.name,
3600
3666
  closedAt: closedBy?.ts ?? this.now().toISOString()
3601
3667
  },
@@ -3658,7 +3724,7 @@ var Board = class _Board {
3658
3724
  for (const agent of await this.listAgents()) {
3659
3725
  const file = this.paths.agentCursors(agent.name);
3660
3726
  if (await exists(file)) {
3661
- const legacy = Legacy.safeParse(JSON.parse(await readFile5(file, "utf8")));
3727
+ const legacy = Legacy.safeParse(JSON.parse(await readFile4(file, "utf8")));
3662
3728
  if (legacy.success) {
3663
3729
  await writeJson(file, { digest: legacy.data.inbox });
3664
3730
  }
@@ -3997,6 +4063,10 @@ ${charter.purpose}
3997
4063
  async latestEventId() {
3998
4064
  return this.events.lastId();
3999
4065
  }
4066
+ /** Calls `listener` after each event the board appends, until the returned function is called. */
4067
+ onEventAppended(listener) {
4068
+ return this.events.onAppend(listener);
4069
+ }
4000
4070
  /** The roster as projected: every citizen with identity, reach, availability, and profile. */
4001
4071
  async listMembers() {
4002
4072
  const members = [];
@@ -4041,7 +4111,7 @@ ${charter.purpose}
4041
4111
  project,
4042
4112
  revision: revision.success ? revision.data : 0,
4043
4113
  updatedBy: updatedBy.success ? updatedBy.data : SYSTEM_ACTOR.name,
4044
- updatedAt: updatedAt.success ? updatedAt.data : (await stat2(file)).mtime.toISOString(),
4114
+ updatedAt: updatedAt.success ? updatedAt.data : (await stat3(file)).mtime.toISOString(),
4045
4115
  body
4046
4116
  };
4047
4117
  }
@@ -4143,6 +4213,28 @@ ${charter.purpose}
4143
4213
  async readThread(id) {
4144
4214
  return (await this.findThread(id)).thread;
4145
4215
  }
4216
+ /**
4217
+ * Where each of the messages named is and who wrote it, for text that names them by id; an id
4218
+ * that names no message is left out. Message files start with their ids, so only the files
4219
+ * asked for are read.
4220
+ */
4221
+ async findMessages(ids) {
4222
+ const wanted = new Set(ids);
4223
+ const found = [];
4224
+ for await (const dir of this.messageDirs()) {
4225
+ for (const file of await listFiles(dir)) {
4226
+ if (!wanted.delete(file.slice(0, 26))) {
4227
+ continue;
4228
+ }
4229
+ const { data } = await readMarkdown(path6.join(dir, file), MessageFrontmatterSchema);
4230
+ found.push(MessageRefSchema.parse(data));
4231
+ }
4232
+ if (wanted.size === 0) {
4233
+ break;
4234
+ }
4235
+ }
4236
+ return found;
4237
+ }
4146
4238
  /**
4147
4239
  * What citizens have asked the user and the user has not answered, oldest first: every message
4148
4240
  * by someone else that mentions the user, in an open thread or a channel the society or an
@@ -4644,12 +4736,12 @@ ${DASHBOARD_LINE}
4644
4736
  continue;
4645
4737
  }
4646
4738
  const named = thread === null ? channelConversation(filedIn, message.channel) : void 0;
4647
- let open = named === void 0 ? void 0 : openChannels.get(message.channel);
4648
- if (named !== void 0 && open === void 0) {
4649
- open = await this.channelOpen(message.channel);
4650
- openChannels.set(message.channel, open);
4739
+ let open2 = named === void 0 ? void 0 : openChannels.get(message.channel);
4740
+ if (named !== void 0 && open2 === void 0) {
4741
+ open2 = await this.channelOpen(message.channel);
4742
+ openChannels.set(message.channel, open2);
4651
4743
  }
4652
- const part = thread ?? (open === true ? `#${named}` : null);
4744
+ const part = thread ?? (open2 === true ? `#${named}` : null);
4653
4745
  if (conversation !== void 0 && (filedIn !== conversation.scope || part !== conversation.part)) {
4654
4746
  continue;
4655
4747
  }
@@ -4714,7 +4806,7 @@ ${task.body}`;
4714
4806
  }
4715
4807
  }
4716
4808
  for (const file of await listFiles(this.paths.projectKnowledge(project))) {
4717
- const content = await readFile5(path6.join(this.paths.projectKnowledge(project), file), "utf8");
4809
+ const content = await readFile4(path6.join(this.paths.projectKnowledge(project), file), "utf8");
4718
4810
  if (content.toLowerCase().includes(needle)) {
4719
4811
  hits.push({
4720
4812
  kind: "knowledge",
@@ -4752,7 +4844,7 @@ ${task.body}`;
4752
4844
  });
4753
4845
  }
4754
4846
  for (const entry of shared) {
4755
- const content = await readFile5(entry.file, "utf8");
4847
+ const content = await readFile4(entry.file, "utf8");
4756
4848
  if (content.toLowerCase().includes(needle)) {
4757
4849
  hits.push({
4758
4850
  kind: entry.kind,
@@ -6241,7 +6333,7 @@ ${rationale}`}`);
6241
6333
  throw new BoardError("NOT_FOUND", `no transcript of turn ${turnId} for ${agent}`);
6242
6334
  }
6243
6335
  const entries = [];
6244
- for (const line of (await readFile5(file, "utf8")).split("\n")) {
6336
+ for (const line of (await readFile4(file, "utf8")).split("\n")) {
6245
6337
  if (line.trim() === "")
6246
6338
  continue;
6247
6339
  const parsed = TranscriptEntrySchema.safeParse(JSON.parse(line));
@@ -6720,7 +6812,7 @@ ${rationale}`}`);
6720
6812
  }
6721
6813
  const file = entry.isFile() ? conflictCopyOf(relative) : null;
6722
6814
  if (file !== null) {
6723
- const since = await this.homes.changedAt(home, relative) ?? (await stat2(path6.join(home, relative)).catch(() => null))?.mtime.toISOString();
6815
+ const since = await this.homes.changedAt(home, relative) ?? (await stat3(path6.join(home, relative)).catch(() => null))?.mtime.toISOString();
6724
6816
  if (since !== void 0) {
6725
6817
  found.push({ path: relative, file, since });
6726
6818
  }
@@ -7298,7 +7390,7 @@ Short, curated, loaded on every turn. Keep entries that change future behavior a
7298
7390
  if (!isSafeRelativePath(entry.path)) {
7299
7391
  continue;
7300
7392
  }
7301
- const bytes = await readFile5(path6.join(dir, ...entry.path.split("/"))).catch(() => null);
7393
+ const bytes = await readFile4(path6.join(dir, ...entry.path.split("/"))).catch(() => null);
7302
7394
  const text = bytes === null || bytes.includes(0) ? null : bytes.toString("utf8");
7303
7395
  files.push({
7304
7396
  ...entry,
@@ -7997,6 +8089,23 @@ ${charter.purpose}
7997
8089
  yield* this.threadMessagesIn(this.paths.threads(slug), since);
7998
8090
  }
7999
8091
  }
8092
+ /** Every folder that holds messages: each channel's and each thread's, in the society and every project. */
8093
+ async *messageDirs() {
8094
+ for (const channel of await listDirs(this.paths.societyChannels())) {
8095
+ yield this.paths.societyChannel(channel);
8096
+ }
8097
+ for (const id of await listDirs(this.paths.societyThreads())) {
8098
+ yield this.paths.threadMessages(this.paths.societyThreads(), id);
8099
+ }
8100
+ for (const slug of await listDirs(this.paths.projects())) {
8101
+ for (const channel of await listDirs(this.paths.projectChannels(slug))) {
8102
+ yield this.paths.projectChannel(slug, channel);
8103
+ }
8104
+ for (const id of await listDirs(this.paths.threads(slug))) {
8105
+ yield this.paths.threadMessages(this.paths.threads(slug), id);
8106
+ }
8107
+ }
8108
+ }
8000
8109
  async *threadMessagesIn(threads, since) {
8001
8110
  for (const id of await listDirs(threads)) {
8002
8111
  yield* await this.readMessagesIn(this.paths.threadMessages(threads, id), since);
@@ -11864,7 +11973,7 @@ function runnerRoutes(board2, hub, enrollments, enrollLimit) {
11864
11973
  }
11865
11974
 
11866
11975
  // apps/server/dist/static.js
11867
- import { readFile as readFile6, stat as stat3 } from "node:fs/promises";
11976
+ import { readFile as readFile5, stat as stat4 } from "node:fs/promises";
11868
11977
  import path8 from "node:path";
11869
11978
  var CONTENT_TYPES = {
11870
11979
  ".html": "text/html; charset=utf-8",
@@ -11881,7 +11990,7 @@ var CONTENT_TYPES = {
11881
11990
  };
11882
11991
  async function isFile(file) {
11883
11992
  try {
11884
- return (await stat3(file)).isFile();
11993
+ return (await stat4(file)).isFile();
11885
11994
  } catch {
11886
11995
  return false;
11887
11996
  }
@@ -11906,7 +12015,7 @@ function spaHandler(staticDir) {
11906
12015
  const file = within(root, pathname);
11907
12016
  const headers = { "x-content-type-options": "nosniff" };
11908
12017
  if (file !== null && await isFile(file)) {
11909
- return c.body(await readFile6(file), 200, {
12018
+ return c.body(await readFile5(file), 200, {
11910
12019
  ...headers,
11911
12020
  "content-type": CONTENT_TYPES[path8.extname(file)] ?? "application/octet-stream",
11912
12021
  "cache-control": pathname.startsWith("/assets/") ? "public, max-age=31536000, immutable" : "no-cache"
@@ -11919,7 +12028,7 @@ function spaHandler(staticDir) {
11919
12028
  if (!await isFile(index)) {
11920
12029
  return c.text("The interface is not built: run pnpm build:web.", 404);
11921
12030
  }
11922
- return c.body(await readFile6(index), 200, {
12031
+ return c.body(await readFile5(index), 200, {
11923
12032
  ...headers,
11924
12033
  "content-type": CONTENT_TYPES[".html"] ?? "text/html",
11925
12034
  "cache-control": "no-cache"
@@ -11937,6 +12046,7 @@ var ERROR_STATUS = {
11937
12046
  INVALID_TRANSITION: 409,
11938
12047
  INVALID_STATE: 409
11939
12048
  };
12049
+ var EVENT_PAGE = 100;
11940
12050
  var RetireBodySchema = z16.object({ reason: z16.string().min(1) });
11941
12051
  var RunnerBodySchema = z16.object({ name: NameSchema });
11942
12052
  var EnrollmentApprovalSchema = z16.object({ name: NameSchema });
@@ -11944,6 +12054,7 @@ var HomeRunnerBodySchema = z16.object({ runner: NameSchema.nullable() });
11944
12054
  var ModelBodySchema = z16.object({ model: ModelNameSchema.nullable() });
11945
12055
  var EffortBodySchema = z16.object({ effort: EffortSchema.nullable() });
11946
12056
  var HistoryLimitSchema = z16.coerce.number().int().min(1).max(500);
12057
+ var MessageIdsSchema = z16.string().transform((ids) => ids.split(",").filter((id) => id !== "")).pipe(z16.array(UlidSchema).min(1).max(MESSAGE_LOOKUP_LIMIT));
11947
12058
  var TimezoneBodySchema = z16.object({ timezone: TimeZoneSchema });
11948
12059
  var ChannelBodySchema = z16.object({
11949
12060
  project: NameSchema.nullable().default(null),
@@ -11957,6 +12068,7 @@ function bearer2(c) {
11957
12068
  }
11958
12069
  function createApp(deps) {
11959
12070
  const { board: board2, version, turns: turns2, scheduler: scheduler2, models } = deps;
12071
+ const eventStreamPingMs = deps.eventStreamPingMs ?? 15e3;
11960
12072
  const app2 = new Hono3();
11961
12073
  app2.get("/health", (c) => c.json({ ok: true, version }));
11962
12074
  app2.onError((error, c) => {
@@ -12054,6 +12166,7 @@ function createApp(deps) {
12054
12166
  api.get("/proposals", async (c) => c.json(await board2.listProposals()));
12055
12167
  api.get("/crons", async (c) => c.json(await board2.listCrons()));
12056
12168
  api.get("/proposals/:id", async (c) => c.json(await board2.readProposal(c.req.param("id"))));
12169
+ api.get("/messages", async (c) => c.json(await board2.findMessages(MessageIdsSchema.parse(c.req.query("ids") ?? ""))));
12057
12170
  api.get("/proposals/:id/files", async (c) => c.json(await board2.readSkillFiles(UlidSchema.parse(c.req.param("id")))));
12058
12171
  api.get("/signals", async (c) => c.json(await board2.listSignals(Number(c.req.query("limit") ?? "100"))));
12059
12172
  api.get("/metrics", async (c) => {
@@ -12158,16 +12271,41 @@ function createApp(deps) {
12158
12271
  api.get("/events/stream", (c) => {
12159
12272
  let cursor = c.req.query("since") ?? null;
12160
12273
  return streamSSE2(c, async (stream) => {
12161
- if (cursor === "latest") {
12162
- cursor = await board2.latestEventId();
12163
- }
12164
- while (!stream.aborted && !stream.closed) {
12165
- const events = await board2.readEvents(cursor === "" ? null : cursor, 100);
12166
- for (const event of events) {
12167
- await stream.writeSSE({ id: event.id, event: event.type, data: JSON.stringify(event) });
12168
- cursor = event.id;
12274
+ let appended = false;
12275
+ let wake = null;
12276
+ const unsubscribe = board2.onEventAppended(() => {
12277
+ appended = true;
12278
+ wake?.();
12279
+ });
12280
+ stream.onAbort(() => wake?.());
12281
+ try {
12282
+ if (cursor === "latest") {
12283
+ cursor = await board2.latestEventId();
12284
+ }
12285
+ while (!stream.aborted && !stream.closed) {
12286
+ appended = false;
12287
+ const events = await board2.readEvents(cursor === "" ? null : cursor, EVENT_PAGE);
12288
+ for (const event of events) {
12289
+ await stream.writeSSE({ id: event.id, event: event.type, data: JSON.stringify(event) });
12290
+ cursor = event.id;
12291
+ }
12292
+ if (appended || events.length === EVENT_PAGE) {
12293
+ continue;
12294
+ }
12295
+ const woken = await new Promise((resolve) => {
12296
+ const timer = setTimeout(() => resolve(false), eventStreamPingMs);
12297
+ wake = () => {
12298
+ clearTimeout(timer);
12299
+ resolve(true);
12300
+ };
12301
+ });
12302
+ wake = null;
12303
+ if (!woken && !stream.aborted) {
12304
+ await stream.writeSSE({ event: "ping", data: "" });
12305
+ }
12169
12306
  }
12170
- await stream.sleep(500);
12307
+ } finally {
12308
+ unsubscribe();
12171
12309
  }
12172
12310
  });
12173
12311
  });
package/package.json CHANGED
@@ -8,7 +8,7 @@
8
8
  "codex",
9
9
  "multi-agent"
10
10
  ],
11
- "version": "0.3.19",
11
+ "version": "0.3.20",
12
12
  "license": "MIT",
13
13
  "homepage": "https://github.com/kr4t0n/stellaris",
14
14
  "repository": {
@@ -0,0 +1 @@
1
+ import{It as e,x as t,xn as n}from"./format-DxXtbdNM.js";import{O as r,n as i,r as a,t as o,v as s}from"./index-BEVCPm4C.js";var c=n(),l=`grid size-7 shrink-0 place-items-center rounded-lg text-fg-tertiary hover:bg-surface-2/70 hover:text-fg-primary`;function u({fallback:n}){let u=e(),d=r({select:e=>e.location.state.__TSR_index}),f=a(),p=s(),m=t(),h=i(f,d);if(h===null)return n;let g=o(h,{entity:e=>p.get(e)?.title,project:e=>m.data?.find(t=>t.slug===e)?.name});return(0,c.jsx)(`button`,{type:`button`,onClick:()=>u.history.back(),"aria-label":`Back to ${g}`,title:`Back to ${g}`,className:l,children:`←`})}export{u as t};
@@ -1 +1 @@
1
- import{A as e,bn as t,t as n,wn as r,xn as i,yn as a}from"./format-SP8GnzAX.js";import{a as o,o as s,s as c}from"./compose-Cx9c9k90.js";import{t as l}from"./link-DxSeFzFf.js";import{n as u,r as d}from"./ThreadForms-DbV3kPCi.js";var f=r(i(),1),p=t();function m({project:t,onDone:n}){let{api:r}=e(),i=a(),l=s(),[m,h]=(0,f.useState)(``),[g,_]=(0,f.useState)(``),v=(0,f.useRef)(null);(0,f.useEffect)(()=>v.current?.focus(),[]);let y=c({mutationFn:()=>r.createChannel({project:t,name:m.trim(),purpose:g.trim()}),onSuccess:async e=>{await i.invalidateQueries({queryKey:[`channels`]}),n(),await l({to:`/c/$`,params:{_splat:e}})}});return(0,p.jsxs)(`form`,{"aria-label":`New channel`,className:`space-y-2 border-b border-line bg-surface-2/20 p-3`,onSubmit:e=>{e.preventDefault(),y.mutate()},children:[(0,p.jsxs)(`div`,{className:`flex items-center gap-2`,children:[(0,p.jsx)(`div`,{className:`w-40 shrink-0`,children:(0,p.jsx)(`input`,{ref:v,value:m,maxLength:64,spellCheck:!1,"aria-label":`Channel name`,placeholder:`release-0-4`,onChange:e=>h(e.target.value),className:`${u} font-mono`})}),(0,p.jsx)(`input`,{value:g,"aria-label":`Purpose`,placeholder:`What the channel is for`,onChange:e=>_(e.target.value),className:`${u} min-w-0 flex-1`}),(0,p.jsx)(o,{onClick:n,children:`Cancel`}),(0,p.jsx)(o,{variant:`primary`,type:`submit`,disabled:y.isPending||m.trim()===``||g.trim()===``,children:y.isPending?`Opening…`:`Open`})]}),(0,p.jsx)(`p`,{className:`text-meta`,children:t===null?`Announced in the society's general, and followed by whoever subscribes to it; you read every channel without following one.`:`Announced in ${t}'s general, where its members choose to follow it; you read every channel without following one. Tasks filed in it hang their threads here, and it is archived once its tasks are done.`}),(0,p.jsx)(d,{error:y.error})]})}function h({channel:t,onDone:n}){let{api:r}=e(),i=a(),[s,l]=(0,f.useState)(``),m=c({mutationFn:()=>r.archiveChannel({channel:t,reason:s.trim()}),onSuccess:async()=>{await i.invalidateQueries({queryKey:[`channels`]}),n()}});return(0,p.jsxs)(`form`,{"aria-label":`Archive the channel`,className:`space-y-2 border-b border-line bg-surface-2/20 p-3`,onSubmit:e=>{e.preventDefault(),m.mutate()},children:[(0,p.jsxs)(`div`,{className:`flex items-center gap-2`,children:[(0,p.jsx)(`input`,{value:s,"aria-label":`Reason`,placeholder:`Why its work is done`,onChange:e=>l(e.target.value),className:`${u} min-w-0 flex-1`}),(0,p.jsx)(o,{onClick:n,children:`Cancel`}),(0,p.jsx)(o,{variant:`primary`,type:`submit`,disabled:m.isPending||s.trim()===``,children:m.isPending?`Archiving…`:`Archive channel`})]}),(0,p.jsx)(`p`,{className:`text-meta`,children:`Its open threads close, nobody follows it, and nothing more is posted or filed here; its posts, threads, and tasks stay readable. The board refuses while a task filed here is in play.`}),(0,p.jsx)(d,{error:m.error})]})}function g({channels:e,now:t}){return e.length===0?(0,p.jsx)(`p`,{className:`text-meta`,children:`No channels.`}):(0,p.jsx)(`ul`,{className:`space-y-1`,children:e.map(e=>(0,p.jsxs)(`li`,{children:[(0,p.jsxs)(l,{to:`/c/$`,params:{_splat:e.ref},className:`text-sm hover:text-fg-primary ${e.archived===void 0?`text-fg-secondary`:`text-fg-tertiary`}`,children:[`# `,e.name]}),(0,p.jsxs)(`span`,{className:`text-meta`,children:[` `,`· `,e.messages,` messages`,e.archived===void 0?``:` · archived ${n(e.archived.at,t)}`]})]},e.ref))})}export{g as n,m as r,h as t};
1
+ import{A as e,Sn as t,Tn as n,bn as r,t as i,xn as a}from"./format-DxXtbdNM.js";import{a as o,o as s,s as c}from"./compose-_6feqP1t.js";import{t as l}from"./link-CTjFMNoY.js";import{n as u,r as d}from"./ThreadForms-1mbMZsbv.js";var f=n(t(),1),p=a();function m({project:t,onDone:n}){let{api:i}=e(),a=r(),l=s(),[m,h]=(0,f.useState)(``),[g,_]=(0,f.useState)(``),v=(0,f.useRef)(null);(0,f.useEffect)(()=>v.current?.focus(),[]);let y=c({mutationFn:()=>i.createChannel({project:t,name:m.trim(),purpose:g.trim()}),onSuccess:async e=>{await a.invalidateQueries({queryKey:[`channels`]}),n(),await l({to:`/c/$`,params:{_splat:e}})}});return(0,p.jsxs)(`form`,{"aria-label":`New channel`,className:`space-y-2 border-b border-line bg-surface-2/20 p-3`,onSubmit:e=>{e.preventDefault(),y.mutate()},children:[(0,p.jsxs)(`div`,{className:`flex items-center gap-2`,children:[(0,p.jsx)(`div`,{className:`w-40 shrink-0`,children:(0,p.jsx)(`input`,{ref:v,value:m,maxLength:64,spellCheck:!1,"aria-label":`Channel name`,placeholder:`release-0-4`,onChange:e=>h(e.target.value),className:`${u} font-mono`})}),(0,p.jsx)(`input`,{value:g,"aria-label":`Purpose`,placeholder:`What the channel is for`,onChange:e=>_(e.target.value),className:`${u} min-w-0 flex-1`}),(0,p.jsx)(o,{onClick:n,children:`Cancel`}),(0,p.jsx)(o,{variant:`primary`,type:`submit`,disabled:y.isPending||m.trim()===``||g.trim()===``,children:y.isPending?`Opening…`:`Open`})]}),(0,p.jsx)(`p`,{className:`text-meta`,children:t===null?`Announced in the society's general, and followed by whoever subscribes to it; you read every channel without following one.`:`Announced in ${t}'s general, where its members choose to follow it; you read every channel without following one. Tasks filed in it hang their threads here, and it is archived once its tasks are done.`}),(0,p.jsx)(d,{error:y.error})]})}function h({channel:t,onDone:n}){let{api:i}=e(),a=r(),[s,l]=(0,f.useState)(``),m=c({mutationFn:()=>i.archiveChannel({channel:t,reason:s.trim()}),onSuccess:async()=>{await a.invalidateQueries({queryKey:[`channels`]}),n()}});return(0,p.jsxs)(`form`,{"aria-label":`Archive the channel`,className:`space-y-2 border-b border-line bg-surface-2/20 p-3`,onSubmit:e=>{e.preventDefault(),m.mutate()},children:[(0,p.jsxs)(`div`,{className:`flex items-center gap-2`,children:[(0,p.jsx)(`input`,{value:s,"aria-label":`Reason`,placeholder:`Why its work is done`,onChange:e=>l(e.target.value),className:`${u} min-w-0 flex-1`}),(0,p.jsx)(o,{onClick:n,children:`Cancel`}),(0,p.jsx)(o,{variant:`primary`,type:`submit`,disabled:m.isPending||s.trim()===``,children:m.isPending?`Archiving…`:`Archive channel`})]}),(0,p.jsx)(`p`,{className:`text-meta`,children:`Its open threads close, nobody follows it, and nothing more is posted or filed here; its posts, threads, and tasks stay readable. The board refuses while a task filed here is in play.`}),(0,p.jsx)(d,{error:m.error})]})}function g({channels:e,now:t}){return e.length===0?(0,p.jsx)(`p`,{className:`text-meta`,children:`No channels.`}):(0,p.jsx)(`ul`,{className:`space-y-1`,children:e.map(e=>(0,p.jsxs)(`li`,{children:[(0,p.jsxs)(l,{to:`/c/$`,params:{_splat:e.ref},className:`text-sm hover:text-fg-primary ${e.archived===void 0?`text-fg-secondary`:`text-fg-tertiary`}`,children:[`# `,e.name]}),(0,p.jsxs)(`span`,{className:`text-meta`,children:[` `,`· `,e.messages,` messages`,e.archived===void 0?``:` · archived ${i(e.archived.at,t)}`]})]},e.ref))})}export{g as n,m as r,h as t};
@@ -0,0 +1,2 @@
1
+ import{A as e,H as t,Sn as n,Tn as r,_ as i,b as a,c as o,en as s,ht as c,t as l,x as u,xn as d,z as f}from"./format-DxXtbdNM.js";import{a as p}from"./compose-_6feqP1t.js";import{t as m}from"./link-CTjFMNoY.js";import{A as h,C as g,i as _,k as v,s as y}from"./index-BEVCPm4C.js";import{i as b}from"./ThreadForms-1mbMZsbv.js";import{t as x}from"./ChannelForms-BYrcUldk.js";import{t as S}from"./MessageItem-D7MUCA_T.js";import{n as C,t as w}from"./Pane-_D7n5uKH.js";import{n as T}from"./ThreadItem-QJSA92C4.js";import{t as E}from"./useStickyScroll-Ca7sWbGH.js";var D=r(n(),1);function O(e,t){let n=e.notices.at(-1);return n!==void 0&&n.kind===`knowledge`&&t.kind===`knowledge`&&n.action!==`removed`&&t.action===`updated`&&n.topic===t.topic&&n.project===t.project}function k(e,t){let n=[],r=null;for(let i of e){let e=t(i),a=e?.notice;e===void 0||a===void 0?(n.push({kind:`item`,item:i}),r=null):r!==null&&O(r,a)?(r.messages.push(e),r.notices.push(a)):(r={messages:[e],notices:[a]},n.push({kind:`notices`,run:r}))}return n}function A(e){let[t]=e,n=e.length-1;return t===void 0||t.action===`removed`?{did:`removed`,times:null}:t.action===`written`?{did:`wrote`,times:n===0?null:`then updated it ${n===1?`once`:`${n} times`}`}:{did:`updated`,times:n===0?null:`${e.length} times`}}function j(e){let t=[...new Set(e)];return t.length<=2?t.join(` and `):`${t.slice(0,-1).join(`, `)}, and ${t.at(-1)??``}`}var M=d();function N(){return(0,M.jsx)(`svg`,{viewBox:`0 0 16 16`,"aria-hidden":`true`,className:`size-3.5`,children:(0,M.jsx)(`path`,{d:`M8 4.2C6.4 3 4.4 2.8 2.2 3.4v9.2c2.2-.6 4.2-.4 5.8.8m0-9.2c1.6-1.2 3.6-1.4 5.8-.8v9.2c-2.2-.6-4.2-.4-5.8.8m0-9.2v9.2`,fill:`none`,stroke:`currentColor`,strokeWidth:`1.2`,strokeLinejoin:`round`})})}function P({notices:e}){let[t]=e;if(t===void 0)return null;let{did:n,times:r}=A(e);return(0,M.jsxs)(M.Fragment,{children:[n,` knowledge`,` `,t.action===`removed`?(0,M.jsx)(`span`,{className:`text-fg-secondary`,children:t.topic}):(0,M.jsx)(m,{to:`/knowledge/$scope/$topic`,params:{scope:t.project??`society`,topic:t.topic},className:`text-fg-secondary hover:text-fg-primary`,children:t.topic}),r===null?null:` · ${r}`]})}function F(e){let t=e.notices.filter(e=>e.kind===`knowledge`);return{mark:(0,M.jsx)(N,{}),text:(0,M.jsx)(P,{notices:t})}}function I({run:e,now:t}){let n=y(),r=e.messages.at(-1);if(r===void 0)return null;let{mark:i,text:a}=F(e);return(0,M.jsxs)(`div`,{className:`flex gap-3 px-4 py-1`,children:[(0,M.jsx)(`span`,{className:`grid h-5 w-7 shrink-0 place-items-center text-fg-muted`,children:i}),(0,M.jsxs)(`div`,{className:`flex min-w-0 flex-1 items-baseline gap-3`,children:[(0,M.jsxs)(`p`,{className:`min-w-0 flex-1 truncate text-sm text-fg-tertiary`,children:[j(e.messages.map(e=>n(e.author))),` `,a]}),(0,M.jsx)(`time`,{dateTime:r.ts,title:e.messages.map(e=>`${n(e.author)} ${e.ts}`).join(`
2
+ `),className:`shrink-0 text-meta`,children:l(r.ts,t)})]})]})}function L(){let{_splat:e=`general`}=h({from:`/c/$`}),{message:t}=v({from:`/c/$`});return(0,M.jsx)(R,{channel:e,target:t},e)}function R({channel:n,target:r}){let{api:d}=e(),m=s({queryKey:[`channel`,n],queryFn:()=>d.channel(n)}),h=f(),v=i(),y=u(),O=o(),A=a(3e4),[j,N]=(0,D.useState)(!1),[P,F]=(0,D.useState)(!1),L=(0,D.useMemo)(()=>{let e=(m.data??[]).map(e=>({kind:`message`,at:e.ts,id:e.id,message:e})),t=(h.data??[]).filter(e=>e.channel===n).map(e=>({kind:`thread`,at:e.openedAt,id:e.id,thread:e}));return[...e,...t].toSorted((e,t)=>e.at.localeCompare(t.at)||e.id.localeCompare(t.id))},[m.data,h.data,n]),R=(0,D.useMemo)(()=>k(L,e=>e.kind===`message`?e.message:void 0),[L]),z=m.data?.at(-1)?.id??null;(0,D.useEffect)(()=>g(n,z),[n,z]);let{ref:B,onScroll:V}=E(r===void 0),[H,U]=n.split(`/`),W=U??H??n,G=U===void 0?void 0:y.data?.find(e=>e.slug===H),K=U===void 0?`society`:G?.name??H,q=O.data?.find(e=>e.ref===n)?.archived,J=G?.archived!==void 0||q!==void 0,Y=!J&&!c(U===void 0?null:H??null,W),X=L.filter(e=>e.kind===`thread`&&e.thread.state===`open`);return(0,M.jsxs)(M.Fragment,{children:[(0,M.jsx)(w,{title:`# ${W}`,subtitle:`${K} · ${m.data?.length??0} messages${X.length>0?` · ${X.length} open threads`:``}`,trailing:j||P||J?null:(0,M.jsxs)(M.Fragment,{children:[Y?(0,M.jsx)(p,{onClick:()=>F(!0),children:`Archive…`}):null,(0,M.jsx)(p,{onClick:()=>N(!0),children:`New thread`})]})}),j?(0,M.jsx)(b,{channel:n,onDone:()=>N(!1)}):null,P?(0,M.jsx)(x,{channel:n,onDone:()=>F(!1)}):null,(0,M.jsx)(`div`,{ref:B,onScroll:V,className:`flex-1 overflow-y-auto py-2`,children:m.error instanceof t&&m.error.status===404?(0,M.jsxs)(C,{children:[`There is no channel `,n,`.`]}):m.data===void 0?(0,M.jsx)(C,{children:`Reading the channel…`}):L.length===0?(0,M.jsx)(C,{children:`Nothing has been said here yet.`}):R.map(e=>{if(e.kind===`notices`){let t=e.run.messages[0]?.id??``;return(0,M.jsx)(I,{run:e.run,now:A},t)}let{item:t}=e;return t.kind===`message`?(0,M.jsx)(S,{message:t.message,members:v.data,now:A,target:t.id===r},t.id):(0,M.jsx)(T,{thread:t.thread,now:A},t.id)})}),m.data===void 0?null:q===void 0?J?(0,M.jsxs)(`p`,{className:`border-t border-line px-4 py-3 text-meta`,children:[K,` is archived, so its channels are read-only.`]}):(0,M.jsx)(_,{target:{channel:n},placeholder:`Message #${W}`}):(0,M.jsxs)(`p`,{className:`border-t border-line px-4 py-3 text-meta`,children:[`Archived `,l(q.at,A),` by `,q.by,`: `,q.reason,`. Its posts, threads, and tasks stay readable; nothing more is posted or filed here.`]})]})}export{L as ChannelView};