@alisio/alisio-code 0.1.0-alpha.15 → 0.1.0-alpha.16

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/dist/tui/state.js CHANGED
@@ -508,19 +508,40 @@ export function addItem(state, item) {
508
508
  const withModel = (stats, model) => typeof model === "string" && model && !stats.models.includes(model)
509
509
  ? { ...stats, models: [...stats.models, model] }
510
510
  : stats;
511
- function appendAssistant(state, field, delta) {
511
+ /**
512
+ * Streaming assistant items stamp the thinking interval: the first reasoning delta records
513
+ * `reasoningStartedAt` and every later one refreshes `reasoningDoneAt`, so a completed thought
514
+ * section can show a `Thought · 2.9s` duration. The stamp stops once text arrives (the model
515
+ * moved on to the answer), so the duration measures the visible reasoning span, never the turn.
516
+ */
517
+ const withReasoningStamp = (item, field, at) => {
518
+ if (field !== "reasoning" || item.text)
519
+ return item;
520
+ return { ...item, reasoningStartedAt: item.reasoningStartedAt ?? at, reasoningDoneAt: at };
521
+ };
522
+ function appendAssistant(state, field, delta, at = Date.now()) {
512
523
  const last = state.items.at(-1);
513
524
  if (last?.kind === "assistant" && !last.done)
514
525
  return {
515
526
  ...state,
516
- items: [...state.items.slice(0, -1), { ...last, [field]: last[field] + delta }],
527
+ items: [
528
+ ...state.items.slice(0, -1),
529
+ withReasoningStamp({ ...last, [field]: last[field] + delta }, field, at),
530
+ ],
517
531
  };
518
- return addItem(state, {
532
+ return addItem(state, withReasoningStamp({
519
533
  kind: "assistant",
520
534
  text: field === "text" ? delta : "",
521
535
  reasoning: field === "reasoning" ? delta : "",
522
536
  done: false,
523
- });
537
+ }, field, at));
538
+ }
539
+ /** Approximated thinking interval of a completed reasoning section, when the events carried timestamps. */
540
+ export function reasoningDurationMs(item) {
541
+ const { reasoningStartedAt, reasoningDoneAt } = item;
542
+ if (reasoningStartedAt === undefined || reasoningDoneAt === undefined)
543
+ return undefined;
544
+ return Math.max(0, reasoningDoneAt - reasoningStartedAt);
524
545
  }
525
546
  function updateTool(state, id, update) {
526
547
  const index = state.items.findLastIndex((i) => i.kind === "tool" && i.id === id);
@@ -543,9 +564,9 @@ export function reduceEvent(state, event) {
543
564
  stats: withModel({ ...state.stats, runs: state.stats.runs + 1 }, d.model),
544
565
  };
545
566
  case "text_delta":
546
- return appendAssistant(state, "text", String(d.delta ?? ""));
567
+ return appendAssistant(state, "text", String(d.delta ?? ""), at);
547
568
  case "reasoning_delta":
548
- return appendAssistant(state, "reasoning", String(d.delta ?? ""));
569
+ return appendAssistant(state, "reasoning", String(d.delta ?? ""), at);
549
570
  case "turn_completed": {
550
571
  const usage = d.usage;
551
572
  const last = state.items.at(-1);
@@ -567,15 +588,18 @@ export function reduceEvent(state, event) {
567
588
  },
568
589
  };
569
590
  }
570
- case "tool_started":
591
+ case "tool_started": {
592
+ const name = String(d.name ?? "tool");
571
593
  return addItem(state, {
572
594
  kind: "tool",
573
595
  id: String(d.id ?? ""),
574
- name: String(d.name ?? "tool"),
596
+ name,
575
597
  args: typeof d.arguments === "string" ? d.arguments : "",
576
598
  summary: summarizeToolArgs(String(d.name ?? ""), typeof d.arguments === "string" ? d.arguments : ""),
577
599
  status: "running",
600
+ toolKind: toolKindOf(name, typeof d.effect === "string" ? d.effect : undefined),
578
601
  });
602
+ }
579
603
  case "approval_requested":
580
604
  return updateTool(state, d.id, { status: "approval" });
581
605
  case "approval_resolved":
@@ -587,6 +611,9 @@ export function reduceEvent(state, event) {
587
611
  status: d.isError ? "error" : "ok",
588
612
  ...(typeof d.durationMs === "number" ? { durationMs: d.durationMs } : {}),
589
613
  ...(typeof d.preview === "string" ? { preview: d.preview } : {}),
614
+ ...exitCodePart(d.preview),
615
+ ...(isUiBlock(d.ui) ? { ui: d.ui } : {}),
616
+ ...(isImagePart(d.image) ? { image: d.image } : {}),
590
617
  });
591
618
  return {
592
619
  ...next,
@@ -696,6 +723,391 @@ export function lastAssistantText(items) {
696
723
  }
697
724
  return undefined;
698
725
  }
726
+ /**
727
+ * True when a printable key should copy the last assistant response instead of typing: bound to
728
+ * `c`/`y` (and uppercase) on an EMPTY editor line, while no autocomplete is showing and no turn
729
+ * is running — the same "empty input" convention Enter and Ctrl+D already use. A non-empty input
730
+ * never triggers, so typing `c` or `y` mid-message is unaffected.
731
+ */
732
+ export function editorCopyKey(data, input) {
733
+ return ((data === "c" || data === "C" || data === "y" || data === "Y") &&
734
+ input.text === "" &&
735
+ !input.autocomplete &&
736
+ !input.busy);
737
+ }
738
+ /** True when an unknown event value is a `Node` of a tree UI block. */
739
+ export function isTreeNode(value) {
740
+ if (!value || typeof value !== "object" || Array.isArray(value))
741
+ return false;
742
+ const record = value;
743
+ if (typeof record.label !== "string")
744
+ return false;
745
+ if (record.meta !== undefined && typeof record.meta !== "string")
746
+ return false;
747
+ if (record.children !== undefined && !Array.isArray(record.children))
748
+ return false;
749
+ return record.children === undefined || record.children.every(isTreeNode);
750
+ }
751
+ /** True when an unknown event value is an Alisio `UiBlock` (validated defensively). */
752
+ export function isUiBlock(value) {
753
+ if (!value || typeof value !== "object" || Array.isArray(value))
754
+ return false;
755
+ const block = value;
756
+ switch (block.kind) {
757
+ case "table":
758
+ return (Array.isArray(block.columns) &&
759
+ block.columns.every((c) => typeof c === "string") &&
760
+ Array.isArray(block.rows) &&
761
+ block.rows.every((r) => Array.isArray(r) && r.every((c) => typeof c === "string")) &&
762
+ (block.caption === undefined || typeof block.caption === "string"));
763
+ case "key-value":
764
+ return (Array.isArray(block.entries) &&
765
+ block.entries.every((e) => Array.isArray(e) &&
766
+ e.length === 2 &&
767
+ typeof e[0] === "string" &&
768
+ typeof e[1] === "string") &&
769
+ (block.caption === undefined || typeof block.caption === "string"));
770
+ case "tree":
771
+ return Array.isArray(block.nodes) && block.nodes.every(isTreeNode);
772
+ case "code":
773
+ return (typeof block.code === "string" &&
774
+ (block.lang === undefined || typeof block.lang === "string") &&
775
+ (block.caption === undefined || typeof block.caption === "string"));
776
+ case "markdown":
777
+ return typeof block.text === "string";
778
+ default:
779
+ return false;
780
+ }
781
+ }
782
+ /** True when an unknown event value is a `{type:"image"}`-style part (mime + base64 data). */
783
+ export function isImagePart(value) {
784
+ if (!value || typeof value !== "object" || Array.isArray(value))
785
+ return false;
786
+ const record = value;
787
+ return typeof record.mimeType === "string" && typeof record.data === "string";
788
+ }
789
+ /** Rich (ui/image) parts of a persisted tool result, if any. */
790
+ export function richPartsOf(result) {
791
+ if (!result)
792
+ return {};
793
+ const ui = result.content.find((part) => part.type === "ui");
794
+ const image = result.content.find((part) => part.type === "image");
795
+ return {
796
+ ...(ui?.type === "ui" ? { ui: ui.block } : {}),
797
+ ...(image?.type === "image" ? { image: { mimeType: image.mimeType, data: image.data } } : {}),
798
+ };
799
+ }
800
+ // ============================================================================
801
+ // Tool presentation model — the pure extension point for transcript display.
802
+ // Everything the TUI renders beyond a plain `TranscriptItem` is a
803
+ // `TranscriptEntry`, and anything collapsible declares a `FoldCandidate` here,
804
+ // so the renderer, the keybinding and (optionally) mouse clicks stay consistent
805
+ // without importing pi-tui. Future block kinds hook in by extending these
806
+ // shapes, never by special-casing components.
807
+ // ============================================================================
808
+ /** Words keyed by their canonical upper-case spelling that stay acronyms when humanized. */
809
+ const ACRONYM_WORDS = new Set([
810
+ "MCP",
811
+ "HTTP",
812
+ "HTTPS",
813
+ "URL",
814
+ "API",
815
+ "CLI",
816
+ "JSON",
817
+ "CSV",
818
+ "XML",
819
+ "HTML",
820
+ "SQL",
821
+ "SQLITE",
822
+ "DNS",
823
+ "SSH",
824
+ "IP",
825
+ "ID",
826
+ "DB",
827
+ "UI",
828
+ "OS",
829
+ "IDE",
830
+ "TUI",
831
+ "PNG",
832
+ "SVG",
833
+ "PDF",
834
+ "YAML",
835
+ "TOML",
836
+ "2FA",
837
+ "V2",
838
+ ]);
839
+ /**
840
+ * `read_file` → `Read File`, `search_text` → `Search Text`, `mcp_devforge_time_diff` →
841
+ * `MCP · Devforge Time Diff`. Splits on `_`/`-`/`.`/spaces, keeps known acronyms (MCP, HTTP…)
842
+ * and bare numeric suffixes (`read_file_2` → `Read File 2`). The machine name stays available
843
+ * dimmed in grouped detail rows and in the `/tools` and `/stats` reports.
844
+ */
845
+ export function humanizeToolName(name) {
846
+ const trimmed = name.trim();
847
+ let rest = trimmed;
848
+ let prefix = "";
849
+ const mcp = /^mcp_/i.exec(rest);
850
+ if (mcp) {
851
+ prefix = "MCP · ";
852
+ rest = rest.slice(mcp[0].length);
853
+ }
854
+ const words = rest
855
+ .split(/[_\-. /]+/)
856
+ .filter(Boolean)
857
+ .map((word) => {
858
+ const upper = word.toUpperCase();
859
+ if (ACRONYM_WORDS.has(upper))
860
+ return upper;
861
+ if (/^\d+$/.test(upper))
862
+ return upper;
863
+ return `${word[0]?.toUpperCase() ?? ""}${word.slice(1).toLowerCase()}`;
864
+ });
865
+ return words.length ? `${prefix}${words.join(" ")}` : trimmed;
866
+ }
867
+ /** Verb and count noun used by grouped batch rows per tool kind. */
868
+ export const TOOL_KIND_VERB = {
869
+ read: "Explored",
870
+ write: "Edited",
871
+ process: "Ran",
872
+ mcp: "Queried",
873
+ internal: "Ran",
874
+ other: "Called",
875
+ };
876
+ export const TOOL_KIND_WORD = {
877
+ read: "reads",
878
+ write: "files",
879
+ process: "commands",
880
+ mcp: "calls",
881
+ internal: "tasks",
882
+ other: "calls",
883
+ };
884
+ /**
885
+ * Capability kind of a tool call. Live events carry the registry `effect` (exact); replayed
886
+ * history falls back to conservative name heuristics. `mcp_*` is always the mcp kind.
887
+ */
888
+ export function toolKindOf(name, effect) {
889
+ if (name.startsWith("mcp_"))
890
+ return "mcp";
891
+ switch (effect) {
892
+ case "read":
893
+ case "write":
894
+ case "process":
895
+ case "internal":
896
+ return effect;
897
+ default:
898
+ break;
899
+ }
900
+ if (/^(read|search|list|git|context|ask|web|fetch)_/.test(name))
901
+ return "read";
902
+ if (/^(write|edit|append|delete|rename|move|mkdir|patch)_/.test(name))
903
+ return "write";
904
+ if (/^(run|shell|execute)/.test(name))
905
+ return "process";
906
+ if (name === "task" || name.startsWith("task_"))
907
+ return "internal";
908
+ return "other";
909
+ }
910
+ /** Terminal-preview caps for collapsed rows (same values the renderer always used). */
911
+ export const OUTPUT_PREVIEW_LINES_OK = 3;
912
+ export const OUTPUT_PREVIEW_LINES_ERROR = 6;
913
+ export const previewLinesFor = (status) => status === "error" ? OUTPUT_PREVIEW_LINES_ERROR : OUTPUT_PREVIEW_LINES_OK;
914
+ /** Preview rows as the renderer shows them: blank lines dropped, tabs kept for the caller to expand. */
915
+ export const previewRows = (preview) => preview.split("\n").filter((line) => line.trim());
916
+ /** True when the preview exceeds the collapsed cap (the row becomes a collapsible "output"). */
917
+ export function previewTruncated(preview, status) {
918
+ return !!preview && previewRows(preview).length > previewLinesFor(status);
919
+ }
920
+ /**
921
+ * Exit code derived safely from a text preview. `run_process`/`shell`/`search_text` return
922
+ * `{stdout, stderr, exitCode, truncated}` JSON; read-effect tools may carry an appended
923
+ * `\n<instructions>…` block, retried on the JSON prefix. Any other shape yields nothing.
924
+ */
925
+ export function exitCodeOf(preview) {
926
+ if (!preview)
927
+ return undefined;
928
+ const codeOf = (text) => {
929
+ try {
930
+ const value = JSON.parse(text);
931
+ const code = value && typeof value === "object" && !Array.isArray(value)
932
+ ? value.exitCode
933
+ : undefined;
934
+ return typeof code === "number" ? code : undefined;
935
+ }
936
+ catch {
937
+ return undefined;
938
+ }
939
+ };
940
+ const direct = codeOf(preview);
941
+ if (direct !== undefined)
942
+ return direct;
943
+ const cut = preview.indexOf("\n<instructions>");
944
+ if (cut > 0)
945
+ return codeOf(preview.slice(0, cut));
946
+ return undefined;
947
+ }
948
+ const exitCodePart = (preview) => {
949
+ if (typeof preview !== "string")
950
+ return {};
951
+ const code = exitCodeOf(preview);
952
+ return code === undefined ? {} : { exitCode: code };
953
+ };
954
+ export function toolItemView(item) {
955
+ return {
956
+ id: item.id,
957
+ name: item.name,
958
+ humanName: humanizeToolName(item.name),
959
+ kind: item.toolKind ?? toolKindOf(item.name),
960
+ summary: item.summary,
961
+ status: item.status,
962
+ ...(item.durationMs !== undefined ? { durationMs: item.durationMs } : {}),
963
+ ...(item.preview !== undefined ? { preview: item.preview } : {}),
964
+ ...(item.ui ? { ui: item.ui } : {}),
965
+ ...(item.image ? { image: item.image } : {}),
966
+ ...(item.exitCode !== undefined ? { exitCode: item.exitCode } : {}),
967
+ };
968
+ }
969
+ /**
970
+ * Consecutive tool calls of the same kind whose statuses are ALL terminal (ok/error) merge into
971
+ * one `group` entry; running/approval calls always stay singleton so their spinner and live state
972
+ * stay visible. A kind change splits the run (`read` never merges with `shell`). Order is
973
+ * preserved; every entry carries its index within `items` (fold keys for assistant items need
974
+ * it). One linear pass — called on state changes only, never per frame.
975
+ */
976
+ export function groupToolEntries(items) {
977
+ const out = [];
978
+ let run = [];
979
+ let runKind;
980
+ const flush = () => {
981
+ if (!run.length)
982
+ return;
983
+ const views = run.map((r) => toolItemView(r.item));
984
+ const first = views[0];
985
+ const groupable = views.length > 1 &&
986
+ first !== undefined &&
987
+ views.every((v) => v.kind === runKind) &&
988
+ views.every((v) => v.status === "ok" || v.status === "error");
989
+ if (groupable) {
990
+ const sameName = views.every((v) => v.name === first.name);
991
+ out.push({
992
+ entry: {
993
+ kind: "group",
994
+ group: {
995
+ id: first.id,
996
+ members: views,
997
+ kind: first.kind,
998
+ status: views.some((v) => v.status === "error") ? "error" : "ok",
999
+ totalMs: views.reduce((sum, v) => sum + (v.durationMs ?? 0), 0),
1000
+ label: sameName ? first.humanName : TOOL_KIND_VERB[first.kind],
1001
+ word: TOOL_KIND_WORD[first.kind],
1002
+ },
1003
+ },
1004
+ at: run[0]?.at ?? 0,
1005
+ });
1006
+ }
1007
+ else {
1008
+ for (const r of run)
1009
+ out.push({ entry: r.item, at: r.at });
1010
+ }
1011
+ run = [];
1012
+ runKind = undefined;
1013
+ };
1014
+ items.forEach((item, at) => {
1015
+ if (item.kind === "tool") {
1016
+ const kind = item.toolKind ?? toolKindOf(item.name);
1017
+ if (runKind === undefined) {
1018
+ runKind = kind;
1019
+ run.push({ item, at });
1020
+ }
1021
+ else if (runKind === kind) {
1022
+ run.push({ item, at });
1023
+ }
1024
+ else {
1025
+ flush();
1026
+ runKind = kind;
1027
+ run.push({ item, at });
1028
+ }
1029
+ }
1030
+ else {
1031
+ flush();
1032
+ out.push({ entry: item, at });
1033
+ }
1034
+ });
1035
+ flush();
1036
+ return out;
1037
+ }
1038
+ /** Content identity of a group (fold/diff purposes): member ids, statuses and durations. */
1039
+ export function groupIdentity(group) {
1040
+ return `${group.id}|${group.members.length}|${group.members
1041
+ .map((m) => `${m.id}:${m.status}:${m.durationMs ?? ""}`)
1042
+ .join(",")}`;
1043
+ }
1044
+ /** Cap for the reasoning text when expanded; longer sections show a truncation note. */
1045
+ export const REASONING_EXPAND_MAX_LINES = 40;
1046
+ /**
1047
+ * The fold contract of one transcript entry: nothing (not collapsible), or a candidate with its
1048
+ * default state. `at` is the entry's index within the ORIGINAL items array (assistant fold keys
1049
+ * are stable because transcript items are append-only).
1050
+ */
1051
+ export function foldCandidateOf(entry, at) {
1052
+ if (entry.kind === "assistant") {
1053
+ const reasoning = entry.reasoning.trim();
1054
+ if (!reasoning || !(entry.done || entry.text))
1055
+ return undefined;
1056
+ return { key: `a:${at}`, kind: "reasoning", defaultExpanded: false };
1057
+ }
1058
+ if (entry.kind === "group")
1059
+ return { key: `g:${entry.group.id}`, kind: "group", defaultExpanded: false };
1060
+ if (entry.kind === "tool") {
1061
+ if (entry.status === "running" || entry.status === "approval")
1062
+ return undefined;
1063
+ // Rich blocks keep their native rendering (tables/trees/images are already bounded);
1064
+ // only plain text output folds when it exceeds the collapsed cap.
1065
+ if (entry.ui || entry.image)
1066
+ return undefined;
1067
+ if (previewTruncated(entry.preview, entry.status))
1068
+ return { key: `t:${entry.id}`, kind: "output", defaultExpanded: false };
1069
+ return undefined;
1070
+ }
1071
+ return undefined;
1072
+ }
1073
+ /** Stable key of a transcript entry (used by the component sync to reuse row instances). */
1074
+ export function entryKeyOf(entry, at) {
1075
+ switch (entry.kind) {
1076
+ case "assistant":
1077
+ return `a:${at}`;
1078
+ case "group":
1079
+ return `g:${entry.group.id}`;
1080
+ case "tool":
1081
+ return `t:${entry.id}`;
1082
+ case "user":
1083
+ return `u:${at}`;
1084
+ case "notice":
1085
+ return `n:${at}`;
1086
+ case "info":
1087
+ return `i:${at}`;
1088
+ case "error":
1089
+ return `e:${at}`;
1090
+ }
1091
+ }
1092
+ /** Every fold candidate of the transcript in display order; the keybinding targets the last one. */
1093
+ export function foldCandidates(items) {
1094
+ const candidates = [];
1095
+ for (const { entry, at } of groupToolEntries(items)) {
1096
+ const candidate = foldCandidateOf(entry, at);
1097
+ if (candidate)
1098
+ candidates.push(candidate);
1099
+ }
1100
+ return candidates;
1101
+ }
1102
+ /**
1103
+ * True when a printable key should toggle the nearest collapsible transcript row instead of
1104
+ * typing: bound to `x`/`X` on an EMPTY editor line (no autocomplete), the same "empty input"
1105
+ * convention Enter, Ctrl+D and `c`/`y` already use. Unlike copy, it also works while a turn runs
1106
+ * (the nearest candidate may still exist mid-stream). A non-empty input never triggers.
1107
+ */
1108
+ export function foldToggleKey(data, input) {
1109
+ return (data === "x" || data === "X") && input.text === "" && !input.autocomplete;
1110
+ }
699
1111
  /** Rebuilds transcript items from persisted history (used by /resume). */
700
1112
  export function itemsFromHistory(messages) {
701
1113
  const items = [];
@@ -711,6 +1123,11 @@ export function itemsFromHistory(messages) {
711
1123
  items.push({ kind: "assistant", text: m.text, reasoning: "", done: true });
712
1124
  for (const c of m.calls) {
713
1125
  const r = results.get(c.id);
1126
+ const preview = r?.result.content
1127
+ .filter((p) => p.type === "text")
1128
+ .map((x) => x.text)
1129
+ .join("\n")
1130
+ .slice(0, 2_000);
714
1131
  items.push({
715
1132
  kind: "tool",
716
1133
  id: c.id,
@@ -718,10 +1135,10 @@ export function itemsFromHistory(messages) {
718
1135
  args: c.arguments,
719
1136
  summary: summarizeToolArgs(c.name, c.arguments),
720
1137
  status: r?.result.isError ? "error" : "ok",
721
- preview: r?.result.content
722
- .map((x) => x.text)
723
- .join("\n")
724
- .slice(0, 2_000),
1138
+ preview,
1139
+ toolKind: toolKindOf(c.name),
1140
+ ...exitCodePart(preview),
1141
+ ...richPartsOf(r?.result),
725
1142
  });
726
1143
  }
727
1144
  }
@@ -1,4 +1,4 @@
1
- import type { EditorTheme, ImageTheme, MarkdownTheme, SelectListTheme } from "@earendil-works/pi-tui";
1
+ import type { DefaultTextStyle, EditorTheme, ImageTheme, MarkdownTheme, SelectListTheme } from "@earendil-works/pi-tui";
2
2
  import type { Level } from "./state.ts";
3
3
  export declare const style: {
4
4
  bold: (text: string) => string;
@@ -17,6 +17,47 @@ export declare const style: {
17
17
  userBg: (text: string) => string;
18
18
  };
19
19
  export declare const levelColor: (level: Level) => (text: string) => string;
20
+ /**
21
+ * Dependency-free, line-based syntax highlighting for code blocks in responses.
22
+ *
23
+ * The tokenizer is deliberately approximate: it walks each line once and colors keywords, strings,
24
+ * comments, numbers, function calls and property keys for the common response languages. It never
25
+ * tries to be a real parser, never throws on malformed input, and strips ANSI/control characters
26
+ * from the source first (a model response must never be able to inject terminal control codes).
27
+ */
28
+ export type TokenStyle = "keyword" | "string" | "comment" | "number" | "function" | "property" | "plain";
29
+ export interface CodeToken {
30
+ text: string;
31
+ style: TokenStyle;
32
+ }
33
+ /** Strips ANSI escapes, OSC sequences and control characters; normalizes tabs. */
34
+ export declare function sanitizeCode(code: string): string;
35
+ export interface ScanState {
36
+ /** Closing delimiter of a block comment started on a previous line. */
37
+ inComment?: string;
38
+ /** Delimiter of a multi-line string started on a previous line. */
39
+ inString?: string;
40
+ }
41
+ /** Tokenizes code into styled segments, maintaining multi-line string/comment state. */
42
+ export declare function tokenizeCode(code: string, lang?: string): CodeToken[][];
43
+ /**
44
+ * Per-line highlighting for markdown code blocks (pi-tui `MarkdownTheme.highlightCode`).
45
+ * Returns one styled (or plain, under NO_COLOR) line per source line; never emits ANSI for
46
+ * bytes that came from the input.
47
+ */
48
+ export declare function highlightCode(code: string, lang?: string): string[];
49
+ /**
50
+ * Normalizes table separator rows so models' sloppy ASCII art still renders as aligned tables:
51
+ * em/en/box dashes become plain `-` runs (marked's table tokenizer only accepts `-`), while
52
+ * alignment colons are preserved. Anything that is not an obvious separator row, including
53
+ * fenced or indented code, is left untouched. A bare `---` (horizontal rule / setext) has no
54
+ * `|` and is never touched.
55
+ */
56
+ export declare function normalizeTableSeparators(markdown: string): string;
57
+ /** Pre-parse hook for the Markdown component: normalizes table separators before marked parses. */
58
+ export declare const markdownTransform: (markdown: string, _availableWidth: number) => string;
59
+ /** Subtle base tone for response body text; inline styles override it. */
60
+ export declare const markdownDefaultTextStyle: DefaultTextStyle;
20
61
  export declare const markdownTheme: MarkdownTheme;
21
62
  export declare const selectListTheme: SelectListTheme;
22
63
  export declare const editorTheme: EditorTheme;