diff --git a/apps/docs/src/content/agents/tools.md b/apps/docs/src/content/agents/tools.md
index d15d97ef..30b0aafe 100644
--- a/apps/docs/src/content/agents/tools.md
+++ b/apps/docs/src/content/agents/tools.md
@@ -4,7 +4,7 @@ description: Every agent tool the devtools expose, grouped by inspector, with wh
---
- Fifty-eight tools, grouped by inspector. Each one answers a question you would otherwise answer by clicking through the panel.
+ Fifty-nine tools, grouped by inspector. Each one answers a question you would otherwise answer by clicking through the panel.
# Tools
@@ -352,20 +352,32 @@ It finds impure pipes used inside `@for`, `| json` left in templates, pure pipes
`explain-pipe` explains one pipe: where it is declared or used, whether it is pure, live instance and call counts, the last input and output, a stale-value warning, `| async` usages that resubscribe on every check or subscribe to the same source as another usage, and lint findings. Reads: source, plus the page for live counts.
-| Argument | Required | Value |
-| -------- | -------- | ----------------------------------------------- |
-| `name` | yes | The pipe name as used after `\|` in a template. |
+| Argument | Required | Value |
+| -------- | -------- | -------------------------------------------------- |
+| `name` | yes | The pipe name as used after `\|` in a template. |
+| `page` | no | The tab to read. Defaults to the most recent page. |
-Live counts, input and output appear when recording is on in the [Pipes inspector](../inspectors/pipes.md).
+Live data comes from one tab: the one `page` names, or the most recent. When more than one tab reports pipes, the answer starts with the tab it read. Live counts, input and output appear while recording is on. Start it with [`record-pipes`](#record-pipe-calls) or with **Record calls** in the [Pipes inspector](../inspectors/pipes.md).
The **Live** line tells these cases apart:
-| Case | What the tool says |
-| ----------------------------------- | --------------------------------------------------------------------- |
-| No page is connected | There is no live data. The stdio server always reports this. |
-| A page is connected, pipe not used | No rendered component on the page uses the pipe. |
-| Pipe in use, recording off | Instance count and components, plus a hint to click **Record calls**. |
-| Pipe in use, recording on, no calls | Instance count and components, and that no calls are recorded yet. |
+| Case | What the tool says |
+| ----------------------------------- | ------------------------------------------------------------------ |
+| No page is connected | There is no live data. The stdio server always reports this. |
+| A page is connected, pipe not used | No rendered component on the page uses the pipe. |
+| Pipe in use, recording off | Instance count and components, plus a hint to start recording. |
+| Pipe in use, recording on, no calls | Instance count and components, and that no calls are recorded yet. |
+
+### Record pipe calls
+
+`record-pipes` starts or stops call recording on a page, the same switch as **Record calls** in the panel. Reads: page.
+
+| Argument | Required | Value |
+| -------- | -------- | ---------------------------------------------------------------------------------------------- |
+| `action` | yes | `start` or `stop`. |
+| `page` | no | The tab to record on. Defaults to the most recent page for `start`, and every page for `stop`. |
+
+`start` records on the tab `page` names, or on the most recent tab. Other tabs stay unpatched. `stop` stops that tab, or every tab without `page`. Each recording starts from zero, and the calls recorded so far stay after a stop. Call `record-pipes` with `action: "start"`, use the app, then call `explain-pipe`. Recording is instrumentation, like the `record` argument of `change-detection`, so `agent.readOnly` and `actions` leave the tool in place. `agent.tools.pipes: false` hides it.
## SSR & HTTP
diff --git a/apps/docs/src/content/inspectors/pipes.md b/apps/docs/src/content/inspectors/pipes.md
index 825ed814..d81f34dd 100644
--- a/apps/docs/src/content/inspectors/pipes.md
+++ b/apps/docs/src/content/inspectors/pipes.md
@@ -129,8 +129,9 @@ Look for **resubscribing** rows in **Async subscriptions**, and for `async-on-ca
| `pangular:get-pipes` | no | Custom pipes and the built-in pipes in use. |
| `pangular:lint-pipes` | no | Runs the lint rules above. |
| `pangular:explain-pipe` | partly | One pipe by `name`: where it is declared or used, purity, live counts, last input and output, the stale warning, resubscribing `\| async` usages and the lint findings. |
+| `pangular:record-pipes` | yes | Starts or stops call recording with `action`, like **Record calls**. |
-Agents can't turn recording on. To give `explain-pipe` call data, click **Record calls** in the panel first. See [Tools](../agents/tools.md).
+`explain-pipe` and `record-pipes` take `page` to pick a tab, and default to the most recent one, except that `record-pipes` with `action: "stop"` and no `page` stops recording on every tab. To give `explain-pipe` call data, call `record-pipes` with `action: "start"` or click **Record calls** in the panel, then use the app. See [Tools](../agents/tools.md#pipes).
## Limits and gotchas
@@ -154,7 +155,7 @@ Recording is off by default. Reload the page and it is off again.
- Recording is off. Agents can't turn it on. Click Record calls in the panel first.
+ Recording is off. Call pangular:record-pipes with action: "start", or click Record calls in the panel, then use the app.
The built-in list covers the @angular/common pipes only.
diff --git a/packages/devtools/src/__tests__/config.test.ts b/packages/devtools/src/__tests__/config.test.ts
index 4539c39e..9c74839e 100644
--- a/packages/devtools/src/__tests__/config.test.ts
+++ b/packages/devtools/src/__tests__/config.test.ts
@@ -284,6 +284,22 @@ describe('agent tool and RPC registration', () => {
expect(tools).toContain('list-routes');
});
+ it('keeps record-pipes with its inspector, not behind a write action', async () => {
+ const pipeTools = ['explain-pipe', 'record-pipes'];
+ expect((await boot()).tools).toEqual(expect.arrayContaining(pipeTools));
+ // Recording is instrumentation, like change detection recording, so
+ // blocking actions or read-only mode leaves it in place.
+ const blocked = await boot({ actions: false, agent: { readOnly: true } });
+ expect(blocked.tools).toEqual(expect.arrayContaining(pipeTools));
+ expect(blocked.actionTools).not.toContain('record-pipes');
+ const hidden = await boot({ agent: { tools: { pipes: false } } });
+ expect(hidden.tools.filter((id) => pipeTools.includes(id))).toEqual([]);
+ expect(hidden.rpc).toContain('request-instrument-pipes');
+ const off = await boot({ inspectors: { pipes: false } });
+ expect(off.tools.filter((id) => pipeTools.includes(id))).toEqual([]);
+ expect(off.rpc).not.toContain('request-instrument-pipes');
+ });
+
it('hides one inspector from the agent while its panel keeps working', async () => {
const { rpc, tools, resources } = await boot({ agent: { tools: { router: false } } });
expect(tools.some((id) => AGENT_INSPECTOR[id] === 'router')).toBe(false);
diff --git a/packages/devtools/src/__tests__/docs-tool-count.test.ts b/packages/devtools/src/__tests__/docs-tool-count.test.ts
index 7d5f952b..53bacb10 100644
--- a/packages/devtools/src/__tests__/docs-tool-count.test.ts
+++ b/packages/devtools/src/__tests__/docs-tool-count.test.ts
@@ -16,6 +16,10 @@ const WORDS = [
'Fifty-six',
'Fifty-seven',
'Fifty-eight',
+ 'Fifty-nine',
+ 'Sixty',
+ 'Sixty-one',
+ 'Sixty-two',
];
it('states on the tools page how many tools the server registers', async () => {
diff --git a/packages/devtools/src/__tests__/pipes-pages.test.ts b/packages/devtools/src/__tests__/pipes-pages.test.ts
index ca941b82..4eadf811 100644
--- a/packages/devtools/src/__tests__/pipes-pages.test.ts
+++ b/packages/devtools/src/__tests__/pipes-pages.test.ts
@@ -2,6 +2,7 @@ import { createHostContext } from 'devframe/node';
import { afterEach, describe, expect, it, vi } from 'vitest';
import pangular from '../devframe.ts';
import { isPipePageReport, mergePipePageReport, type PipePageReport } from '../rpc/pipes-tools.ts';
+import { fixtureDir } from '../rpc/__tests__/fixture-dir.ts';
async function boot() {
const host = {
@@ -9,17 +10,17 @@ async function boot() {
resolveOrigin: () => 'http://localhost',
getStorageDir: () => '',
};
- const ctx = await createHostContext({ cwd: process.cwd(), mode: 'dev', host: host as never });
+ // An empty project: explain-pipe scans the source on every call, and these
+ // tests are about live data only.
+ const cwd = fixtureDir('pangular-pipes-pages-');
+ const ctx = await createHostContext({ cwd, mode: 'dev', host: host as never });
await pangular.setup(ctx as never);
const invoke = (name: string, ...args: unknown[]) =>
ctx.rpc.invokeLocal(`pangular:${name}` as never, ...(args as never));
- const explain = async () =>
- (
- (await ctx.agent.invoke('pangular:explain-pipe', { name: 'async' })) as {
- markdown: string;
- }
- ).markdown;
- return { ctx, invoke, explain };
+ const tool = async (id: string, args: Record) =>
+ ((await ctx.agent.invoke(`pangular:${id}`, args)) as { markdown: string }).markdown;
+ const explain = (page?: string) => tool('explain-pipe', { name: 'async', page });
+ return { ctx, invoke, explain, tool };
}
function page(pageId: string): PipePageReport {
@@ -68,17 +69,18 @@ describe('pipe pages and their connections', () => {
await invoke('push-pipes', page('tab-1'));
host.getCurrentRpcSession = () => ({ meta: { id: 2 } });
await invoke('push-pipes', page('tab-2'));
- expect(await explain()).toContain('6 instance(s)');
+ expect(await explain('tab-1')).toContain('3 instance(s)');
host._emitSessionDisconnected({ id: 1 });
- expect(await explain()).toContain('3 instance(s)');
+ expect(await explain('tab-1')).toMatch(/^No page `tab-1` is reporting pipe usage/);
+ expect(await explain('tab-2')).toContain('3 instance(s)');
// A reconnect re-binds the page to its new connection, so the old
// connection closing afterwards must not drop it.
host.getCurrentRpcSession = () => ({ meta: { id: 3 } });
await invoke('push-pipes', page('tab-2'));
host._emitSessionDisconnected({ id: 2 });
- expect(await explain()).toContain('3 instance(s)');
+ expect(await explain('tab-2')).toContain('3 instance(s)');
});
});
@@ -181,3 +183,126 @@ describe('pipe recording requests', () => {
}
});
});
+
+function pageWith(pageId: string, instanceCount: number, callCount?: number): PipePageReport {
+ const base = page(pageId);
+ return {
+ ...base,
+ pipes: [
+ {
+ ...base.pipes[0],
+ instanceCount,
+ components: [{ name: 'ExamplePage', count: instanceCount }],
+ ...(callCount === undefined
+ ? {}
+ : { call: { callCount, lastArgs: [pageId], lastResult: pageId } }),
+ },
+ ],
+ instrumented: callCount !== undefined,
+ };
+}
+
+describe('explain-pipe page scoping', () => {
+ it('answers from the most recent page, or the one `page` names, without mixing tabs', async () => {
+ vi.useFakeTimers();
+ try {
+ const { invoke, explain } = await boot();
+ await invoke('push-pipes', pageWith('tab-1', 2, 5));
+ await vi.advanceTimersByTimeAsync(1000);
+ await invoke('push-pipes', pageWith('tab-2', 4));
+
+ const latest = await explain();
+ expect(latest).toMatch(/^Live data from page `tab-2`; 2 pages report pipe usage/);
+ expect(latest).toContain('4 instance(s)');
+ expect(latest).toContain('recording is off');
+ expect(latest).not.toContain('6 instance(s)');
+
+ const first = await explain('tab-1');
+ expect(first).toContain('Live data from page `tab-1`');
+ expect(first).toContain('2 instance(s)');
+ expect(first).toContain('Called 5 time(s)');
+ expect(first).toContain('Last input: `[tab-1]`');
+ } finally {
+ vi.useRealTimers();
+ }
+ });
+
+ it('names the reporting pages when `page` is unknown', async () => {
+ const { invoke, explain } = await boot();
+ expect(await explain('gone')).toBe(
+ 'No page `gone` is reporting pipe usage. No page reports pipe usage right now.',
+ );
+ await invoke('push-pipes', page('tab-1'));
+ expect(await explain('gone')).toBe(
+ 'No page `gone` is reporting pipe usage. Pages that report pipe usage: `tab-1`. Pass one of these ids as `page`, or leave `page` out for the most recent.',
+ );
+ });
+
+ it('adds no page line when only one page reports', async () => {
+ const { invoke, explain } = await boot();
+ await invoke('push-pipes', page('tab-1'));
+ const text = await explain();
+ expect(text).not.toContain('Live data from page');
+ expect(text).toContain('3 instance(s)');
+ });
+});
+
+describe('record-pipes agent tool', () => {
+ const recordings = (broadcast: { mock: { calls: unknown[][] } }) =>
+ broadcast.mock.calls
+ .map(([options]) => options as { method: string; args: unknown[] })
+ .filter((options) => options.method === 'pangular:instrument-pipes')
+ .map((options) => options.args[0]);
+
+ it('starts and stops recording through the same request the panel sends', async () => {
+ vi.useFakeTimers();
+ try {
+ const { ctx, invoke, tool } = await boot();
+ const broadcast = vi.spyOn(ctx.rpc, 'broadcast');
+ await invoke('push-pipes', page('tab-1'));
+ await vi.advanceTimersByTimeAsync(1000);
+ await invoke('push-pipes', page('tab-2'));
+
+ expect(await tool('record-pipes', { action: 'start' })).toMatch(
+ /^Recording pipe calls on page `tab-2`\./,
+ );
+ expect(await tool('record-pipes', { action: 'start', page: 'tab-1' })).toMatch(
+ /^Recording pipe calls on page `tab-1`\./,
+ );
+ expect(await tool('record-pipes', { action: 'stop', page: 'tab-1' })).toMatch(
+ /^Recording stopped on page `tab-1`\./,
+ );
+ expect(await tool('record-pipes', { action: 'stop' })).toMatch(
+ /^Recording stopped on 2 page\(s\)\./,
+ );
+ expect(recordings(broadcast)).toEqual([
+ { pageId: 'tab-2', on: true },
+ { pageId: 'tab-1', on: true },
+ { pageId: 'tab-1', on: false },
+ { on: false },
+ ]);
+ } finally {
+ vi.useRealTimers();
+ }
+ });
+
+ it('sends nothing for an unknown page, a missing action or before any page reports', async () => {
+ const { ctx, invoke, tool } = await boot();
+ const broadcast = vi.spyOn(ctx.rpc, 'broadcast');
+ expect(await tool('record-pipes', { action: 'start' })).toMatch(
+ /^No page reports pipe usage, so there is nothing to record/,
+ );
+ await invoke('push-pipes', page('tab-1'));
+ expect(await tool('record-pipes', { action: 'start', page: 'gone' })).toMatch(
+ /^No page `gone` is reporting pipe usage\. Pages that report pipe usage: `tab-1`/,
+ );
+ expect(await tool('record-pipes', {})).toBe('Pass `action`: "start" or "stop".');
+ expect(recordings(broadcast)).toEqual([]);
+ });
+
+ it('points explain-pipe at the agent tool while recording is off', async () => {
+ const { invoke, explain } = await boot();
+ await invoke('push-pipes', page('tab-1'));
+ expect(await explain()).toContain('Start it with pangular:record-pipes');
+ });
+});
diff --git a/packages/devtools/src/config.ts b/packages/devtools/src/config.ts
index f879140a..aa987a28 100644
--- a/packages/devtools/src/config.ts
+++ b/packages/devtools/src/config.ts
@@ -531,6 +531,7 @@ export const AGENT_INSPECTOR: Record = {
navigate: 'router',
'lint-pipes': 'pipes',
'explain-pipe': 'pipes',
+ 'record-pipes': 'pipes',
'list-ssr-requests': 'http',
'explain-ssr-request': 'http',
'list-http-calls': 'http',
@@ -581,6 +582,7 @@ export const PAGE_AGENT_ENTRIES: readonly string[] = [
'router-config',
'export-navigation',
'navigate',
+ 'record-pipes',
'analog-current-page',
'analog-server-calls',
'analog-call-api',
diff --git a/packages/devtools/src/devframe.ts b/packages/devtools/src/devframe.ts
index 27280266..4147e27a 100644
--- a/packages/devtools/src/devframe.ts
+++ b/packages/devtools/src/devframe.ts
@@ -6,7 +6,7 @@ import { getComponents } from './rpc/get-components.ts';
import { getPipes } from './rpc/get-pipes.ts';
import { inspectProvidersText } from './rpc/injector-tools.ts';
import { lintPipes, lintPipesText } from './rpc/pipe-lint.ts';
-import { explainPipeText } from './rpc/pipe-explain.ts';
+import { explainPipeOnPage, pipeRecordRequest, pipeRecordText } from './rpc/pipes-agent.ts';
import { trackPageSessions } from './rpc/page-sessions.ts';
import { getBuildMeta } from './rpc/build-meta.ts';
import { getSignals } from './rpc/get-signals.ts';
@@ -452,18 +452,21 @@ const pangular = defineDevframe({
},
});
+ // Shared by the panel's Record calls button and the record-pipes agent tool.
+ const requestPipeRecording = (message: unknown) => {
+ const request = pipeInstrumentRequest(message, pipePages);
+ // No page has reported yet: starting now would patch every connected tab.
+ if (request.on && !request.pageId) return { request, pages: { pages: 0 } };
+ void my.rpc.broadcast({ method: 'instrument-pipes', args: [request], optional: true });
+ if (!request.pageId) return { request, pages: { pages: pipePages.size } };
+ return { request, pages: { pages: pipePages.has(request.pageId) ? 1 : 0 } };
+ };
+
register({
name: 'request-instrument-pipes',
type: 'action',
jsonSerializable: true,
- handler: (message: unknown) => {
- const request = pipeInstrumentRequest(message, pipePages);
- // No page has reported yet: starting now would patch every connected tab.
- if (request.on && !request.pageId) return { pages: 0 };
- void my.rpc.broadcast({ method: 'instrument-pipes', args: [request], optional: true });
- if (!request.pageId) return { pages: pipePages.size };
- return { pages: pipePages.has(request.pageId) ? 1 : 0 };
- },
+ handler: (message: unknown) => requestPipeRecording(message).pages,
});
register({
@@ -2599,19 +2602,34 @@ const pangular = defineDevframe({
type: 'object',
properties: {
name: { type: 'string', description: 'The pipe name as used after `|` in a template.' },
+ page: PAGE_ARGUMENT,
},
required: ['name'],
},
- handler: async (args: { name?: string }) => {
+ handler: async (args: { name?: string; page?: string }) => {
if (!args?.name) return { markdown: 'Pass a pipe `name`.' };
- return {
- markdown: explainPipeText(
- args.name,
- ctx.cwd,
- pipesState.value() as PipesState,
- pipePages.size,
- ),
- };
+ return { markdown: explainPipeOnPage(args.name, ctx.cwd, pipePages, pageArgument(args)) };
+ },
+ });
+
+ agent.registerTool({
+ id: 'pangular:record-pipes',
+ description:
+ "Start or stop recording pipe calls on the running page, the same switch as Record calls in the Pipes panel. Recording wraps each pipe's transform() to count calls and keep the last input, output and caller, and turns on the experimental stale-argument check; pangular:explain-pipe then shows them. `start` records on the page `page` names, or on the most recent page; `stop` stops that page, or every page without `page`. Calls recorded so far stay after a stop.",
+ safety: 'read',
+ inputSchema: {
+ type: 'object',
+ properties: {
+ action: { type: 'string', enum: ['start', 'stop'] },
+ page: PAGE_ARGUMENT,
+ },
+ required: ['action'],
+ },
+ handler: async (args: { action?: string; page?: string }) => {
+ const asked = pipeRecordRequest(args?.action, pipePages, pageArgument(args));
+ if ('markdown' in asked) return asked;
+ const sent = requestPipeRecording(asked.request);
+ return { markdown: pipeRecordText(sent.request, sent.pages.pages) };
},
});
diff --git a/packages/devtools/src/rpc/__tests__/pipe-explain.test.ts b/packages/devtools/src/rpc/__tests__/pipe-explain.test.ts
index bad2bc1e..5a87d2d1 100644
--- a/packages/devtools/src/rpc/__tests__/pipe-explain.test.ts
+++ b/packages/devtools/src/rpc/__tests__/pipe-explain.test.ts
@@ -67,7 +67,7 @@ describe('explainPipeText', () => {
expect(text).not.toContain('Record calls');
});
- it('names the Record calls button when the pipe is in use and recording is off', () => {
+ it('names record-pipes and the Record calls button when the pipe is in use and recording is off', () => {
const text = explainPipeText(
'appPrice',
fixture(),
@@ -77,7 +77,9 @@ describe('explainPipeText', () => {
const live = liveLine(text);
expect(live).toContain('1 instance(s), used by CartComponent (1).');
expect(live).toContain('No calls recorded, recording is off.');
- expect(live).toContain('Click **Record calls** in the Pipes panel');
+ expect(live).toContain(
+ 'Start it with pangular:record-pipes (or **Record calls** in the Pipes panel)',
+ );
expect(live).not.toContain('Instrument');
expect(live).not.toContain('—');
});
diff --git a/packages/devtools/src/rpc/pipe-explain.ts b/packages/devtools/src/rpc/pipe-explain.ts
index 0477874c..ca4e5aa2 100644
--- a/packages/devtools/src/rpc/pipe-explain.ts
+++ b/packages/devtools/src/rpc/pipe-explain.ts
@@ -85,7 +85,7 @@ const NOT_IN_USE =
'**Live:** not in use on the connected page(s). No rendered component uses this pipe right now. Pipe discovery is always on, so this does not depend on recording.';
const RECORD_HINT =
- 'No calls recorded, recording is off. Click **Record calls** in the Pipes panel for call counts, last input/output and stale-argument detection.';
+ 'No calls recorded, recording is off. Start it with pangular:record-pipes (or **Record calls** in the Pipes panel) for call counts, last input/output and stale-argument detection.';
function runtimeText(
runtime: PipeUsageInfo | undefined,
diff --git a/packages/devtools/src/rpc/pipes-agent.ts b/packages/devtools/src/rpc/pipes-agent.ts
new file mode 100644
index 00000000..206834c4
--- /dev/null
+++ b/packages/devtools/src/rpc/pipes-agent.ts
@@ -0,0 +1,77 @@
+import { code } from './forms-tools.ts';
+import { byRecency, unknownPageText } from './pages.ts';
+import { explainPipeText } from './pipe-explain.ts';
+import { currentPipes, type PipePageReport, type PipesState } from './pipes-tools.ts';
+
+type PipePages = Map;
+
+const WHAT = 'pipe usage';
+
+/**
+ * The live pipe data of one page: the one `page` names, or the page that
+ * reported most recently. Returns the answer for an unknown `page` instead.
+ */
+export function pipePageFor(
+ pages: PipePages,
+ page: string | undefined,
+): { pageId?: string; state: PipesState } | { markdown: string } {
+ if (page && !pages.has(page)) {
+ return { markdown: unknownPageText(page, [...pages.values()], WHAT) };
+ }
+ const pageId = page ?? byRecency(pages.values())[0]?.pageId;
+ const one: PipePages = new Map();
+ const report = pageId ? pages.get(pageId) : undefined;
+ if (pageId && report) one.set(pageId, report);
+ return { pageId: report ? pageId : undefined, state: currentPipes(one) };
+}
+
+/** `explain-pipe` for one page, so several open tabs never mix their counts. */
+export function explainPipeOnPage(
+ name: string,
+ cwd: string,
+ pages: PipePages,
+ page: string | undefined,
+): string {
+ const picked = pipePageFor(pages, page);
+ if ('markdown' in picked) return picked.markdown;
+ const text = explainPipeText(name, cwd, picked.state, picked.pageId ? 1 : 0);
+ if (!picked.pageId || pages.size < 2) return text;
+ return `Live data from page ${code(picked.pageId)}; ${pages.size} pages report ${WHAT}, pass \`page\` to pick another.\n\n${text}`;
+}
+
+export type PipeRecordAction = 'start' | 'stop';
+
+/**
+ * What `record-pipes` sends through `request-instrument-pipes`, or the answer
+ * when there is nothing to send. Start needs a page; stop without `page`
+ * reaches every page.
+ */
+export function pipeRecordRequest(
+ action: unknown,
+ pages: PipePages,
+ page: string | undefined,
+): { request: { pageId?: string; on: boolean } } | { markdown: string } {
+ if (action !== 'start' && action !== 'stop') {
+ return { markdown: 'Pass `action`: "start" or "stop".' };
+ }
+ if (page && !pages.has(page)) {
+ return { markdown: unknownPageText(page, [...pages.values()], WHAT) };
+ }
+ if (!pages.size) {
+ return {
+ markdown:
+ 'No page reports pipe usage, so there is nothing to record. Live data needs a page: connect through the MCP endpoint of the server that runs the app, with the app open in a browser.',
+ };
+ }
+ const on = action === 'start';
+ return { request: page ? { pageId: page, on } : { on } };
+}
+
+/** The answer once `request-instrument-pipes` has sent the request. */
+export function pipeRecordText(request: { pageId?: string; on: boolean }, reached: number): string {
+ const where = request.pageId ? `page ${code(request.pageId)}` : `${reached} page(s)`;
+ if (request.on) {
+ return `Recording pipe calls on ${where}. Use the app, then call pangular:explain-pipe with the pipe name${request.pageId ? ' and the same `page`' : ''} for call counts, the last input, output and caller, and stale-argument checks. Recording wraps each pipe's transform(), so stop it when you are done.`;
+ }
+ return `Recording stopped on ${where}. The calls recorded so far stay until recording starts again.`;
+}