@timqi/pier 0.1.14 → 0.3.0

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 (130) hide show
  1. package/README.md +44 -22
  2. package/dist/agent/config.js +2 -2
  3. package/dist/agent/events.js +32 -3
  4. package/dist/agent/listing.js +14 -10
  5. package/dist/agent/packages.js +6 -10
  6. package/dist/agent/pi.js +113 -70
  7. package/dist/agent/roles.js +104 -0
  8. package/dist/agent/types.js +78 -0
  9. package/dist/boards/boards.js +42 -32
  10. package/dist/channels/chains.js +0 -4
  11. package/dist/channels/commands.js +7 -12
  12. package/dist/channels/config.js +27 -1
  13. package/dist/channels/control.js +5 -16
  14. package/dist/channels/conversations.js +4 -9
  15. package/dist/channels/lark-api.js +20 -26
  16. package/dist/channels/lark-outbound.js +21 -29
  17. package/dist/channels/lark-panel.js +1 -1
  18. package/dist/channels/lark.js +81 -39
  19. package/dist/channels/lines.js +33 -1
  20. package/dist/channels/panel.js +17 -107
  21. package/dist/channels/routes.js +11 -21
  22. package/dist/channels/runtime.js +79 -25
  23. package/dist/channels/slack-outbound.js +12 -14
  24. package/dist/channels/slack-panel.js +1 -1
  25. package/dist/channels/slack-render.js +1 -1
  26. package/dist/channels/slack-thread.js +65 -0
  27. package/dist/channels/slack.js +85 -124
  28. package/dist/channels/types.js +1 -1
  29. package/dist/cli.js +7 -2
  30. package/dist/config-sync.js +42 -37
  31. package/dist/core/chain.js +221 -0
  32. package/dist/core/identity.js +36 -13
  33. package/dist/core/inbound-file.js +11 -4
  34. package/dist/core/reply.js +111 -81
  35. package/dist/core/router.js +111 -118
  36. package/dist/core/search.js +65 -0
  37. package/dist/core/types.js +38 -77
  38. package/dist/db.js +41 -2
  39. package/dist/drain.js +1 -1
  40. package/dist/main.js +65 -40
  41. package/dist/settings.js +26 -19
  42. package/dist/socket.js +1 -0
  43. package/dist/tasks/agent.js +11 -5
  44. package/dist/tasks/callbacks.js +55 -5
  45. package/dist/tasks/cli.js +22 -11
  46. package/dist/tasks/definitions.js +30 -17
  47. package/dist/tasks/execution.js +11 -5
  48. package/dist/tasks/groups.js +5 -2
  49. package/dist/tasks/messages.js +7 -7
  50. package/dist/tasks/open-items.js +88 -0
  51. package/dist/tasks/operations.js +76 -39
  52. package/dist/tasks/outbox.js +19 -6
  53. package/dist/tasks/routes.js +19 -249
  54. package/dist/tasks/service.js +166 -34
  55. package/dist/tasks/store.js +123 -57
  56. package/dist/tasks/types.js +3 -0
  57. package/dist/tools.js +12 -5
  58. package/dist/web/auth.js +2 -2
  59. package/dist/web/config-sync.js +4 -5
  60. package/dist/web/config.js +9 -2
  61. package/dist/web/explorer.js +2 -2
  62. package/dist/web/fs.js +16 -1
  63. package/dist/web/instance.js +8 -5
  64. package/dist/web/packages.js +1 -1
  65. package/dist/web/passkeys.js +3 -3
  66. package/dist/web/providers.js +3 -3
  67. package/dist/web/public/assets/code-CWT27KH1.js +2 -0
  68. package/dist/web/public/assets/code-CWT27KH1.js.br +0 -0
  69. package/dist/web/public/assets/code-CWT27KH1.js.gz +0 -0
  70. package/dist/web/public/assets/explorer-C4DhXFCm.js +5 -0
  71. package/dist/web/public/assets/explorer-C4DhXFCm.js.br +0 -0
  72. package/dist/web/public/assets/explorer-C4DhXFCm.js.gz +0 -0
  73. package/dist/web/public/assets/highlight-CDSk2hRJ.js +72 -0
  74. package/dist/web/public/assets/highlight-CDSk2hRJ.js.br +0 -0
  75. package/dist/web/public/assets/highlight-CDSk2hRJ.js.gz +0 -0
  76. package/dist/web/public/assets/{hljs-tWqyD59G.js → hljs-BRz6a8Dq.js} +2 -2
  77. package/dist/web/public/assets/hljs-BRz6a8Dq.js.br +0 -0
  78. package/dist/web/public/assets/hljs-BRz6a8Dq.js.gz +0 -0
  79. package/dist/web/public/assets/index-D0ZYdweB.js +14 -0
  80. package/dist/web/public/assets/index-D0ZYdweB.js.br +0 -0
  81. package/dist/web/public/assets/index-D0ZYdweB.js.gz +0 -0
  82. package/dist/web/public/assets/index-JN0Ic8K5.css +2 -0
  83. package/dist/web/public/assets/index-JN0Ic8K5.css.br +0 -0
  84. package/dist/web/public/assets/index-JN0Ic8K5.css.gz +0 -0
  85. package/dist/web/public/assets/settings-DSTeuIyc.js +9 -0
  86. package/dist/web/public/assets/settings-DSTeuIyc.js.br +0 -0
  87. package/dist/web/public/assets/settings-DSTeuIyc.js.gz +0 -0
  88. package/dist/web/public/index.html +39 -109
  89. package/dist/web/public/index.html.br +0 -0
  90. package/dist/web/public/index.html.gz +0 -0
  91. package/dist/web/public/manifest.webmanifest +0 -2
  92. package/dist/web/push.js +3 -3
  93. package/dist/web/server.js +89 -116
  94. package/dist/web/session-state.js +4 -40
  95. package/docs/deploy.md +5 -3
  96. package/package.json +2 -1
  97. package/skills/pier-boards/SKILL.md +14 -4
  98. package/skills/pier-help/SKILL.md +62 -23
  99. package/skills/pier-search/SKILL.md +27 -0
  100. package/skills/pier-tasks/SKILL.md +68 -18
  101. package/dist/channels/handoff.js +0 -94
  102. package/dist/web/public/assets/activity-Ds6fCHnb.js +0 -5
  103. package/dist/web/public/assets/activity-Ds6fCHnb.js.br +0 -0
  104. package/dist/web/public/assets/activity-Ds6fCHnb.js.gz +0 -0
  105. package/dist/web/public/assets/boards-BKCj6EwK.js +0 -1
  106. package/dist/web/public/assets/boards-BKCj6EwK.js.br +0 -0
  107. package/dist/web/public/assets/boards-BKCj6EwK.js.gz +0 -0
  108. package/dist/web/public/assets/explorer-DnTm975c.js +0 -4
  109. package/dist/web/public/assets/explorer-DnTm975c.js.br +0 -0
  110. package/dist/web/public/assets/explorer-DnTm975c.js.gz +0 -0
  111. package/dist/web/public/assets/hljs-tWqyD59G.js.br +0 -0
  112. package/dist/web/public/assets/hljs-tWqyD59G.js.gz +0 -0
  113. package/dist/web/public/assets/index-BQo-haPN.js +0 -85
  114. package/dist/web/public/assets/index-BQo-haPN.js.br +0 -0
  115. package/dist/web/public/assets/index-BQo-haPN.js.gz +0 -0
  116. package/dist/web/public/assets/index-DiHj0w1i.css +0 -2
  117. package/dist/web/public/assets/index-DiHj0w1i.css.br +0 -0
  118. package/dist/web/public/assets/index-DiHj0w1i.css.gz +0 -0
  119. package/dist/web/public/assets/runs-C_AthWcW.js +0 -1
  120. package/dist/web/public/assets/runs-C_AthWcW.js.br +0 -0
  121. package/dist/web/public/assets/runs-C_AthWcW.js.gz +0 -0
  122. package/dist/web/public/assets/settings-DDaAtFlc.js +0 -5
  123. package/dist/web/public/assets/settings-DDaAtFlc.js.br +0 -0
  124. package/dist/web/public/assets/settings-DDaAtFlc.js.gz +0 -0
  125. package/dist/web/public/assets/task-runs-0pvdITiV.js +0 -3
  126. package/dist/web/public/assets/task-runs-0pvdITiV.js.br +0 -0
  127. package/dist/web/public/assets/task-runs-0pvdITiV.js.gz +0 -0
  128. package/dist/web/public/assets/tasks-Bz29caHJ.js +0 -4
  129. package/dist/web/public/assets/tasks-Bz29caHJ.js.br +0 -0
  130. package/dist/web/public/assets/tasks-Bz29caHJ.js.gz +0 -0
@@ -1,71 +1,27 @@
1
1
  // Assistant-reply presentation, computed once for every surface: the next-step
2
- // block syntax, silence, completion stats and the emphasis repair. Web chat and
3
- // every IM adapter render these, so the wording and units live here.
4
- /** The surface contract handed to every agent Pier launches (main.ts); the
5
- * syntax the agent is told to emit sits beside the parser that reads it back. */
6
- const REPLY_SURFACE_PROMPT = `## Pier chat surface
7
-
8
- Your replies render in a chat UI (web and IM). Three optional markdown
9
- conventions:
10
-
11
- - **Next-step buttons** — a last line of \`---\`, then up to 5 \`[label]\` tokens
12
- separated by \`|\`: \`---\` / \`[Run it] | [Show the diff]\`. A click sends that
13
- label as the user's next message. Only for short, obvious next moves, never
14
- for anything destructive.
15
- - **Attachments** — link a file you produced by absolute \`file://\` URL:
16
- \`[report.md](file:///abs/path/report.md)\`. Images render as thumbnails,
17
- other files as a download card, wherever on disk you wrote it. The same
18
- convention runs inbound: a user message ending in \`[name](file:///…)\`
19
- lines is carrying files the sender attached, already saved to disk — read
20
- one only when it matters to the task; every read puts its content in your
21
- context for good.
22
- - **Staying silent** — \`<silent>why</silent>\` is stripped, and if nothing else
23
- remains no message is sent. In a group chat you are handed every message,
24
- including humans talking to each other: stay silent rather than acknowledge
25
- what was not addressed to you.
26
-
27
- A message may start with \`[name<id> time place]\` — the sender and the chat,
28
- added by Pier, not typed by them. It appears only on a change — new speaker, a
29
- ~10-minute gap, a new day — so the last one still applies; a gap alone shows as
30
- time only, like \`[14:23]\`. Use that \`id\` to mention someone; never ask for
31
- their own. \`place\` is \`<platform>:<conversation>\` (Slack:
32
- \`slack:<channel>/<thread_ts>\`), said once per session: the channel and thread a
33
- script takes. Where no tool of yours takes that platform's ids, the header
34
- carries neither and reads \`[name time platform]\`.
35
- `;
36
- /** Deployment facts an agent cannot discover: a guessed path is wrong wherever
37
- * `PIER_HOME` moved and fails as "nothing is configured"; GPT models carry
38
- * `apply_patch` from post-training and go hunting for it in the shell. */
39
- export function surfacePrompt(instance) {
40
- const reach = instance.publicUrl
41
- ? `Address: ${instance.publicUrl} — a board's link is that plus ` +
42
- "`/boards/<slug>/`, or `/p/<slug>-<token>/` once published, where `token` " +
43
- "is the random field the manifest carries beside `public`."
44
- : "No public address is configured (the user sets one in Console → Settings), " +
45
- "so give paths and never guess a host.";
46
- return `${REPLY_SURFACE_PROMPT}
47
- ## This Pier instance
48
-
49
- Boards: \`${instance.boardsDir}/<slug>/\` — this path, not \`~/.pier\`. ${reach}
50
-
51
- Editing: files change through the \`edit\` tool (exact text replacement) or
52
- \`write\`. There is no \`apply_patch\` here — not as a tool, not as a command —
53
- so do not call one or go looking for one in the shell.
54
- `;
55
- }
2
+ // block syntax, silence, the open-item markers, completion stats and the
3
+ // emphasis repair. Web chat and every IM adapter render these, so the wording
4
+ // and units live here.
5
+ import { replaceOutsideCode } from "./inbound-file.js";
6
+ import { NOT_IN_LEDGER, TASK_RUN_STATES } from "./types.js";
7
+ // The syntax is told in agent/roles.ts; this file parses it back.
56
8
  /** Where a system input came from; wording every surface must spell the same. */
57
9
  export function originLabel(origin) {
58
- if (origin.kind === "error")
59
- return "\u26a0 failed";
60
- if (origin.kind !== "task-message") {
61
- return origin.kind === "task-delegation" ? "\u25b6 delegated task" : "\u21a9 task callback";
10
+ switch (origin.kind) {
11
+ case "error": return "\u26a0 failed";
12
+ case "task-delegation": return "\u25b6 delegated task";
13
+ case "task-callback": return "\u21a9 task callback";
14
+ case "session-seed": return `\u21ba new session \u00b7 ${origin.reason}`;
15
+ case "chat-command": return `/${origin.command}`;
16
+ case "task-message":
17
+ return `from a supervisor \u00b7 ${origin.messageKind === "steer" ? "\u270e steer" : "\uff0b follow-up"}`;
62
18
  }
63
- return `from a supervisor \u00b7 ${origin.messageKind === "steer" ? "\u270e steer" : "\uff0b follow-up"}`;
64
19
  }
65
20
  /** Is a turn coming once this note is posted? On IM the note is the only
66
- * message that turn has to wear the 👀; an error note reports a turn that
67
- * already ended, and a receipt on it would hang until the stale sweep. */
68
- export const awaitsTurn = (origin) => origin.kind !== "error";
21
+ * message that turn has to wear the 👀; a receipt on any other would hang
22
+ * until the stale sweep: an error reports a turn that already ended, a chat
23
+ * command's answer starts none, and the message that made a seed wears its own. */
24
+ export const awaitsTurn = (origin) => origin.kind !== "error" && origin.kind !== "chat-command" && origin.kind !== "session-seed";
69
25
  /** Punctuation that may be lifted out of a `**strong**` run: nothing a reader
70
26
  * can see changes, and the delimiter comes off a character the parser refuses
71
27
  * to close on. */
@@ -73,14 +29,9 @@ const LIFTABLE = /[\u201c\u201d"\u2018\u2019'()\uff08\uff09\u300c\u300d\u300e\u3
73
29
  /** CommonMark's right-flanking rule does not count CJK punctuation, so
74
30
  * `**“怎么做”**:` never closes (a spec hole, not a platform bug, and constant
75
31
  * in model output). Moving the punctuation outside — `“**怎么做**”:` — lands
76
- * the `**` against a letter. Fences and code spans are content, protected first. */
77
- export function cjkFriendly(markdown) {
78
- const stash = [];
79
- const keep = (text) => `\uE010${stash.push(text) - 1}\uE011`;
80
- let out = markdown
81
- .replace(/```[\s\S]*?```/g, (m) => keep(m))
82
- .replace(/`[^`\n]+`/g, (m) => keep(m));
83
- out = out.replace(/\*\*(\S|\S[\s\S]*?\S)\*\*/g, (whole, inner) => {
32
+ * the `**` against a letter. Fences and code spans are content, never touched. */
33
+ function cjkFriendly(markdown) {
34
+ return replaceOutsideCode(markdown, /\*\*(\S|\S[\s\S]*?\S)\*\*/g, ([whole, inner = ""]) => {
84
35
  let lead = "";
85
36
  let trail = "";
86
37
  let body = inner;
@@ -94,7 +45,6 @@ export function cjkFriendly(markdown) {
94
45
  }
95
46
  return lead || trail ? `${lead}**${body}**${trail}` : whole;
96
47
  });
97
- return out.replace(/\uE010(\d+)\uE011/g, (_m, i) => stash[Number(i)] ?? "");
98
48
  }
99
49
  /** The reason arrives pre-escaped: each surface escapes for its own markup,
100
50
  * and the label itself contains nothing any of them escape. */
@@ -105,6 +55,31 @@ export const isSilentReply = (reply) => !reply.text.trim() && reply.suggestions.
105
55
  export const thinkingLabel = (level) => level === "xhigh" ? "Extra high" : level[0].toUpperCase() + level.slice(1);
106
56
  /** `max` characters, the last one an ellipsis when something was cut. */
107
57
  export const cut = (text, max) => (text.length > max ? `${text.slice(0, max - 1)}…` : text);
58
+ /** Compact age of `ts` at `now` ("now", "12m", "3h", "2d"): every surface ages things the same way. */
59
+ export function relTime(ts, now) {
60
+ const mins = Math.round((now - ts) / 60_000);
61
+ if (mins < 1)
62
+ return "now";
63
+ if (mins < 60)
64
+ return `${String(mins)}m`;
65
+ if (mins < 1440)
66
+ return `${String(Math.round(mins / 60))}h`;
67
+ return `${String(Math.round(mins / 1440))}d`;
68
+ }
69
+ /** The age as it reads beside a wall clock, where "now" would be a fragment. */
70
+ export const agoLabel = (ts, now) => {
71
+ const age = relTime(ts, now);
72
+ return age === "now" ? "just now" : `${age} ago`;
73
+ };
74
+ /** A run's state and its age: how long it has been going, or how long ago it ended. */
75
+ export const runStatus = (r, now) => r.finishedAt === null ? `${r.state} ${relTime(r.queuedAt, now)}` : `${r.state} ${agoLabel(r.finishedAt, now)}`;
76
+ /** A lead's launches by state, in the states' own order: "2 running, 1 failed". */
77
+ export const workerCounts = (workers) => TASK_RUN_STATES.filter((s) => workers[s] > 0).map((s) => `${String(workers[s])} ${s}`).join(", ") || "none";
78
+ /** A run behind an open item, `run <id8>… <state> <age>`, a lead's with its workers;
79
+ * web/ui/turn-activity.ts `linkRuns` reads the token back. */
80
+ export const openRunText = (r, now) => r.state === NOT_IN_LEDGER
81
+ ? `run ${r.runId} — ${NOT_IN_LEDGER}`
82
+ : `run ${r.runId.length > 8 ? `${r.runId.slice(0, 8)}…` : r.runId} ${runStatus(r, now)}${r.workers ? ` · workers: ${workerCounts(r.workers)}` : ""}`;
108
83
  /** 1200 → "1.2K", 12_000 → "12K" — absolute token counts read badly inline. */
109
84
  export const compact = (n) => {
110
85
  if (n < 1000)
@@ -134,7 +109,47 @@ const MAX_SUGGESTIONS = 5;
134
109
  const ZW = String.raw `[\u200B-\u200D\u2060\uFEFF]*`;
135
110
  const tag = (literal) => literal.split("").join(ZW);
136
111
  const SILENT = new RegExp(`${tag("<silent>")}([\\s\\S]*?)${tag("</silent>")}`, "gi");
137
- const SILENT_TAG = new RegExp(`<(\\/?)${tag("silent>")}`, "gi");
112
+ /** A tag that hides what it wraps: a streaming renderer never cuts inside one. */
113
+ const HIDDEN_TAG = new RegExp(`<(\\/?)(${["silent", "open", "done"].map(tag).join("|")})${ZW}>`, "gi");
114
+ const ZW_CHARS = new RegExp(ZW.slice(0, -1), "g");
115
+ /** Main's open-item markers (docs/design/10-continuous-session.md): stripped like
116
+ * `<silent>`, and read back by core/chain.ts on the head's turn end. Code is
117
+ * content, so a marker inside a fence or a code span is neither. Group 1 is an
118
+ * `<open>` body, group 2 a `<done>` body. */
119
+ const MARKER = new RegExp(`${tag("<open>")}([\\s\\S]*?)${tag("</open>")}[ \\t]*\\n?|${tag("<done>")}([\\s\\S]*?)${tag("</done>")}[ \\t]*\\n?`, "gi");
120
+ const RUN_TOKEN = /\s*\(run\s+([^\s()]+)\)\s*$/i;
121
+ const oneLine = (text) => text.replace(/\s+/g, " ").trim();
122
+ /** The markers in reply order; `dropped` holds the ones with no problem text,
123
+ * for the caller to log. */
124
+ export function openItemMarkers(markdown) {
125
+ const markers = [];
126
+ const dropped = [];
127
+ replaceOutsideCode(markdown, MARKER, (m) => {
128
+ if (m[1] === undefined) {
129
+ const problem = oneLine(m[2] ?? "");
130
+ if (problem)
131
+ markers.push({ op: "done", problem });
132
+ else
133
+ dropped.push(m[0].trim());
134
+ return "";
135
+ }
136
+ let rest = oneLine(m[1]);
137
+ const runIds = [];
138
+ for (let run = RUN_TOKEN.exec(rest); run; run = RUN_TOKEN.exec(rest)) {
139
+ runIds.unshift(run[1]);
140
+ rest = rest.slice(0, run.index);
141
+ }
142
+ const dash = rest.indexOf("\u2014");
143
+ const problem = (dash < 0 ? rest : rest.slice(0, dash)).trim();
144
+ const stage = dash < 0 ? "" : rest.slice(dash + 1).trim();
145
+ if (problem)
146
+ markers.push({ op: "open", problem, stage, runIds });
147
+ else
148
+ dropped.push(m[0].trim());
149
+ return "";
150
+ });
151
+ return { markers, dropped };
152
+ }
138
153
  /** Why the agent stayed quiet; hidden from the chat, shown on the workbench,
139
154
  * where a silent turn must not look like a broken one. */
140
155
  export function silentReason(markdown) {
@@ -160,21 +175,31 @@ export function splitReply(rawMarkdown, meta) {
160
175
  }
161
176
  /** For rendering mid-turn: everything `splitReply` repairs, minus the
162
177
  * next-step block, which is only one at the very end of a turn. */
163
- export const streamBody = (markdown) => cjkFriendly(markdown.replace(SILENT, "").trim());
178
+ export const streamBody = (markdown) => cjkFriendly(unmarked(markdown));
179
+ const unmarked = (markdown) => replaceOutsideCode(markdown.replace(SILENT, ""), MARKER, () => "").trim();
180
+ /** What a message says with Pier's markup off — `<silent>`, the open-item
181
+ * markers, the next-step block — and otherwise untouched: search cuts its
182
+ * snippets from this. */
183
+ export function saidText(markdown) {
184
+ const body = unmarked(markdown);
185
+ const m = BLOCK.exec(body);
186
+ return m?.[1] ? body.slice(0, m.index).trimEnd() : body;
187
+ }
164
188
  /** Lines a blank line does not necessarily separate (a loose list is still one
165
189
  * list). Over-matching is fine: one boundary too few costs only a repaint. */
166
190
  const CONTINUES = /^(?:\s|[-*+>]|\d+[.)])/;
167
191
  /** Offset past the last blank line after `from` that closes a block, so a
168
192
  * streaming renderer can parse each closed prefix once (whole-reply reparse is
169
193
  * O(N²) over a turn). A boundary is claimed only where the two sides render the
170
- * same apart as together: never inside a fence, a `<silent>` block, or a loose
194
+ * same apart as together: never inside a fence, a `<silent>`/`<open>`/`<done>` block, or a loose
171
195
  * list/quote. A ```` fence is not closed by the ``` it quotes. */
172
196
  export function stableBlockEnd(markdown, from = 0) {
173
197
  // The last line is still growing, so it decides nothing.
174
198
  const end = markdown.lastIndexOf("\n") + 1;
175
199
  /** Marker run of the fence currently open; empty = closed. */
176
200
  let open = "";
177
- let silent = false;
201
+ /** The hiding tag currently open; empty = none. */
202
+ let hidden = "";
178
203
  let cut = from;
179
204
  /** Start of the block after a blank line, waiting for its first line. */
180
205
  let pending = -1;
@@ -184,21 +209,26 @@ export function stableBlockEnd(markdown, from = 0) {
184
209
  const line = markdown.slice(at, nl);
185
210
  at = nl + 1;
186
211
  const fence = /^\s*(`{3,}|~{3,})(.*)$/.exec(line);
187
- if (!open && !silent && !line.trim()) {
212
+ if (!open && !hidden && !line.trim()) {
188
213
  if (pending < 0 && prev)
189
214
  pending = at;
190
215
  continue;
191
216
  }
192
- if (!open && !silent && pending >= 0 && !(CONTINUES.test(prev) && CONTINUES.test(line)))
217
+ if (!open && !hidden && pending >= 0 && !(CONTINUES.test(prev) && CONTINUES.test(line)))
193
218
  cut = pending;
194
219
  const run = fence?.[1] ?? "";
195
- if (!open && run && !silent)
220
+ if (!open && run && !hidden)
196
221
  open = run;
197
222
  else if (open && run[0] === open[0] && run.length >= open.length && !fence?.[2]?.trim())
198
223
  open = "";
199
- if (!open && (!run || silent)) {
200
- for (const t of line.matchAll(SILENT_TAG))
201
- silent = !t[1];
224
+ if (!open && (!run || hidden)) {
225
+ for (const t of line.matchAll(HIDDEN_TAG)) {
226
+ const name = t[2].replace(ZW_CHARS, "").toLowerCase();
227
+ if (!t[1] && !hidden)
228
+ hidden = name;
229
+ else if (t[1] && name === hidden)
230
+ hidden = "";
231
+ }
202
232
  }
203
233
  pending = -1;
204
234
  prev = line;