@buoy-gg/console 4.0.2

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 (151) hide show
  1. package/LICENSE +58 -0
  2. package/README.md +55 -0
  3. package/lib/commonjs/components/ConsoleIcon.js +47 -0
  4. package/lib/commonjs/components/ConsoleIcon.js.map +1 -0
  5. package/lib/commonjs/components/ConsoleLogList.js +145 -0
  6. package/lib/commonjs/components/ConsoleLogList.js.map +1 -0
  7. package/lib/commonjs/components/ConsoleModal.js +48 -0
  8. package/lib/commonjs/components/ConsoleModal.js.map +1 -0
  9. package/lib/commonjs/components/ConsoleRoot.js +28 -0
  10. package/lib/commonjs/components/ConsoleRoot.js.map +1 -0
  11. package/lib/commonjs/devtools/ConsoleBody.js +235 -0
  12. package/lib/commonjs/devtools/ConsoleBody.js.map +1 -0
  13. package/lib/commonjs/devtools/ConsoleHeader.js +146 -0
  14. package/lib/commonjs/devtools/ConsoleHeader.js.map +1 -0
  15. package/lib/commonjs/devtools/ConsoleMessageRow.js +338 -0
  16. package/lib/commonjs/devtools/ConsoleMessageRow.js.map +1 -0
  17. package/lib/commonjs/devtools/ConsoleValue.js +254 -0
  18. package/lib/commonjs/devtools/ConsoleValue.js.map +1 -0
  19. package/lib/commonjs/devtools/ConsoleView.js +51 -0
  20. package/lib/commonjs/devtools/ConsoleView.js.map +1 -0
  21. package/lib/commonjs/devtools/consoleFilter.js +125 -0
  22. package/lib/commonjs/devtools/consoleFilter.js.map +1 -0
  23. package/lib/commonjs/devtools/consoleFormat.js +313 -0
  24. package/lib/commonjs/devtools/consoleFormat.js.map +1 -0
  25. package/lib/commonjs/devtools/origin.js +191 -0
  26. package/lib/commonjs/devtools/origin.js.map +1 -0
  27. package/lib/commonjs/devtools/theme.js +143 -0
  28. package/lib/commonjs/devtools/theme.js.map +1 -0
  29. package/lib/commonjs/devtools/useConsoleController.js +82 -0
  30. package/lib/commonjs/devtools/useConsoleController.js.map +1 -0
  31. package/lib/commonjs/devtools/useConsoleMessages.js +241 -0
  32. package/lib/commonjs/devtools/useConsoleMessages.js.map +1 -0
  33. package/lib/commonjs/hooks/useConsoleLogs.js +23 -0
  34. package/lib/commonjs/hooks/useConsoleLogs.js.map +1 -0
  35. package/lib/commonjs/index.js +175 -0
  36. package/lib/commonjs/index.js.map +1 -0
  37. package/lib/commonjs/package.json +1 -0
  38. package/lib/commonjs/preset.js +45 -0
  39. package/lib/commonjs/preset.js.map +1 -0
  40. package/lib/commonjs/store/consoleLogStore.js +298 -0
  41. package/lib/commonjs/store/consoleLogStore.js.map +1 -0
  42. package/lib/commonjs/store/sanitize.js +64 -0
  43. package/lib/commonjs/store/sanitize.js.map +1 -0
  44. package/lib/commonjs/sync/consoleSyncAdapter.js +47 -0
  45. package/lib/commonjs/sync/consoleSyncAdapter.js.map +1 -0
  46. package/lib/module/components/ConsoleIcon.js +42 -0
  47. package/lib/module/components/ConsoleIcon.js.map +1 -0
  48. package/lib/module/components/ConsoleLogList.js +140 -0
  49. package/lib/module/components/ConsoleLogList.js.map +1 -0
  50. package/lib/module/components/ConsoleModal.js +44 -0
  51. package/lib/module/components/ConsoleModal.js.map +1 -0
  52. package/lib/module/components/ConsoleRoot.js +25 -0
  53. package/lib/module/components/ConsoleRoot.js.map +1 -0
  54. package/lib/module/devtools/ConsoleBody.js +230 -0
  55. package/lib/module/devtools/ConsoleBody.js.map +1 -0
  56. package/lib/module/devtools/ConsoleHeader.js +141 -0
  57. package/lib/module/devtools/ConsoleHeader.js.map +1 -0
  58. package/lib/module/devtools/ConsoleMessageRow.js +333 -0
  59. package/lib/module/devtools/ConsoleMessageRow.js.map +1 -0
  60. package/lib/module/devtools/ConsoleValue.js +249 -0
  61. package/lib/module/devtools/ConsoleValue.js.map +1 -0
  62. package/lib/module/devtools/ConsoleView.js +46 -0
  63. package/lib/module/devtools/ConsoleView.js.map +1 -0
  64. package/lib/module/devtools/consoleFilter.js +119 -0
  65. package/lib/module/devtools/consoleFilter.js.map +1 -0
  66. package/lib/module/devtools/consoleFormat.js +309 -0
  67. package/lib/module/devtools/consoleFormat.js.map +1 -0
  68. package/lib/module/devtools/origin.js +183 -0
  69. package/lib/module/devtools/origin.js.map +1 -0
  70. package/lib/module/devtools/theme.js +140 -0
  71. package/lib/module/devtools/theme.js.map +1 -0
  72. package/lib/module/devtools/useConsoleController.js +79 -0
  73. package/lib/module/devtools/useConsoleController.js.map +1 -0
  74. package/lib/module/devtools/useConsoleMessages.js +238 -0
  75. package/lib/module/devtools/useConsoleMessages.js.map +1 -0
  76. package/lib/module/hooks/useConsoleLogs.js +19 -0
  77. package/lib/module/hooks/useConsoleLogs.js.map +1 -0
  78. package/lib/module/index.js +52 -0
  79. package/lib/module/index.js.map +1 -0
  80. package/lib/module/preset.js +40 -0
  81. package/lib/module/preset.js.map +1 -0
  82. package/lib/module/store/consoleLogStore.js +284 -0
  83. package/lib/module/store/consoleLogStore.js.map +1 -0
  84. package/lib/module/store/sanitize.js +60 -0
  85. package/lib/module/store/sanitize.js.map +1 -0
  86. package/lib/module/sync/consoleSyncAdapter.js +43 -0
  87. package/lib/module/sync/consoleSyncAdapter.js.map +1 -0
  88. package/lib/typescript/components/ConsoleIcon.d.ts +14 -0
  89. package/lib/typescript/components/ConsoleIcon.d.ts.map +1 -0
  90. package/lib/typescript/components/ConsoleLogList.d.ts +8 -0
  91. package/lib/typescript/components/ConsoleLogList.d.ts.map +1 -0
  92. package/lib/typescript/components/ConsoleModal.d.ts +18 -0
  93. package/lib/typescript/components/ConsoleModal.d.ts.map +1 -0
  94. package/lib/typescript/components/ConsoleRoot.d.ts +14 -0
  95. package/lib/typescript/components/ConsoleRoot.d.ts.map +1 -0
  96. package/lib/typescript/devtools/ConsoleBody.d.ts +11 -0
  97. package/lib/typescript/devtools/ConsoleBody.d.ts.map +1 -0
  98. package/lib/typescript/devtools/ConsoleHeader.d.ts +17 -0
  99. package/lib/typescript/devtools/ConsoleHeader.d.ts.map +1 -0
  100. package/lib/typescript/devtools/ConsoleMessageRow.d.ts +20 -0
  101. package/lib/typescript/devtools/ConsoleMessageRow.d.ts.map +1 -0
  102. package/lib/typescript/devtools/ConsoleValue.d.ts +21 -0
  103. package/lib/typescript/devtools/ConsoleValue.d.ts.map +1 -0
  104. package/lib/typescript/devtools/ConsoleView.d.ts +10 -0
  105. package/lib/typescript/devtools/ConsoleView.d.ts.map +1 -0
  106. package/lib/typescript/devtools/consoleFilter.d.ts +31 -0
  107. package/lib/typescript/devtools/consoleFilter.d.ts.map +1 -0
  108. package/lib/typescript/devtools/consoleFormat.d.ts +29 -0
  109. package/lib/typescript/devtools/consoleFormat.d.ts.map +1 -0
  110. package/lib/typescript/devtools/origin.d.ts +39 -0
  111. package/lib/typescript/devtools/origin.d.ts.map +1 -0
  112. package/lib/typescript/devtools/theme.d.ts +57 -0
  113. package/lib/typescript/devtools/theme.d.ts.map +1 -0
  114. package/lib/typescript/devtools/useConsoleController.d.ts +35 -0
  115. package/lib/typescript/devtools/useConsoleController.d.ts.map +1 -0
  116. package/lib/typescript/devtools/useConsoleMessages.d.ts +74 -0
  117. package/lib/typescript/devtools/useConsoleMessages.d.ts.map +1 -0
  118. package/lib/typescript/hooks/useConsoleLogs.d.ts +11 -0
  119. package/lib/typescript/hooks/useConsoleLogs.d.ts.map +1 -0
  120. package/lib/typescript/index.d.ts +26 -0
  121. package/lib/typescript/index.d.ts.map +1 -0
  122. package/lib/typescript/preset.d.ts +38 -0
  123. package/lib/typescript/preset.d.ts.map +1 -0
  124. package/lib/typescript/store/consoleLogStore.d.ts +71 -0
  125. package/lib/typescript/store/consoleLogStore.d.ts.map +1 -0
  126. package/lib/typescript/store/sanitize.d.ts +11 -0
  127. package/lib/typescript/store/sanitize.d.ts.map +1 -0
  128. package/lib/typescript/sync/consoleSyncAdapter.d.ts +30 -0
  129. package/lib/typescript/sync/consoleSyncAdapter.d.ts.map +1 -0
  130. package/package.json +56 -0
  131. package/src/components/ConsoleIcon.tsx +49 -0
  132. package/src/components/ConsoleLogList.tsx +133 -0
  133. package/src/components/ConsoleModal.tsx +48 -0
  134. package/src/components/ConsoleRoot.tsx +25 -0
  135. package/src/devtools/ConsoleBody.tsx +225 -0
  136. package/src/devtools/ConsoleHeader.tsx +149 -0
  137. package/src/devtools/ConsoleMessageRow.tsx +345 -0
  138. package/src/devtools/ConsoleValue.tsx +265 -0
  139. package/src/devtools/ConsoleView.tsx +41 -0
  140. package/src/devtools/consoleFilter.ts +123 -0
  141. package/src/devtools/consoleFormat.ts +314 -0
  142. package/src/devtools/origin.ts +190 -0
  143. package/src/devtools/theme.ts +137 -0
  144. package/src/devtools/useConsoleController.ts +104 -0
  145. package/src/devtools/useConsoleMessages.ts +337 -0
  146. package/src/hooks/useConsoleLogs.ts +26 -0
  147. package/src/index.tsx +69 -0
  148. package/src/preset.tsx +35 -0
  149. package/src/store/consoleLogStore.ts +346 -0
  150. package/src/store/sanitize.ts +58 -0
  151. package/src/sync/consoleSyncAdapter.ts +49 -0
@@ -0,0 +1,137 @@
1
+ /**
2
+ * Console palette — a dark theme matched to Buoy's shared design tokens
3
+ * (`macOSColors` in @buoy-gg/shared-ui) so the console reads as part of the
4
+ * tool suite, tuned for contrast per WCAG AA:
5
+ * - body text #F5F5F7 on #0A0A0C ≈ 17:1
6
+ * - secondary #A1A1A6 on #0A0A0C ≈ 7:1
7
+ * - links #40CCFF on #0A0A0C ≈ 9:1
8
+ * - dividers / nesting guides lightened to ~#3A3A40 so lines are clearly
9
+ * visible against the near-black surface.
10
+ * The light values (the exact DevTools light tokens) are kept for a future
11
+ * toggle.
12
+ */
13
+
14
+ import { Platform } from "react-native";
15
+
16
+ /**
17
+ * Monospace font. On the desktop dashboard (react-native-web) the generic
18
+ * `"monospace"` resolves to a thin, poorly-hinted browser default that's hard
19
+ * to read at small sizes — so we use a curated mono stack there. Native (mobile)
20
+ * keeps its system monospace, which already renders well.
21
+ */
22
+ export const monoFont: string =
23
+ Platform.select({
24
+ web: 'ui-monospace, "SF Mono", "SFMono-Regular", Menlo, Monaco, "Cascadia Code", "Roboto Mono", Consolas, "Liberation Mono", monospace',
25
+ default: "monospace",
26
+ }) ?? "monospace";
27
+
28
+ export interface ConsoleColors {
29
+ baseContainer: string;
30
+ surfaceError: string;
31
+ surfaceYellow: string;
32
+ divider: string;
33
+ inputBackground: string;
34
+ menuBackground: string;
35
+ onSurface: string;
36
+ onSurfaceYellow: string;
37
+ onErrorContainer: string;
38
+ tokenSubtle: string;
39
+ primary: string;
40
+ valueNumber: string;
41
+ valueString: string;
42
+ valueBigint: string;
43
+ valueNode: string;
44
+ valueNullish: string;
45
+ valuePreview: string;
46
+ }
47
+
48
+ /** Light theme (active) — resolved DevTools light-theme tokens. */
49
+ export const lightTheme: ConsoleColors = {
50
+ baseContainer: "#ffffff", // --sys-color-cdt-base-container (neutral99 ≈ white)
51
+ surfaceError: "#fceaea", // --sys-color-surface-error rgb(252 235 235)
52
+ surfaceYellow: "#fef6d5", // --sys-color-surface-yellow rgb(254 246 213)
53
+ divider: "#e8eaed", // --sys-color-divider (primary90, lightened for contrast)
54
+ inputBackground: "#f1f3f4",
55
+ menuBackground: "#ffffff",
56
+ onSurface: "#1f1f1f", // neutral10
57
+ onSurfaceYellow: "#3e2f00", // yellow20
58
+ onErrorContainer: "#410e0b", // error10
59
+ tokenSubtle: "#757575", // neutral50
60
+ primary: "#0b57d0", // primary40 (links)
61
+ valueNumber: "#0842a0", // token-attribute-value (blue30)
62
+ valueString: "#c5221f", // token-property-special (error50, slightly deepened)
63
+ valueBigint: "#146c2e", // token-comment (green40)
64
+ valueNode: "#1f1f1f", // token-variable (= on-surface)
65
+ valueNullish: "rgba(31,31,31,0.5)", // state-disabled
66
+ valuePreview: "rgba(31,31,31,0.62)",
67
+ };
68
+
69
+ /** Dark theme (active) — matched to Buoy macOSColors, contrast-tuned. */
70
+ export const darkTheme: ConsoleColors = {
71
+ baseContainer: "#0A0A0C", // macOSColors.background.base
72
+ surfaceError: "rgba(255,69,58,0.15)", // semantic.errorBackground
73
+ surfaceYellow: "rgba(255,235,59,0.13)", // semantic.warningBackground
74
+ divider: "#3A3A40", // lightened from border.default (#2D2D2F) for visible lines
75
+ inputBackground: "#26262A", // background.input
76
+ menuBackground: "#1A1A1C", // background.card
77
+ onSurface: "#F5F5F7", // text.primary (~17:1)
78
+ onSurfaceYellow: "#FFE9A3", // light warm yellow on dark warning row
79
+ onErrorContainer: "#FFB4AB", // light red on dark error row
80
+ tokenSubtle: "#A1A1A6", // text.secondary (~7:1 — timestamps, previews)
81
+ primary: "#40CCFF", // semantic.infoLight (links, ~9:1)
82
+ valueNumber: "#FF9F1C", // orange (number + boolean)
83
+ valueString: "#7EE787", // green (string/regexp/symbol)
84
+ valueBigint: "#A1A1A6", // muted (bigint)
85
+ valueNode: "#F5F5F7", // default text (DOM node / variable)
86
+ valueNullish: "#8E8E93", // text.muted (null/undefined)
87
+ valuePreview: "#A1A1A6", // object preview (italic)
88
+ };
89
+
90
+ /** The active console palette. */
91
+ export const consoleTheme: ConsoleColors = darkTheme;
92
+
93
+ /** ANSI 16-color palette (dark theme), from consoleView.css `--console-color-*`. */
94
+ export const ansiColors: Record<string, string> = {
95
+ black: "#000",
96
+ red: "rgb(237 78 76)",
97
+ green: "rgb(1 200 1)",
98
+ yellow: "rgb(210 192 87)",
99
+ blue: "rgb(39 116 240)",
100
+ magenta: "rgb(161 66 244)",
101
+ cyan: "rgb(18 181 203)",
102
+ gray: "rgb(207 208 208)",
103
+ darkgray: "rgb(137 137 137)",
104
+ lightred: "rgb(242 139 130)",
105
+ lightgreen: "rgb(161 247 181)",
106
+ lightyellow: "rgb(221 251 85)",
107
+ lightblue: "rgb(102 157 246)",
108
+ lightmagenta: "rgb(214 112 214)",
109
+ lightcyan: "rgb(132 240 255)",
110
+ white: "#fff",
111
+ };
112
+
113
+ export type ConsoleLevel = "verbose" | "info" | "warning" | "error";
114
+
115
+ export interface LevelStyle {
116
+ /** Row background (undefined = base container). */
117
+ background?: string;
118
+ /** Message text color. */
119
+ text: string;
120
+ /** Material icon name analog (rendered as a glyph). */
121
+ icon?: "warning" | "error";
122
+ }
123
+
124
+ export const levelStyles: Record<ConsoleLevel, LevelStyle> = {
125
+ verbose: { text: consoleTheme.tokenSubtle },
126
+ info: { text: consoleTheme.onSurface },
127
+ warning: {
128
+ background: consoleTheme.surfaceYellow,
129
+ text: consoleTheme.onSurfaceYellow,
130
+ icon: "warning",
131
+ },
132
+ error: {
133
+ background: consoleTheme.surfaceError,
134
+ text: consoleTheme.onErrorContainer,
135
+ icon: "error",
136
+ },
137
+ };
@@ -0,0 +1,104 @@
1
+ /**
2
+ * useConsoleController — owns all Console view state so the header and the body
3
+ * can be composed separately (the shared ModalHeader pattern: controls live in
4
+ * the modal header's `customContent`, the list/sub-views live in the body).
5
+ */
6
+
7
+ import { useCallback, useState, type Dispatch, type SetStateAction } from "react";
8
+ import {
9
+ useConsoleMessages,
10
+ type DisplayRow,
11
+ type LayoutMode,
12
+ } from "./useConsoleMessages";
13
+ import {
14
+ DEFAULT_LEVELS,
15
+ levelMenuSummary,
16
+ type LevelsMask,
17
+ } from "./consoleFilter";
18
+ import { clearEntries } from "../store/consoleLogStore";
19
+
20
+ /** Which screen the modal body shows (list, or the back-navigable filters page). */
21
+ export type ConsoleViewState = "list" | "filters";
22
+
23
+ export interface ConsoleController {
24
+ query: string;
25
+ setQuery: Dispatch<SetStateAction<string>>;
26
+ searchActive: boolean;
27
+ setSearchActive: Dispatch<SetStateAction<boolean>>;
28
+ levelsMask: LevelsMask;
29
+ setLevelsMask: Dispatch<SetStateAction<LevelsMask>>;
30
+ showTimestamps: boolean;
31
+ setShowTimestamps: Dispatch<SetStateAction<boolean>>;
32
+ layoutMode: LayoutMode;
33
+ setLayoutMode: Dispatch<SetStateAction<LayoutMode>>;
34
+ view: ConsoleViewState;
35
+ setView: Dispatch<SetStateAction<ConsoleViewState>>;
36
+ rows: DisplayRow[];
37
+ hiddenCount: number;
38
+ onToggleGroup: (groupId: number, currentlyCollapsed: boolean) => void;
39
+ onToggleCluster: (key: string) => void;
40
+ clear: () => void;
41
+ /** Level menu summary ("Default levels" / "Errors only" / …). */
42
+ levelSummary: string;
43
+ /** Whether the level mask differs from all/default (drives the active tint). */
44
+ isLevelCustom: boolean;
45
+ }
46
+
47
+ export function useConsoleController(): ConsoleController {
48
+ const [query, setQuery] = useState("");
49
+ const [searchActive, setSearchActive] = useState(false);
50
+ const [levelsMask, setLevelsMask] = useState<LevelsMask>({ ...DEFAULT_LEVELS });
51
+ const [showTimestamps, setShowTimestamps] = useState(false);
52
+ const [collapsedOverrides, setCollapsedOverrides] = useState<Record<number, boolean>>({});
53
+ const [layoutMode, setLayoutMode] = useState<LayoutMode>("chrono");
54
+ const [view, setView] = useState<ConsoleViewState>("list");
55
+ const [clusterExpanded, setClusterExpanded] = useState<Record<string, boolean>>({});
56
+
57
+ const { rows, hiddenCount } = useConsoleMessages({
58
+ levelsMask,
59
+ query,
60
+ showTimestamps,
61
+ collapsedOverrides,
62
+ groupDimension: "function",
63
+ layoutMode,
64
+ clusterExpanded,
65
+ });
66
+
67
+ const onToggleGroup = useCallback((groupId: number, currentlyCollapsed: boolean) => {
68
+ setCollapsedOverrides((prev) => ({ ...prev, [groupId]: !currentlyCollapsed }));
69
+ }, []);
70
+
71
+ const onToggleCluster = useCallback((key: string) => {
72
+ setClusterExpanded((prev) => ({ ...prev, [key]: !(prev[key] ?? false) }));
73
+ }, []);
74
+
75
+ const clear = useCallback(() => {
76
+ clearEntries();
77
+ setCollapsedOverrides({});
78
+ }, []);
79
+
80
+ const levelSummary = levelMenuSummary(levelsMask);
81
+ const isLevelCustom = levelSummary !== "All levels" && levelSummary !== "Default levels";
82
+
83
+ return {
84
+ query,
85
+ setQuery,
86
+ searchActive,
87
+ setSearchActive,
88
+ levelsMask,
89
+ setLevelsMask,
90
+ showTimestamps,
91
+ setShowTimestamps,
92
+ layoutMode,
93
+ setLayoutMode,
94
+ view,
95
+ setView,
96
+ rows,
97
+ hiddenCount,
98
+ onToggleGroup,
99
+ onToggleCluster,
100
+ clear,
101
+ levelSummary,
102
+ isLevelCustom,
103
+ };
104
+ }
@@ -0,0 +1,337 @@
1
+ /**
2
+ * useConsoleMessages — derives the displayed row model from raw captured
3
+ * entries, mirroring DevTools `ConsoleView.updateMessageList`:
4
+ *
5
+ * - level + text filtering (consoleFilter)
6
+ * - consecutive-identical collapse into a repeat-count badge (when timestamps
7
+ * are off, matching `tryToCollapseMessages`)
8
+ * - console.group / groupCollapsed nesting + collapse/expand
9
+ *
10
+ * Returns the visible rows plus the count of messages hidden by filters
11
+ * ("N hidden"), and per-level totals for the toolbar.
12
+ */
13
+
14
+ import { useMemo, useSyncExternalStore } from "react";
15
+ import {
16
+ subscribe,
17
+ getEntries,
18
+ type ConsoleLogEntry,
19
+ type ConsoleLevel,
20
+ } from "../store/consoleLogStore";
21
+ import {
22
+ parseFilterQuery,
23
+ shouldBeVisible,
24
+ type LevelsMask,
25
+ } from "./consoleFilter";
26
+ import {
27
+ sourceKey,
28
+ sourceLabel,
29
+ colorForKey,
30
+ bracketGlyph,
31
+ type GroupDimension,
32
+ type Bracket,
33
+ } from "./origin";
34
+
35
+ /** How the origin gutter is laid out. */
36
+ export type LayoutMode = "chrono" | "swimlane" | "cluster";
37
+
38
+ /** One swimlane gutter cell. */
39
+ export interface LaneCell {
40
+ glyph: string;
41
+ color?: string;
42
+ }
43
+
44
+ export interface DisplayRow {
45
+ key: string;
46
+ entry: ConsoleLogEntry;
47
+ /** Group-nesting indentation level. */
48
+ depth: number;
49
+ /** >1 when consecutive identical messages were collapsed. */
50
+ repeatCount: number;
51
+ /** True for console.group / groupCollapsed header rows. */
52
+ isGroupHeader: boolean;
53
+ /** For headers: the group id (the start entry's id). */
54
+ groupId?: number;
55
+ /** For headers: whether the group is currently collapsed. */
56
+ groupCollapsed?: boolean;
57
+ /** The grouping key for this row's origin (per the active dimension). */
58
+ sourceKey?: string | null;
59
+ /** lnav-style single-column gutter glyph (┌ │ └ ─) — chrono/cluster modes. */
60
+ bracketGlyph?: string;
61
+ /** Multi-column swimlane gutter cells — swimlane mode. */
62
+ lanes?: LaneCell[];
63
+ /** Color for this row's origin bracket + label. */
64
+ originColor?: string;
65
+ /** Human label for the row's origin (component / function / call site). */
66
+ originLabel?: string;
67
+ /** Set on the synthetic header of a collapsed/large cluster (Grouped mode). */
68
+ clusterHeader?: { key: string; count: number; expanded: boolean };
69
+ }
70
+
71
+ export interface ConsoleMessagesResult {
72
+ rows: DisplayRow[];
73
+ hiddenCount: number;
74
+ levelCounts: Record<ConsoleLevel, number>;
75
+ totalCount: number;
76
+ }
77
+
78
+ interface BuildOptions {
79
+ levelsMask: LevelsMask;
80
+ query: string;
81
+ showTimestamps: boolean;
82
+ /** groupId → user-overridden collapsed state. */
83
+ collapsedOverrides: Record<number, boolean>;
84
+ /** Which origin dimension to group/color rows by. */
85
+ groupDimension: GroupDimension;
86
+ /** Gutter layout: chronological brackets, swimlanes, or clustered. */
87
+ layoutMode: LayoutMode;
88
+ /** Grouped mode: which large clusters the user has expanded (key → true). */
89
+ clusterExpanded: Record<string, boolean>;
90
+ }
91
+
92
+ const MAX_LANES = 5;
93
+
94
+ /** Grouped mode collapses any source cluster larger than this into a header. */
95
+ const CLUSTER_THRESHOLD = 5;
96
+
97
+ /** First pass: tag each row with its origin source key, color, and label. */
98
+ function computeSourceInfo(rows: DisplayRow[], dim: GroupDimension): void {
99
+ for (const row of rows) {
100
+ const key = sourceKey(row.entry.origin, dim);
101
+ row.sourceKey = key;
102
+ row.originColor = key ? colorForKey(key) : undefined;
103
+ row.originLabel = dim === "off" ? undefined : sourceLabel(row.entry.origin, dim);
104
+ }
105
+ }
106
+
107
+ /** lnav single-column gutter: bracket consecutive same-source runs. */
108
+ function attachContiguousBrackets(rows: DisplayRow[]): void {
109
+ for (let i = 0; i < rows.length; i++) {
110
+ const key = rows[i].sourceKey;
111
+ if (key == null) {
112
+ rows[i].bracketGlyph = undefined;
113
+ continue;
114
+ }
115
+ const sameAsPrev = i > 0 && rows[i - 1].sourceKey === key;
116
+ const sameAsNext = i < rows.length - 1 && rows[i + 1].sourceKey === key;
117
+ const bracket: Bracket =
118
+ !sameAsPrev && !sameAsNext
119
+ ? "single"
120
+ : !sameAsPrev
121
+ ? "top"
122
+ : !sameAsNext
123
+ ? "bottom"
124
+ : "mid";
125
+ rows[i].bracketGlyph = bracketGlyph(bracket);
126
+ }
127
+ }
128
+
129
+ /**
130
+ * Re-order rows so all rows of one source are contiguous (groups ordered by
131
+ * first appearance, chronological within each). Clusters larger than
132
+ * CLUSTER_THRESHOLD collapse into a single expandable header (Chrome-style),
133
+ * unless the user has expanded that cluster.
134
+ */
135
+ function clusterRows(rows: DisplayRow[], expanded: Record<string, boolean>): DisplayRow[] {
136
+ const order: string[] = [];
137
+ const groups = new Map<string, DisplayRow[]>();
138
+ for (const r of rows) {
139
+ const k = r.sourceKey ?? "—";
140
+ let g = groups.get(k);
141
+ if (!g) {
142
+ g = [];
143
+ groups.set(k, g);
144
+ order.push(k);
145
+ }
146
+ g.push(r);
147
+ }
148
+
149
+ const out: DisplayRow[] = [];
150
+ for (const k of order) {
151
+ const group = groups.get(k)!;
152
+ for (const r of group) r.depth = 0; // clustering ignores console.group nesting
153
+
154
+ const isLarge = group.length > CLUSTER_THRESHOLD;
155
+ const isExpanded = expanded[k] ?? false;
156
+
157
+ if (isLarge) {
158
+ out.push({
159
+ key: `cluster:${k}`,
160
+ entry: group[0].entry,
161
+ depth: 0,
162
+ repeatCount: 1,
163
+ isGroupHeader: false,
164
+ sourceKey: k,
165
+ originColor: group[0].originColor,
166
+ originLabel: group[0].originLabel,
167
+ clusterHeader: { key: k, count: group.length, expanded: isExpanded },
168
+ });
169
+ if (!isExpanded) continue;
170
+ }
171
+
172
+ group.forEach((r, i) => {
173
+ const single = group.length === 1;
174
+ const bracket: Bracket = single
175
+ ? "single"
176
+ : i === 0
177
+ ? "top"
178
+ : i === group.length - 1
179
+ ? "bottom"
180
+ : "mid";
181
+ r.bracketGlyph = bracketGlyph(bracket);
182
+ out.push(r);
183
+ });
184
+ }
185
+ return out;
186
+ }
187
+
188
+ /** Git-graph swimlanes: each source gets a continuous vertical track so its
189
+ * line stays unbroken when other sources interrupt it. */
190
+ function attachSwimlanes(rows: DisplayRow[]): void {
191
+ const first = new Map<string, number>();
192
+ const last = new Map<string, number>();
193
+ rows.forEach((r, i) => {
194
+ const k = r.sourceKey;
195
+ if (k == null) return;
196
+ if (!first.has(k)) first.set(k, i);
197
+ last.set(k, i);
198
+ });
199
+
200
+ // Assign each source a lane, reusing a lane once its occupant's run ended.
201
+ const sources = [...first.keys()].sort((a, b) => first.get(a)! - first.get(b)!);
202
+ const laneOf = new Map<string, number>();
203
+ const laneEnd: number[] = [];
204
+ for (const s of sources) {
205
+ let lane = laneEnd.findIndex((end) => end < first.get(s)!);
206
+ if (lane === -1) {
207
+ lane = laneEnd.length < MAX_LANES ? laneEnd.length : MAX_LANES - 1;
208
+ if (lane >= laneEnd.length) laneEnd.push(0);
209
+ }
210
+ laneOf.set(s, lane);
211
+ laneEnd[lane] = Math.max(laneEnd[lane] ?? 0, last.get(s)!);
212
+ }
213
+ const laneCount = Math.max(1, Math.min(laneEnd.length, MAX_LANES));
214
+
215
+ rows.forEach((r, i) => {
216
+ const cells: LaneCell[] = Array.from({ length: laneCount }, () => ({ glyph: " " }));
217
+ for (const s of sources) {
218
+ const lane = laneOf.get(s)!;
219
+ if (lane >= laneCount) continue;
220
+ if (i >= first.get(s)! && i <= last.get(s)!) {
221
+ cells[lane] = { glyph: r.sourceKey === s ? "●" : "│", color: colorForKey(s) };
222
+ }
223
+ }
224
+ r.lanes = cells;
225
+ });
226
+ }
227
+
228
+ interface OpenGroup {
229
+ id: number;
230
+ collapsed: boolean;
231
+ }
232
+
233
+ const GROUP_STARTS = new Set(["group", "groupCollapsed"]);
234
+
235
+ function buildDisplayRows(
236
+ entries: ConsoleLogEntry[],
237
+ opts: BuildOptions
238
+ ): ConsoleMessagesResult {
239
+ const { levelsMask, query, showTimestamps, collapsedOverrides } = opts;
240
+ const parsed = parseFilterQuery(query.trim());
241
+
242
+ const rows: DisplayRow[] = [];
243
+ const levelCounts: Record<ConsoleLevel, number> = {
244
+ verbose: 0,
245
+ info: 0,
246
+ warning: 0,
247
+ error: 0,
248
+ };
249
+ let hiddenCount = 0;
250
+ const stack: OpenGroup[] = [];
251
+
252
+ const anyAncestorCollapsed = () => stack.some((g) => g.collapsed);
253
+
254
+ for (const entry of entries) {
255
+ levelCounts[entry.level]++;
256
+
257
+ if (entry.method === "groupEnd") {
258
+ stack.pop();
259
+ continue;
260
+ }
261
+
262
+ const isHeader = GROUP_STARTS.has(entry.method);
263
+ const hiddenByGroup = anyAncestorCollapsed();
264
+ const passesFilter = shouldBeVisible(entry, levelsMask, parsed);
265
+
266
+ if (!passesFilter) {
267
+ hiddenCount++;
268
+ } else if (!hiddenByGroup) {
269
+ // Repeat-count collapse: only when timestamps are hidden and the previous
270
+ // visible row is an identical, non-header message at the same depth.
271
+ const prev = rows[rows.length - 1];
272
+ if (
273
+ !showTimestamps &&
274
+ !isHeader &&
275
+ prev &&
276
+ !prev.isGroupHeader &&
277
+ prev.depth === stack.length &&
278
+ prev.entry.method === entry.method &&
279
+ prev.entry.message === entry.message
280
+ ) {
281
+ prev.repeatCount++;
282
+ } else {
283
+ rows.push({
284
+ key: String(entry.id),
285
+ entry,
286
+ depth: stack.length,
287
+ repeatCount: 1,
288
+ isGroupHeader: isHeader,
289
+ groupId: isHeader ? entry.id : undefined,
290
+ groupCollapsed: undefined,
291
+ });
292
+ }
293
+ }
294
+
295
+ if (isHeader) {
296
+ const defaultCollapsed = entry.method === "groupCollapsed";
297
+ const collapsed = collapsedOverrides[entry.id] ?? defaultCollapsed;
298
+ // Record the collapse state on the just-pushed header row (if visible).
299
+ const headerRow = rows[rows.length - 1];
300
+ if (headerRow && headerRow.groupId === entry.id) {
301
+ headerRow.groupCollapsed = collapsed;
302
+ }
303
+ stack.push({ id: entry.id, collapsed: collapsed || hiddenByGroup });
304
+ }
305
+ }
306
+
307
+ computeSourceInfo(rows, opts.groupDimension);
308
+ let finalRows = rows;
309
+ if (opts.groupDimension !== "off") {
310
+ if (opts.layoutMode === "cluster") {
311
+ finalRows = clusterRows(rows, opts.clusterExpanded);
312
+ } else if (opts.layoutMode === "swimlane") {
313
+ attachSwimlanes(rows);
314
+ } else {
315
+ attachContiguousBrackets(rows);
316
+ }
317
+ }
318
+
319
+ return { rows: finalRows, hiddenCount, levelCounts, totalCount: entries.length };
320
+ }
321
+
322
+ export function useConsoleMessages(opts: BuildOptions): ConsoleMessagesResult {
323
+ const entries = useSyncExternalStore(subscribe, getEntries, getEntries);
324
+ return useMemo(
325
+ () => buildDisplayRows(entries, opts),
326
+ [
327
+ entries,
328
+ opts.levelsMask,
329
+ opts.query,
330
+ opts.showTimestamps,
331
+ opts.collapsedOverrides,
332
+ opts.groupDimension,
333
+ opts.layoutMode,
334
+ opts.clusterExpanded,
335
+ ]
336
+ );
337
+ }
@@ -0,0 +1,26 @@
1
+ import { useSyncExternalStore, useEffect } from "react";
2
+ import {
3
+ subscribe,
4
+ getEntries,
5
+ installConsoleCapture,
6
+ type ConsoleLogEntry,
7
+ } from "../store/consoleLogStore";
8
+
9
+ export interface UseConsoleLogsResult {
10
+ logs: ConsoleLogEntry[];
11
+ }
12
+
13
+ /**
14
+ * Subscribe to captured console logs. Installs the console interceptor on mount
15
+ * (idempotent) and re-renders whenever a new entry is captured or the buffer is
16
+ * cleared.
17
+ */
18
+ export function useConsoleLogs(): UseConsoleLogsResult {
19
+ useEffect(() => {
20
+ installConsoleCapture();
21
+ }, []);
22
+
23
+ const logs = useSyncExternalStore(subscribe, getEntries, getEntries);
24
+
25
+ return { logs };
26
+ }
package/src/index.tsx ADDED
@@ -0,0 +1,69 @@
1
+ /**
2
+ * @buoy-gg/console
3
+ *
4
+ * Captures global `console.*` output and renders it as a Buoy devtool.
5
+ *
6
+ * This is the BASIC version: direct console patching + a flat list, used to
7
+ * verify the capture pipeline end to end. Next step is a 1:1 port of Chrome
8
+ * DevTools' console view.
9
+ */
10
+
11
+ // =============================================================================
12
+ // PRESET (Primary entry point — pass to <FloatingDevTools apps={[...]} />)
13
+ // =============================================================================
14
+ export { consoleToolPreset, createConsoleTool } from "./preset";
15
+
16
+ // =============================================================================
17
+ // ROOT (Mount once at app root to capture logs from startup)
18
+ // =============================================================================
19
+ export { ConsoleRoot } from "./components/ConsoleRoot";
20
+
21
+ // =============================================================================
22
+ // COMPONENTS
23
+ // =============================================================================
24
+ export { ConsoleModal } from "./components/ConsoleModal";
25
+ export { ConsoleIcon, CONSOLE_ICON_COLOR } from "./components/ConsoleIcon";
26
+
27
+ // DevTools-style console panel (the 1:1 Chrome console port)
28
+ export { ConsoleView } from "./devtools/ConsoleView";
29
+ export { ConsoleMessageRow } from "./devtools/ConsoleMessageRow";
30
+ export { ConsoleValue } from "./devtools/ConsoleValue";
31
+ export { useConsoleMessages } from "./devtools/useConsoleMessages";
32
+ export type { DisplayRow, ConsoleMessagesResult } from "./devtools/useConsoleMessages";
33
+ export { consoleTheme, levelStyles } from "./devtools/theme";
34
+
35
+ // Basic flat list (kept for simple/embedded use)
36
+ export { ConsoleLogList } from "./components/ConsoleLogList";
37
+
38
+ // =============================================================================
39
+ // HOOK
40
+ // =============================================================================
41
+ export { useConsoleLogs } from "./hooks/useConsoleLogs";
42
+ export type { UseConsoleLogsResult } from "./hooks/useConsoleLogs";
43
+
44
+ // =============================================================================
45
+ // EXTERNAL SYNC (device → desktop dashboard)
46
+ // =============================================================================
47
+ export { consoleSyncAdapter } from "./sync/consoleSyncAdapter";
48
+
49
+ // =============================================================================
50
+ // CAPTURE STORE (install/teardown + types)
51
+ // =============================================================================
52
+ export {
53
+ installConsoleCapture,
54
+ uninstallConsoleCapture,
55
+ clearEntries,
56
+ getEntries,
57
+ subscribe,
58
+ replaceEntries,
59
+ onClear,
60
+ initConsolePersistence,
61
+ getPreserveLog,
62
+ setPreserveLog,
63
+ subscribePreserveLog,
64
+ } from "./store/consoleLogStore";
65
+ export type {
66
+ ConsoleLogEntry,
67
+ ConsoleLevel,
68
+ ConsoleMethod,
69
+ } from "./store/consoleLogStore";