@amanm/openpaw 0.1.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.
Files changed (72) hide show
  1. package/AGENTS.md +1 -0
  2. package/README.md +144 -0
  3. package/agent/agent.ts +217 -0
  4. package/agent/context-scan.ts +81 -0
  5. package/agent/file-editor-store.ts +27 -0
  6. package/agent/index.ts +31 -0
  7. package/agent/memory-store.ts +404 -0
  8. package/agent/model.ts +14 -0
  9. package/agent/prompt-builder.ts +139 -0
  10. package/agent/prompt-context-files.ts +151 -0
  11. package/agent/sandbox-paths.ts +52 -0
  12. package/agent/session-store.ts +80 -0
  13. package/agent/skill-catalog.ts +25 -0
  14. package/agent/skills/discover.ts +100 -0
  15. package/agent/tool-stream-format.ts +126 -0
  16. package/agent/tool-yaml-like.ts +96 -0
  17. package/agent/tools/bash.ts +100 -0
  18. package/agent/tools/file-editor.ts +293 -0
  19. package/agent/tools/list-dir.ts +58 -0
  20. package/agent/tools/load-skill.ts +40 -0
  21. package/agent/tools/memory.ts +84 -0
  22. package/agent/turn-context.ts +46 -0
  23. package/agent/types.ts +37 -0
  24. package/agent/workspace-bootstrap.ts +98 -0
  25. package/bin/openpaw.cjs +177 -0
  26. package/bundled-skills/find-skills/SKILL.md +163 -0
  27. package/cli/components/chat-app.tsx +759 -0
  28. package/cli/components/onboard-ui.tsx +325 -0
  29. package/cli/components/theme.ts +16 -0
  30. package/cli/configure.tsx +0 -0
  31. package/cli/lib/chat-transcript-types.ts +11 -0
  32. package/cli/lib/markdown-render-node.ts +523 -0
  33. package/cli/lib/onboard-markdown-syntax-style.ts +55 -0
  34. package/cli/lib/ui-messages-to-chat-transcript.ts +157 -0
  35. package/cli/lib/use-auto-copy-selection.ts +38 -0
  36. package/cli/onboard.tsx +248 -0
  37. package/cli/openpaw.tsx +144 -0
  38. package/cli/reset.ts +12 -0
  39. package/cli/tui.tsx +31 -0
  40. package/config/index.ts +3 -0
  41. package/config/paths.ts +71 -0
  42. package/config/personality-copy.ts +68 -0
  43. package/config/storage.ts +80 -0
  44. package/config/types.ts +37 -0
  45. package/gateway/bootstrap.ts +25 -0
  46. package/gateway/channel-adapter.ts +8 -0
  47. package/gateway/daemon-manager.ts +191 -0
  48. package/gateway/index.ts +18 -0
  49. package/gateway/session-key.ts +13 -0
  50. package/gateway/slash-command-tokens.ts +39 -0
  51. package/gateway/start-messaging.ts +40 -0
  52. package/gateway/telegram/active-thread-store.ts +89 -0
  53. package/gateway/telegram/adapter.ts +290 -0
  54. package/gateway/telegram/assistant-markdown.ts +48 -0
  55. package/gateway/telegram/bot-commands.ts +40 -0
  56. package/gateway/telegram/chat-preferences.ts +100 -0
  57. package/gateway/telegram/constants.ts +5 -0
  58. package/gateway/telegram/index.ts +4 -0
  59. package/gateway/telegram/message-html.ts +138 -0
  60. package/gateway/telegram/message-queue.ts +19 -0
  61. package/gateway/telegram/reserved-command-filter.ts +33 -0
  62. package/gateway/telegram/session-file-discovery.ts +62 -0
  63. package/gateway/telegram/session-key.ts +13 -0
  64. package/gateway/telegram/session-label.ts +14 -0
  65. package/gateway/telegram/sessions-list-reply.ts +39 -0
  66. package/gateway/telegram/stream-delivery.ts +618 -0
  67. package/gateway/tui/constants.ts +2 -0
  68. package/gateway/tui/tui-active-thread-store.ts +103 -0
  69. package/gateway/tui/tui-session-discovery.ts +94 -0
  70. package/gateway/tui/tui-session-label.ts +22 -0
  71. package/gateway/tui/tui-sessions-list-message.ts +37 -0
  72. package/package.json +52 -0
@@ -0,0 +1,523 @@
1
+ /**
2
+ * OpenTUI {@code MarkdownRenderable} prose blocks normally use Tree-sitter on a merged
3
+ * markdown string; when highlighting fails, raw syntax appears. This module supplies a
4
+ * {@code renderNode} implementation that draws headings, paragraphs, lists, blockquotes,
5
+ * and rules from Marked tokens using {@code TextRenderable} / {@code BoxRenderable},
6
+ * while delegating fenced {@code code} and {@code table} to the default renderer.
7
+ *
8
+ * Maintainer note: one file by design (single integration surface). Inter-block vertical
9
+ * gap for prose is intentionally 0 here to match OpenTUI {@code getInterBlockMargin} for
10
+ * non-{@code code}/non-{@code table}/non-{@code blockquote} tokens; spacing after tables
11
+ * and code still comes from {@code defaultRender()}.
12
+ */
13
+ import type { Renderable, RenderContext, SyntaxStyle } from "@opentui/core";
14
+ import {
15
+ BoxRenderable,
16
+ CodeRenderable,
17
+ RGBA,
18
+ StyledText,
19
+ TextRenderable,
20
+ createTextAttributes,
21
+ } from "@opentui/core";
22
+ import type { Token, Tokens } from "marked";
23
+
24
+ /**
25
+ * Same fields as OpenTUI {@code MarkdownRenderable}'s {@code RenderNodeContext} (see
26
+ * {@code node_modules/@opentui/core/renderables/Markdown.d.ts}); duplicated here because
27
+ * {@code @opentui/core} does not export that type from the package root.
28
+ */
29
+ export type OpenpawRenderNodeContext = {
30
+ syntaxStyle: SyntaxStyle;
31
+ conceal: boolean;
32
+ concealCode: boolean;
33
+ /** Opaque here: {@code @opentui/core} does not export {@code TreeSitterClient} from the package root. */
34
+ treeSitterClient?: unknown;
35
+ defaultRender: () => Renderable | null;
36
+ };
37
+
38
+ /** Minimal {@code TextChunk} shape for {@link StyledText} (see OpenTUI text-buffer). */
39
+ type MdTextChunk = {
40
+ __isChunk: true;
41
+ text: string;
42
+ fg?: RGBA;
43
+ bg?: RGBA;
44
+ attributes?: number;
45
+ link?: { url: string };
46
+ };
47
+
48
+ /** Terminal palette aligned with {@link ONBOARD}; hex strings are converted to {@link RGBA}. */
49
+ export type OpenpawMarkdownPalette = {
50
+ accent: string;
51
+ text: string;
52
+ muted: string;
53
+ hint: string;
54
+ success: string;
55
+ code: string;
56
+ linkUrl: string;
57
+ };
58
+
59
+ /** Builds one styled fragment for {@link StyledText}. */
60
+ function makeChunk(
61
+ text: string,
62
+ fg?: RGBA,
63
+ attributes?: number,
64
+ link?: { url: string },
65
+ ): MdTextChunk {
66
+ return { __isChunk: true, text, fg, attributes, link };
67
+ }
68
+
69
+ type InlineStyle = {
70
+ fg: RGBA;
71
+ bold?: boolean;
72
+ italic?: boolean;
73
+ strikethrough?: boolean;
74
+ };
75
+
76
+ /** Maps bold/italic/strike flags to OpenTUI text attribute bits. */
77
+ function attrsFromStyle(s: InlineStyle): number {
78
+ return createTextAttributes({
79
+ bold: s.bold,
80
+ italic: s.italic,
81
+ strikethrough: s.strikethrough,
82
+ });
83
+ }
84
+
85
+ /**
86
+ * Converts Marked inline {@code tokens} into styled text chunks (conceal mirrors OpenTUI markdown).
87
+ */
88
+ function inlineTokensToChunks(
89
+ tokens: Token[] | undefined,
90
+ palette: OpenpawMarkdownPalette,
91
+ conceal: boolean,
92
+ style: InlineStyle,
93
+ chunks: MdTextChunk[],
94
+ ): void {
95
+ if (!tokens?.length) {
96
+ return;
97
+ }
98
+ const accent = RGBA.fromHex(palette.accent);
99
+ const muted = RGBA.fromHex(palette.muted);
100
+ const codeFg = RGBA.fromHex(palette.code);
101
+ const linkUrlFg = RGBA.fromHex(palette.linkUrl);
102
+ const underline = createTextAttributes({ underline: true });
103
+
104
+ for (const tok of tokens) {
105
+ switch (tok.type) {
106
+ case "text":
107
+ chunks.push(
108
+ makeChunk(tok.text, style.fg, attrsFromStyle(style)),
109
+ );
110
+ break;
111
+ case "escape":
112
+ chunks.push(makeChunk(tok.text, style.fg, attrsFromStyle(style)));
113
+ break;
114
+ case "strong":
115
+ inlineTokensToChunks(tok.tokens, palette, conceal, { ...style, bold: true }, chunks);
116
+ break;
117
+ case "em":
118
+ inlineTokensToChunks(tok.tokens, palette, conceal, { ...style, italic: true }, chunks);
119
+ break;
120
+ case "del":
121
+ inlineTokensToChunks(tok.tokens, palette, conceal, { ...style, strikethrough: true }, chunks);
122
+ break;
123
+ case "codespan":
124
+ chunks.push(
125
+ makeChunk(tok.text, codeFg, createTextAttributes({})),
126
+ );
127
+ break;
128
+ case "br":
129
+ chunks.push(makeChunk("\n", style.fg, attrsFromStyle(style)));
130
+ break;
131
+ case "link": {
132
+ const linkTok = tok as Tokens.Link;
133
+ if (conceal) {
134
+ inlineTokensToChunks(linkTok.tokens, palette, conceal, { ...style, fg: accent }, chunks);
135
+ chunks.push(makeChunk(" (", muted, attrsFromStyle({ ...style, fg: muted })));
136
+ chunks.push(
137
+ makeChunk(linkTok.href, linkUrlFg, underline, { url: linkTok.href }),
138
+ );
139
+ chunks.push(makeChunk(")", muted, attrsFromStyle({ ...style, fg: muted })));
140
+ } else {
141
+ chunks.push(makeChunk("[", muted, attrsFromStyle({ ...style, fg: muted })));
142
+ inlineTokensToChunks(linkTok.tokens, palette, conceal, { ...style, fg: accent }, chunks);
143
+ chunks.push(makeChunk("](", muted, attrsFromStyle({ ...style, fg: muted })));
144
+ chunks.push(
145
+ makeChunk(linkTok.href, linkUrlFg, underline, { url: linkTok.href }),
146
+ );
147
+ chunks.push(makeChunk(")", muted, attrsFromStyle({ ...style, fg: muted })));
148
+ }
149
+ break;
150
+ }
151
+ case "image": {
152
+ const img = tok as Tokens.Image;
153
+ const label = img.text || "image";
154
+ if (conceal) {
155
+ chunks.push(
156
+ makeChunk(label, accent, underline, { url: img.href }),
157
+ );
158
+ } else {
159
+ chunks.push(makeChunk("![", muted, attrsFromStyle({ ...style, fg: muted })));
160
+ chunks.push(makeChunk(label, style.fg, attrsFromStyle(style)));
161
+ chunks.push(makeChunk("](", muted, attrsFromStyle({ ...style, fg: muted })));
162
+ chunks.push(
163
+ makeChunk(img.href, linkUrlFg, underline, { url: img.href }),
164
+ );
165
+ chunks.push(makeChunk(")", muted, attrsFromStyle({ ...style, fg: muted })));
166
+ }
167
+ break;
168
+ }
169
+ default:
170
+ if ("tokens" in tok && Array.isArray(tok.tokens)) {
171
+ inlineTokensToChunks(tok.tokens as Token[], palette, conceal, style, chunks);
172
+ } else if ("text" in tok && typeof (tok as Tokens.Text).text === "string") {
173
+ chunks.push(
174
+ makeChunk((tok as Tokens.Text).text, style.fg, attrsFromStyle(style)),
175
+ );
176
+ }
177
+ break;
178
+ }
179
+ }
180
+ }
181
+
182
+ /** Inline Marked tokens as {@link StyledText} with the given base foreground color. */
183
+ function styledParagraphContent(
184
+ tokens: Token[] | undefined,
185
+ palette: OpenpawMarkdownPalette,
186
+ conceal: boolean,
187
+ baseFg: RGBA,
188
+ ): StyledText {
189
+ const chunks: MdTextChunk[] = [];
190
+ inlineTokensToChunks(tokens, palette, conceal, { fg: baseFg }, chunks);
191
+ if (chunks.length === 0) {
192
+ return new StyledText([makeChunk(" ", baseFg)]);
193
+ }
194
+ return new StyledText(chunks);
195
+ }
196
+
197
+ /** Normalizes fenced-block language tags for {@link CodeRenderable} filetype. */
198
+ function mapCodeLang(lang?: string): string {
199
+ const l = (lang ?? "").trim().toLowerCase();
200
+ if (!l) {
201
+ return "text";
202
+ }
203
+ return l;
204
+ }
205
+
206
+ /**
207
+ * Marked sometimes emits a {@code text} block with only {@code .text} and no inline
208
+ * {@code .tokens}; use the plain string. Otherwise render inline tokens as styled text.
209
+ */
210
+ function textBlockContent(
211
+ tok: Tokens.Text,
212
+ palette: OpenpawMarkdownPalette,
213
+ conceal: boolean,
214
+ ): string | StyledText {
215
+ const styled = styledParagraphContent(
216
+ tok.tokens ?? [],
217
+ palette,
218
+ conceal,
219
+ RGBA.fromHex(palette.text),
220
+ );
221
+ return tok.text.length > 0 && (!tok.tokens || tok.tokens.length === 0) ? tok.text : styled;
222
+ }
223
+
224
+ /**
225
+ * Builds the {@code renderNode} callback for {@code <markdown>}; keeps Tree-sitter for
226
+ * fenced code blocks and tables only.
227
+ */
228
+ export function createOpenpawMarkdownRenderNode(palette: OpenpawMarkdownPalette) {
229
+ let ctxCache: RenderContext | null = null;
230
+ let idSeq = 0;
231
+ const nextId = (kind: string) => `openpaw-md-${kind}-${++idSeq}`;
232
+
233
+ /** Obtains {@link RenderContext} once via a throwaway default code renderable. */
234
+ function resolveCtx(context: OpenpawRenderNodeContext): RenderContext {
235
+ if (ctxCache) {
236
+ return ctxCache;
237
+ }
238
+ const probe = context.defaultRender();
239
+ if (!probe) {
240
+ throw new Error("openpaw markdown: defaultRender() returned null (cannot obtain RenderContext)");
241
+ }
242
+ const nextCtx = probe.ctx;
243
+ probe.destroyRecursively();
244
+ ctxCache = nextCtx;
245
+ return nextCtx;
246
+ }
247
+
248
+ /** Renders a vertical stack of block tokens (e.g. inside a blockquote). */
249
+ function renderBlockChildren(
250
+ ctx: RenderContext,
251
+ context: OpenpawRenderNodeContext,
252
+ tokens: Token[],
253
+ ): Renderable {
254
+ const col = new BoxRenderable(ctx, {
255
+ id: nextId("col"),
256
+ flexDirection: "column",
257
+ gap: 0,
258
+ width: "100%",
259
+ marginBottom: 0,
260
+ });
261
+ for (const t of tokens) {
262
+ if (t.type === "space") {
263
+ continue;
264
+ }
265
+ const inner = renderBlock(ctx, context, t);
266
+ if (inner) {
267
+ col.add(inner);
268
+ }
269
+ }
270
+ return col;
271
+ }
272
+
273
+ /** Paragraph block as wrapped styled text. */
274
+ function renderParagraphBox(
275
+ ctx: RenderContext,
276
+ tok: Tokens.Paragraph,
277
+ context: OpenpawRenderNodeContext,
278
+ ): Renderable {
279
+ const base = RGBA.fromHex(palette.text);
280
+ const styled = styledParagraphContent(tok.tokens, palette, context.conceal, base);
281
+ return new TextRenderable(ctx, {
282
+ id: nextId("p"),
283
+ width: "100%",
284
+ wrapMode: "word",
285
+ marginBottom: 0,
286
+ content: styled,
287
+ });
288
+ }
289
+
290
+ /** ATX heading with depth-based color and bold body text. */
291
+ function renderHeadingBox(
292
+ ctx: RenderContext,
293
+ tok: Tokens.Heading,
294
+ context: OpenpawRenderNodeContext,
295
+ ): Renderable {
296
+ const fgHex = tok.depth <= 3 ? palette.accent : palette.muted;
297
+ const fg = RGBA.fromHex(fgHex);
298
+ const styled = styledParagraphContent(tok.tokens, palette, context.conceal, fg);
299
+ return new TextRenderable(ctx, {
300
+ id: nextId("h"),
301
+ width: "100%",
302
+ wrapMode: "word",
303
+ marginBottom: 0,
304
+ content: styled,
305
+ attributes: createTextAttributes({ bold: true }),
306
+ });
307
+ }
308
+
309
+ /** Thematic break as a bottom border strip. */
310
+ function renderHr(ctx: RenderContext): Renderable {
311
+ return new BoxRenderable(ctx, {
312
+ id: nextId("hr"),
313
+ width: "100%",
314
+ marginBottom: 0,
315
+ border: ["bottom"],
316
+ borderStyle: "single",
317
+ borderColor: RGBA.fromHex(palette.hint),
318
+ minHeight: 1,
319
+ });
320
+ }
321
+
322
+ /** Blockquote with left border and nested blocks. */
323
+ function renderBlockquoteBox(
324
+ ctx: RenderContext,
325
+ tok: Tokens.Blockquote,
326
+ context: OpenpawRenderNodeContext,
327
+ ): Renderable {
328
+ const inner = renderBlockChildren(ctx, context, tok.tokens);
329
+ const wrap = new BoxRenderable(ctx, {
330
+ id: nextId("bq"),
331
+ flexDirection: "row",
332
+ width: "100%",
333
+ marginBottom: 0,
334
+ border: ["left"],
335
+ borderStyle: "single",
336
+ borderColor: RGBA.fromHex(palette.hint),
337
+ paddingLeft: 1,
338
+ });
339
+ wrap.add(inner);
340
+ return wrap;
341
+ }
342
+
343
+ /** Ordered or unordered list as a column of items. */
344
+ function renderListBox(
345
+ ctx: RenderContext,
346
+ tok: Tokens.List,
347
+ context: OpenpawRenderNodeContext,
348
+ ): Renderable {
349
+ const col = new BoxRenderable(ctx, {
350
+ id: nextId("ul"),
351
+ flexDirection: "column",
352
+ gap: 0,
353
+ width: "100%",
354
+ marginBottom: 0,
355
+ });
356
+ let index = typeof tok.start === "number" ? tok.start : 1;
357
+ for (const item of tok.items) {
358
+ col.add(renderListItem(ctx, tok, item, context, index));
359
+ if (tok.ordered) {
360
+ index += 1;
361
+ }
362
+ }
363
+ return col;
364
+ }
365
+
366
+ /** One list row: marker column plus body blocks. */
367
+ function renderListItem(
368
+ ctx: RenderContext,
369
+ list: Tokens.List,
370
+ item: Tokens.ListItem,
371
+ context: OpenpawRenderNodeContext,
372
+ ordinal: number,
373
+ ): Renderable {
374
+ const listFg = RGBA.fromHex(palette.success);
375
+ const bullet =
376
+ item.task === true
377
+ ? item.checked
378
+ ? "[x] "
379
+ : "[ ] "
380
+ : list.ordered
381
+ ? `${ordinal}. `
382
+ : "• ";
383
+ const row = new BoxRenderable(ctx, {
384
+ id: nextId("li-row"),
385
+ flexDirection: "row",
386
+ alignItems: "flex-start",
387
+ gap: 0,
388
+ width: "100%",
389
+ });
390
+ const mark = new TextRenderable(ctx, {
391
+ id: nextId("li-mark"),
392
+ content: bullet,
393
+ fg: listFg,
394
+ wrapMode: "none",
395
+ });
396
+ const body = new BoxRenderable(ctx, {
397
+ id: nextId("li-body"),
398
+ flexDirection: "column",
399
+ flexGrow: 1,
400
+ gap: 0,
401
+ width: "100%",
402
+ });
403
+ for (const t of item.tokens) {
404
+ if (t.type === "space") {
405
+ continue;
406
+ }
407
+ const block = renderBlockInList(ctx, context, t);
408
+ if (block) {
409
+ body.add(block);
410
+ }
411
+ }
412
+ row.add(mark);
413
+ row.add(body);
414
+ return row;
415
+ }
416
+
417
+ /** Block token inside a list item (paragraph, nested list, code, etc.). */
418
+ function renderBlockInList(ctx: RenderContext, context: OpenpawRenderNodeContext, token: Token): Renderable | null {
419
+ if (token.type === "paragraph") {
420
+ return renderParagraphBox(ctx, token as Tokens.Paragraph, context);
421
+ }
422
+ if (token.type === "list") {
423
+ return renderListBox(ctx, token as Tokens.List, context);
424
+ }
425
+ if (token.type === "blockquote") {
426
+ return renderBlockquoteBox(ctx, token as Tokens.Blockquote, context);
427
+ }
428
+ if (token.type === "code") {
429
+ const codeTok = token as Tokens.Code;
430
+ return new CodeRenderable(ctx, {
431
+ id: nextId("code-nested"),
432
+ content: codeTok.text,
433
+ filetype: mapCodeLang(codeTok.lang),
434
+ syntaxStyle: context.syntaxStyle,
435
+ width: "100%",
436
+ marginBottom: 0,
437
+ conceal: context.concealCode,
438
+ streaming: false,
439
+ });
440
+ }
441
+ if (token.type === "heading") {
442
+ return renderHeadingBox(ctx, token as Tokens.Heading, context);
443
+ }
444
+ if (token.type === "text") {
445
+ const t = token as Tokens.Text;
446
+ return new TextRenderable(ctx, {
447
+ id: nextId("li-fallback"),
448
+ width: "100%",
449
+ wrapMode: "word",
450
+ content: textBlockContent(t, palette, context.conceal),
451
+ });
452
+ }
453
+ if (token.type === "html") {
454
+ const htmlTok = token as Tokens.HTML;
455
+ return new TextRenderable(ctx, {
456
+ id: nextId("li-html"),
457
+ width: "100%",
458
+ wrapMode: "word",
459
+ content: htmlTok.text,
460
+ fg: RGBA.fromHex(palette.muted),
461
+ });
462
+ }
463
+ return renderBlock(ctx, context, token);
464
+ }
465
+
466
+ /** Standalone {@code text} token at document level. */
467
+ function renderTopLevelText(
468
+ ctx: RenderContext,
469
+ tok: Tokens.Text,
470
+ context: OpenpawRenderNodeContext,
471
+ ): Renderable {
472
+ return new TextRenderable(ctx, {
473
+ id: nextId("text"),
474
+ width: "100%",
475
+ wrapMode: "word",
476
+ marginBottom: 0,
477
+ content: textBlockContent(tok, palette, context.conceal),
478
+ });
479
+ }
480
+
481
+ /** Top-level block types handled without Tree-sitter markdown. */
482
+ function renderBlock(
483
+ ctx: RenderContext,
484
+ context: OpenpawRenderNodeContext,
485
+ token: Token,
486
+ ): Renderable | null {
487
+ switch (token.type) {
488
+ case "paragraph":
489
+ return renderParagraphBox(ctx, token as Tokens.Paragraph, context);
490
+ case "heading":
491
+ return renderHeadingBox(ctx, token as Tokens.Heading, context);
492
+ case "blockquote":
493
+ return renderBlockquoteBox(ctx, token as Tokens.Blockquote, context);
494
+ case "list":
495
+ return renderListBox(ctx, token as Tokens.List, context);
496
+ case "hr":
497
+ return renderHr(ctx);
498
+ case "text":
499
+ return renderTopLevelText(ctx, token as Tokens.Text, context);
500
+ default:
501
+ return null;
502
+ }
503
+ }
504
+
505
+ /**
506
+ * OpenTUI {@code renderNode} callback: custom prose, default code/tables.
507
+ * Prose uses {@code marginBottom: 0} on blocks to match OpenTUI {@code getInterBlockMargin} for non-separate tokens.
508
+ */
509
+ return function openpawMarkdownRenderNode(
510
+ token: Token,
511
+ context: OpenpawRenderNodeContext,
512
+ ): Renderable | undefined | null {
513
+ if (token.type === "code" || token.type === "table") {
514
+ return context.defaultRender();
515
+ }
516
+ const ctx = resolveCtx(context);
517
+ const rendered = renderBlock(ctx, context, token);
518
+ if (rendered) {
519
+ return rendered;
520
+ }
521
+ return context.defaultRender();
522
+ };
523
+ }
@@ -0,0 +1,55 @@
1
+ /**
2
+ * Builds a {@link SyntaxStyle} for OpenTUI {@code <markdown>} using the same
3
+ * palette as {@link ONBOARD} (Tokyo Night–aligned CLI theme).
4
+ */
5
+ import { RGBA, SyntaxStyle } from "@opentui/core";
6
+ import { ONBOARD } from "../components/theme";
7
+
8
+ /**
9
+ * Creates syntax + markup styles for markdown rendering (headings, lists, code, etc.).
10
+ */
11
+ export function createOnboardMarkdownSyntaxStyle(): SyntaxStyle {
12
+ const text = RGBA.fromHex(ONBOARD.text);
13
+ const accent = RGBA.fromHex(ONBOARD.accent);
14
+ const muted = RGBA.fromHex(ONBOARD.muted);
15
+ const success = RGBA.fromHex(ONBOARD.success);
16
+ const error = RGBA.fromHex(ONBOARD.error);
17
+
18
+ return SyntaxStyle.fromStyles({
19
+ default: { fg: text },
20
+ /** Table header row styling inside {@code MarkdownRenderable} (uses this key, not h1–h6). */
21
+ "markup.heading": { fg: accent, bold: true },
22
+ "markup.heading.1": { fg: accent, bold: true },
23
+ "markup.heading.2": { fg: accent, bold: true },
24
+ "markup.heading.3": { fg: accent, bold: true },
25
+ "markup.heading.4": { fg: muted, bold: true },
26
+ "markup.heading.5": { fg: muted, bold: true },
27
+ "markup.heading.6": { fg: muted, bold: true },
28
+ "markup.list": { fg: success },
29
+ "markup.list.checked": { fg: success },
30
+ "markup.list.unchecked": { fg: muted },
31
+ "markup.quote": { fg: muted, italic: true },
32
+ "markup.raw": { fg: RGBA.fromHex("#89ddff") },
33
+ "markup.raw.block": { fg: RGBA.fromHex("#89ddff") },
34
+ /** Tree-sitter markdown_inline uses {@code markup.strong}; marked inline uses the same in OpenTUI. */
35
+ "markup.strong": { fg: text, bold: true },
36
+ "markup.bold": { fg: text, bold: true },
37
+ "markup.italic": { fg: text, italic: true },
38
+ "markup.strikethrough": { fg: muted, dim: true },
39
+ "markup.link": { fg: accent },
40
+ "markup.link.label": { fg: accent, underline: true },
41
+ "markup.link.url": { fg: RGBA.fromHex("#7dcfff"), underline: true },
42
+ "punctuation.special": { fg: muted },
43
+ keyword: { fg: RGBA.fromHex("#bb9af7"), bold: true },
44
+ string: { fg: RGBA.fromHex("#9ece6a") },
45
+ comment: { fg: muted, italic: true },
46
+ function: { fg: RGBA.fromHex("#7dcfff") },
47
+ number: { fg: RGBA.fromHex("#e0af68") },
48
+ operator: { fg: RGBA.fromHex("#bb9af7") },
49
+ type: { fg: RGBA.fromHex("#2ac3de") },
50
+ variable: { fg: text },
51
+ property: { fg: RGBA.fromHex("#73daca") },
52
+ punctuation: { fg: muted },
53
+ regexp: { fg: error },
54
+ });
55
+ }