@timqi/pier 0.1.0 → 0.1.1
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 +58 -125
- package/dist/agent/config.js +6 -15
- package/dist/agent/credentials.js +11 -23
- package/dist/agent/events.js +42 -64
- package/dist/agent/listing.js +39 -92
- package/dist/agent/pi.js +103 -252
- package/dist/boards/boards.js +19 -29
- package/dist/channels/attach.js +14 -42
- package/dist/channels/chains.js +33 -37
- package/dist/channels/chunk.js +8 -28
- package/dist/channels/commands.js +3 -14
- package/dist/channels/config.js +33 -52
- package/dist/channels/control.js +4 -13
- package/dist/channels/conversations.js +8 -25
- package/dist/channels/dedup.js +8 -17
- package/dist/channels/gatekeeper.js +13 -23
- package/dist/channels/lark-api.js +23 -63
- package/dist/channels/lark-outbound.js +12 -44
- package/dist/channels/lark-panel.js +7 -23
- package/dist/channels/lark-render.js +18 -62
- package/dist/channels/lark.js +52 -141
- package/dist/channels/lines.js +13 -15
- package/dist/channels/panel.js +16 -36
- package/dist/channels/receipts.js +29 -52
- package/dist/channels/routes.js +3 -9
- package/dist/channels/runtime.js +12 -23
- package/dist/channels/slack-api.js +34 -86
- package/dist/channels/slack-directory.js +7 -23
- package/dist/channels/slack-outbound.js +12 -56
- package/dist/channels/slack-panel.js +4 -13
- package/dist/channels/slack-render.js +23 -91
- package/dist/channels/slack-tool.js +48 -171
- package/dist/channels/slack.js +73 -239
- package/dist/channels/telegram-api.js +8 -20
- package/dist/channels/telegram-panel.js +5 -21
- package/dist/channels/telegram-render.js +13 -40
- package/dist/channels/telegram.js +54 -146
- package/dist/channels/types.js +5 -16
- package/dist/cli.js +17 -41
- package/dist/config-sync.js +87 -4
- package/dist/core/hub.js +7 -20
- package/dist/core/identity.js +20 -59
- package/dist/core/inbound-file.js +15 -49
- package/dist/core/inbox.js +12 -34
- package/dist/core/queue.js +3 -5
- package/dist/core/reply.js +41 -142
- package/dist/core/router.js +209 -264
- package/dist/core/types.js +4 -0
- package/dist/db.js +88 -272
- package/dist/drain.js +57 -50
- package/dist/extensions/index.js +3 -11
- package/dist/extensions/web/anthropic.js +3 -9
- package/dist/extensions/web/artifacts.js +2 -5
- package/dist/extensions/web/content.js +4 -12
- package/dist/extensions/web/http.js +2 -6
- package/dist/extensions/web/language.js +8 -18
- package/dist/extensions/web/openai.js +1 -1
- package/dist/extensions/web/provider.js +5 -18
- package/dist/extensions/web/tools.js +19 -63
- package/dist/lock.js +98 -0
- package/dist/log.js +9 -26
- package/dist/main.js +84 -183
- package/dist/paths.js +10 -26
- package/dist/secrets.js +18 -45
- package/dist/service.js +31 -73
- package/dist/settings.js +19 -63
- package/dist/tasks/agent.js +24 -45
- package/dist/tasks/callbacks.js +8 -16
- package/dist/tasks/command.js +2 -6
- package/dist/tasks/definitions.js +39 -62
- package/dist/tasks/execution.js +41 -39
- package/dist/tasks/groups.js +8 -11
- package/dist/tasks/messages.js +88 -155
- package/dist/tasks/outbox.js +33 -54
- package/dist/tasks/routes.js +4 -7
- package/dist/tasks/runs.js +4 -9
- package/dist/tasks/service.js +29 -42
- package/dist/tasks/store.js +36 -27
- package/dist/tasks/tool.js +59 -60
- package/dist/tools-task.js +20 -60
- package/dist/tools.js +98 -325
- package/dist/update.js +20 -43
- package/dist/web/auth.js +118 -179
- package/dist/web/config-sync.js +2 -2
- package/dist/web/config.js +3 -7
- package/dist/web/explorer.js +10 -21
- package/dist/web/fs.js +20 -42
- package/dist/web/instance.js +35 -82
- package/dist/web/providers.js +5 -11
- package/dist/web/public/assets/{activity-Bl3vZukb.js → activity-B89_hH7q.js} +1 -1
- package/dist/web/public/assets/activity-B89_hH7q.js.br +0 -0
- package/dist/web/public/assets/activity-B89_hH7q.js.gz +0 -0
- package/dist/web/public/assets/{boards-DYuf4Mlj.js → boards-BeKW0ZXK.js} +1 -1
- package/dist/web/public/assets/boards-BeKW0ZXK.js.br +0 -0
- package/dist/web/public/assets/boards-BeKW0ZXK.js.gz +0 -0
- package/dist/web/public/assets/explorer-DIuMlaV3.js +4 -0
- package/dist/web/public/assets/explorer-DIuMlaV3.js.br +0 -0
- package/dist/web/public/assets/explorer-DIuMlaV3.js.gz +0 -0
- package/dist/web/public/assets/index-DzXDXra_.js +85 -0
- package/dist/web/public/assets/index-DzXDXra_.js.br +0 -0
- package/dist/web/public/assets/index-DzXDXra_.js.gz +0 -0
- package/dist/web/public/assets/index-eqQLVS8Q.css +2 -0
- package/dist/web/public/assets/index-eqQLVS8Q.css.br +0 -0
- package/dist/web/public/assets/index-eqQLVS8Q.css.gz +0 -0
- package/dist/web/public/assets/{runs-BLJu7EXN.js → runs-Cwy0mN8i.js} +1 -1
- package/dist/web/public/assets/runs-Cwy0mN8i.js.br +0 -0
- package/dist/web/public/assets/runs-Cwy0mN8i.js.gz +0 -0
- package/dist/web/public/assets/{settings-BrdVh-Zi.js → settings-DzZLmujq.js} +1 -1
- package/dist/web/public/assets/settings-DzZLmujq.js.br +0 -0
- package/dist/web/public/assets/settings-DzZLmujq.js.gz +0 -0
- package/dist/web/public/assets/{task-runs-CeQS1rxa.js → task-runs-BCakxFk8.js} +1 -1
- package/dist/web/public/assets/task-runs-BCakxFk8.js.br +0 -0
- package/dist/web/public/assets/task-runs-BCakxFk8.js.gz +0 -0
- package/dist/web/public/assets/tasks-BlzEbk11.js +4 -0
- package/dist/web/public/assets/tasks-BlzEbk11.js.br +0 -0
- package/dist/web/public/assets/tasks-BlzEbk11.js.gz +0 -0
- package/dist/web/public/index.html +30 -16
- package/dist/web/public/index.html.br +0 -0
- package/dist/web/public/index.html.gz +0 -0
- package/dist/web/public/sw.js +14 -2
- package/dist/web/public/sw.js.br +0 -0
- package/dist/web/public/sw.js.gz +0 -0
- package/dist/web/push.js +55 -77
- package/dist/web/route.js +3 -7
- package/dist/web/server.js +109 -190
- package/dist/web/session-state.js +13 -53
- package/dist/web/types.js +2 -4
- package/dist/web/webpush.js +10 -25
- package/docs/deploy.md +115 -330
- package/package.json +1 -1
- package/skills/pier-boards/SKILL.md +81 -160
- package/skills/pier-help/SKILL.md +23 -20
- package/skills/pier-slack/SKILL.md +2 -2
- package/skills/pier-tasks/SKILL.md +23 -15
- package/dist/config-sync-fetch.js +0 -84
- package/dist/limits.js +0 -14
- package/dist/web/public/assets/activity-Bl3vZukb.js.br +0 -0
- package/dist/web/public/assets/activity-Bl3vZukb.js.gz +0 -0
- package/dist/web/public/assets/boards-DYuf4Mlj.js.br +0 -0
- package/dist/web/public/assets/boards-DYuf4Mlj.js.gz +0 -0
- package/dist/web/public/assets/explorer-qJH_9nTE.js +0 -4
- package/dist/web/public/assets/explorer-qJH_9nTE.js.br +0 -0
- package/dist/web/public/assets/explorer-qJH_9nTE.js.gz +0 -0
- package/dist/web/public/assets/index-Dqdb-Eqt.js +0 -85
- package/dist/web/public/assets/index-Dqdb-Eqt.js.br +0 -0
- package/dist/web/public/assets/index-Dqdb-Eqt.js.gz +0 -0
- package/dist/web/public/assets/index-DzmMzvi_.css +0 -2
- package/dist/web/public/assets/index-DzmMzvi_.css.br +0 -0
- package/dist/web/public/assets/index-DzmMzvi_.css.gz +0 -0
- package/dist/web/public/assets/runs-BLJu7EXN.js.br +0 -0
- package/dist/web/public/assets/runs-BLJu7EXN.js.gz +0 -0
- package/dist/web/public/assets/settings-BrdVh-Zi.js.br +0 -0
- package/dist/web/public/assets/settings-BrdVh-Zi.js.gz +0 -0
- package/dist/web/public/assets/task-runs-CeQS1rxa.js.br +0 -0
- package/dist/web/public/assets/task-runs-CeQS1rxa.js.gz +0 -0
- package/dist/web/public/assets/tasks-bcb3fYdK.js +0 -4
- package/dist/web/public/assets/tasks-bcb3fYdK.js.br +0 -0
- package/dist/web/public/assets/tasks-bcb3fYdK.js.gz +0 -0
package/dist/core/reply.js
CHANGED
|
@@ -1,18 +1,8 @@
|
|
|
1
1
|
// Assistant-reply presentation, computed once for every surface: the next-step
|
|
2
|
-
// block
|
|
3
|
-
//
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
// An agent may
|
|
7
|
-
// end a turn with a "next step" block — a `---` rule followed by a row of
|
|
8
|
-
// `[label]` tokens — and each surface renders those labels as buttons whose
|
|
9
|
-
// click sends the label back as an ordinary user message. The syntax lives
|
|
10
|
-
// here so web, IM adapters and Show pages never re-implement it.
|
|
11
|
-
/**
|
|
12
|
-
* The surface contract handed to every agent Pier launches (main.ts wires it
|
|
13
|
-
* into the factory). Both halves of the feature live in this file: the syntax
|
|
14
|
-
* the agent is told to emit, and the parser that reads it back.
|
|
15
|
-
*/
|
|
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. */
|
|
16
6
|
const REPLY_SURFACE_PROMPT = `## Pier chat surface
|
|
17
7
|
|
|
18
8
|
Your replies render in a chat UI (web and IM). Three optional markdown
|
|
@@ -39,22 +29,9 @@ typed by them. It appears only on a change — new speaker, a ~10-minute gap, a
|
|
|
39
29
|
new day — so the last one still applies; a gap alone shows as time only, like
|
|
40
30
|
\`[14:23]\`. Use that \`id\` to mention someone; never ask for their own.
|
|
41
31
|
`;
|
|
42
|
-
/**
|
|
43
|
-
*
|
|
44
|
-
*
|
|
45
|
-
*
|
|
46
|
-
* Told, not discovered. Pier knows them — the home is its own startup argument
|
|
47
|
-
* and the address is a file it wrote — while an agent can only guess at a
|
|
48
|
-
* path and shell out to look. The guess is wrong on every instance that moves
|
|
49
|
-
* `PIER_HOME`, and it fails as "nothing is configured": indistinguishable from
|
|
50
|
-
* the truth, and reported to the user as such. Which is what happened.
|
|
51
|
-
*
|
|
52
|
-
* The editing line is the same shape of fact, learned the same way: GPT models
|
|
53
|
-
* carry `apply_patch` from their own post-training, and not finding it in the
|
|
54
|
-
* tool list they go looking in the shell — one failed call, then a filesystem
|
|
55
|
-
* hunt, and in one session a stale Codex binary that worked, which put every
|
|
56
|
-
* later edit outside Pi's tooling.
|
|
57
|
-
*/
|
|
32
|
+
/** Deployment facts an agent cannot discover: a guessed path is wrong wherever
|
|
33
|
+
* `PIER_HOME` moved and fails as "nothing is configured"; GPT models carry
|
|
34
|
+
* `apply_patch` from post-training and go hunting for it in the shell. */
|
|
58
35
|
export function surfacePrompt(instance) {
|
|
59
36
|
const reach = instance.publicUrl
|
|
60
37
|
? `Address: ${instance.publicUrl} — a board's link is that plus ` +
|
|
@@ -72,13 +49,7 @@ Editing: files change through the \`edit\` tool (exact text replacement) or
|
|
|
72
49
|
so do not call one or go looking for one in the shell.
|
|
73
50
|
`;
|
|
74
51
|
}
|
|
75
|
-
/**
|
|
76
|
-
* Where a system input came from, in the fewest words that still say it.
|
|
77
|
-
*
|
|
78
|
-
* Lives here with `formatTurnMeta` for the same reason: it is *wording*, and
|
|
79
|
-
* every surface must spell it the same way. It was copied verbatim into the
|
|
80
|
-
* second adapter before landing here.
|
|
81
|
-
*/
|
|
52
|
+
/** Where a system input came from; wording every surface must spell the same. */
|
|
82
53
|
export function originLabel(origin) {
|
|
83
54
|
if (origin.kind === "error")
|
|
84
55
|
return "\u26a0 failed";
|
|
@@ -94,40 +65,18 @@ export function originLabel(origin) {
|
|
|
94
65
|
};
|
|
95
66
|
return `from a subagent \u00b7 ${kinds[origin.messageKind] ?? origin.messageKind}`;
|
|
96
67
|
}
|
|
97
|
-
/**
|
|
98
|
-
*
|
|
99
|
-
*
|
|
100
|
-
* A system input is context for the turn it triggers, so on IM the note is the
|
|
101
|
-
* only message that turn has to wear the 👀 — nobody typed one. An error note
|
|
102
|
-
* is the opposite: it reports a turn that already ended or never began, and a
|
|
103
|
-
* receipt marked on it would hang until the stale sweep.
|
|
104
|
-
*
|
|
105
|
-
* Beside `originLabel` for the same reason it is: three adapters ask this, and
|
|
106
|
-
* three copies of the answer are three chances to drift.
|
|
107
|
-
*/
|
|
68
|
+
/** Is a turn coming once this note is posted? On IM the note is the only
|
|
69
|
+
* message that turn has to wear the 👀; an error note reports a turn that
|
|
70
|
+
* already ended, and a receipt on it would hang until the stale sweep. */
|
|
108
71
|
export const awaitsTurn = (origin) => origin.kind !== "error";
|
|
109
|
-
/**
|
|
110
|
-
*
|
|
111
|
-
*
|
|
112
|
-
* longer bold — while taking the delimiter off a character the parser refuses
|
|
113
|
-
* to close on.
|
|
114
|
-
*/
|
|
72
|
+
/** Punctuation that may be lifted out of a `**strong**` run: nothing a reader
|
|
73
|
+
* can see changes, and the delimiter comes off a character the parser refuses
|
|
74
|
+
* to close on. */
|
|
115
75
|
const LIFTABLE = /[\u201c\u201d"\u2018\u2019'()\uff08\uff09\u300c\u300d\u300e\u300f\u3010\u3011\u300a\u300b\u3008\u3009[\]]/;
|
|
116
|
-
/**
|
|
117
|
-
*
|
|
118
|
-
*
|
|
119
|
-
*
|
|
120
|
-
* either not preceded by punctuation or else followed by whitespace or
|
|
121
|
-
* punctuation. `**\u201c\u600e\u4e48\u505a\u201d**\uff1a` fails both halves \u2014 preceded by a quote,
|
|
122
|
-
* followed by a CJK colon the rule does not count \u2014 so the run never closes and
|
|
123
|
-
* the reader sees literal asterisks. `**\u95f2\u804a**\uff1a` is fine, because a letter
|
|
124
|
-
* precedes the delimiter. This is a known ten-year-old hole in the spec around
|
|
125
|
-
* CJK, not a Slack bug, and it shows up constantly in model output.
|
|
126
|
-
*
|
|
127
|
-
* The repair is to move the punctuation outside the delimiters, so the `**`
|
|
128
|
-
* lands against a letter: `\u201c**\u600e\u4e48\u505a**\u201d\uff1a`. Code is protected first \u2014 asterisks
|
|
129
|
-
* inside a fence or a code span are content, not markup.
|
|
130
|
-
*/
|
|
76
|
+
/** CommonMark's right-flanking rule does not count CJK punctuation, so
|
|
77
|
+
* `**“怎么做”**:` never closes (a spec hole, not a platform bug, and constant
|
|
78
|
+
* in model output). Moving the punctuation outside — `“**怎么做**”:` — lands
|
|
79
|
+
* the `**` against a letter. Fences and code spans are content, protected first. */
|
|
131
80
|
export function cjkFriendly(markdown) {
|
|
132
81
|
const stash = [];
|
|
133
82
|
const keep = (text) => `\uE010${stash.push(text) - 1}\uE011`;
|
|
@@ -146,21 +95,14 @@ export function cjkFriendly(markdown) {
|
|
|
146
95
|
trail = body.at(-1) + trail;
|
|
147
96
|
body = body.slice(0, -1);
|
|
148
97
|
}
|
|
149
|
-
// Nothing was on the edges, or the run is only punctuation: leave it be.
|
|
150
98
|
return lead || trail ? `${lead}**${body}**${trail}` : whole;
|
|
151
99
|
});
|
|
152
100
|
return out.replace(/\uE010(\d+)\uE011/g, (_m, i) => stash[Number(i)] ?? "");
|
|
153
101
|
}
|
|
154
|
-
/**
|
|
155
|
-
*
|
|
156
|
-
* chat surfaces and the task result path. The reason arrives pre-escaped
|
|
157
|
-
* because each surface escapes for its own markup; the label itself contains
|
|
158
|
-
* nothing any of them escape.
|
|
159
|
-
*/
|
|
102
|
+
/** The reason arrives pre-escaped: each surface escapes for its own markup,
|
|
103
|
+
* and the label itself contains nothing any of them escape. */
|
|
160
104
|
export const quietLabel = (silence) => silence ? `stayed silent — ${silence}` : "no reply";
|
|
161
|
-
/**
|
|
162
|
-
* the buttons are the answer, so a turn that is only its options is not
|
|
163
|
-
* "nothing". Every surface decides through here. */
|
|
105
|
+
/** Options count as a reply: a turn that is only its buttons is not "nothing". */
|
|
164
106
|
export const isSilentReply = (reply) => !reply.text.trim() && reply.suggestions.length === 0;
|
|
165
107
|
/** How a reasoning level is spelled wherever a human reads it. */
|
|
166
108
|
export const thinkingLabel = (level) => level === "xhigh" ? "Extra high" : level[0].toUpperCase() + level.slice(1);
|
|
@@ -169,8 +111,7 @@ export const compact = (n) => {
|
|
|
169
111
|
if (n < 1000)
|
|
170
112
|
return String(n);
|
|
171
113
|
const k = n / 1000;
|
|
172
|
-
// Precision
|
|
173
|
-
// a "10.0K" that claims a decimal it does not have.
|
|
114
|
+
// Precision from the rounded value: 9_990 reads "10K", not "10.0K".
|
|
174
115
|
return k >= 9.95 ? `${Math.round(k)}K` : `${k.toFixed(1)}K`;
|
|
175
116
|
};
|
|
176
117
|
/** "45s" / "1m14s". Floored at one second: sub-second precision is noise. */
|
|
@@ -178,42 +119,20 @@ function formatDuration(ms) {
|
|
|
178
119
|
const secs = Math.max(1, Math.round(ms / 1000));
|
|
179
120
|
return secs < 60 ? `${secs}s` : `${Math.floor(secs / 60)}m${secs % 60}s`;
|
|
180
121
|
}
|
|
181
|
-
/**
|
|
182
|
-
* A turn's cost, in the one wording every surface uses: how long it took and
|
|
183
|
-
* how big the context is now. `tokens` is the context size at completion, not
|
|
184
|
-
* a per-turn sum (see TurnMeta) — the label stays "tok" because that is what
|
|
185
|
-
* the number is counted in.
|
|
186
|
-
*/
|
|
122
|
+
/** `tokens` is the context size at completion, not a per-turn sum (TurnMeta). */
|
|
187
123
|
export const formatTurnMeta = (meta) => `${formatDuration(meta.durationMs)} · ${compact(meta.tokens)} tok`;
|
|
188
|
-
/**
|
|
189
|
-
*
|
|
190
|
-
*
|
|
191
|
-
* blocks stay message content instead of turning into buttons.
|
|
192
|
-
*
|
|
193
|
-
* `(?:^|\n)` because a turn is allowed to be nothing but its options: anchoring
|
|
194
|
-
* on a preceding newline left that whole block sitting in the message as raw
|
|
195
|
-
* text, with no buttons anywhere.
|
|
196
|
-
*/
|
|
124
|
+
/** Trailing `---` line + a row of bracket tokens, optionally `|`-separated. A
|
|
125
|
+
* markdown link leaves `(url)` unmatched, so reference-link blocks stay
|
|
126
|
+
* content. `(?:^|\n)`: a turn may be nothing but its options. */
|
|
197
127
|
const BLOCK = /(?:^|\n)[ \t]*-{3,}[ \t]*\r?\n((?:[ \t]*\[[^\]\r\n]+\][ \t]*(?:[||][ \t]*)?)+)\s*$/;
|
|
198
128
|
const TOKEN = /\[([^\]\r\n]+)\]/g;
|
|
199
129
|
const MAX_SUGGESTIONS = 5;
|
|
200
|
-
/**
|
|
201
|
-
*
|
|
202
|
-
*
|
|
203
|
-
* unusable. Stripping the block here means an adapter needs no new concept: a
|
|
204
|
-
* turn whose text is empty already posts nothing and still retires its per-turn
|
|
205
|
-
* UI, which is exactly "listened, said nothing".
|
|
206
|
-
*
|
|
207
|
-
* The reason inside stays in the Pi transcript, so the choice is auditable
|
|
208
|
-
* without being broadcast to the chat.
|
|
209
|
-
*/
|
|
130
|
+
/** A deliberate non-answer. Stripped here so an adapter needs no new concept:
|
|
131
|
+
* an empty turn already posts nothing and retires its per-turn UI. The reason
|
|
132
|
+
* stays in the transcript, auditable without being broadcast. */
|
|
210
133
|
const SILENT = /<silent>([\s\S]*?)<\/silent>/gi;
|
|
211
|
-
/**
|
|
212
|
-
*
|
|
213
|
-
* workbench is the operator's own view of the run — "it said nothing and here
|
|
214
|
-
* is why" is observability, and hiding it there would just make a silent turn
|
|
215
|
-
* look like a broken one.
|
|
216
|
-
*/
|
|
134
|
+
/** Why the agent stayed quiet; hidden from the chat, shown on the workbench,
|
|
135
|
+
* where a silent turn must not look like a broken one. */
|
|
217
136
|
export function silentReason(markdown) {
|
|
218
137
|
const reasons = [...markdown.matchAll(SILENT)]
|
|
219
138
|
.map((m) => (m[1] ?? "").trim())
|
|
@@ -222,9 +141,6 @@ export function silentReason(markdown) {
|
|
|
222
141
|
}
|
|
223
142
|
/** Split an assistant turn's markdown into renderable text + next-step labels. */
|
|
224
143
|
export function splitReply(rawMarkdown, meta) {
|
|
225
|
-
// Every surface that renders this goes through here, and every CommonMark
|
|
226
|
-
// parser has some version of the CJK emphasis hole — so the repair belongs
|
|
227
|
-
// once, at the seam, not per adapter.
|
|
228
144
|
const markdown = streamBody(rawMarkdown);
|
|
229
145
|
const silence = silentReason(rawMarkdown);
|
|
230
146
|
const m = BLOCK.exec(markdown);
|
|
@@ -238,36 +154,19 @@ export function splitReply(rawMarkdown, meta) {
|
|
|
238
154
|
return { text: markdown, suggestions: [], meta, silence };
|
|
239
155
|
return { text: markdown.slice(0, m.index).trimEnd(), suggestions, meta, silence };
|
|
240
156
|
}
|
|
241
|
-
/**
|
|
242
|
-
*
|
|
243
|
-
* before the turn ends: everything `splitReply` repairs, minus the next-step
|
|
244
|
-
* block. That block is only a next-step block at the very *end* of a turn, so
|
|
245
|
-
* text a later paragraph has already closed behind keeps it as body — which is
|
|
246
|
-
* what the final render will do with it too.
|
|
247
|
-
*/
|
|
157
|
+
/** For rendering mid-turn: everything `splitReply` repairs, minus the
|
|
158
|
+
* next-step block, which is only one at the very end of a turn. */
|
|
248
159
|
export const streamBody = (markdown) => cjkFriendly(markdown.replace(SILENT, "").trim());
|
|
249
|
-
/** Lines a blank line does not necessarily separate
|
|
250
|
-
*
|
|
251
|
-
* Deliberately over-matching (`*` and `-` also catch emphasis and a rule):
|
|
252
|
-
* claiming one boundary too few costs nothing but a repaint. */
|
|
160
|
+
/** Lines a blank line does not necessarily separate (a loose list is still one
|
|
161
|
+
* list). Over-matching is fine: one boundary too few costs only a repaint. */
|
|
253
162
|
const CONTINUES = /^(?:\s|[-*+>]|\d+[.)])/;
|
|
254
|
-
/**
|
|
255
|
-
*
|
|
256
|
-
*
|
|
257
|
-
*
|
|
258
|
-
*
|
|
259
|
-
* Streaming markdown is the caller. Re-parsing the whole reply on every
|
|
260
|
-
* repaint costs O(N²) over a turn, so the web chat renders each closed prefix
|
|
261
|
-
* once and keeps its DOM. A boundary is therefore only claimed where parsing
|
|
262
|
-
* the two sides separately renders the same as parsing them together: never
|
|
263
|
-
* inside a backtick or tilde fence, inside a block stripped on completion, or
|
|
264
|
-
* between two lines a blank line leaves in the same list or quote. Fence runs
|
|
265
|
-
* keep their length as in `channels/chunk.ts`: a ```` fence is not closed by
|
|
266
|
-
* the ``` it quotes.
|
|
267
|
-
*/
|
|
163
|
+
/** Offset past the last blank line after `from` that closes a block, so a
|
|
164
|
+
* streaming renderer can parse each closed prefix once (whole-reply reparse is
|
|
165
|
+
* O(N²) over a turn). A boundary is claimed only where the two sides render the
|
|
166
|
+
* same apart as together: never inside a fence, a `<silent>` block, or a loose
|
|
167
|
+
* list/quote. A ```` fence is not closed by the ``` it quotes. */
|
|
268
168
|
export function stableBlockEnd(markdown, from = 0) {
|
|
269
|
-
// The last line is still growing, so it decides nothing
|
|
270
|
-
// state, and not whether the blank line above it ended a block.
|
|
169
|
+
// The last line is still growing, so it decides nothing.
|
|
271
170
|
const end = markdown.lastIndexOf("\n") + 1;
|
|
272
171
|
/** Marker run of the fence currently open; empty = closed. */
|
|
273
172
|
let open = "";
|