@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.
- package/README.md +44 -22
- package/dist/agent/config.js +2 -2
- package/dist/agent/events.js +32 -3
- package/dist/agent/listing.js +14 -10
- package/dist/agent/packages.js +6 -10
- package/dist/agent/pi.js +113 -70
- package/dist/agent/roles.js +104 -0
- package/dist/agent/types.js +78 -0
- package/dist/boards/boards.js +42 -32
- package/dist/channels/chains.js +0 -4
- package/dist/channels/commands.js +7 -12
- package/dist/channels/config.js +27 -1
- package/dist/channels/control.js +5 -16
- package/dist/channels/conversations.js +4 -9
- package/dist/channels/lark-api.js +20 -26
- package/dist/channels/lark-outbound.js +21 -29
- package/dist/channels/lark-panel.js +1 -1
- package/dist/channels/lark.js +81 -39
- package/dist/channels/lines.js +33 -1
- package/dist/channels/panel.js +17 -107
- package/dist/channels/routes.js +11 -21
- package/dist/channels/runtime.js +79 -25
- package/dist/channels/slack-outbound.js +12 -14
- package/dist/channels/slack-panel.js +1 -1
- package/dist/channels/slack-render.js +1 -1
- package/dist/channels/slack-thread.js +65 -0
- package/dist/channels/slack.js +85 -124
- package/dist/channels/types.js +1 -1
- package/dist/cli.js +7 -2
- package/dist/config-sync.js +42 -37
- package/dist/core/chain.js +221 -0
- package/dist/core/identity.js +36 -13
- package/dist/core/inbound-file.js +11 -4
- package/dist/core/reply.js +111 -81
- package/dist/core/router.js +111 -118
- package/dist/core/search.js +65 -0
- package/dist/core/types.js +38 -77
- package/dist/db.js +41 -2
- package/dist/drain.js +1 -1
- package/dist/main.js +65 -40
- package/dist/settings.js +26 -19
- package/dist/socket.js +1 -0
- package/dist/tasks/agent.js +11 -5
- package/dist/tasks/callbacks.js +55 -5
- package/dist/tasks/cli.js +22 -11
- package/dist/tasks/definitions.js +30 -17
- package/dist/tasks/execution.js +11 -5
- package/dist/tasks/groups.js +5 -2
- package/dist/tasks/messages.js +7 -7
- package/dist/tasks/open-items.js +88 -0
- package/dist/tasks/operations.js +76 -39
- package/dist/tasks/outbox.js +19 -6
- package/dist/tasks/routes.js +19 -249
- package/dist/tasks/service.js +166 -34
- package/dist/tasks/store.js +123 -57
- package/dist/tasks/types.js +3 -0
- package/dist/tools.js +12 -5
- package/dist/web/auth.js +2 -2
- package/dist/web/config-sync.js +4 -5
- package/dist/web/config.js +9 -2
- package/dist/web/explorer.js +2 -2
- package/dist/web/fs.js +16 -1
- package/dist/web/instance.js +8 -5
- package/dist/web/packages.js +1 -1
- package/dist/web/passkeys.js +3 -3
- package/dist/web/providers.js +3 -3
- package/dist/web/public/assets/code-CWT27KH1.js +2 -0
- package/dist/web/public/assets/code-CWT27KH1.js.br +0 -0
- package/dist/web/public/assets/code-CWT27KH1.js.gz +0 -0
- package/dist/web/public/assets/explorer-C4DhXFCm.js +5 -0
- package/dist/web/public/assets/explorer-C4DhXFCm.js.br +0 -0
- package/dist/web/public/assets/explorer-C4DhXFCm.js.gz +0 -0
- package/dist/web/public/assets/highlight-CDSk2hRJ.js +72 -0
- package/dist/web/public/assets/highlight-CDSk2hRJ.js.br +0 -0
- package/dist/web/public/assets/highlight-CDSk2hRJ.js.gz +0 -0
- package/dist/web/public/assets/{hljs-tWqyD59G.js → hljs-BRz6a8Dq.js} +2 -2
- package/dist/web/public/assets/hljs-BRz6a8Dq.js.br +0 -0
- package/dist/web/public/assets/hljs-BRz6a8Dq.js.gz +0 -0
- package/dist/web/public/assets/index-D0ZYdweB.js +14 -0
- package/dist/web/public/assets/index-D0ZYdweB.js.br +0 -0
- package/dist/web/public/assets/index-D0ZYdweB.js.gz +0 -0
- package/dist/web/public/assets/index-JN0Ic8K5.css +2 -0
- package/dist/web/public/assets/index-JN0Ic8K5.css.br +0 -0
- package/dist/web/public/assets/index-JN0Ic8K5.css.gz +0 -0
- package/dist/web/public/assets/settings-DSTeuIyc.js +9 -0
- package/dist/web/public/assets/settings-DSTeuIyc.js.br +0 -0
- package/dist/web/public/assets/settings-DSTeuIyc.js.gz +0 -0
- package/dist/web/public/index.html +39 -109
- package/dist/web/public/index.html.br +0 -0
- package/dist/web/public/index.html.gz +0 -0
- package/dist/web/public/manifest.webmanifest +0 -2
- package/dist/web/push.js +3 -3
- package/dist/web/server.js +89 -116
- package/dist/web/session-state.js +4 -40
- package/docs/deploy.md +5 -3
- package/package.json +2 -1
- package/skills/pier-boards/SKILL.md +14 -4
- package/skills/pier-help/SKILL.md +62 -23
- package/skills/pier-search/SKILL.md +27 -0
- package/skills/pier-tasks/SKILL.md +68 -18
- package/dist/channels/handoff.js +0 -94
- package/dist/web/public/assets/activity-Ds6fCHnb.js +0 -5
- package/dist/web/public/assets/activity-Ds6fCHnb.js.br +0 -0
- package/dist/web/public/assets/activity-Ds6fCHnb.js.gz +0 -0
- package/dist/web/public/assets/boards-BKCj6EwK.js +0 -1
- package/dist/web/public/assets/boards-BKCj6EwK.js.br +0 -0
- package/dist/web/public/assets/boards-BKCj6EwK.js.gz +0 -0
- package/dist/web/public/assets/explorer-DnTm975c.js +0 -4
- package/dist/web/public/assets/explorer-DnTm975c.js.br +0 -0
- package/dist/web/public/assets/explorer-DnTm975c.js.gz +0 -0
- package/dist/web/public/assets/hljs-tWqyD59G.js.br +0 -0
- package/dist/web/public/assets/hljs-tWqyD59G.js.gz +0 -0
- package/dist/web/public/assets/index-BQo-haPN.js +0 -85
- package/dist/web/public/assets/index-BQo-haPN.js.br +0 -0
- package/dist/web/public/assets/index-BQo-haPN.js.gz +0 -0
- package/dist/web/public/assets/index-DiHj0w1i.css +0 -2
- package/dist/web/public/assets/index-DiHj0w1i.css.br +0 -0
- package/dist/web/public/assets/index-DiHj0w1i.css.gz +0 -0
- package/dist/web/public/assets/runs-C_AthWcW.js +0 -1
- package/dist/web/public/assets/runs-C_AthWcW.js.br +0 -0
- package/dist/web/public/assets/runs-C_AthWcW.js.gz +0 -0
- package/dist/web/public/assets/settings-DDaAtFlc.js +0 -5
- package/dist/web/public/assets/settings-DDaAtFlc.js.br +0 -0
- package/dist/web/public/assets/settings-DDaAtFlc.js.gz +0 -0
- package/dist/web/public/assets/task-runs-0pvdITiV.js +0 -3
- package/dist/web/public/assets/task-runs-0pvdITiV.js.br +0 -0
- package/dist/web/public/assets/task-runs-0pvdITiV.js.gz +0 -0
- package/dist/web/public/assets/tasks-Bz29caHJ.js +0 -4
- package/dist/web/public/assets/tasks-Bz29caHJ.js.br +0 -0
- package/dist/web/public/assets/tasks-Bz29caHJ.js.gz +0 -0
package/dist/core/reply.js
CHANGED
|
@@ -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
|
|
3
|
-
// every IM adapter render these, so the wording
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
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
|
-
|
|
59
|
-
return "\u26a0 failed";
|
|
60
|
-
|
|
61
|
-
|
|
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 👀;
|
|
67
|
-
*
|
|
68
|
-
|
|
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,
|
|
77
|
-
|
|
78
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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 && !
|
|
212
|
+
if (!open && !hidden && !line.trim()) {
|
|
188
213
|
if (pending < 0 && prev)
|
|
189
214
|
pending = at;
|
|
190
215
|
continue;
|
|
191
216
|
}
|
|
192
|
-
if (!open && !
|
|
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 && !
|
|
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 ||
|
|
200
|
-
for (const t of line.matchAll(
|
|
201
|
-
|
|
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;
|