@hicaru/pi-rlm 0.3.19 → 0.3.21
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 +8 -5
- package/package.json +1 -1
- package/src/bridge/handlers/completion.ts +3 -0
- package/src/bridge/handlers/emitting.ts +0 -4
- package/src/bridge/handlers/rlm-query.ts +3 -3
- package/src/bridge/handlers/task-registry.ts +46 -19
- package/src/bridge/handlers/types.ts +6 -3
- package/src/bridge/model.ts +4 -0
- package/src/commands/rlm.ts +14 -7
- package/src/config/defaults.ts +41 -13
- package/src/config/settings.ts +7 -3
- package/src/config/skillstate.ts +236 -44
- package/src/context/merge.ts +10 -3
- package/src/context/namespace.ts +6 -2
- package/src/context/refresh.ts +32 -11
- package/src/core/answer.ts +15 -0
- package/src/core/budget.ts +39 -17
- package/src/core/compaction.ts +85 -9
- package/src/core/engine.ts +117 -32
- package/src/core/iteration.ts +4 -0
- package/src/core/limits.ts +10 -14
- package/src/core/root-context.ts +83 -19
- package/src/core/root-digest.ts +48 -11
- package/src/core/root-state.ts +39 -12
- package/src/core/run-state.ts +86 -14
- package/src/core/session-archive.ts +174 -0
- package/src/core/types.ts +13 -2
- package/src/index.ts +142 -12
- package/src/mode/rlm-mode.ts +2 -2
- package/src/prompts/glossary.ts +36 -5
- package/src/prompts/native.ts +8 -2
- package/src/prompts/user.ts +6 -4
- package/src/sandbox/py/__pycache__/retrieval.cpython-314.pyc +0 -0
- package/src/sandbox/py/__pycache__/scaffold.cpython-314.pyc +0 -0
- package/src/sandbox/py/retrieval.py +202 -36
- package/src/sandbox/py/scaffold.py +20 -5
- package/src/sandbox/py/worker.py +1 -1
- package/src/sandbox/sandbox-manager.ts +19 -0
- package/src/sandbox/sandbox.ts +13 -1
- package/src/text/parsing.ts +133 -2
- package/src/text/tokens.ts +39 -4
- package/src/tool/repl-details.ts +2 -2
- package/src/tool/repl-render.ts +38 -2
- package/src/tool/repl-tool.ts +37 -23
- package/src/tool/rlm-aggregator.ts +1 -1
- package/src/tool/rlm-details.ts +1 -2
- package/src/tool/rlm-events.ts +3 -6
- package/src/tool/rlm-tool.ts +1 -1
- package/src/tool/subcall-render.ts +7 -4
- package/src/tool/subcall-store.ts +5 -18
- package/src/ui/config-panel.ts +4 -19
- package/src/ui/intro.ts +1 -1
- package/src/ui/panel/run-registry.ts +2 -2
- package/src/ui/python-highlight.ts +49 -0
- package/src/ui/stage-cards.ts +192 -0
- package/src/ui/tree/tree-model.ts +69 -19
- package/src/ui/tree/tree-rows.ts +2 -1
- package/src/util/abort.ts +34 -0
- package/src/util/bm25.ts +170 -21
- package/src/util/errors.ts +1 -1
package/src/text/tokens.ts
CHANGED
|
@@ -7,6 +7,9 @@
|
|
|
7
7
|
*/
|
|
8
8
|
|
|
9
9
|
const CHARS_PER_TOKEN = 4;
|
|
10
|
+
// CJK ideographs/kana/hangul encode ~1.5 chars per token — the flat /4 heuristic under-counts
|
|
11
|
+
// CJK-heavy content ~2.7x, delaying compaction and budget walls until near overflow.
|
|
12
|
+
const CJK_CHARS_PER_TOKEN = 1.5;
|
|
10
13
|
|
|
11
14
|
/** Rough token count for a character length (≈4 chars/token). Always ≥ 1 for non-empty text. */
|
|
12
15
|
export function estimateTokens(charCount: number): number {
|
|
@@ -14,11 +17,43 @@ export function estimateTokens(charCount: number): number {
|
|
|
14
17
|
return Math.ceil(charCount / CHARS_PER_TOKEN);
|
|
15
18
|
}
|
|
16
19
|
|
|
17
|
-
/**
|
|
20
|
+
/** CJK codepoints (kana, ideographs, hangul, compatibility forms) in `text`. */
|
|
21
|
+
function cjkChars(text: string): number {
|
|
22
|
+
let n = 0;
|
|
23
|
+
for (let i = 0; i < text.length; i++) {
|
|
24
|
+
const c = text.charCodeAt(i);
|
|
25
|
+
if (
|
|
26
|
+
(c >= 0x3040 && c <= 0x30ff) || // kana
|
|
27
|
+
(c >= 0x3400 && c <= 0x9fff) || // ideograph extensions + unified ideographs
|
|
28
|
+
(c >= 0xac00 && c <= 0xd7af) || // hangul syllables
|
|
29
|
+
(c >= 0xf900 && c <= 0xfaff) // compatibility ideographs
|
|
30
|
+
) {
|
|
31
|
+
n += 1;
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
return n;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/** Blended token estimate for actual TEXT: ASCII at 4 chars/token, CJK at 1.5. Pure-ASCII
|
|
38
|
+
* input is byte-identical to estimateTokens(length) — only non-English content moves. */
|
|
39
|
+
export function estimateTextTokens(text: string): number {
|
|
40
|
+
const cjk = cjkChars(text);
|
|
41
|
+
if (cjk === 0) return estimateTokens(text.length);
|
|
42
|
+
const ascii = text.length - cjk;
|
|
43
|
+
return Math.ceil(ascii / CHARS_PER_TOKEN + cjk / CJK_CHARS_PER_TOKEN);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/** Rough token count for a list of role/content messages (script-aware — see estimateTextTokens). */
|
|
18
47
|
export function estimateMessageTokens(messages: { content: string }[]): number {
|
|
19
|
-
let
|
|
20
|
-
|
|
21
|
-
|
|
48
|
+
let ascii = 0;
|
|
49
|
+
let cjk = 0;
|
|
50
|
+
for (const m of messages) {
|
|
51
|
+
ascii += m.content.length + 8; // small per-message overhead
|
|
52
|
+
cjk += cjkChars(m.content);
|
|
53
|
+
}
|
|
54
|
+
if (cjk === 0) return estimateTokens(ascii);
|
|
55
|
+
const nonCjk = ascii - cjk;
|
|
56
|
+
return nonCjk <= 0 ? 0 : Math.ceil(nonCjk / CHARS_PER_TOKEN + cjk / CJK_CHARS_PER_TOKEN);
|
|
22
57
|
}
|
|
23
58
|
|
|
24
59
|
/**
|
package/src/tool/repl-details.ts
CHANGED
|
@@ -18,8 +18,8 @@ export interface ReplDetails {
|
|
|
18
18
|
readonly executionTimeMs: number;
|
|
19
19
|
/** Sub-calls triggered during this execution (llm_query, rlm_query, etc.). */
|
|
20
20
|
readonly subcalls: readonly RlmSubcall[];
|
|
21
|
-
/** Running totals for this repl() call (
|
|
22
|
-
readonly totals: { readonly
|
|
21
|
+
/** Running totals for this repl() call (tokens from sub-LLM calls). */
|
|
22
|
+
readonly totals: { readonly tokens: number };
|
|
23
23
|
/** Final answer submitted through answer["ready"] without echoing it to the model. */
|
|
24
24
|
readonly finalAnswer?: string;
|
|
25
25
|
/** Detached spawn() sub-calls still running when this call returned. Absent when none. */
|
package/src/tool/repl-render.ts
CHANGED
|
@@ -1,14 +1,37 @@
|
|
|
1
|
-
/** repl() tool TUI views —
|
|
1
|
+
/** repl() tool TUI views — call card (code payload), collapsed/expanded result views.
|
|
2
2
|
* Sub-call trees are not rendered here; the live tree widget owns agent visualization. */
|
|
3
3
|
|
|
4
4
|
import type { Theme } from "@earendil-works/pi-coding-agent";
|
|
5
5
|
import { Container, Spacer, Text } from "@earendil-works/pi-tui";
|
|
6
|
+
import { CALL_PREVIEW_CHARS, previewText } from "../text/preview.ts";
|
|
6
7
|
import type { ReplDetails } from "./repl-details.ts";
|
|
8
|
+
import { highlightPython } from "../ui/python-highlight.ts";
|
|
7
9
|
import { cardHeader, cardStatsLine, renderCollapsedCard } from "./subcall-render.ts";
|
|
8
10
|
|
|
9
11
|
/** Chars of stdout/stderr shown in the expanded view. */
|
|
10
12
|
const EXPANDED_STDOUT_CHARS = 2_000;
|
|
11
13
|
const EXPANDED_STDERR_CHARS = 500;
|
|
14
|
+
/** Lines of Python source shown on the collapsed card before the "+N more lines" cut. */
|
|
15
|
+
const CODE_PREVIEW_LINES = 40;
|
|
16
|
+
|
|
17
|
+
// ── Call card ──
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* The tool-call row. The collapsed card's payload IS the source — expanding swaps it for the
|
|
21
|
+
* result view (context.expanded). While args still stream in (context.argsComplete false),
|
|
22
|
+
* keep the one-line preview: half-arrived code reads as garbage.
|
|
23
|
+
*/
|
|
24
|
+
export function replCallView(
|
|
25
|
+
args: { readonly code: string },
|
|
26
|
+
theme: Theme,
|
|
27
|
+
context?: { readonly expanded?: boolean; readonly argsComplete?: boolean },
|
|
28
|
+
): Text {
|
|
29
|
+
const header = theme.fg("toolTitle", theme.bold("repl ")) + theme.fg("dim", previewText(args.code, CALL_PREVIEW_CHARS));
|
|
30
|
+
if (context?.expanded === true || context?.argsComplete === false) {
|
|
31
|
+
return new Text(header, 0, 0);
|
|
32
|
+
}
|
|
33
|
+
return new Text([header, "", renderReplCode(args.code, theme)].join("\n"), 0, 0);
|
|
34
|
+
}
|
|
12
35
|
|
|
13
36
|
// ── Collapsed view ──
|
|
14
37
|
|
|
@@ -18,7 +41,20 @@ function replStats(details: ReplDetails, theme: Theme): string {
|
|
|
18
41
|
}
|
|
19
42
|
|
|
20
43
|
export function renderReplCollapsed(details: ReplDetails, theme: Theme): Text {
|
|
21
|
-
|
|
44
|
+
// Collapsed shows the CODE (replCallView); expanding reveals the result — say so.
|
|
45
|
+
return renderCollapsedCard("REPL", details.status, replStats(details, theme), theme, "to show result");
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* The collapsed card's payload — the cell's Python source, capped. Full source lives in the
|
|
50
|
+
* session args; expanding swaps this block for the result view (see replCallView). Sliced
|
|
51
|
+
* BEFORE highlighting so a triple-quoted string cut by the cap can only fall back to plain.
|
|
52
|
+
*/
|
|
53
|
+
export function renderReplCode(code: string, theme: Theme): string {
|
|
54
|
+
const lines = code.split("\n");
|
|
55
|
+
const shown = highlightPython(lines.slice(0, CODE_PREVIEW_LINES).join("\n"), theme);
|
|
56
|
+
const rest = lines.length - CODE_PREVIEW_LINES;
|
|
57
|
+
return rest > 0 ? `${shown}\n${theme.fg("muted", `… +${String(rest)} more lines`)}` : shown;
|
|
22
58
|
}
|
|
23
59
|
|
|
24
60
|
// ── Expanded view ──
|
package/src/tool/repl-tool.ts
CHANGED
|
@@ -23,6 +23,7 @@ import type { ModelRegistry } from "@earendil-works/pi-coding-agent";
|
|
|
23
23
|
import { buildAddContextHandler, type AddContextHandlerBundle } from "../bridge/add-context.ts";
|
|
24
24
|
import { contextPrefixesIn } from "../context/namespace.ts";
|
|
25
25
|
import type { SubcallGates } from "../util/concurrency.ts";
|
|
26
|
+
import { raceAbort } from "../util/abort.ts";
|
|
26
27
|
import { LimitGuard, limitsFromConfig } from "../core/limits.ts";
|
|
27
28
|
import type { RlmConfig, RlmInput, RlmResult } from "../core/types.ts";
|
|
28
29
|
import { SandboxManager } from "../sandbox/sandbox-manager.ts";
|
|
@@ -40,7 +41,7 @@ import { createEngine } from "../core/engine.ts";
|
|
|
40
41
|
import type { RunState } from "../core/run-state.ts";
|
|
41
42
|
import { modelRef } from "../config/settings.ts";
|
|
42
43
|
import { spinnerFrame } from "../ui/theme.ts";
|
|
43
|
-
import {
|
|
44
|
+
import { previewText } from "../text/preview.ts";
|
|
44
45
|
import { errorMessage } from "../util/errors.ts";
|
|
45
46
|
import {
|
|
46
47
|
groundLeafPrompt,
|
|
@@ -49,7 +50,7 @@ import {
|
|
|
49
50
|
} from "../config/skillstate.ts";
|
|
50
51
|
import { createProgressNotifier, validateToolParams } from "./tool-utils.ts";
|
|
51
52
|
import { buildReplResultText, collectReplWarnings } from "./repl-result.ts";
|
|
52
|
-
import { renderReplCollapsed, renderReplExpanded } from "./repl-render.ts";
|
|
53
|
+
import { renderReplCollapsed, renderReplExpanded, replCallView } from "./repl-render.ts";
|
|
53
54
|
import { attachTracer, trace, traceEnabled } from "../util/trace.ts";
|
|
54
55
|
|
|
55
56
|
/** Last non-empty line of a Python traceback — the `TypeError: …` line, not the frames. */
|
|
@@ -129,7 +130,12 @@ interface ReplToolDeps {
|
|
|
129
130
|
readonly getSkillBlock?: (task: string) => string | undefined;
|
|
130
131
|
/** Session tree panel index; omitted → runs don't appear in the widget. */
|
|
131
132
|
readonly runRegistry?: RunRegistry;
|
|
132
|
-
|
|
133
|
+
/**
|
|
134
|
+
* Live abort signal accessor — read per child engine / sub-call / exec, NOT captured at
|
|
135
|
+
* registration. /rlm-stop aborts and ROTATES the session controller, so a stale signal
|
|
136
|
+
* must never outlive the call that started it.
|
|
137
|
+
*/
|
|
138
|
+
readonly getSignal?: () => AbortSignal | undefined;
|
|
133
139
|
readonly onUsage?: (usage: Usage, role: "sub") => void;
|
|
134
140
|
readonly ensureContext?: () => Promise<void>;
|
|
135
141
|
/** Register a reset hook for sandbox death/dispose (e.g. add_context prefix cache). */
|
|
@@ -142,7 +148,9 @@ interface ReplToolDeps {
|
|
|
142
148
|
}
|
|
143
149
|
|
|
144
150
|
export function createReplTool(deps: ReplToolDeps): ToolDefinition<typeof ReplToolParams, ReplDetails> {
|
|
145
|
-
const { sandboxManager, llmModel, registry, getConfig,
|
|
151
|
+
const { sandboxManager, llmModel, registry, getConfig, onUsage, background } = deps;
|
|
152
|
+
// Read per use, never captured: see ReplToolDeps.getSignal.
|
|
153
|
+
const currentSignal = (): AbortSignal | undefined => deps.getSignal?.();
|
|
146
154
|
const bridgeState = new NativeBridgeState(background);
|
|
147
155
|
// v5: one session-wide blackboard for the native repl() path — the same claim/coalesce/
|
|
148
156
|
// demote logic the engine gets per run, shared by every turn and every child it spawns.
|
|
@@ -177,7 +185,7 @@ export function createReplTool(deps: ReplToolDeps): ToolDefinition<typeof ReplTo
|
|
|
177
185
|
llmModel: getLlmModel(),
|
|
178
186
|
registry,
|
|
179
187
|
config: getConfig(),
|
|
180
|
-
signal,
|
|
188
|
+
signal: currentSignal(),
|
|
181
189
|
gates: currentGates(),
|
|
182
190
|
// Same emitter the parent subcall node lives on — see SubcallHandlerDeps.runChild.
|
|
183
191
|
emitter: inv.emitter,
|
|
@@ -202,7 +210,9 @@ export function createReplTool(deps: ReplToolDeps): ToolDefinition<typeof ReplTo
|
|
|
202
210
|
getLlmModel,
|
|
203
211
|
getModel,
|
|
204
212
|
getConfig,
|
|
205
|
-
signal
|
|
213
|
+
get signal() {
|
|
214
|
+
return currentSignal();
|
|
215
|
+
},
|
|
206
216
|
onUsage,
|
|
207
217
|
runChild,
|
|
208
218
|
// The session sandbox's context is the child's world. Read lazily so an add_context from an
|
|
@@ -224,7 +234,9 @@ export function createReplTool(deps: ReplToolDeps): ToolDefinition<typeof ReplTo
|
|
|
224
234
|
// committed for an append that did not happen.
|
|
225
235
|
getContext: () => sandboxManager.contextPayload,
|
|
226
236
|
parentId: undefined,
|
|
227
|
-
signal
|
|
237
|
+
get signal() {
|
|
238
|
+
return currentSignal();
|
|
239
|
+
},
|
|
228
240
|
// Keep the manager's replay copy in step with the worker's live `context`, and with it
|
|
229
241
|
// whatever a child spawned after this load will inherit.
|
|
230
242
|
onLoaded: (payload) => { sandboxManager.appendContext(payload); },
|
|
@@ -270,7 +282,7 @@ export function createReplTool(deps: ReplToolDeps): ToolDefinition<typeof ReplTo
|
|
|
270
282
|
stderr: errors,
|
|
271
283
|
executionTimeMs: 0,
|
|
272
284
|
subcalls: [],
|
|
273
|
-
totals: {
|
|
285
|
+
totals: { tokens: 0 },
|
|
274
286
|
}));
|
|
275
287
|
if (!validation.ok) return validation.error;
|
|
276
288
|
const params = validation.value;
|
|
@@ -318,7 +330,7 @@ export function createReplTool(deps: ReplToolDeps): ToolDefinition<typeof ReplTo
|
|
|
318
330
|
stderr: capturedStderr,
|
|
319
331
|
executionTimeMs: Date.now() - startedAt,
|
|
320
332
|
subcalls: live.length > 0 ? [...store.getSubcalls(), ...live] : store.getSubcalls(),
|
|
321
|
-
totals: {
|
|
333
|
+
totals: { tokens: own.tokens + bg.tokens },
|
|
322
334
|
backgroundPending: background.pending > 0 ? background.pending : undefined,
|
|
323
335
|
};
|
|
324
336
|
},
|
|
@@ -368,12 +380,19 @@ export function createReplTool(deps: ReplToolDeps): ToolDefinition<typeof ReplTo
|
|
|
368
380
|
// spawns claim against an empty stack, so an originator can never echo against
|
|
369
381
|
// itself. Duplicates are caught by the ledger's claim store (exact/near
|
|
370
382
|
// coalescing + rlmBudget demotion), never by silent suppression.
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
383
|
+
// The cell races pi's per-call signal (esc) against /rlm-stop's session signal.
|
|
384
|
+
const cellAbort = raceAbort(execSignal, currentSignal());
|
|
385
|
+
let result: ReplResult;
|
|
386
|
+
try {
|
|
387
|
+
result = await sandboxManager.execWithSetup(params.code, () => {
|
|
388
|
+
// Wire per-invocation mutable state only after the serialized exec slot
|
|
389
|
+
// is active. Swapping earlier would let queued repl() calls overwrite
|
|
390
|
+
// emitter/limits for the currently running REPL execution.
|
|
391
|
+
bridgeState.swap({ emitter, parentId: undefined, depth: 0, limits });
|
|
392
|
+
}, cellAbort.signal);
|
|
393
|
+
} finally {
|
|
394
|
+
cellAbort.dispose();
|
|
395
|
+
}
|
|
377
396
|
const elapsed = Date.now() - start;
|
|
378
397
|
capturedStdout = result.stdout;
|
|
379
398
|
capturedStderr = result.stderr;
|
|
@@ -400,12 +419,11 @@ export function createReplTool(deps: ReplToolDeps): ToolDefinition<typeof ReplTo
|
|
|
400
419
|
? [...store.getSubcalls(), ...adopted.subcalls]
|
|
401
420
|
: store.getSubcalls();
|
|
402
421
|
const totals = {
|
|
403
|
-
costUsd: store.getTotals().costUsd + adopted.totals.costUsd,
|
|
404
422
|
tokens: store.getTotals().tokens + adopted.totals.tokens,
|
|
405
423
|
};
|
|
406
424
|
const subUsage: Usage = {
|
|
407
425
|
input: 0, output: 0, cacheRead: 0, cacheWrite: 0, totalTokens: totals.tokens,
|
|
408
|
-
cost: { total:
|
|
426
|
+
cost: { total: 0, input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
|
|
409
427
|
};
|
|
410
428
|
onUsage?.(subUsage, "sub");
|
|
411
429
|
|
|
@@ -453,7 +471,6 @@ export function createReplTool(deps: ReplToolDeps): ToolDefinition<typeof ReplTo
|
|
|
453
471
|
executionTimeMs: 0,
|
|
454
472
|
subcalls: [...store.getSubcalls(), ...adopted.subcalls],
|
|
455
473
|
totals: {
|
|
456
|
-
costUsd: store.getTotals().costUsd + adopted.totals.costUsd,
|
|
457
474
|
tokens: store.getTotals().tokens + adopted.totals.tokens,
|
|
458
475
|
},
|
|
459
476
|
backgroundPending: background.pending > 0 ? background.pending : undefined,
|
|
@@ -472,11 +489,8 @@ export function createReplTool(deps: ReplToolDeps): ToolDefinition<typeof ReplTo
|
|
|
472
489
|
}
|
|
473
490
|
},
|
|
474
491
|
|
|
475
|
-
renderCall(args, theme) {
|
|
476
|
-
return
|
|
477
|
-
theme.fg("toolTitle", theme.bold("repl ")) + theme.fg("dim", previewText(args.code, CALL_PREVIEW_CHARS)),
|
|
478
|
-
0, 0,
|
|
479
|
-
);
|
|
492
|
+
renderCall(args, theme, context) {
|
|
493
|
+
return replCallView(args, theme, context);
|
|
480
494
|
},
|
|
481
495
|
|
|
482
496
|
renderResult(result, { expanded }, theme) {
|
|
@@ -55,7 +55,7 @@ export class RlmEventAggregator extends EmitterListener {
|
|
|
55
55
|
}
|
|
56
56
|
|
|
57
57
|
private handleRootUsage(event: RootUsageEvent): void {
|
|
58
|
-
this.store.addRootUsage(event.
|
|
58
|
+
this.store.addRootUsage(event.tokens, event.tokensIn, event.tokensOut);
|
|
59
59
|
this.notify();
|
|
60
60
|
}
|
|
61
61
|
|
package/src/tool/rlm-details.ts
CHANGED
|
@@ -31,7 +31,6 @@ export interface RlmSubcall {
|
|
|
31
31
|
readonly resultPreview?: string;
|
|
32
32
|
readonly startedAt: number;
|
|
33
33
|
readonly endedAt?: number;
|
|
34
|
-
readonly costUsd: number;
|
|
35
34
|
readonly tokens: number;
|
|
36
35
|
/** In/out split (input / output) — mirrors tokens. */
|
|
37
36
|
readonly tokensIn: number;
|
|
@@ -49,7 +48,7 @@ export interface RlmDetails {
|
|
|
49
48
|
readonly rootPrompt: string;
|
|
50
49
|
readonly turns: { readonly current: number; readonly max: number };
|
|
51
50
|
readonly subcalls: readonly RlmSubcall[];
|
|
52
|
-
readonly totals: { readonly
|
|
51
|
+
readonly totals: { readonly tokens: number; readonly tokensIn: number; readonly tokensOut: number };
|
|
53
52
|
readonly answer?: string;
|
|
54
53
|
}
|
|
55
54
|
|
package/src/tool/rlm-events.ts
CHANGED
|
@@ -38,8 +38,6 @@ export interface SubcallUpdatedEvent {
|
|
|
38
38
|
readonly args?: string;
|
|
39
39
|
readonly resultPreview?: string;
|
|
40
40
|
/** Delta — additive on both the subcall and running totals. */
|
|
41
|
-
readonly costUsd?: number;
|
|
42
|
-
/** Delta — additive on both the subcall and running totals. */
|
|
43
41
|
readonly tokens?: number;
|
|
44
42
|
/** Deltas for the in/out split shown in the tree (input / output). Additive like tokens. */
|
|
45
43
|
readonly tokensIn?: number;
|
|
@@ -56,7 +54,6 @@ export interface TurnEvent {
|
|
|
56
54
|
}
|
|
57
55
|
|
|
58
56
|
export interface RootUsageEvent {
|
|
59
|
-
readonly costUsd: number;
|
|
60
57
|
readonly tokens: number;
|
|
61
58
|
readonly tokensIn?: number;
|
|
62
59
|
readonly tokensOut?: number;
|
|
@@ -108,7 +105,7 @@ export class RlmEmitter {
|
|
|
108
105
|
return id;
|
|
109
106
|
}
|
|
110
107
|
|
|
111
|
-
/** Update an existing sub-call. All fields are partial.
|
|
108
|
+
/** Update an existing sub-call. All fields are partial. tokens are additive. */
|
|
112
109
|
emitSubcallUpdated(event: SubcallUpdatedEvent): void {
|
|
113
110
|
this.ee.emit("subcall:updated", event);
|
|
114
111
|
}
|
|
@@ -119,8 +116,8 @@ export class RlmEmitter {
|
|
|
119
116
|
}
|
|
120
117
|
|
|
121
118
|
/** Accumulate usage directly to root-level totals. */
|
|
122
|
-
emitRootUsage(
|
|
123
|
-
this.ee.emit("root-usage", {
|
|
119
|
+
emitRootUsage(tokens: number, tokensIn?: number, tokensOut?: number): void {
|
|
120
|
+
this.ee.emit("root-usage", { tokens, tokensIn, tokensOut } satisfies RootUsageEvent);
|
|
124
121
|
}
|
|
125
122
|
|
|
126
123
|
/** Set the final answer text (root-only). */
|
package/src/tool/rlm-tool.ts
CHANGED
|
@@ -51,7 +51,7 @@ export function createRlmTool(controller: RlmController, runRegistry?: RunRegist
|
|
|
51
51
|
rootPrompt: "",
|
|
52
52
|
turns: { current: 0, max: 0 },
|
|
53
53
|
subcalls: [],
|
|
54
|
-
totals: {
|
|
54
|
+
totals: { tokens: 0, tokensIn: 0, tokensOut: 0 },
|
|
55
55
|
}));
|
|
56
56
|
if (!validation.ok) return validation.error;
|
|
57
57
|
const params = validation.value;
|
|
@@ -64,21 +64,24 @@ export function cardHeader(
|
|
|
64
64
|
* Deliberately `keyText` + the injected theme rather than pi's `keyHint`: `keyHint` colours via
|
|
65
65
|
* pi's module-global theme, which throws when that global is uninitialized — the same jiti
|
|
66
66
|
* hazard `ui/theme-adapter.ts` exists to avoid. `keyText` only reads the keybinding registry.
|
|
67
|
+
* Shared by tool cards AND the [rlm.stage] transcript cards (ui/stage-cards.ts).
|
|
67
68
|
*/
|
|
68
|
-
function expandHint(theme: Theme): string {
|
|
69
|
+
export function expandHint(theme: Theme, action = "to expand"): string {
|
|
69
70
|
// Empty outside a live pi session (the app installs the real binding registry at startup) —
|
|
70
71
|
// the phrase stays the same, only the key prefix drops out.
|
|
71
72
|
const key = keyText("app.tools.expand");
|
|
72
|
-
return theme.fg("muted", key ? `${key}
|
|
73
|
+
return theme.fg("muted", key ? `${key} ${action}` : action);
|
|
73
74
|
}
|
|
74
75
|
|
|
75
|
-
/** The collapsed card: header line, then the expand hint once settled.
|
|
76
|
+
/** The collapsed card: header line, then the expand hint once settled. `action` labels what
|
|
77
|
+
* expanding reveals — the repl card says "to show result" (collapsed shows the code instead). */
|
|
76
78
|
export function renderCollapsedCard(
|
|
77
79
|
title: string,
|
|
78
80
|
status: SubcallStatus | "aborted" | "done",
|
|
79
81
|
stats: string,
|
|
80
82
|
theme: Theme,
|
|
83
|
+
action = "to expand",
|
|
81
84
|
): Text {
|
|
82
|
-
const hint = status === "running" ? "" : `\n${expandHint(theme)}`;
|
|
85
|
+
const hint = status === "running" ? "" : `\n${expandHint(theme, action)}`;
|
|
83
86
|
return new Text(`${cardHeader(title, status, stats, theme)}${hint}`, 0, 0);
|
|
84
87
|
}
|
|
@@ -14,9 +14,8 @@ type MutableSubcall = {
|
|
|
14
14
|
-readonly [Key in keyof RlmSubcall]: RlmSubcall[Key];
|
|
15
15
|
};
|
|
16
16
|
|
|
17
|
-
/** Accumulated
|
|
17
|
+
/** Accumulated tokens, shared by getTotals() and takeSettledSubtrees(). */
|
|
18
18
|
export interface SubcallTotals {
|
|
19
|
-
readonly costUsd: number;
|
|
20
19
|
readonly tokens: number;
|
|
21
20
|
/** In/out split (input / output) — mirrors tokens, shown separately in the tree. */
|
|
22
21
|
readonly tokensIn: number;
|
|
@@ -26,11 +25,9 @@ export interface SubcallTotals {
|
|
|
26
25
|
export class SubcallStore extends EmitterListener {
|
|
27
26
|
private readonly subcalls = new Map<string, MutableSubcall>();
|
|
28
27
|
|
|
29
|
-
private totalCostUsd = 0;
|
|
30
28
|
private totalTokens = 0;
|
|
31
29
|
private totalTokensIn = 0;
|
|
32
30
|
private totalTokensOut = 0;
|
|
33
|
-
private rootCostUsd = 0;
|
|
34
31
|
private rootTokens = 0;
|
|
35
32
|
private rootTokensIn = 0;
|
|
36
33
|
private rootTokensOut = 0;
|
|
@@ -57,7 +54,6 @@ export class SubcallStore extends EmitterListener {
|
|
|
57
54
|
detail: event.detail,
|
|
58
55
|
args: event.args,
|
|
59
56
|
startedAt: Date.now(),
|
|
60
|
-
costUsd: 0,
|
|
61
57
|
tokens: 0,
|
|
62
58
|
tokensIn: 0,
|
|
63
59
|
tokensOut: 0,
|
|
@@ -76,10 +72,6 @@ export class SubcallStore extends EmitterListener {
|
|
|
76
72
|
if (event.detail !== undefined) sc.detail = event.detail;
|
|
77
73
|
if (event.args !== undefined) sc.args = event.args;
|
|
78
74
|
if (event.resultPreview !== undefined) sc.resultPreview = event.resultPreview;
|
|
79
|
-
if (event.costUsd !== undefined) {
|
|
80
|
-
sc.costUsd += event.costUsd;
|
|
81
|
-
this.totalCostUsd += event.costUsd;
|
|
82
|
-
}
|
|
83
75
|
if (event.tokens !== undefined) {
|
|
84
76
|
sc.tokens += event.tokens;
|
|
85
77
|
this.totalTokens += event.tokens;
|
|
@@ -105,7 +97,7 @@ export class SubcallStore extends EmitterListener {
|
|
|
105
97
|
|
|
106
98
|
/** Snapshot running totals. O(1). */
|
|
107
99
|
getTotals(): SubcallTotals {
|
|
108
|
-
return {
|
|
100
|
+
return { tokens: this.totalTokens, tokensIn: this.totalTokensIn, tokensOut: this.totalTokensOut };
|
|
109
101
|
}
|
|
110
102
|
|
|
111
103
|
/**
|
|
@@ -141,7 +133,6 @@ export class SubcallStore extends EmitterListener {
|
|
|
141
133
|
};
|
|
142
134
|
|
|
143
135
|
const taken: RlmSubcall[] = [];
|
|
144
|
-
let costUsd = 0;
|
|
145
136
|
let tokens = 0;
|
|
146
137
|
let tokensIn = 0;
|
|
147
138
|
let tokensOut = 0;
|
|
@@ -149,7 +140,6 @@ export class SubcallStore extends EmitterListener {
|
|
|
149
140
|
const subtree = settledSubtree(root);
|
|
150
141
|
if (subtree === undefined) continue;
|
|
151
142
|
for (const node of subtree) {
|
|
152
|
-
costUsd += node.costUsd;
|
|
153
143
|
tokens += node.tokens;
|
|
154
144
|
tokensIn += node.tokensIn;
|
|
155
145
|
tokensOut += node.tokensOut;
|
|
@@ -157,22 +147,19 @@ export class SubcallStore extends EmitterListener {
|
|
|
157
147
|
this.subcalls.delete(node.id);
|
|
158
148
|
}
|
|
159
149
|
}
|
|
160
|
-
this.totalCostUsd -= costUsd;
|
|
161
150
|
this.totalTokens -= tokens;
|
|
162
151
|
this.totalTokensIn -= tokensIn;
|
|
163
152
|
this.totalTokensOut -= tokensOut;
|
|
164
|
-
return { subcalls: taken, totals: {
|
|
153
|
+
return { subcalls: taken, totals: { tokens, tokensIn, tokensOut } };
|
|
165
154
|
}
|
|
166
155
|
|
|
167
156
|
// ── Root usage (delegated from RlmEventAggregator) ──
|
|
168
157
|
|
|
169
158
|
/** Accumulate root-level usage into shared totals. Called by aggregator. */
|
|
170
|
-
addRootUsage(
|
|
171
|
-
this.totalCostUsd += costUsd;
|
|
159
|
+
addRootUsage(tokens: number, tokensIn = 0, tokensOut = 0): void {
|
|
172
160
|
this.totalTokens += tokens;
|
|
173
161
|
this.totalTokensIn += tokensIn;
|
|
174
162
|
this.totalTokensOut += tokensOut;
|
|
175
|
-
this.rootCostUsd += costUsd;
|
|
176
163
|
this.rootTokens += tokens;
|
|
177
164
|
this.rootTokensIn += tokensIn;
|
|
178
165
|
this.rootTokensOut += tokensOut;
|
|
@@ -180,6 +167,6 @@ export class SubcallStore extends EmitterListener {
|
|
|
180
167
|
|
|
181
168
|
/** Root engine's OWN spend (driver-model turns only) — never blends sub-call models. */
|
|
182
169
|
getRootUsage(): SubcallTotals {
|
|
183
|
-
return {
|
|
170
|
+
return { tokens: this.rootTokens, tokensIn: this.rootTokensIn, tokensOut: this.rootTokensOut };
|
|
184
171
|
}
|
|
185
172
|
}
|
package/src/ui/config-panel.ts
CHANGED
|
@@ -9,7 +9,7 @@ import { THINKING_LEVELS } from "../config/settings.ts";
|
|
|
9
9
|
|
|
10
10
|
const CHOICES = Object.freeze({
|
|
11
11
|
maxDepth: Object.freeze(["1", "2", "3", "4"]),
|
|
12
|
-
maxIterations: Object.freeze(["
|
|
12
|
+
maxIterations: Object.freeze(["100", "200", "500", "1000"]),
|
|
13
13
|
execTimeoutS: Object.freeze(["30", "60", "120", "300"]),
|
|
14
14
|
maxConcurrentSubcalls: Object.freeze(["2", "4", "8", "16", "32"]),
|
|
15
15
|
maxConcurrentChildren: Object.freeze(["1", "2", "3", "4", "6", "8"]),
|
|
@@ -20,7 +20,6 @@ const CHOICES = Object.freeze({
|
|
|
20
20
|
compaction: Object.freeze(["on", "off"]),
|
|
21
21
|
compactionThresholdPct: Object.freeze(["50", "65", "80", "90"]),
|
|
22
22
|
rootSamplingMaxTokens: Object.freeze(["4096", "8192", "16384", "32768"]),
|
|
23
|
-
rootSamplingTemperature: Object.freeze(["0", "0.3", "0.7", "1.0", "default"]),
|
|
24
23
|
smartReasoning: Object.freeze(["default", ...Object.keys(THINKING_LEVELS)]),
|
|
25
24
|
subSamplingMaxTokens: Object.freeze(["1024", "2048", "4096", "8192"]),
|
|
26
25
|
subSamplingTemperature: Object.freeze(["0", "0.3", "0.7", "1.0", "default"]),
|
|
@@ -43,7 +42,7 @@ export async function showConfigPanel(ctx: ExtensionContext, config: RlmConfig):
|
|
|
43
42
|
let edited = config;
|
|
44
43
|
const items: SettingItem[] = [
|
|
45
44
|
item("maxDepth", "Max recursion depth", String(config.maxDepth), CHOICES.maxDepth, "rlm_query past this depth degrades to plain llm_query (1 = no recursion)."),
|
|
46
|
-
item("maxIterations", "Max iterations", String(config.maxIterations), CHOICES.maxIterations, "Maximum root REPL turns before RLM asks the model for a final answer."),
|
|
45
|
+
item("maxIterations", "Max iterations", String(config.maxIterations), CHOICES.maxIterations, "Maximum root REPL turns before RLM asks the model for a final answer. Large by design — runs end on FINAL/errors/wall-clock first."),
|
|
47
46
|
item("execTimeoutS", "REPL block timeout (s)", String(config.execTimeoutS), CHOICES.execTimeoutS, "Wall-clock limit for one model-authored Python REPL block."),
|
|
48
47
|
item("maxConcurrentSubcalls", "Max concurrent sub-calls", String(config.maxConcurrentSubcalls), CHOICES.maxConcurrentSubcalls, "Concurrency pool size for llm_batch and rlm_batch."),
|
|
49
48
|
item("maxConcurrentChildren", "Max concurrent children", String(config.maxConcurrentChildren), CHOICES.maxConcurrentChildren, "Concurrent rlm_query child engines per depth. Each is a Python process holding its own copy of the inherited context."),
|
|
@@ -54,8 +53,6 @@ export async function showConfigPanel(ctx: ExtensionContext, config: RlmConfig):
|
|
|
54
53
|
item("compaction", "Trajectory compaction", config.compaction ? "on" : "off", CHOICES.compaction, "Summarize old turns when history approaches the model context window."),
|
|
55
54
|
item("compactionThresholdPct", "Compaction threshold (%)", String(Math.round(config.compactionThresholdPct * 100)), CHOICES.compactionThresholdPct, "DEPRECATED — ignored: compaction uses the absolute 256k ceiling (COMPACTION_CEILING_TOKENS)."),
|
|
56
55
|
item("rootSamplingMaxTokens", "Root model output cap (tok)", String(config.rootSampling?.maxTokens ?? 16384), CHOICES.rootSamplingMaxTokens, "Max output tokens per root-model turn. Lower values keep each turn lean."),
|
|
57
|
-
item("rootSamplingTemperature", "Root sampling temperature", config.rootSampling?.temperature === undefined ? "default" : String(config.rootSampling?.temperature), CHOICES.rootSamplingTemperature,
|
|
58
|
-
"Sampling temperature for RLM root turns, finalize included — 0 = deterministic (the r3 reproducibility setting); 'default' = provider default. Applies to RLM-mode runs, rlm() delegation and child recursion; the native Pi agent loop follows Pi's own session settings."),
|
|
59
56
|
item("smartReasoning", "Root reasoning effort", config.smartReasoning ?? "default", CHOICES.smartReasoning,
|
|
60
57
|
"Thinking effort for the root model ('default' = none). Only models whose registry entry supports reasoning will think; others silently run without it. Reasoning tokens share the output cap — raise the root output cap when thinking is on."),
|
|
61
58
|
item("subSamplingMaxTokens", "Worker output cap (tok)", String(config.subSampling?.maxTokens ?? 8192), CHOICES.subSamplingMaxTokens,
|
|
@@ -68,17 +65,11 @@ export async function showConfigPanel(ctx: ExtensionContext, config: RlmConfig):
|
|
|
68
65
|
"Allow add_context() to pull an external dir, file, document, or git repo into context."),
|
|
69
66
|
item("autoSeedCwd", "Auto-seed cwd", config.autoSeedCwd ? "on" : "off", CHOICES.autoSeedCwd,
|
|
70
67
|
"Seed the working directory into context on the first repl() call (otherwise starts empty)."),
|
|
71
|
-
// R0 (/tmp/ROOT_FULL_SKILLSTATE_PLAN.md): the SKILL.state / Root Σ paradigm flags are
|
|
72
|
-
// ENFORCED — rendered as a read-only badge so the truth is visible instead of hidden.
|
|
73
|
-
// No toggle exists: applySetting has no case for them and the validator forces true.
|
|
74
|
-
item("__sigma_enforced__", "SKILL.state / Root Σ", "enforced", ["enforced"],
|
|
75
|
-
"ENFORCED (no opt-out): run state, skill state + distill, root context transform, state fences, digest compaction. " +
|
|
76
|
-
"Override attempts in rlm.json are traced (skillstate.override-ignored) and ignored; RLM_BENCH_NO_ROOTCONTEXT=1 is the dev-only measurement hatch."),
|
|
77
68
|
// R5: the window calibrations are rlm.json-only knobs — shown read-only with live values.
|
|
78
69
|
item("__sigma_window__", "Root Σ window (calibration)",
|
|
79
|
-
`keepTurns=${config.rootContextKeepTurns} · elide=${config.rootContextElideChars} · snapshot=${config.rootContextSnapshot ? "on" : "off"}`,
|
|
70
|
+
`keepTurns=${config.rootContextKeepTurns} · elide=${config.rootContextElideChars} · snapshot=${config.rootContextSnapshot ? "on" : "off"} · archive=${config.rootArchiveMaxChars > 0 ? `${Math.round(config.rootArchiveMaxChars / 1000)}k` : "off"}`,
|
|
80
71
|
["rlm.json"],
|
|
81
|
-
"Query-time window calibrations, rlm.json only: rootContextKeepTurns (
|
|
72
|
+
"Query-time window calibrations, rlm.json only: rootContextKeepTurns (4 = default), rootContextElideChars, rootContextSnapshot, rootArchiveMaxChars (0 = archive off; elided turns are otherwise unrecoverable). " +
|
|
82
73
|
"Session resume/fork: the tracker is reborn lazily and Σ re-grows from live observations — the first call after a resume has an empty Σ by design."),
|
|
83
74
|
item("__save__", "Save & close", "↵", ["↵"], "Save these settings and close (Esc also saves)."),
|
|
84
75
|
];
|
|
@@ -138,12 +129,6 @@ export function applySetting(config: RlmConfig, id: string, value: string): RlmC
|
|
|
138
129
|
case "compactionThresholdPct": return Object.freeze({ ...config, compactionThresholdPct: Number(value) / 100 });
|
|
139
130
|
case "rootSamplingMaxTokens":
|
|
140
131
|
return Object.freeze({ ...config, rootSampling: Object.freeze({ ...config.rootSampling, maxTokens: Number(value) }) });
|
|
141
|
-
case "rootSamplingTemperature": {
|
|
142
|
-
const t = optionalTemperature(value);
|
|
143
|
-
// Reject invalid values (NaN / out of range) — keep the current setting.
|
|
144
|
-
if (t === undefined && value !== "default") return config;
|
|
145
|
-
return Object.freeze({ ...config, rootSampling: Object.freeze({ ...config.rootSampling, temperature: t }) });
|
|
146
|
-
}
|
|
147
132
|
case "smartReasoning":
|
|
148
133
|
if (value === "default") return Object.freeze({ ...config, smartReasoning: undefined });
|
|
149
134
|
return Object.hasOwn(THINKING_LEVELS, value)
|
package/src/ui/intro.ts
CHANGED
|
@@ -14,7 +14,7 @@ const RLM_GUIDE = `# RLM mode
|
|
|
14
14
|
- \`/rlm-llm\` — pin the LLM model for llm_query / llm_batch / map_files
|
|
15
15
|
- \`/rlm-rlm\` — pin the model for rlm_query / rlm_batch child engines (default: session model)
|
|
16
16
|
- \`/rlm-config\` — run limits and engine settings
|
|
17
|
-
- \`/rlm-stop\` —
|
|
17
|
+
- \`/rlm-stop\` — abort all in-flight RLM work: RLM runs, native repl cells and background tasks (use /rlm or Ctrl+Shift+R to leave RLM mode)
|
|
18
18
|
|
|
19
19
|
## Live tree
|
|
20
20
|
|
|
@@ -20,7 +20,7 @@ interface RunRegistration {
|
|
|
20
20
|
readonly label: string;
|
|
21
21
|
readonly emitter: RlmEmitter;
|
|
22
22
|
readonly subcalls: () => readonly RlmSubcall[];
|
|
23
|
-
readonly totals: () => { readonly
|
|
23
|
+
readonly totals: () => { readonly tokens: number };
|
|
24
24
|
/** Live root state; defaults: running, no phase, no turns. */
|
|
25
25
|
readonly rootStatus?: () => RlmRunStatus;
|
|
26
26
|
readonly rootPhase?: () => SubcallPhase | undefined;
|
|
@@ -40,7 +40,7 @@ export interface RunEntry {
|
|
|
40
40
|
readonly label: string;
|
|
41
41
|
readonly timeline: TimelineStore;
|
|
42
42
|
readonly subcalls: () => readonly RlmSubcall[];
|
|
43
|
-
readonly totals: () => { readonly
|
|
43
|
+
readonly totals: () => { readonly tokens: number };
|
|
44
44
|
readonly rootStatus: () => RlmRunStatus;
|
|
45
45
|
readonly rootPhase: () => SubcallPhase | undefined;
|
|
46
46
|
readonly turns: () => { readonly current: number; readonly max: number };
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* python-highlight — minimal Python syntax coloring for the repl card's code payload.
|
|
3
|
+
*
|
|
4
|
+
* Deliberately NOT pi's highlightCode(): that closes over the module-global theme,
|
|
5
|
+
* which is unreliable inside a jiti-loaded plugin (see ui/theme-adapter.ts). The
|
|
6
|
+
* grammar here is a small, honest subset — comments, strings (triple-quoted and
|
|
7
|
+
* prefixed), keywords, numbers, decorators — enough to read a cell at a glance.
|
|
8
|
+
* Colors come from the theme pi hands each render pass.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import type { Theme } from "@earendil-works/pi-coding-agent";
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Master pattern; alternation order is priority — a `#` comment consumes to end-of-line
|
|
15
|
+
* before strings can match, leftmost match wins everywhere else. Group indices:
|
|
16
|
+
* 1 comment, 2 triple-quoted string, 3 single-line string (optional prefix), 4 decorator,
|
|
17
|
+
* 5 number, 6 keyword. Module-level snapshot: a regex is immutable state, not session state.
|
|
18
|
+
*/
|
|
19
|
+
const TOKEN_RE = new RegExp(
|
|
20
|
+
[
|
|
21
|
+
"(#[^\\n]*)", // 1
|
|
22
|
+
'("""[\\s\\S]*?"""|\'\'\'[\\s\\S]*?\'\'\')', // 2
|
|
23
|
+
'([fFrRbBuU]{0,2}"(?:\\\\.|[^"\\\\\\n])*"|[fFrRbBuU]{0,2}\'(?:\\\\.|[^\'\\\\\\n])*\')', // 3
|
|
24
|
+
"(@[A-Za-z_][\\w.]*)", // 4
|
|
25
|
+
"\\b(\\d[\\d_]*(?:\\.\\d+)?(?:[eE][+-]?\\d+)?j?)\\b", // 5
|
|
26
|
+
"\\b(False|None|True|and|as|assert|async|await|break|class|continue|def|del|elif|else|except|" +
|
|
27
|
+
"finally|for|from|global|if|import|in|is|lambda|nonlocal|not|or|pass|raise|return|try|while|with|yield)\\b", // 6
|
|
28
|
+
].join("|"),
|
|
29
|
+
"g",
|
|
30
|
+
);
|
|
31
|
+
|
|
32
|
+
/** The whole code colored in one pass; spans never overlap, text content is preserved byte-for-byte. */
|
|
33
|
+
export function highlightPython(code: string, theme: Theme): string {
|
|
34
|
+
const out: string[] = [];
|
|
35
|
+
let last = 0;
|
|
36
|
+
for (const m of code.matchAll(TOKEN_RE)) {
|
|
37
|
+
const at = m.index ?? 0;
|
|
38
|
+
if (at > last) out.push(code.slice(last, at));
|
|
39
|
+
const [raw = "", comment, triple, str, decorator, num, keyword] = m;
|
|
40
|
+
if (comment !== undefined) out.push(theme.fg("muted", comment));
|
|
41
|
+
else if (triple !== undefined || str !== undefined) out.push(theme.fg("mdCode", raw));
|
|
42
|
+
else if (decorator !== undefined) out.push(theme.fg("mdHeading", decorator));
|
|
43
|
+
else if (num !== undefined) out.push(theme.fg("warning", num));
|
|
44
|
+
else out.push(theme.fg("accent", keyword));
|
|
45
|
+
last = at + raw.length;
|
|
46
|
+
}
|
|
47
|
+
if (last < code.length) out.push(code.slice(last));
|
|
48
|
+
return out.join("");
|
|
49
|
+
}
|