@treenity/mods 3.0.2 → 3.0.4
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/agent/client.ts +2 -0
- package/agent/guardian.ts +492 -0
- package/agent/seed.ts +74 -0
- package/agent/server.ts +4 -0
- package/agent/service.ts +644 -0
- package/agent/types.ts +184 -0
- package/agent/view.tsx +431 -0
- package/dist/agent/client.d.ts +3 -0
- package/dist/agent/client.d.ts.map +1 -0
- package/dist/agent/client.js +3 -0
- package/dist/agent/client.js.map +1 -0
- package/dist/agent/guardian.d.ts +47 -0
- package/dist/agent/guardian.d.ts.map +1 -0
- package/dist/agent/guardian.js +452 -0
- package/dist/agent/guardian.js.map +1 -0
- package/dist/agent/seed.d.ts +2 -0
- package/dist/agent/seed.d.ts.map +1 -0
- package/dist/agent/seed.js +68 -0
- package/dist/agent/seed.js.map +1 -0
- package/dist/agent/server.d.ts +5 -0
- package/dist/agent/server.d.ts.map +1 -0
- package/dist/agent/server.js +5 -0
- package/dist/agent/server.js.map +1 -0
- package/dist/agent/service.d.ts +2 -0
- package/dist/agent/service.d.ts.map +1 -0
- package/dist/agent/service.js +556 -0
- package/dist/agent/service.js.map +1 -0
- package/dist/agent/types.d.ts +115 -0
- package/dist/agent/types.d.ts.map +1 -0
- package/dist/agent/types.js +168 -0
- package/dist/agent/types.js.map +1 -0
- package/dist/agent/view.d.ts +2 -0
- package/dist/agent/view.d.ts.map +1 -0
- package/dist/agent/view.js +137 -0
- package/dist/agent/view.js.map +1 -0
- package/dist/mcp/mcp-server.d.ts +16 -0
- package/dist/mcp/mcp-server.d.ts.map +1 -0
- package/dist/mcp/mcp-server.js +344 -0
- package/dist/mcp/mcp-server.js.map +1 -0
- package/dist/mcp/server.d.ts +3 -0
- package/dist/mcp/server.d.ts.map +1 -0
- package/dist/mcp/server.js +3 -0
- package/dist/mcp/server.js.map +1 -0
- package/dist/mcp/service.d.ts +2 -0
- package/dist/mcp/service.d.ts.map +1 -0
- package/dist/mcp/service.js +16 -0
- package/dist/mcp/service.js.map +1 -0
- package/dist/mcp/types.d.ts +4 -0
- package/dist/mcp/types.d.ts.map +1 -0
- package/dist/mcp/types.js +6 -0
- package/dist/mcp/types.js.map +1 -0
- package/dist/metatron/claude.d.ts +30 -0
- package/dist/metatron/claude.d.ts.map +1 -0
- package/dist/metatron/claude.js +201 -0
- package/dist/metatron/claude.js.map +1 -0
- package/dist/metatron/client.d.ts +3 -0
- package/dist/metatron/client.d.ts.map +1 -0
- package/dist/metatron/client.js +3 -0
- package/dist/metatron/client.js.map +1 -0
- package/dist/metatron/mentions.d.ts +9 -0
- package/dist/metatron/mentions.d.ts.map +1 -0
- package/dist/metatron/mentions.js +21 -0
- package/dist/metatron/mentions.js.map +1 -0
- package/dist/metatron/permissions.d.ts +16 -0
- package/dist/metatron/permissions.d.ts.map +1 -0
- package/dist/metatron/permissions.js +52 -0
- package/dist/metatron/permissions.js.map +1 -0
- package/dist/metatron/seed.d.ts +2 -0
- package/dist/metatron/seed.d.ts.map +1 -0
- package/dist/metatron/seed.js +41 -0
- package/dist/metatron/seed.js.map +1 -0
- package/dist/metatron/server.d.ts +4 -0
- package/dist/metatron/server.d.ts.map +1 -0
- package/dist/metatron/server.js +4 -0
- package/dist/metatron/server.js.map +1 -0
- package/dist/metatron/service.d.ts +2 -0
- package/dist/metatron/service.d.ts.map +1 -0
- package/dist/metatron/service.js +361 -0
- package/dist/metatron/service.js.map +1 -0
- package/dist/metatron/types.d.ts +76 -0
- package/dist/metatron/types.d.ts.map +1 -0
- package/dist/metatron/types.js +112 -0
- package/dist/metatron/types.js.map +1 -0
- package/dist/metatron/view.d.ts +4 -0
- package/dist/metatron/view.d.ts.map +1 -0
- package/dist/metatron/view.js +5 -0
- package/dist/metatron/view.js.map +1 -0
- package/dist/metatron/views/config.d.ts +2 -0
- package/dist/metatron/views/config.d.ts.map +1 -0
- package/dist/metatron/views/config.js +116 -0
- package/dist/metatron/views/config.js.map +1 -0
- package/dist/metatron/views/log.d.ts +18 -0
- package/dist/metatron/views/log.d.ts.map +1 -0
- package/dist/metatron/views/log.js +224 -0
- package/dist/metatron/views/log.js.map +1 -0
- package/dist/metatron/views/shared.d.ts +13 -0
- package/dist/metatron/views/shared.d.ts.map +1 -0
- package/dist/metatron/views/shared.js +33 -0
- package/dist/metatron/views/shared.js.map +1 -0
- package/dist/metatron/views/task.d.ts +4 -0
- package/dist/metatron/views/task.d.ts.map +1 -0
- package/dist/metatron/views/task.js +106 -0
- package/dist/metatron/views/task.js.map +1 -0
- package/dist/metatron/views/workspace.d.ts +2 -0
- package/dist/metatron/views/workspace.d.ts.map +1 -0
- package/dist/metatron/views/workspace.js +138 -0
- package/dist/metatron/views/workspace.js.map +1 -0
- package/mcp/mcp-server.ts +393 -0
- package/mcp/server.ts +2 -0
- package/mcp/service.ts +18 -0
- package/mcp/types.ts +6 -0
- package/metatron/CLAUDE.md +22 -0
- package/metatron/claude.ts +258 -0
- package/metatron/client.ts +2 -0
- package/metatron/mentions.ts +31 -0
- package/metatron/permissions.ts +76 -0
- package/metatron/seed.ts +50 -0
- package/metatron/server.ts +3 -0
- package/metatron/service.ts +406 -0
- package/metatron/types.ts +120 -0
- package/metatron/view.tsx +4 -0
- package/metatron/views/config.tsx +408 -0
- package/metatron/views/log.tsx +412 -0
- package/metatron/views/shared.tsx +40 -0
- package/metatron/views/task.tsx +255 -0
- package/metatron/views/workspace.tsx +418 -0
- package/package.json +6 -2
- package/dist/mindmap/radial-tree.d.ts +0 -14
- package/dist/mindmap/radial-tree.d.ts.map +0 -1
- package/dist/mindmap/radial-tree.js +0 -184
- package/dist/mindmap/radial-tree.js.map +0 -1
- package/dist/mindmap/use-tree-data.d.ts +0 -14
- package/dist/mindmap/use-tree-data.d.ts.map +0 -1
- package/dist/mindmap/use-tree-data.js +0 -95
- package/dist/mindmap/use-tree-data.js.map +0 -1
|
@@ -0,0 +1,412 @@
|
|
|
1
|
+
import { useNavigate } from '@treenity/react/hooks';
|
|
2
|
+
import { minimd } from '@treenity/react/lib/minimd';
|
|
3
|
+
import { cn } from '@treenity/react/lib/utils';
|
|
4
|
+
import { useEffect, useLayoutEffect, useMemo, useRef, useState } from 'react';
|
|
5
|
+
|
|
6
|
+
// ── Structured log parser ──
|
|
7
|
+
|
|
8
|
+
type LogBlock =
|
|
9
|
+
| { type: 'text'; content: string }
|
|
10
|
+
| { type: 'tool'; name: string; input: string; time?: string }
|
|
11
|
+
| { type: 'thinking'; content: string; time?: string }
|
|
12
|
+
| { type: 'result'; content: string; time?: string }
|
|
13
|
+
| { type: 'interrupted'; time?: string };
|
|
14
|
+
|
|
15
|
+
const BLOCK_RE = /\n?\[(tool|thinking|result|interrupted)(?: (\d\d:\d\d:\d\d))?\](?: ([^\n]*))?\n?/;
|
|
16
|
+
|
|
17
|
+
function parseLog(raw: unknown): LogBlock[] {
|
|
18
|
+
if (typeof raw !== 'string' || !raw) return [];
|
|
19
|
+
const blocks: LogBlock[] = [];
|
|
20
|
+
let rest = raw;
|
|
21
|
+
|
|
22
|
+
while (rest) {
|
|
23
|
+
const m = BLOCK_RE.exec(rest);
|
|
24
|
+
if (!m) {
|
|
25
|
+
if (rest.trim()) blocks.push({ type: 'text', content: rest.trim() });
|
|
26
|
+
break;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const before = rest.slice(0, m.index).trim();
|
|
30
|
+
if (before) blocks.push({ type: 'text', content: before });
|
|
31
|
+
|
|
32
|
+
const tag = m[1];
|
|
33
|
+
const time = m[2] || undefined; // HH:mm:ss or undefined
|
|
34
|
+
const inline = m[3] ?? '';
|
|
35
|
+
rest = rest.slice(m.index + m[0].length);
|
|
36
|
+
|
|
37
|
+
if (tag === 'tool') {
|
|
38
|
+
// Consume everything until next block marker (JSON input is multi-line)
|
|
39
|
+
const nextBlock = BLOCK_RE.exec(rest);
|
|
40
|
+
const input = nextBlock ? rest.slice(0, nextBlock.index) : rest;
|
|
41
|
+
rest = nextBlock ? rest.slice(nextBlock.index) : '';
|
|
42
|
+
blocks.push({ type: 'tool', name: inline, input: input.trim(), time });
|
|
43
|
+
} else if (tag === 'thinking') {
|
|
44
|
+
const nextBlock = BLOCK_RE.exec(rest);
|
|
45
|
+
const content = nextBlock ? rest.slice(0, nextBlock.index) : rest;
|
|
46
|
+
rest = nextBlock ? rest.slice(nextBlock.index) : '';
|
|
47
|
+
blocks.push({ type: 'thinking', content: content.trim(), time });
|
|
48
|
+
} else if (tag === 'result') {
|
|
49
|
+
const nextBlock = BLOCK_RE.exec(rest);
|
|
50
|
+
const content = inline
|
|
51
|
+
? (nextBlock ? inline + '\n' + rest.slice(0, nextBlock.index) : inline + '\n' + rest)
|
|
52
|
+
: (nextBlock ? rest.slice(0, nextBlock.index) : rest);
|
|
53
|
+
rest = nextBlock ? rest.slice(nextBlock.index) : '';
|
|
54
|
+
blocks.push({ type: 'result', content: content.trim(), time });
|
|
55
|
+
} else if (tag === 'interrupted') {
|
|
56
|
+
blocks.push({ type: 'interrupted', time });
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
return blocks;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// ── Linkified text (clickable /paths) ──
|
|
63
|
+
|
|
64
|
+
const PATH_RE = /(?:^|\s)(\/[\w./-]+)/g;
|
|
65
|
+
|
|
66
|
+
function Linkified({ text }: { text: string }) {
|
|
67
|
+
const navigate = useNavigate();
|
|
68
|
+
const parts: React.ReactNode[] = [];
|
|
69
|
+
let lastIndex = 0;
|
|
70
|
+
|
|
71
|
+
for (const match of text.matchAll(PATH_RE)) {
|
|
72
|
+
const path = match[1];
|
|
73
|
+
const start = match.index + match[0].indexOf(path);
|
|
74
|
+
if (start > lastIndex) parts.push(text.slice(lastIndex, start));
|
|
75
|
+
parts.push(
|
|
76
|
+
<button
|
|
77
|
+
key={`${start}-${path}`}
|
|
78
|
+
onClick={() => navigate(path)}
|
|
79
|
+
className="text-violet-400 hover:text-violet-300 hover:underline transition-colors"
|
|
80
|
+
>
|
|
81
|
+
{path}
|
|
82
|
+
</button>
|
|
83
|
+
);
|
|
84
|
+
lastIndex = match.index + match[0].length;
|
|
85
|
+
}
|
|
86
|
+
if (lastIndex < text.length) parts.push(text.slice(lastIndex));
|
|
87
|
+
return <>{parts.length ? parts : text}</>;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// ── Code syntax highlighting ──
|
|
91
|
+
// XXX: BAD one, use general lib
|
|
92
|
+
const TOKEN = new RegExp([
|
|
93
|
+
'(\\/\\/[^\\n]*)', // 0: comments
|
|
94
|
+
'(\'(?:[^\'\\\\]|\\\\.)*\'|"(?:[^"\\\\]|\\\\.)*"|`(?:[^`\\\\]|\\\\.)*`)', // 1: strings
|
|
95
|
+
'\\b(import|export|from|const|let|var|type|interface|class|function|async|await|return|if|else|for|of|in|while|new|throw|try|catch|finally|default|extends|implements|as|typeof|instanceof)\\b', // 2: keywords
|
|
96
|
+
'\\b(string|number|boolean|void|null|undefined|true|false|any|unknown|never|Promise|Record|Map|Set|Array)\\b', // 3: types
|
|
97
|
+
'(\\b\\d+(?:\\.\\d+)?\\b)', // 4: numbers
|
|
98
|
+
].join('|'), 'g');
|
|
99
|
+
|
|
100
|
+
const TOKEN_CLS = ['text-zinc-600', 'text-emerald-400', 'text-violet-400', 'text-sky-400', 'text-amber-400'];
|
|
101
|
+
|
|
102
|
+
function colorize(code: string): React.ReactNode[] {
|
|
103
|
+
const nodes: React.ReactNode[] = [];
|
|
104
|
+
let last = 0;
|
|
105
|
+
let k = 0;
|
|
106
|
+
for (const m of code.matchAll(TOKEN)) {
|
|
107
|
+
if (m.index! > last) nodes.push(<span key={k++} className="text-zinc-300">{code.slice(last, m.index!)}</span>);
|
|
108
|
+
const gi = m.slice(1).findIndex(g => g !== undefined);
|
|
109
|
+
nodes.push(<span key={k++} className={TOKEN_CLS[gi] || 'text-zinc-300'}>{m[0]}</span>);
|
|
110
|
+
last = m.index! + m[0].length;
|
|
111
|
+
}
|
|
112
|
+
if (last < code.length) nodes.push(<span key={k++} className="text-zinc-300">{code.slice(last)}</span>);
|
|
113
|
+
return nodes;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
function CodeResult({ text }: { text: string }) {
|
|
117
|
+
const rendered = useMemo(() => {
|
|
118
|
+
return text.split('\n').map((line, i) => {
|
|
119
|
+
const ln = line.match(/^(\s*\d+→)(.*)/);
|
|
120
|
+
return (
|
|
121
|
+
<div key={i}>
|
|
122
|
+
{ln ? <><span className="text-zinc-700 select-none">{ln[1]}</span>{colorize(ln[2])}</> : colorize(line)}
|
|
123
|
+
</div>
|
|
124
|
+
);
|
|
125
|
+
});
|
|
126
|
+
}, [text]);
|
|
127
|
+
|
|
128
|
+
return <pre className="font-mono text-[11px] leading-snug">{rendered}</pre>;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
// ── Markdown content ──
|
|
132
|
+
// XXX: rewrite
|
|
133
|
+
function isJson(s: string): boolean {
|
|
134
|
+
const t = s.trim();
|
|
135
|
+
return (t.startsWith('{') && t.endsWith('}')) || (t.startsWith('[') && t.endsWith(']'));
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
// ── JSON tree viewer ──
|
|
139
|
+
|
|
140
|
+
function JsonValue({ value, depth = 0 }: { value: unknown; depth?: number }) {
|
|
141
|
+
if (value === null) return <span className="text-zinc-600">null</span>;
|
|
142
|
+
if (value === undefined) return <span className="text-zinc-600">undefined</span>;
|
|
143
|
+
if (typeof value === 'boolean') return <span className="text-amber-400">{String(value)}</span>;
|
|
144
|
+
if (typeof value === 'number') return <span className="text-sky-400">{value}</span>;
|
|
145
|
+
if (typeof value === 'string') {
|
|
146
|
+
if (value.length > 200) {
|
|
147
|
+
return <CollapsibleJson label={`"${value.slice(0, 60)}…" (${value.length})`} className="text-emerald-400">{value}</CollapsibleJson>;
|
|
148
|
+
}
|
|
149
|
+
return <span className="text-emerald-400">"{value}"</span>;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
if (Array.isArray(value)) {
|
|
153
|
+
if (value.length === 0) return <span className="text-zinc-500">[]</span>;
|
|
154
|
+
return (
|
|
155
|
+
<CollapsibleJson label={`Array(${value.length})`} className="text-zinc-400" defaultOpen={depth < 2}>
|
|
156
|
+
<div className="pl-3 border-l border-zinc-800/60">
|
|
157
|
+
{value.map((item, i) => (
|
|
158
|
+
<div key={i} className="flex gap-1">
|
|
159
|
+
<span className="text-zinc-600 shrink-0 select-none">{i}:</span>
|
|
160
|
+
<JsonValue value={item} depth={depth + 1} />
|
|
161
|
+
</div>
|
|
162
|
+
))}
|
|
163
|
+
</div>
|
|
164
|
+
</CollapsibleJson>
|
|
165
|
+
);
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
if (typeof value === 'object') {
|
|
169
|
+
const entries = Object.entries(value);
|
|
170
|
+
if (entries.length === 0) return <span className="text-zinc-500">{'{}'}</span>;
|
|
171
|
+
return (
|
|
172
|
+
<CollapsibleJson label={`{${entries.length}}`} className="text-zinc-400" defaultOpen={depth < 2}>
|
|
173
|
+
<div className="pl-3 border-l border-zinc-800/60">
|
|
174
|
+
{entries.map(([k, v]) => (
|
|
175
|
+
<div key={k} className="flex gap-1">
|
|
176
|
+
<span className="text-violet-400/80 shrink-0">{k}:</span>
|
|
177
|
+
<JsonValue value={v} depth={depth + 1} />
|
|
178
|
+
</div>
|
|
179
|
+
))}
|
|
180
|
+
</div>
|
|
181
|
+
</CollapsibleJson>
|
|
182
|
+
);
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
return <span className="text-zinc-400">{String(value)}</span>;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
function CollapsibleJson({ label, className, defaultOpen = false, children }: {
|
|
189
|
+
label: string; className?: string; defaultOpen?: boolean; children: React.ReactNode;
|
|
190
|
+
}) {
|
|
191
|
+
const [open, setOpen] = useState(defaultOpen);
|
|
192
|
+
return (
|
|
193
|
+
<span>
|
|
194
|
+
<button onClick={() => setOpen(!open)} className={cn('hover:underline', className)}>
|
|
195
|
+
<svg width="8" height="8" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="3"
|
|
196
|
+
className={cn('inline mr-0.5 transition-transform duration-100', open && 'rotate-90')}
|
|
197
|
+
><path d="M9 18l6-6-6-6" /></svg>
|
|
198
|
+
{label}
|
|
199
|
+
</button>
|
|
200
|
+
{open && <div className="mt-0.5">{children}</div>}
|
|
201
|
+
</span>
|
|
202
|
+
);
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
function JsonTree({ text, className }: { text: string; className?: string }) {
|
|
206
|
+
const parsed = useMemo(() => {
|
|
207
|
+
try { return JSON.parse(text); }
|
|
208
|
+
catch { return null; }
|
|
209
|
+
}, [text]);
|
|
210
|
+
|
|
211
|
+
if (parsed === null) return null;
|
|
212
|
+
return (
|
|
213
|
+
<div className={cn('font-mono text-[11px] leading-relaxed', className)}>
|
|
214
|
+
<JsonValue value={parsed} />
|
|
215
|
+
</div>
|
|
216
|
+
);
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
export function Md({ text, className }: { text: string; className?: string }) {
|
|
220
|
+
const json = isJson(text);
|
|
221
|
+
const html = useMemo(() => json ? '' : minimd(text), [text, json]);
|
|
222
|
+
|
|
223
|
+
if (json) return <JsonTree text={text} className={className} />;
|
|
224
|
+
return <div className={cn('minimd', className)} dangerouslySetInnerHTML={{ __html: html }} />;
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
// ── Collapsible block ──
|
|
228
|
+
// forceOpen syncs state on change but allows individual toggle after
|
|
229
|
+
|
|
230
|
+
function truncate(s: string, max = 80): string {
|
|
231
|
+
const line = s.split('\n')[0];
|
|
232
|
+
return line.length > max ? line.slice(0, max) + '...' : line;
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
// Height threshold: below this the label stays inline (left), above it goes on top
|
|
236
|
+
const LABEL_TOP_THRESHOLD = 120;
|
|
237
|
+
|
|
238
|
+
export function CollapsibleBlock({ label, labelClass, preview, children, wrap, forceOpen, defaultOpen }: {
|
|
239
|
+
label: React.ReactNode;
|
|
240
|
+
labelClass?: string;
|
|
241
|
+
preview?: string;
|
|
242
|
+
children: React.ReactNode;
|
|
243
|
+
wrap: boolean;
|
|
244
|
+
forceOpen?: boolean;
|
|
245
|
+
defaultOpen?: boolean;
|
|
246
|
+
}) {
|
|
247
|
+
const [isOpen, setIsOpen] = useState(defaultOpen ?? false);
|
|
248
|
+
const [labelTop, setLabelTop] = useState(false);
|
|
249
|
+
const contentRef = useRef<HTMLDivElement>(null);
|
|
250
|
+
|
|
251
|
+
// Sync with parent fold/unfold — but user can override after
|
|
252
|
+
useEffect(() => {
|
|
253
|
+
if (forceOpen !== undefined) setIsOpen(forceOpen);
|
|
254
|
+
}, [forceOpen]);
|
|
255
|
+
|
|
256
|
+
// One-time measurement after content renders — decide label position
|
|
257
|
+
useLayoutEffect(() => {
|
|
258
|
+
if (isOpen && contentRef.current) {
|
|
259
|
+
setLabelTop(contentRef.current.scrollHeight > LABEL_TOP_THRESHOLD);
|
|
260
|
+
}
|
|
261
|
+
}, [isOpen]);
|
|
262
|
+
|
|
263
|
+
return (
|
|
264
|
+
<div className={cn(isOpen && !labelTop && 'flex items-start')}>
|
|
265
|
+
<button
|
|
266
|
+
onClick={() => setIsOpen(!isOpen)}
|
|
267
|
+
className={cn(
|
|
268
|
+
'flex items-center gap-1.5 px-2 py-1 text-left text-xs font-medium transition-colors rounded shrink-0',
|
|
269
|
+
isOpen && !labelTop && 'w-auto',
|
|
270
|
+
!isOpen && 'w-full',
|
|
271
|
+
labelClass
|
|
272
|
+
)}
|
|
273
|
+
>
|
|
274
|
+
<svg
|
|
275
|
+
width="10" height="10" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.5"
|
|
276
|
+
className={cn('shrink-0 transition-transform duration-150', isOpen && 'rotate-90')}
|
|
277
|
+
>
|
|
278
|
+
<path d="M9 18l6-6-6-6" />
|
|
279
|
+
</svg>
|
|
280
|
+
{label}
|
|
281
|
+
{!isOpen && preview && (
|
|
282
|
+
<span className="text-zinc-600 font-mono text-[10px] truncate ml-1 flex-1">{preview}</span>
|
|
283
|
+
)}
|
|
284
|
+
</button>
|
|
285
|
+
{isOpen && (
|
|
286
|
+
<div
|
|
287
|
+
ref={contentRef}
|
|
288
|
+
className={cn(
|
|
289
|
+
'py-1 text-xs leading-relaxed max-h-[40vh] overflow-y-auto flex-1 min-w-0',
|
|
290
|
+
labelTop ? 'pl-4 pr-1' : 'pl-2 pr-1',
|
|
291
|
+
wrap ? 'whitespace-pre-wrap break-words' : 'whitespace-pre overflow-x-auto'
|
|
292
|
+
)}
|
|
293
|
+
>
|
|
294
|
+
{children}
|
|
295
|
+
</div>
|
|
296
|
+
)}
|
|
297
|
+
</div>
|
|
298
|
+
);
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
// ── Log renderer ──
|
|
302
|
+
|
|
303
|
+
export function LogRenderer({ text, className }: { text: string; className?: string }) {
|
|
304
|
+
const blocks = useMemo(() => parseLog(text), [text]);
|
|
305
|
+
const [wrap, setWrap] = useState(true);
|
|
306
|
+
const [expandAll, setExpandAll] = useState<boolean | undefined>(undefined);
|
|
307
|
+
const bottomRef = useRef<HTMLDivElement>(null);
|
|
308
|
+
|
|
309
|
+
// Auto-scroll to bottom when new blocks arrive
|
|
310
|
+
useEffect(() => {
|
|
311
|
+
bottomRef.current?.scrollIntoView({ behavior: 'smooth', block: 'nearest' });
|
|
312
|
+
}, [blocks.length]);
|
|
313
|
+
|
|
314
|
+
if (!blocks.length) return <pre className={className}>No output yet</pre>;
|
|
315
|
+
|
|
316
|
+
const hasCollapsible = blocks.some(b => b.type === 'tool' || b.type === 'result' || b.type === 'thinking');
|
|
317
|
+
|
|
318
|
+
return (
|
|
319
|
+
<div className={cn('flex flex-col gap-1', className)}>
|
|
320
|
+
<div className="flex justify-end gap-1 px-1">
|
|
321
|
+
{hasCollapsible && (
|
|
322
|
+
<button
|
|
323
|
+
onClick={() => setExpandAll(prev => !prev)}
|
|
324
|
+
className="text-[11px] text-zinc-600 hover:text-zinc-400 transition-colors font-mono px-1.5 py-0.5 rounded border border-zinc-800/60 hover:border-zinc-700"
|
|
325
|
+
>
|
|
326
|
+
{expandAll ? 'fold' : 'unfold'}
|
|
327
|
+
</button>
|
|
328
|
+
)}
|
|
329
|
+
<button
|
|
330
|
+
onClick={() => setWrap(!wrap)}
|
|
331
|
+
className="text-[11px] text-zinc-600 hover:text-zinc-400 transition-colors font-mono px-1.5 py-0.5 rounded border border-zinc-800/60 hover:border-zinc-700"
|
|
332
|
+
title={wrap ? 'Unwrap lines' : 'Wrap lines'}
|
|
333
|
+
>
|
|
334
|
+
{wrap ? 'unwrap' : 'wrap'}
|
|
335
|
+
</button>
|
|
336
|
+
</div>
|
|
337
|
+
{blocks.map((block, i) => {
|
|
338
|
+
const isRecent = i >= blocks.length - 2;
|
|
339
|
+
|
|
340
|
+
switch (block.type) {
|
|
341
|
+
case 'text':
|
|
342
|
+
return (
|
|
343
|
+
<Md key={i} text={block.content} className="text-zinc-300 text-sm" />
|
|
344
|
+
);
|
|
345
|
+
|
|
346
|
+
case 'tool':
|
|
347
|
+
return (
|
|
348
|
+
<CollapsibleBlock
|
|
349
|
+
key={i}
|
|
350
|
+
wrap={wrap}
|
|
351
|
+
forceOpen={expandAll}
|
|
352
|
+
defaultOpen={isRecent}
|
|
353
|
+
label={<span className="text-sky-400 font-mono truncate">{block.name}{block.time && <span className="text-sky-400/40 ml-2 text-[10px]">{block.time}</span>}</span>}
|
|
354
|
+
labelClass="bg-sky-500/8 hover:bg-sky-500/12 text-sky-400/80"
|
|
355
|
+
preview={truncate(block.input)}
|
|
356
|
+
>
|
|
357
|
+
{isJson(block.input)
|
|
358
|
+
? <JsonTree text={block.input} className="text-zinc-500" />
|
|
359
|
+
: <pre className="text-zinc-500 font-mono text-[11px]">{block.input}</pre>
|
|
360
|
+
}
|
|
361
|
+
</CollapsibleBlock>
|
|
362
|
+
);
|
|
363
|
+
|
|
364
|
+
case 'result':
|
|
365
|
+
return (
|
|
366
|
+
<CollapsibleBlock
|
|
367
|
+
key={i}
|
|
368
|
+
wrap={wrap}
|
|
369
|
+
forceOpen={expandAll}
|
|
370
|
+
defaultOpen={isRecent}
|
|
371
|
+
label={<span className="text-emerald-400">result{block.time && <span className="text-emerald-400/40 ml-2 text-[10px]">{block.time}</span>}</span>}
|
|
372
|
+
labelClass="bg-emerald-500/8 hover:bg-emerald-500/12 text-emerald-400/80"
|
|
373
|
+
preview={truncate(block.content)}
|
|
374
|
+
>
|
|
375
|
+
{isJson(block.content)
|
|
376
|
+
? <JsonTree text={block.content} className="text-zinc-500" />
|
|
377
|
+
: /^\s*\d+→/.test(block.content)
|
|
378
|
+
? <CodeResult text={block.content} />
|
|
379
|
+
: <Md text={block.content} className="text-zinc-400 text-xs" />
|
|
380
|
+
}
|
|
381
|
+
</CollapsibleBlock>
|
|
382
|
+
);
|
|
383
|
+
|
|
384
|
+
case 'thinking':
|
|
385
|
+
return (
|
|
386
|
+
<CollapsibleBlock
|
|
387
|
+
key={i}
|
|
388
|
+
wrap={wrap}
|
|
389
|
+
forceOpen={expandAll}
|
|
390
|
+
defaultOpen={isRecent}
|
|
391
|
+
label={<span className="text-amber-400/70 italic">thinking{block.time && <span className="text-amber-400/40 ml-2 text-[10px] not-italic">{block.time}</span>}</span>}
|
|
392
|
+
labelClass="bg-amber-500/5 hover:bg-amber-500/10 text-amber-400/60"
|
|
393
|
+
preview={truncate(block.content)}
|
|
394
|
+
>
|
|
395
|
+
<Md text={block.content} className="text-zinc-500 italic text-xs" />
|
|
396
|
+
</CollapsibleBlock>
|
|
397
|
+
);
|
|
398
|
+
|
|
399
|
+
case 'interrupted':
|
|
400
|
+
return (
|
|
401
|
+
<div key={i} className="flex items-center gap-2 px-2.5 py-1.5 rounded-lg bg-red-500/10 border border-red-500/20">
|
|
402
|
+
<span className="w-2 h-2 rounded-sm bg-red-400" />
|
|
403
|
+
<span className="text-[11px] text-red-400 font-medium">Interrupted</span>
|
|
404
|
+
{block.time && <span className="text-[10px] text-red-400/40 ml-auto font-mono">{block.time}</span>}
|
|
405
|
+
</div>
|
|
406
|
+
);
|
|
407
|
+
}
|
|
408
|
+
})}
|
|
409
|
+
<div ref={bottomRef} />
|
|
410
|
+
</div>
|
|
411
|
+
);
|
|
412
|
+
}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { cn } from '@treenity/react/lib/utils';
|
|
2
|
+
import dayjs from 'dayjs';
|
|
3
|
+
|
|
4
|
+
// ── Design tokens ──
|
|
5
|
+
|
|
6
|
+
export const STATUS: Record<string, { bg: string; text: string; dot: string }> = {
|
|
7
|
+
pending: { bg: 'bg-amber-500/15', text: 'text-amber-400', dot: 'bg-amber-400' },
|
|
8
|
+
running: { bg: 'bg-sky-500/15', text: 'text-sky-400', dot: 'bg-sky-400 animate-pulse' },
|
|
9
|
+
done: { bg: 'bg-emerald-500/15', text: 'text-emerald-400', dot: 'bg-emerald-400' },
|
|
10
|
+
error: { bg: 'bg-red-500/15', text: 'text-red-400', dot: 'bg-red-400' },
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
export function formatTime(ts: number): string {
|
|
14
|
+
if (!ts) return '';
|
|
15
|
+
const d = dayjs(ts);
|
|
16
|
+
const now = dayjs();
|
|
17
|
+
|
|
18
|
+
// Same day — just time
|
|
19
|
+
if (d.isSame(now, 'day')) return d.format('HH:mm');
|
|
20
|
+
|
|
21
|
+
// This year — date + time
|
|
22
|
+
if (d.isSame(now, 'year')) return d.format('MMM D HH:mm');
|
|
23
|
+
|
|
24
|
+
// Different year
|
|
25
|
+
return d.format('MMM D YYYY HH:mm');
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export function StatusDot({ status }: { status: string }) {
|
|
29
|
+
const s = STATUS[status] || STATUS.pending;
|
|
30
|
+
return <span className={cn('inline-block w-1.5 h-1.5 rounded-full shrink-0', s.dot)} />;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export function StatusBadge({ status }: { status: string }) {
|
|
34
|
+
const s = STATUS[status] || STATUS.pending;
|
|
35
|
+
return (
|
|
36
|
+
<span className={cn('px-2 py-0.5 rounded-full text-[10px] font-semibold tracking-wide uppercase', s.bg, s.text)}>
|
|
37
|
+
{status}
|
|
38
|
+
</span>
|
|
39
|
+
);
|
|
40
|
+
}
|