@mnemahq/cli 0.10.0 → 0.12.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/package.json +2 -2
- package/src/cli.mjs +69 -5
- package/src/read-commands.mjs +62 -1
- package/src/tui/app.mjs +12 -5
- package/src/tui/launch.mjs +8 -2
- package/src/tui/screens/flows.mjs +77 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@mnemahq/cli",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.12.0",
|
|
4
4
|
"description": "Mnema CLI — connect a repo to your Mnema workspace: install session capture, sweep past sessions, and search from the terminal.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -34,7 +34,7 @@
|
|
|
34
34
|
"htm": "^3.1.1",
|
|
35
35
|
"ink": "^6.8.0",
|
|
36
36
|
"react": "^19.2.7",
|
|
37
|
-
"@mnemahq/sdk": "0.4.
|
|
37
|
+
"@mnemahq/sdk": "0.4.1"
|
|
38
38
|
},
|
|
39
39
|
"scripts": {
|
|
40
40
|
"build": "node -e \"process.exit(0)\"",
|
package/src/cli.mjs
CHANGED
|
@@ -15,6 +15,7 @@ import { cmdLogin, cmdLogout, accessToken } from './login.mjs';
|
|
|
15
15
|
import { makeClient, call, hasApiKey, canAuthenticate, renderError, mintHookToken } from './client.mjs';
|
|
16
16
|
import {
|
|
17
17
|
cmdTasks, cmdNext, cmdDocs, cmdDoc, cmdProjects, cmdAsk, cmdGraph, cmdBriefing,
|
|
18
|
+
cmdFlows, cmdFlow,
|
|
18
19
|
} from './read-commands.mjs';
|
|
19
20
|
import {
|
|
20
21
|
DEFAULT_ORIGIN, DEFAULT_APP_URL, c, truncate, width, mark, gitInfo, canonicalRepo, readConfig, writeConfig,
|
|
@@ -510,17 +511,22 @@ Commands:
|
|
|
510
511
|
uninstall Remove hooks and stored secrets
|
|
511
512
|
tui Open the interactive briefing explicitly (Node 20+, a terminal)
|
|
512
513
|
|
|
514
|
+
On a terminal, the read commands below open a NAVIGABLE view — arrow keys to move,
|
|
515
|
+
enter to open, esc to go back. Piped or with --json they print as they always have.
|
|
516
|
+
|
|
513
517
|
Read your workspace:
|
|
514
518
|
tasks List tasks [--status --project --limit]
|
|
515
519
|
next The next task to pick up, with a ready-made branch name
|
|
516
520
|
docs List documents [--limit]
|
|
517
521
|
doc [id] Print one document as markdown; no id opens a picker
|
|
518
522
|
projects List projects
|
|
523
|
+
flows List flows
|
|
524
|
+
flow [slug] One flow; no slug opens a picker
|
|
519
525
|
briefing What deserves attention — pulse, deltas, findings
|
|
520
526
|
|
|
521
527
|
Ask the knowledge graph (paid feature):
|
|
522
528
|
ask "q" A cited answer, with the confidence it deserves
|
|
523
|
-
graph
|
|
529
|
+
graph Walk a node's neighbours; graph <a> <b> prints the path between two
|
|
524
530
|
|
|
525
531
|
Examples:
|
|
526
532
|
mnema the interactive briefing
|
|
@@ -565,6 +571,49 @@ writes plain text.
|
|
|
565
571
|
* because `node --check` parses without resolving and would never notice a stray
|
|
566
572
|
* `import { Box } from 'ink'` in read-commands.mjs.
|
|
567
573
|
*/
|
|
574
|
+
/**
|
|
575
|
+
* ⭐ THE COMMAND YOU TYPED SHOULD DO THE THING YOU NAMED.
|
|
576
|
+
*
|
|
577
|
+
* `mnema graph "Workspace Security & Management"` printed 25 rows and "… 176
|
|
578
|
+
* more" and stopped. The navigable walker existed, but it lived behind bare
|
|
579
|
+
* `mnema` and a `g` keybinding — so the obvious command gave a static dump and
|
|
580
|
+
* the answer was "run a different command and press a key". Nobody does that.
|
|
581
|
+
* They type the thing they want.
|
|
582
|
+
*
|
|
583
|
+
* ⚠️ THE ONE-SHOT PATH IS UNCHANGED. --json, a pipe, no TTY, --no-tui, CI: all
|
|
584
|
+
* still print exactly what they printed before, byte for byte. Scripts are the
|
|
585
|
+
* contract. This only changes what a HUMAN AT A TERMINAL gets.
|
|
586
|
+
*
|
|
587
|
+
* @returns true if the interactive view took over.
|
|
588
|
+
*/
|
|
589
|
+
/**
|
|
590
|
+
* Which screen a read command should open on, or null to stay one-shot.
|
|
591
|
+
*
|
|
592
|
+
* Exported so the ROUTING is testable without spawning ink — the decision is the
|
|
593
|
+
* part that was wrong, and a test that re-implements it in the test file proves
|
|
594
|
+
* nothing about this function.
|
|
595
|
+
*/
|
|
596
|
+
export function screenFor(cmd, flags, rest = []) {
|
|
597
|
+
if (flags.json) return null;
|
|
598
|
+
if (cmd === 'graph') {
|
|
599
|
+
// `graph <a> <b>` is a path query — one answer, not a place to walk.
|
|
600
|
+
if (rest.length > 1) return null;
|
|
601
|
+
return rest[0] ? { name: 'node', target: { label: rest[0] } } : { name: 'graph' };
|
|
602
|
+
}
|
|
603
|
+
if (cmd === 'tasks' || cmd === 'docs' || cmd === 'briefing' || cmd === 'flows') return { name: cmd };
|
|
604
|
+
return null;
|
|
605
|
+
}
|
|
606
|
+
|
|
607
|
+
async function maybeInteractive(flags, screen) {
|
|
608
|
+
if (!screen) return false;
|
|
609
|
+
if (flags.json) return false;
|
|
610
|
+
if (!tuiEligibility(flags).ok) return false;
|
|
611
|
+
const { startTui } = await import('./tui/launch.mjs');
|
|
612
|
+
const ctx = resolveContext(flags);
|
|
613
|
+
await startTui({ ...ctx, version: VERSION, call: (fn) => call(ctx, fn) }, screen);
|
|
614
|
+
return true;
|
|
615
|
+
}
|
|
616
|
+
|
|
568
617
|
async function openTuiOrHelp(flags) {
|
|
569
618
|
const gate = tuiEligibility(flags);
|
|
570
619
|
if (gate.ok) {
|
|
@@ -612,14 +661,29 @@ export async function run(argv) {
|
|
|
612
661
|
|
|
613
662
|
// Reads. Each resolves context once and hands the SDK client to a wrapper;
|
|
614
663
|
// none of them knows a URL or an envelope key (§A2, t-623).
|
|
615
|
-
case 'tasks':
|
|
664
|
+
case 'tasks':
|
|
665
|
+
if (await maybeInteractive(flags, screenFor('tasks', flags))) return 0;
|
|
666
|
+
return cmdTasks(flags, resolveContext(flags));
|
|
616
667
|
case 'next': return cmdNext(flags, resolveContext(flags));
|
|
617
|
-
case 'docs':
|
|
668
|
+
case 'docs':
|
|
669
|
+
if (await maybeInteractive(flags, screenFor('docs', flags))) return 0;
|
|
670
|
+
return cmdDocs(flags, resolveContext(flags));
|
|
618
671
|
case 'doc': return cmdDoc(flags, resolveContext(flags), rest);
|
|
619
672
|
case 'projects': return cmdProjects(flags, resolveContext(flags));
|
|
673
|
+
case 'flows':
|
|
674
|
+
if (await maybeInteractive(flags, screenFor('flows', flags))) return 0;
|
|
675
|
+
return cmdFlows(flags, resolveContext(flags));
|
|
676
|
+
case 'flow': return cmdFlow(flags, resolveContext(flags), rest);
|
|
620
677
|
case 'ask': return cmdAsk(flags, resolveContext(flags), rest);
|
|
621
|
-
case 'graph':
|
|
622
|
-
|
|
678
|
+
case 'graph':
|
|
679
|
+
// With a node named, open the walker AT it. With no argument, open the hub
|
|
680
|
+
// list. `graph <a> <b>` is a path query — a single answer, not a place to
|
|
681
|
+
// walk — so that one stays one-shot.
|
|
682
|
+
if (await maybeInteractive(flags, screenFor('graph', flags, rest))) return 0;
|
|
683
|
+
return cmdGraph(flags, resolveContext(flags), rest);
|
|
684
|
+
case 'briefing':
|
|
685
|
+
if (await maybeInteractive(flags, screenFor('briefing', flags))) return 0;
|
|
686
|
+
return cmdBriefing(flags, resolveContext(flags));
|
|
623
687
|
case 'tui':
|
|
624
688
|
// An EXPLICIT request that cannot be honoured fails loudly (exit 1); the
|
|
625
689
|
// implicit one below degrades to help and exits 0. That asymmetry is
|
package/src/read-commands.mjs
CHANGED
|
@@ -27,7 +27,7 @@
|
|
|
27
27
|
import { call, hasApiKey, canAuthenticate, renderError } from './client.mjs';
|
|
28
28
|
import { take } from './paging.mjs';
|
|
29
29
|
import { c, truncate, link, mark } from './util.mjs';
|
|
30
|
-
import { heading, empty, rows, more, copyable } from './render/layout.mjs';
|
|
30
|
+
import { heading, entries, empty, rows, more, copyable } from './render/layout.mjs';
|
|
31
31
|
import { pick } from './render/pick.mjs';
|
|
32
32
|
|
|
33
33
|
/** Machine output is the whole object; humans get the formatted view. */
|
|
@@ -274,3 +274,64 @@ export async function cmdBriefing(flags, ctx) {
|
|
|
274
274
|
});
|
|
275
275
|
} catch (e) { renderError(e, { usedApiKey: hasApiKey(ctx.workspaceId), context: 'briefing' }); }
|
|
276
276
|
}
|
|
277
|
+
|
|
278
|
+
// ── flows ─────────────────────────────────────────────────────────────────────
|
|
279
|
+
|
|
280
|
+
/**
|
|
281
|
+
* ⚠️ THE PUBLIC FLOW ENDPOINT RETURNS METADATA ONLY — no nodes, no steps, no run
|
|
282
|
+
* history. Rendering a "Steps" heading with nothing under it would be the exact
|
|
283
|
+
* silent-empty failure this codebase is named for: it would read as "this flow
|
|
284
|
+
* has no steps" when it means "this endpoint does not serve them". So the detail
|
|
285
|
+
* shows what is genuinely there and says where the rest lives.
|
|
286
|
+
*/
|
|
287
|
+
export async function cmdFlows(flags, ctx) {
|
|
288
|
+
await guard(ctx.workspaceId);
|
|
289
|
+
const limit = limitOf(flags, 50);
|
|
290
|
+
try {
|
|
291
|
+
const page = await call(ctx, (m) => take(m.flows.list(), limit));
|
|
292
|
+
emit(flags, page.rows, () => {
|
|
293
|
+
heading('Flows', page.rows.length);
|
|
294
|
+
if (!page.rows.length) return empty('No flows yet.', 'Build one in the app and it shows up here.');
|
|
295
|
+
rows(page.rows.map((f) => ({
|
|
296
|
+
cells: [f.slug ?? '', f.name ?? ''],
|
|
297
|
+
style: [c.cyan, undefined],
|
|
298
|
+
})));
|
|
299
|
+
more(page.more, `mnema flows --limit ${limit * 2}`);
|
|
300
|
+
});
|
|
301
|
+
} catch (e) { renderError(e, { usedApiKey: hasApiKey(ctx.workspaceId), context: 'flows' }); }
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
export async function cmdFlow(flags, ctx, rest) {
|
|
305
|
+
await guard(ctx.workspaceId);
|
|
306
|
+
let slug = rest[0];
|
|
307
|
+
const interactive = Boolean(process.stdin.isTTY && process.stderr.isTTY);
|
|
308
|
+
if (!slug && !flags.json && interactive) {
|
|
309
|
+
const page = await call(ctx, (m) => take(m.flows.list(), 50)).catch(() => null);
|
|
310
|
+
if (page?.rows.length) {
|
|
311
|
+
const chosen = await pick(page.rows.map((f) => ({ label: `${f.name ?? f.slug}` })), { title: 'Which flow?' });
|
|
312
|
+
if (chosen === null) return;
|
|
313
|
+
slug = page.rows[chosen].slug;
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
if (!slug) {
|
|
317
|
+
console.error(c.red('Usage: mnema flow <slug>'));
|
|
318
|
+
console.error(c.dim(' Run it without a slug in a terminal to pick from a list.'));
|
|
319
|
+
process.exit(1);
|
|
320
|
+
}
|
|
321
|
+
try {
|
|
322
|
+
const f = await call(ctx, (m) => m.flows.get(slug));
|
|
323
|
+
emit(flags, f, () => {
|
|
324
|
+
heading(f.name ?? slug);
|
|
325
|
+
entries([
|
|
326
|
+
{ label: 'slug', value: f.slug ?? slug },
|
|
327
|
+
{ label: 'published', value: f.publishedVersionId ? 'yes' : 'draft', state: Boolean(f.publishedVersionId) },
|
|
328
|
+
...(f.projectId ? [{ label: 'project', value: f.projectId }] : []),
|
|
329
|
+
{ label: 'updated', value: String(f.updatedAt ?? '').slice(0, 10), dim: true },
|
|
330
|
+
]);
|
|
331
|
+
if (f.description) console.log(`\n ${c.dim(f.description)}`);
|
|
332
|
+
// Saying this is the point: the absence of steps here is the endpoint's
|
|
333
|
+
// shape, not the flow's emptiness.
|
|
334
|
+
console.log(c.dim('\n Steps and runs are not on the public API — open the flow in the app to walk it.'));
|
|
335
|
+
});
|
|
336
|
+
} catch (e) { renderError(e, { usedApiKey: hasApiKey(ctx.workspaceId), context: 'flow' }); }
|
|
337
|
+
}
|
package/src/tui/app.mjs
CHANGED
|
@@ -23,12 +23,13 @@ import { Header } from './components/header.mjs';
|
|
|
23
23
|
import { GraphHome } from './screens/graph-home.mjs';
|
|
24
24
|
import { Tasks, Task } from './screens/tasks.mjs';
|
|
25
25
|
import { Docs, Doc } from './screens/docs.mjs';
|
|
26
|
+
import { Flows, Flow } from './screens/flows.mjs';
|
|
26
27
|
|
|
27
28
|
const HOME = { name: 'briefing' };
|
|
28
29
|
|
|
29
|
-
export function App({ ctx }) {
|
|
30
|
+
export function App({ ctx, initial }) {
|
|
30
31
|
const { exit } = useApp();
|
|
31
|
-
const [stack, setStack] = useState([HOME]);
|
|
32
|
+
const [stack, setStack] = useState([initial ?? HOME]);
|
|
32
33
|
const [fatal, setFatal] = useState(null);
|
|
33
34
|
|
|
34
35
|
// ⭐ THE AUTH TAKEOVER HAS TO BE WIRED, not merely rendered. Every screen fetches
|
|
@@ -71,6 +72,7 @@ export function App({ ctx }) {
|
|
|
71
72
|
if (input === 'g') { setStack([{ name: 'graph' }]); return; }
|
|
72
73
|
if (input === 't') { setStack([{ name: 'tasks' }]); return; }
|
|
73
74
|
if (input === 'd') { setStack([{ name: 'docs' }]); return; }
|
|
75
|
+
if (input === 'f') { setStack([{ name: 'flows' }]); return; }
|
|
74
76
|
});
|
|
75
77
|
|
|
76
78
|
if (fatal?.kind === 'auth') {
|
|
@@ -100,6 +102,11 @@ export function App({ ctx }) {
|
|
|
100
102
|
onOpen=${(d) => push({ name: 'doc', doc: d })} />`;
|
|
101
103
|
} else if (top.name === 'doc') {
|
|
102
104
|
body = html`<${Doc} ctx=${wrapped} doc=${top.doc} focused=${true} />`;
|
|
105
|
+
} else if (top.name === 'flows') {
|
|
106
|
+
body = html`<${Flows} ctx=${wrapped} focused=${true}
|
|
107
|
+
onOpen=${(f) => push({ name: 'flow', flow: f })} />`;
|
|
108
|
+
} else if (top.name === 'flow') {
|
|
109
|
+
body = html`<${Flow} ctx=${wrapped} flow=${top.flow} />`;
|
|
103
110
|
} else if (top.name === 'briefing') {
|
|
104
111
|
body = html`<${Briefing} ctx=${wrapped} focused=${true} onOpen=${(f) => push({ name: 'finding', finding: f })} />`;
|
|
105
112
|
} else if (top.name === 'node') {
|
|
@@ -144,13 +151,13 @@ export function App({ ctx }) {
|
|
|
144
151
|
* which is worse than not mentioning it. The global jumps are appended once, in
|
|
145
152
|
* one place, so they cannot drift per screen.
|
|
146
153
|
*/
|
|
147
|
-
const JUMPS = 'b briefing · g graph · t tasks · d docs · q quit';
|
|
154
|
+
const JUMPS = 'b briefing · g graph · t tasks · d docs · f flows · q quit';
|
|
148
155
|
|
|
149
156
|
function footerFor(top) {
|
|
150
157
|
if (top.name === 'briefing') return `↑↓ move · enter open · r refresh · ${JUMPS}`;
|
|
151
158
|
if (top.name === 'graph') return `↑↓ move · enter walk in · ${JUMPS}`;
|
|
152
|
-
if (top.name === 'tasks' || top.name === 'docs') return `↑↓ move · enter open · ${JUMPS}`;
|
|
153
|
-
if (top.name === 'task') return `esc back · ${JUMPS}`;
|
|
159
|
+
if (top.name === 'tasks' || top.name === 'docs' || top.name === 'flows') return `↑↓ move · enter open · ${JUMPS}`;
|
|
160
|
+
if (top.name === 'task' || top.name === 'flow') return `esc back · ${JUMPS}`;
|
|
154
161
|
if (top.name === 'doc') return `j/k scroll · esc back · ${JUMPS}`;
|
|
155
162
|
if (top.name === 'node') return `↑↓ move · enter walk in · esc back · r refresh · ${JUMPS}`;
|
|
156
163
|
if (top.name === 'finding') {
|
package/src/tui/launch.mjs
CHANGED
|
@@ -28,8 +28,14 @@ import { App } from './app.mjs';
|
|
|
28
28
|
|
|
29
29
|
const SHOW_CURSOR = '\x1b[?25h';
|
|
30
30
|
|
|
31
|
-
|
|
32
|
-
|
|
31
|
+
/**
|
|
32
|
+
* @param initial the screen to open on. `mnema graph "X"` opens the walker AT X
|
|
33
|
+
* rather than at the briefing, because the command someone typed
|
|
34
|
+
* should do the thing they named — not drop them at a home screen
|
|
35
|
+
* with a keybinding to hunt for.
|
|
36
|
+
*/
|
|
37
|
+
export async function startTui(ctx, initial) {
|
|
38
|
+
const instance = render(html`<${App} ctx=${ctx} initial=${initial} />`, {
|
|
33
39
|
// Ink's own exitOnCtrlC would leave our own key handling out of the loop.
|
|
34
40
|
exitOnCtrlC: true,
|
|
35
41
|
});
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Flows (t-640).
|
|
3
|
+
*
|
|
4
|
+
* ⚠️ THE PUBLIC ENDPOINT SERVES METADATA ONLY — no nodes, no steps, no runs. A
|
|
5
|
+
* "Steps" heading with nothing under it would read as "this flow is empty" when
|
|
6
|
+
* it means "this endpoint does not serve them", which is exactly the silent-empty
|
|
7
|
+
* failure this codebase is named for. So the detail shows what is genuinely there
|
|
8
|
+
* and names where the rest lives.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import { Box, Text } from 'ink';
|
|
12
|
+
import { html } from '../h.mjs';
|
|
13
|
+
import { useResource, ago } from '../store.mjs';
|
|
14
|
+
import { List } from '../components/list.mjs';
|
|
15
|
+
import { take } from '../../paging.mjs';
|
|
16
|
+
|
|
17
|
+
export function Flows({ ctx, focused, onOpen }) {
|
|
18
|
+
const { status, data, error, elapsed, at } = useResource('flows', () =>
|
|
19
|
+
ctx.call((m) => take(m.flows.list(), 60)));
|
|
20
|
+
|
|
21
|
+
if (status === 'loading') {
|
|
22
|
+
return html`<${Box} paddingX=${1}><${Text} dimColor>Loading flows… ${elapsed > 1 ? `${elapsed}s` : ''}<//><//>`;
|
|
23
|
+
}
|
|
24
|
+
if (error && !data) {
|
|
25
|
+
return html`<${Box} paddingX=${1}><${Text} color="red">${error.message ?? String(error)}<//><//>`;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const rows = data?.rows ?? [];
|
|
29
|
+
return html`
|
|
30
|
+
<${Box} flexDirection="column" paddingX=${1}>
|
|
31
|
+
<${Box}>
|
|
32
|
+
<${Text} bold>Flows<//>
|
|
33
|
+
<${Text} dimColor> ${rows.length}${data?.more ? '+' : ''} · ${ago(at)}<//>
|
|
34
|
+
<//>
|
|
35
|
+
${rows.length === 0
|
|
36
|
+
? html`<${Text} dimColor>No flows yet.<//>`
|
|
37
|
+
: html`<${List}
|
|
38
|
+
items=${rows.map((f) => ({ key: f.id ?? f.slug, left: f.slug ?? '', label: f.name ?? '' }))}
|
|
39
|
+
focused=${focused}
|
|
40
|
+
height=${12}
|
|
41
|
+
onSelect=${(i) => onOpen(rows[i])}
|
|
42
|
+
/>`}
|
|
43
|
+
<//>`;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export function Flow({ ctx, flow }) {
|
|
47
|
+
const { status, data, error, elapsed } = useResource(`flow:${flow?.slug}`, () =>
|
|
48
|
+
ctx.call((m) => m.flows.get(flow.slug)));
|
|
49
|
+
|
|
50
|
+
if (status === 'loading') {
|
|
51
|
+
return html`<${Box} paddingX=${1}><${Text} dimColor>Opening ${flow?.name ?? ''}… ${elapsed > 1 ? `${elapsed}s` : ''}<//><//>`;
|
|
52
|
+
}
|
|
53
|
+
if (error) {
|
|
54
|
+
return html`<${Box} flexDirection="column" paddingX=${1}>
|
|
55
|
+
<${Text} color="red">${error.message ?? String(error)}<//>
|
|
56
|
+
<${Text} dimColor>esc to go back<//>
|
|
57
|
+
<//>`;
|
|
58
|
+
}
|
|
59
|
+
const f = data ?? flow ?? {};
|
|
60
|
+
|
|
61
|
+
return html`
|
|
62
|
+
<${Box} flexDirection="column" paddingX=${1}>
|
|
63
|
+
<${Text} bold>${f.name ?? f.slug ?? '(unnamed)'}<//>
|
|
64
|
+
<${Box} flexDirection="column" marginTop=${1}>
|
|
65
|
+
<${Box}><${Text} dimColor>slug <//><${Text} color="cyan">${f.slug ?? ''}<//><//>
|
|
66
|
+
<${Box}><${Text} dimColor>published <//>
|
|
67
|
+
<${Text} color=${f.publishedVersionId ? 'green' : undefined}>${f.publishedVersionId ? 'yes' : 'draft'}<//><//>
|
|
68
|
+
<${Box}><${Text} dimColor>updated <//><${Text}>${String(f.updatedAt ?? '').slice(0, 10)}<//><//>
|
|
69
|
+
<//>
|
|
70
|
+
${f.description
|
|
71
|
+
? html`<${Box} marginTop=${1}><${Text} dimColor>${f.description}<//><//>`
|
|
72
|
+
: null}
|
|
73
|
+
<${Box} marginTop=${1}>
|
|
74
|
+
<${Text} dimColor>Steps and runs are not on the public API — open the flow in the app to walk it.<//>
|
|
75
|
+
<//>
|
|
76
|
+
<//>`;
|
|
77
|
+
}
|