@treenity/mods 3.0.3 → 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/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/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 +2 -1
|
@@ -0,0 +1,255 @@
|
|
|
1
|
+
import { register } from '@treenity/core';
|
|
2
|
+
import { type View } from '@treenity/react/context';
|
|
3
|
+
import { execute } from '@treenity/react/hooks';
|
|
4
|
+
import { cn } from '@treenity/react/lib/utils';
|
|
5
|
+
import { useCallback, useEffect, useState } from 'react';
|
|
6
|
+
|
|
7
|
+
import type { MetatronTask } from '../types';
|
|
8
|
+
import { LogRenderer, Md } from './log';
|
|
9
|
+
import { formatTime, StatusBadge, StatusDot } from './shared';
|
|
10
|
+
|
|
11
|
+
// ── Stop button ──
|
|
12
|
+
|
|
13
|
+
function StopButton({ taskPath }: { taskPath: string }) {
|
|
14
|
+
const [stopping, setStopping] = useState(false);
|
|
15
|
+
|
|
16
|
+
const handleStop = useCallback(async (e: React.MouseEvent) => {
|
|
17
|
+
e.stopPropagation();
|
|
18
|
+
setStopping(true);
|
|
19
|
+
try {
|
|
20
|
+
await execute(taskPath, 'stop', {});
|
|
21
|
+
} finally {
|
|
22
|
+
setStopping(false);
|
|
23
|
+
}
|
|
24
|
+
}, [taskPath]);
|
|
25
|
+
|
|
26
|
+
return (
|
|
27
|
+
<button
|
|
28
|
+
onClick={handleStop}
|
|
29
|
+
disabled={stopping}
|
|
30
|
+
className="w-5 h-5 rounded flex items-center justify-center bg-red-500/15 hover:bg-red-500/25 transition-all duration-150 shrink-0"
|
|
31
|
+
title="Stop task"
|
|
32
|
+
>
|
|
33
|
+
<span className={cn('w-2 h-2 rounded-[1px] bg-red-400', stopping && 'opacity-50')} />
|
|
34
|
+
</button>
|
|
35
|
+
);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// ── Inject input ──
|
|
39
|
+
|
|
40
|
+
function InjectInput({ taskPath }: { taskPath: string }) {
|
|
41
|
+
const [text, setText] = useState('');
|
|
42
|
+
const [sending, setSending] = useState(false);
|
|
43
|
+
|
|
44
|
+
const handleSend = useCallback(async () => {
|
|
45
|
+
const t = text.trim();
|
|
46
|
+
if (!t) return;
|
|
47
|
+
setSending(true);
|
|
48
|
+
try {
|
|
49
|
+
await execute(taskPath, 'inject', { text: t });
|
|
50
|
+
setText('');
|
|
51
|
+
} finally {
|
|
52
|
+
setSending(false);
|
|
53
|
+
}
|
|
54
|
+
}, [text, taskPath]);
|
|
55
|
+
|
|
56
|
+
return (
|
|
57
|
+
<div className="mt-2 pt-2 border-t border-zinc-800/40">
|
|
58
|
+
<div className="relative">
|
|
59
|
+
<textarea
|
|
60
|
+
value={text}
|
|
61
|
+
onChange={e => setText(e.target.value)}
|
|
62
|
+
onKeyDown={e => { if (e.key === 'Enter' && (e.metaKey || e.ctrlKey)) handleSend(); }}
|
|
63
|
+
rows={1}
|
|
64
|
+
placeholder="Queue a follow-up message..."
|
|
65
|
+
className="w-full bg-zinc-950 border border-zinc-800 rounded-lg px-3 py-2 pr-16 text-xs text-zinc-200 resize-none focus:outline-none focus:border-violet-600/50 placeholder:text-zinc-700"
|
|
66
|
+
/>
|
|
67
|
+
<button
|
|
68
|
+
onClick={handleSend}
|
|
69
|
+
disabled={sending || !text.trim()}
|
|
70
|
+
className={cn(
|
|
71
|
+
'absolute right-1.5 bottom-1.5 px-2.5 py-1 rounded-md text-[10px] font-semibold transition-all duration-150',
|
|
72
|
+
text.trim()
|
|
73
|
+
? 'bg-amber-600 hover:bg-amber-500 text-white'
|
|
74
|
+
: 'bg-zinc-800 text-zinc-600 cursor-not-allowed'
|
|
75
|
+
)}
|
|
76
|
+
>
|
|
77
|
+
{sending ? '...' : 'Queue'}
|
|
78
|
+
</button>
|
|
79
|
+
</div>
|
|
80
|
+
</div>
|
|
81
|
+
);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// ── Task row (used in config lists) ──
|
|
85
|
+
|
|
86
|
+
export function TaskRow({ task }: { task: Record<string, unknown> }) {
|
|
87
|
+
const status = (task.status as string) || 'pending';
|
|
88
|
+
const isRunning = status === 'running';
|
|
89
|
+
const isActive = isRunning || status === 'done';
|
|
90
|
+
const [expanded, setExpanded] = useState(isActive);
|
|
91
|
+
const prompt = (task.prompt as string) || '';
|
|
92
|
+
const result = (task.result as string) || '';
|
|
93
|
+
const taskLog = (task.log as string) || '';
|
|
94
|
+
const hasLog = !!taskLog;
|
|
95
|
+
const [tab, setTab] = useState<'result' | 'log'>(hasLog ? 'log' : 'result');
|
|
96
|
+
const time = task.createdAt ? formatTime(task.createdAt as number) : '';
|
|
97
|
+
|
|
98
|
+
return (
|
|
99
|
+
<div className={cn(
|
|
100
|
+
'rounded-xl border transition-all duration-200',
|
|
101
|
+
expanded ? 'bg-zinc-900/80 border-zinc-800' : 'bg-zinc-900/40 border-zinc-800/60 hover:border-zinc-700'
|
|
102
|
+
)}>
|
|
103
|
+
<div
|
|
104
|
+
className="flex items-center gap-2.5 px-3.5 py-2.5 cursor-pointer select-none"
|
|
105
|
+
onClick={() => setExpanded(!expanded)}
|
|
106
|
+
>
|
|
107
|
+
<StatusDot status={status} />
|
|
108
|
+
<span className="text-sm text-zinc-300 truncate flex-1">{prompt}</span>
|
|
109
|
+
{isRunning && <StopButton taskPath={task.$path as string} />}
|
|
110
|
+
<span className="text-[10px] text-zinc-600 shrink-0 font-mono">{time}</span>
|
|
111
|
+
<svg
|
|
112
|
+
width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2"
|
|
113
|
+
className={cn('text-zinc-600 transition-transform duration-200 shrink-0', expanded && 'rotate-90')}
|
|
114
|
+
>
|
|
115
|
+
<path d="M9 18l6-6-6-6" />
|
|
116
|
+
</svg>
|
|
117
|
+
</div>
|
|
118
|
+
|
|
119
|
+
{expanded && (
|
|
120
|
+
<div className="border-t border-zinc-800/60">
|
|
121
|
+
<div className="flex gap-1 px-2 py-1">
|
|
122
|
+
{(['log', 'result'] as const).map(t => (
|
|
123
|
+
<button
|
|
124
|
+
key={t}
|
|
125
|
+
onClick={() => setTab(t)}
|
|
126
|
+
className={cn(
|
|
127
|
+
'px-2 py-0.5 rounded-md text-[11px] font-medium transition-all duration-150',
|
|
128
|
+
tab === t
|
|
129
|
+
? 'bg-zinc-800 text-zinc-200'
|
|
130
|
+
: 'text-zinc-600 hover:text-zinc-400'
|
|
131
|
+
)}
|
|
132
|
+
>
|
|
133
|
+
{t}
|
|
134
|
+
</button>
|
|
135
|
+
))}
|
|
136
|
+
</div>
|
|
137
|
+
<div className="max-h-[60vh] overflow-y-auto">
|
|
138
|
+
<LogRenderer text={tab === 'result' ? (result || 'No result yet') : (taskLog || result || 'No log yet')} />
|
|
139
|
+
</div>
|
|
140
|
+
|
|
141
|
+
{isRunning && <InjectInput taskPath={task.$path as string} />}
|
|
142
|
+
</div>
|
|
143
|
+
)}
|
|
144
|
+
</div>
|
|
145
|
+
);
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
// ── Running indicator — animated ping ──
|
|
149
|
+
|
|
150
|
+
function RunningPing({ color = 'sky' }: { color?: 'sky' | 'emerald' | 'amber' }) {
|
|
151
|
+
const colors = {
|
|
152
|
+
sky: { ping: 'bg-sky-400', dot: 'bg-sky-500 shadow-sky-500/50' },
|
|
153
|
+
emerald: { ping: 'bg-emerald-400', dot: 'bg-emerald-500 shadow-emerald-500/50' },
|
|
154
|
+
amber: { ping: 'bg-amber-400', dot: 'bg-amber-500 shadow-amber-500/50' },
|
|
155
|
+
};
|
|
156
|
+
const c = colors[color];
|
|
157
|
+
return (
|
|
158
|
+
<span className="relative flex h-3 w-3 shrink-0">
|
|
159
|
+
<span className={cn('animate-ping absolute inline-flex h-full w-full rounded-full opacity-75', c.ping)} />
|
|
160
|
+
<span className={cn('relative inline-flex h-3 w-3 rounded-full shadow-lg', c.dot)} />
|
|
161
|
+
</span>
|
|
162
|
+
);
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
// ── Elapsed time hook ──
|
|
166
|
+
// XXX: should be two different operations, n ot one. use elapset return just number, and then it formatted as wanted
|
|
167
|
+
function useElapsed(startTs: number, active: boolean): string {
|
|
168
|
+
const [now, setNow] = useState(Date.now());
|
|
169
|
+
useEffect(() => {
|
|
170
|
+
if (!active || !startTs) return;
|
|
171
|
+
const id = setInterval(() => setNow(Date.now()), 1000);
|
|
172
|
+
return () => clearInterval(id);
|
|
173
|
+
}, [active, startTs]);
|
|
174
|
+
|
|
175
|
+
if (!startTs) return '';
|
|
176
|
+
const s = Math.floor(((active ? now : Date.now()) - startTs) / 1000);
|
|
177
|
+
if (s < 60) return `${s}s`;
|
|
178
|
+
if (s < 3600) return `${Math.floor(s / 60)}m ${s % 60}s`;
|
|
179
|
+
return `${Math.floor(s / 3600)}h ${Math.floor((s % 3600) / 60)}m`;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
// ── Task view (individual task page /t/.../tasks/xxx) ──
|
|
183
|
+
|
|
184
|
+
const TaskView: View<MetatronTask> = ({ value, ctx }) => {
|
|
185
|
+
const path = ctx!.path;
|
|
186
|
+
const status = value.status || 'pending';
|
|
187
|
+
const isRunning = status === 'running';
|
|
188
|
+
const result = value.result || '';
|
|
189
|
+
const taskLog = value.log || '';
|
|
190
|
+
const [tab, setTab] = useState<'result' | 'log'>(taskLog ? 'log' : 'result');
|
|
191
|
+
const elapsed = useElapsed(value.createdAt, isRunning);
|
|
192
|
+
|
|
193
|
+
return (
|
|
194
|
+
<div className="flex flex-col gap-5 p-5 max-w-4xl">
|
|
195
|
+
|
|
196
|
+
{/* ── Header ── */}
|
|
197
|
+
<div className="flex items-center gap-3">
|
|
198
|
+
{isRunning ? <RunningPing /> : <StatusDot status={status} />}
|
|
199
|
+
<StatusBadge status={status} />
|
|
200
|
+
|
|
201
|
+
<div className="flex items-center gap-2 ml-auto">
|
|
202
|
+
{elapsed && (
|
|
203
|
+
<span className={cn(
|
|
204
|
+
'text-[10px] font-mono tabular-nums',
|
|
205
|
+
isRunning ? 'text-sky-400/70' : 'text-zinc-600',
|
|
206
|
+
)}>
|
|
207
|
+
{elapsed}
|
|
208
|
+
</span>
|
|
209
|
+
)}
|
|
210
|
+
<span className="text-[10px] text-zinc-600 font-mono tabular-nums">
|
|
211
|
+
{value.createdAt ? formatTime(value.createdAt) : ''}
|
|
212
|
+
</span>
|
|
213
|
+
{isRunning && <StopButton taskPath={path} />}
|
|
214
|
+
</div>
|
|
215
|
+
</div>
|
|
216
|
+
|
|
217
|
+
{/* ── Prompt ── */}
|
|
218
|
+
<div className="bg-zinc-900/40 rounded-xl px-4 py-3 border border-zinc-800/40">
|
|
219
|
+
<Md className="text-sm text-zinc-300 leading-relaxed" text={value.prompt} />
|
|
220
|
+
</div>
|
|
221
|
+
|
|
222
|
+
{/* ── Tabs ── */}
|
|
223
|
+
<div className="flex gap-0.5 bg-zinc-900/40 rounded-lg p-0.5 w-fit">
|
|
224
|
+
{(['log', 'result'] as const).map(t => (
|
|
225
|
+
<button
|
|
226
|
+
key={t}
|
|
227
|
+
onClick={() => setTab(t)}
|
|
228
|
+
className={cn(
|
|
229
|
+
'px-3.5 py-1 rounded-md text-xs font-medium transition-all duration-150',
|
|
230
|
+
tab === t
|
|
231
|
+
? 'bg-zinc-800 text-zinc-200 shadow-sm'
|
|
232
|
+
: 'text-zinc-500 hover:text-zinc-300',
|
|
233
|
+
)}
|
|
234
|
+
>
|
|
235
|
+
{t}
|
|
236
|
+
</button>
|
|
237
|
+
))}
|
|
238
|
+
</div>
|
|
239
|
+
|
|
240
|
+
{/* ── Log / Result ── */}
|
|
241
|
+
<div className={cn(
|
|
242
|
+
'rounded-xl px-3 py-2.5 max-h-[75vh] overflow-y-auto border transition-all duration-300',
|
|
243
|
+
isRunning
|
|
244
|
+
? 'bg-zinc-950 border-sky-500/15 shadow-lg shadow-sky-500/5'
|
|
245
|
+
: 'bg-zinc-950 border-zinc-800',
|
|
246
|
+
)}>
|
|
247
|
+
<LogRenderer text={tab === 'result' ? (result || 'No result yet') : (taskLog || result || 'No log yet')} />
|
|
248
|
+
</div>
|
|
249
|
+
|
|
250
|
+
{isRunning && <InjectInput taskPath={path} />}
|
|
251
|
+
</div>
|
|
252
|
+
);
|
|
253
|
+
};
|
|
254
|
+
|
|
255
|
+
register('metatron.task', 'react', TaskView);
|
|
@@ -0,0 +1,418 @@
|
|
|
1
|
+
import { register } from '@treenity/core';
|
|
2
|
+
import { Render, type View } from '@treenity/react/context';
|
|
3
|
+
import { execute, useChildren, usePath } from '@treenity/react/hooks';
|
|
4
|
+
import { cn } from '@treenity/react/lib/utils';
|
|
5
|
+
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
|
6
|
+
|
|
7
|
+
import { MetatronWorkspace } from '../types';
|
|
8
|
+
import { LogRenderer } from './log';
|
|
9
|
+
import { formatTime, StatusDot } from './shared';
|
|
10
|
+
|
|
11
|
+
// ── Types ──
|
|
12
|
+
|
|
13
|
+
type SessionInfo = { id: string; date: string; messageCount: number; firstMessage: string };
|
|
14
|
+
type SearchResult = { sessionId: string; date: string; role: string; snippet: string; score: number };
|
|
15
|
+
|
|
16
|
+
// ── Command Picker (modal overlay for adding columns) ──
|
|
17
|
+
|
|
18
|
+
function CommandPicker({ configPath, exclude, onSelect, onNewTask, onClose }: {
|
|
19
|
+
configPath: string;
|
|
20
|
+
exclude: string[];
|
|
21
|
+
onSelect: (ref: string) => void;
|
|
22
|
+
onNewTask: (prompt: string) => void;
|
|
23
|
+
onClose: () => void;
|
|
24
|
+
}) {
|
|
25
|
+
const [query, setQuery] = useState('');
|
|
26
|
+
const [sessions, setSessions] = useState<SessionInfo[]>([]);
|
|
27
|
+
const [searchResults, setSearchResults] = useState<SearchResult[]>([]);
|
|
28
|
+
const [searching, setSearching] = useState(false);
|
|
29
|
+
const [newTaskPrompt, setNewTaskPrompt] = useState('');
|
|
30
|
+
const inputRef = useRef<HTMLInputElement>(null);
|
|
31
|
+
const backdropRef = useRef<HTMLDivElement>(null);
|
|
32
|
+
|
|
33
|
+
const allTasks = useChildren(`${configPath}/tasks`, { watch: true });
|
|
34
|
+
const tasks = (allTasks ?? [])
|
|
35
|
+
.filter(t => t.$type === 'metatron.task' && !exclude.includes(t.$path as string))
|
|
36
|
+
.sort((a, b) => ((b.createdAt as number) || 0) - ((a.createdAt as number) || 0));
|
|
37
|
+
|
|
38
|
+
// Load sessions on mount
|
|
39
|
+
useEffect(() => {
|
|
40
|
+
execute(configPath, 'listSessions', { lastN: 50 }).then(r => {
|
|
41
|
+
if (Array.isArray(r)) setSessions(r);
|
|
42
|
+
});
|
|
43
|
+
}, [configPath]);
|
|
44
|
+
|
|
45
|
+
// Auto-focus
|
|
46
|
+
useEffect(() => { inputRef.current?.focus(); }, []);
|
|
47
|
+
|
|
48
|
+
// Close on Escape
|
|
49
|
+
useEffect(() => {
|
|
50
|
+
const handler = (e: KeyboardEvent) => { if (e.key === 'Escape') onClose(); };
|
|
51
|
+
window.addEventListener('keydown', handler);
|
|
52
|
+
return () => window.removeEventListener('keydown', handler);
|
|
53
|
+
}, [onClose]);
|
|
54
|
+
|
|
55
|
+
// Debounced search
|
|
56
|
+
useEffect(() => {
|
|
57
|
+
if (!query.trim()) { setSearchResults([]); return; }
|
|
58
|
+
const timer = setTimeout(async () => {
|
|
59
|
+
setSearching(true);
|
|
60
|
+
try {
|
|
61
|
+
const r = await execute(configPath, 'searchSessions', { query: query.trim(), maxResults: 20 });
|
|
62
|
+
if (Array.isArray(r)) setSearchResults(r);
|
|
63
|
+
} finally {
|
|
64
|
+
setSearching(false);
|
|
65
|
+
}
|
|
66
|
+
}, 300);
|
|
67
|
+
return () => clearTimeout(timer);
|
|
68
|
+
}, [query, configPath]);
|
|
69
|
+
|
|
70
|
+
const isSearching = !!query.trim();
|
|
71
|
+
|
|
72
|
+
const filteredTasks = isSearching
|
|
73
|
+
? tasks.filter(t => String(t.prompt ?? '').toLowerCase().includes(query.toLowerCase()))
|
|
74
|
+
: tasks;
|
|
75
|
+
|
|
76
|
+
const filteredSessions = isSearching
|
|
77
|
+
? sessions.filter(s => s.firstMessage.toLowerCase().includes(query.toLowerCase()))
|
|
78
|
+
: sessions;
|
|
79
|
+
|
|
80
|
+
const handleNewTask = () => {
|
|
81
|
+
const text = newTaskPrompt.trim();
|
|
82
|
+
if (!text) return;
|
|
83
|
+
onNewTask(text);
|
|
84
|
+
onClose();
|
|
85
|
+
};
|
|
86
|
+
|
|
87
|
+
return (
|
|
88
|
+
<div
|
|
89
|
+
ref={backdropRef}
|
|
90
|
+
className="fixed inset-0 z-50 flex items-start justify-center pt-[15vh] bg-black/60 backdrop-blur-sm"
|
|
91
|
+
onClick={e => { if (e.target === backdropRef.current) onClose(); }}
|
|
92
|
+
>
|
|
93
|
+
<div className="w-full max-w-lg bg-zinc-900 border border-zinc-800 rounded-2xl shadow-2xl shadow-black/40 overflow-hidden flex flex-col max-h-[60vh]">
|
|
94
|
+
{/* Search */}
|
|
95
|
+
<div className="flex items-center gap-2 px-4 py-3 border-b border-zinc-800">
|
|
96
|
+
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" className="text-zinc-500 shrink-0">
|
|
97
|
+
<circle cx="11" cy="11" r="8" /><path d="M21 21l-4.35-4.35" />
|
|
98
|
+
</svg>
|
|
99
|
+
<input
|
|
100
|
+
ref={inputRef}
|
|
101
|
+
value={query}
|
|
102
|
+
onChange={e => setQuery(e.target.value)}
|
|
103
|
+
placeholder="Search tasks and sessions..."
|
|
104
|
+
className="flex-1 bg-transparent text-sm text-zinc-200 placeholder:text-zinc-600 focus:outline-none"
|
|
105
|
+
/>
|
|
106
|
+
{searching && (
|
|
107
|
+
<span className="text-[10px] text-zinc-600 animate-pulse">searching...</span>
|
|
108
|
+
)}
|
|
109
|
+
<button onClick={onClose} className="text-zinc-600 hover:text-zinc-400 transition-colors">
|
|
110
|
+
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2"><path d="M18 6L6 18M6 6l12 12"/></svg>
|
|
111
|
+
</button>
|
|
112
|
+
</div>
|
|
113
|
+
|
|
114
|
+
<div className="overflow-y-auto flex-1">
|
|
115
|
+
{/* New task */}
|
|
116
|
+
{!isSearching && (
|
|
117
|
+
<div className="flex flex-col gap-2 px-4 py-2.5 border-b border-zinc-800/60">
|
|
118
|
+
<textarea
|
|
119
|
+
value={newTaskPrompt}
|
|
120
|
+
onChange={e => setNewTaskPrompt(e.target.value)}
|
|
121
|
+
onKeyDown={e => { if (e.key === 'Enter' && (e.metaKey || e.ctrlKey) && newTaskPrompt.trim()) handleNewTask(); }}
|
|
122
|
+
rows={2}
|
|
123
|
+
placeholder="New task prompt..."
|
|
124
|
+
className="w-full bg-zinc-950 border border-zinc-800 rounded-lg px-3 py-2 text-xs text-zinc-200 resize-none focus:outline-none focus:border-violet-600/50 placeholder:text-zinc-700"
|
|
125
|
+
/>
|
|
126
|
+
<div className="flex items-center gap-2">
|
|
127
|
+
<button
|
|
128
|
+
onClick={handleNewTask}
|
|
129
|
+
disabled={!newTaskPrompt.trim()}
|
|
130
|
+
className="px-3 py-1.5 rounded-lg text-[11px] font-medium bg-violet-600 hover:bg-violet-500 text-white disabled:bg-zinc-800 disabled:text-zinc-600 transition-all shrink-0"
|
|
131
|
+
>
|
|
132
|
+
Create & add
|
|
133
|
+
</button>
|
|
134
|
+
<span className="text-[10px] text-zinc-700">Cmd+Enter</span>
|
|
135
|
+
</div>
|
|
136
|
+
</div>
|
|
137
|
+
)}
|
|
138
|
+
|
|
139
|
+
{/* Search results (when searching) */}
|
|
140
|
+
{isSearching && searchResults.length > 0 && (
|
|
141
|
+
<div className="py-1">
|
|
142
|
+
<div className="px-4 py-1.5">
|
|
143
|
+
<span className="text-[10px] text-zinc-600 uppercase tracking-wider font-medium">
|
|
144
|
+
Session matches ({searchResults.length})
|
|
145
|
+
</span>
|
|
146
|
+
</div>
|
|
147
|
+
{searchResults.map((r, i) => (
|
|
148
|
+
<button
|
|
149
|
+
key={`sr-${i}`}
|
|
150
|
+
onClick={() => { onSelect(`session:${r.sessionId}`); onClose(); }}
|
|
151
|
+
className="flex flex-col gap-1 w-full px-4 py-2.5 text-left hover:bg-zinc-800/60 transition-colors"
|
|
152
|
+
>
|
|
153
|
+
<div className="flex items-center gap-2">
|
|
154
|
+
<span className="text-[10px] text-zinc-600 font-mono">{r.date}</span>
|
|
155
|
+
<span className={cn(
|
|
156
|
+
'text-[10px] font-medium',
|
|
157
|
+
r.role === 'user' ? 'text-sky-500' : 'text-emerald-500'
|
|
158
|
+
)}>
|
|
159
|
+
{r.role}
|
|
160
|
+
</span>
|
|
161
|
+
<span className="text-[10px] text-zinc-700 ml-auto font-mono">{r.score.toFixed(1)}</span>
|
|
162
|
+
</div>
|
|
163
|
+
<span className="text-xs text-zinc-400 line-clamp-2 leading-relaxed">{r.snippet}</span>
|
|
164
|
+
</button>
|
|
165
|
+
))}
|
|
166
|
+
</div>
|
|
167
|
+
)}
|
|
168
|
+
|
|
169
|
+
{/* Tasks */}
|
|
170
|
+
{filteredTasks.length > 0 && (
|
|
171
|
+
<div className="py-1">
|
|
172
|
+
<div className="px-4 py-1.5">
|
|
173
|
+
<span className="text-[10px] text-zinc-600 uppercase tracking-wider font-medium">Tasks</span>
|
|
174
|
+
</div>
|
|
175
|
+
{filteredTasks.map(t => (
|
|
176
|
+
<button
|
|
177
|
+
key={t.$path}
|
|
178
|
+
onClick={() => { onSelect(t.$path as string); onClose(); }}
|
|
179
|
+
className="flex items-center gap-2.5 w-full px-4 py-2 text-left hover:bg-zinc-800/60 transition-colors"
|
|
180
|
+
>
|
|
181
|
+
<StatusDot status={(t.status as string) || 'pending'} />
|
|
182
|
+
<span className="text-xs text-zinc-300 truncate flex-1">{String(t.prompt ?? '').slice(0, 80)}</span>
|
|
183
|
+
<span className="text-[10px] text-zinc-700 font-mono shrink-0">
|
|
184
|
+
{t.createdAt ? formatTime(t.createdAt as number) : ''}
|
|
185
|
+
</span>
|
|
186
|
+
</button>
|
|
187
|
+
))}
|
|
188
|
+
</div>
|
|
189
|
+
)}
|
|
190
|
+
|
|
191
|
+
{/* Sessions (browse mode) */}
|
|
192
|
+
{!isSearching && filteredSessions.length > 0 && (
|
|
193
|
+
<div className="py-1 border-t border-zinc-800/60">
|
|
194
|
+
<div className="px-4 py-1.5">
|
|
195
|
+
<span className="text-[10px] text-zinc-600 uppercase tracking-wider font-medium">Claude Code sessions</span>
|
|
196
|
+
</div>
|
|
197
|
+
{filteredSessions.slice(0, 20).map(s => (
|
|
198
|
+
<button
|
|
199
|
+
key={s.id}
|
|
200
|
+
onClick={() => { onSelect(`session:${s.id}`); onClose(); }}
|
|
201
|
+
className="flex items-center gap-2.5 w-full px-4 py-2 text-left hover:bg-zinc-800/60 transition-colors"
|
|
202
|
+
>
|
|
203
|
+
<span className="w-1.5 h-1.5 rounded-full bg-zinc-600 shrink-0" />
|
|
204
|
+
<span className="text-xs text-zinc-400 truncate flex-1">{s.firstMessage}</span>
|
|
205
|
+
<span className="text-[10px] text-zinc-700 font-mono shrink-0">{s.messageCount}m</span>
|
|
206
|
+
<span className="text-[10px] text-zinc-700 font-mono shrink-0">{s.date.slice(5)}</span>
|
|
207
|
+
</button>
|
|
208
|
+
))}
|
|
209
|
+
</div>
|
|
210
|
+
)}
|
|
211
|
+
|
|
212
|
+
{/* Empty state */}
|
|
213
|
+
{filteredTasks.length === 0 && filteredSessions.length === 0 && searchResults.length === 0 && (
|
|
214
|
+
<div className="px-4 py-8 text-center text-xs text-zinc-600">
|
|
215
|
+
{isSearching ? 'No matches found' : 'No tasks or sessions available'}
|
|
216
|
+
</div>
|
|
217
|
+
)}
|
|
218
|
+
</div>
|
|
219
|
+
</div>
|
|
220
|
+
</div>
|
|
221
|
+
);
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
// ── Session column viewer ──
|
|
225
|
+
|
|
226
|
+
function SessionColumn({ sessionId, configPath }: { sessionId: string; configPath: string }) {
|
|
227
|
+
const [text, setText] = useState('');
|
|
228
|
+
const [loading, setLoading] = useState(true);
|
|
229
|
+
|
|
230
|
+
useEffect(() => {
|
|
231
|
+
execute(configPath, 'readSession', { sessionId, maxLength: 50000 }).then(r => {
|
|
232
|
+
setText(typeof r === 'string' ? r : JSON.stringify(r));
|
|
233
|
+
setLoading(false);
|
|
234
|
+
});
|
|
235
|
+
}, [sessionId, configPath]);
|
|
236
|
+
|
|
237
|
+
if (loading) {
|
|
238
|
+
return <div className="p-4 text-zinc-600 text-xs animate-pulse">Loading session {sessionId}...</div>;
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
const blocks = text.split(/\n\n(?=\[(?:user|assistant)\])/).filter(Boolean);
|
|
242
|
+
|
|
243
|
+
return (
|
|
244
|
+
<div className="flex flex-col gap-2 p-3 overflow-y-auto h-full">
|
|
245
|
+
{blocks.map((block, i) => {
|
|
246
|
+
const match = block.match(/^\[(user|assistant)\]\s*/);
|
|
247
|
+
if (!match) return <pre key={i} className="text-[11px] text-zinc-500 whitespace-pre-wrap">{block}</pre>;
|
|
248
|
+
|
|
249
|
+
const role = match[1];
|
|
250
|
+
const content = block.slice(match[0].length);
|
|
251
|
+
|
|
252
|
+
return (
|
|
253
|
+
<div
|
|
254
|
+
key={i}
|
|
255
|
+
className={cn(
|
|
256
|
+
'rounded-lg px-3 py-2',
|
|
257
|
+
role === 'user'
|
|
258
|
+
? 'bg-zinc-800/80 text-zinc-300 ml-6'
|
|
259
|
+
: 'bg-zinc-900/80 text-zinc-400 mr-4'
|
|
260
|
+
)}
|
|
261
|
+
>
|
|
262
|
+
<span className={cn(
|
|
263
|
+
'text-[9px] uppercase tracking-wider font-semibold mb-1 block',
|
|
264
|
+
role === 'user' ? 'text-sky-500/70' : 'text-emerald-500/70'
|
|
265
|
+
)}>
|
|
266
|
+
{role}
|
|
267
|
+
</span>
|
|
268
|
+
<LogRenderer text={content} />
|
|
269
|
+
</div>
|
|
270
|
+
);
|
|
271
|
+
})}
|
|
272
|
+
</div>
|
|
273
|
+
);
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
// ── Workspace Column ──
|
|
277
|
+
|
|
278
|
+
function WorkspaceColumn({ columnRef, configPath, onRemove }: {
|
|
279
|
+
columnRef: string;
|
|
280
|
+
configPath: string;
|
|
281
|
+
onRemove: () => void;
|
|
282
|
+
}) {
|
|
283
|
+
const isSession = columnRef.startsWith('session:');
|
|
284
|
+
const sessionId = isSession ? columnRef.slice(8) : '';
|
|
285
|
+
const node = usePath(isSession ? '' : columnRef);
|
|
286
|
+
|
|
287
|
+
const label = isSession
|
|
288
|
+
? `session ${sessionId}`
|
|
289
|
+
: (node ? String((node as Record<string, unknown>).prompt ?? '').slice(0, 40) || columnRef.split('/').at(-1) : columnRef.split('/').at(-1));
|
|
290
|
+
|
|
291
|
+
const status = !isSession && node ? ((node as Record<string, unknown>).status as string) || 'pending' : '';
|
|
292
|
+
|
|
293
|
+
return (
|
|
294
|
+
<div className="flex-1 min-w-[340px] max-w-[50vw] flex flex-col border-r border-zinc-800 last:border-r-0">
|
|
295
|
+
{/* Column header */}
|
|
296
|
+
<div className="flex items-center gap-2 px-3 py-2 bg-zinc-950 border-b border-zinc-800 shrink-0">
|
|
297
|
+
{isSession ? (
|
|
298
|
+
<span className="w-1.5 h-1.5 rounded-full bg-zinc-500 shrink-0" />
|
|
299
|
+
) : (
|
|
300
|
+
status && <StatusDot status={status} />
|
|
301
|
+
)}
|
|
302
|
+
<span className="text-[11px] text-zinc-400 truncate flex-1 font-mono">{label}</span>
|
|
303
|
+
<button
|
|
304
|
+
onClick={onRemove}
|
|
305
|
+
className="text-zinc-700 hover:text-red-400 transition-colors duration-200 p-0.5"
|
|
306
|
+
title="Remove column"
|
|
307
|
+
>
|
|
308
|
+
<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2"><path d="M18 6L6 18M6 6l12 12"/></svg>
|
|
309
|
+
</button>
|
|
310
|
+
</div>
|
|
311
|
+
|
|
312
|
+
{/* Column content */}
|
|
313
|
+
<div className="flex-1 overflow-y-auto min-h-0">
|
|
314
|
+
{isSession ? (
|
|
315
|
+
<SessionColumn sessionId={sessionId} configPath={configPath} />
|
|
316
|
+
) : node ? (
|
|
317
|
+
<Render value={node} />
|
|
318
|
+
) : (
|
|
319
|
+
<div className="p-3 text-[11px] text-zinc-700 animate-pulse">Loading...</div>
|
|
320
|
+
)}
|
|
321
|
+
</div>
|
|
322
|
+
</div>
|
|
323
|
+
);
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
// ── Workspace View ──
|
|
327
|
+
|
|
328
|
+
const WorkspaceView: View<MetatronWorkspace> = ({ value, ctx }) => {
|
|
329
|
+
const [showPicker, setShowPicker] = useState(false);
|
|
330
|
+
const path = ctx!.path;
|
|
331
|
+
const ws = usePath(path, MetatronWorkspace);
|
|
332
|
+
|
|
333
|
+
const configPath = useMemo(() => path.replace(/\/workspaces\/[^/]+$/, ''), [path]);
|
|
334
|
+
const columns = value.columns;
|
|
335
|
+
|
|
336
|
+
const handleRemoveColumn = useCallback(async (taskPath: string) => {
|
|
337
|
+
await ws.removeColumn({ taskPath });
|
|
338
|
+
}, [ws]);
|
|
339
|
+
|
|
340
|
+
const handleAddColumn = useCallback(async (ref: string) => {
|
|
341
|
+
await ws.addColumn({ taskPath: ref });
|
|
342
|
+
}, [ws]);
|
|
343
|
+
|
|
344
|
+
const handleNewTask = useCallback(async (prompt: string) => {
|
|
345
|
+
// task action is on config service, not a typed class method
|
|
346
|
+
const result = await execute(configPath, 'task', { prompt });
|
|
347
|
+
if (result && typeof result === 'object' && 'taskPath' in result) {
|
|
348
|
+
await ws.addColumn({ taskPath: (result as { taskPath: string }).taskPath });
|
|
349
|
+
}
|
|
350
|
+
}, [configPath, ws]);
|
|
351
|
+
|
|
352
|
+
if (columns.length === 0) {
|
|
353
|
+
return (
|
|
354
|
+
<div className="flex flex-col items-center justify-center gap-4 h-64">
|
|
355
|
+
<div className="w-10 h-10 rounded-xl bg-zinc-800 flex items-center justify-center">
|
|
356
|
+
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.5" className="text-zinc-600">
|
|
357
|
+
<rect x="3" y="3" width="7" height="18" rx="1.5" /><rect x="14" y="3" width="7" height="18" rx="1.5" />
|
|
358
|
+
</svg>
|
|
359
|
+
</div>
|
|
360
|
+
<div className="text-center">
|
|
361
|
+
<h2 className="text-sm font-medium text-zinc-300">{String(value.name) || 'Workspace'}</h2>
|
|
362
|
+
<p className="text-[11px] text-zinc-600 mt-0.5">Add columns to compare tasks side by side</p>
|
|
363
|
+
</div>
|
|
364
|
+
<button
|
|
365
|
+
onClick={() => setShowPicker(true)}
|
|
366
|
+
className="px-4 py-2 rounded-lg text-xs font-medium bg-violet-600 hover:bg-violet-500 text-white shadow-sm shadow-violet-600/20 transition-all duration-200"
|
|
367
|
+
>
|
|
368
|
+
Add column
|
|
369
|
+
</button>
|
|
370
|
+
{showPicker && (
|
|
371
|
+
<CommandPicker
|
|
372
|
+
configPath={configPath}
|
|
373
|
+
exclude={columns}
|
|
374
|
+
onSelect={handleAddColumn}
|
|
375
|
+
onNewTask={handleNewTask}
|
|
376
|
+
onClose={() => setShowPicker(false)}
|
|
377
|
+
/>
|
|
378
|
+
)}
|
|
379
|
+
</div>
|
|
380
|
+
);
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
return (
|
|
384
|
+
<div className="flex h-full w-full overflow-x-auto">
|
|
385
|
+
{columns.map(ref => (
|
|
386
|
+
<WorkspaceColumn
|
|
387
|
+
key={ref}
|
|
388
|
+
columnRef={ref}
|
|
389
|
+
configPath={configPath}
|
|
390
|
+
onRemove={() => handleRemoveColumn(ref)}
|
|
391
|
+
/>
|
|
392
|
+
))}
|
|
393
|
+
|
|
394
|
+
{/* Add column button */}
|
|
395
|
+
<div className="flex items-center justify-center w-12 shrink-0 border-l border-zinc-800/60">
|
|
396
|
+
<button
|
|
397
|
+
onClick={() => setShowPicker(true)}
|
|
398
|
+
className="w-8 h-8 rounded-lg flex items-center justify-center text-zinc-700 hover:text-violet-400 hover:bg-violet-500/10 transition-all duration-200"
|
|
399
|
+
title="Add column"
|
|
400
|
+
>
|
|
401
|
+
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2"><path d="M12 5v14M5 12h14"/></svg>
|
|
402
|
+
</button>
|
|
403
|
+
</div>
|
|
404
|
+
|
|
405
|
+
{showPicker && (
|
|
406
|
+
<CommandPicker
|
|
407
|
+
configPath={configPath}
|
|
408
|
+
exclude={columns}
|
|
409
|
+
onSelect={handleAddColumn}
|
|
410
|
+
onNewTask={handleNewTask}
|
|
411
|
+
onClose={() => setShowPicker(false)}
|
|
412
|
+
/>
|
|
413
|
+
)}
|
|
414
|
+
</div>
|
|
415
|
+
);
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
register('metatron.workspace', 'react', WorkspaceView);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@treenity/mods",
|
|
3
|
-
"version": "3.0.
|
|
3
|
+
"version": "3.0.4",
|
|
4
4
|
"description": "Official Treenity modules — board, sim, cafe, doc, mindmap, and more.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"publishConfig": {
|
|
@@ -46,6 +46,7 @@
|
|
|
46
46
|
"doc",
|
|
47
47
|
"launcher",
|
|
48
48
|
"mcp",
|
|
49
|
+
"metatron",
|
|
49
50
|
"mindmap",
|
|
50
51
|
"sensor-demo",
|
|
51
52
|
"sensor-generator",
|