opentradex 0.1.3 → 0.1.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/.env.example +8 -0
- package/README.md +3 -1
- package/gossip/__pycache__/polymarket.cpython-314.pyc +0 -0
- package/main.py +38 -7
- package/package.json +1 -1
- package/src/catalog.mjs +76 -4
- package/src/cli.mjs +44 -18
- package/src/index.mjs +433 -19
- package/web/next-env.d.ts +1 -1
- package/web/src/app/api/workspace/route.ts +12 -0
- package/web/src/app/globals.css +28 -0
- package/web/src/app/guide/page.tsx +262 -0
- package/web/src/app/layout.tsx +2 -1
- package/web/src/app/page.tsx +15 -0
- package/web/src/components/DashboardApp.tsx +192 -88
- package/web/src/components/HarnessBootPanel.tsx +160 -0
- package/web/src/components/LiveStream.tsx +632 -255
- package/web/src/components/TopBar.tsx +135 -82
- package/web/src/lib/demo-data.ts +25 -0
- package/web/src/lib/trading-guide-content.ts +337 -0
- package/web/src/lib/types.ts +30 -0
- package/web/src/lib/workspace.ts +117 -0
|
@@ -1,120 +1,562 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
|
|
3
|
-
import {
|
|
4
|
-
import { ScrollArea } from "@/components/ui/scroll-area";
|
|
3
|
+
import { useEffect, useMemo, useRef, useState } from "react";
|
|
5
4
|
import ReactMarkdown from "react-markdown";
|
|
6
5
|
import remarkGfm from "remark-gfm";
|
|
7
6
|
import {
|
|
7
|
+
CheckCircle2,
|
|
8
|
+
ChevronDown,
|
|
9
|
+
ChevronRight,
|
|
10
|
+
Link2,
|
|
11
|
+
Radio,
|
|
8
12
|
Send,
|
|
13
|
+
ShieldCheck,
|
|
9
14
|
Terminal,
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
CheckCircle2,
|
|
15
|
+
TrendingUp,
|
|
16
|
+
Waves,
|
|
13
17
|
} from "lucide-react";
|
|
14
|
-
import
|
|
18
|
+
import { ScrollArea } from "@/components/ui/scroll-area";
|
|
19
|
+
import { HarnessBootPanel } from "@/components/HarnessBootPanel";
|
|
20
|
+
import type { PromptEntry, StreamLine, WorkspaceSummary } from "@/lib/types";
|
|
15
21
|
|
|
16
22
|
interface LiveStreamProps {
|
|
17
23
|
lines: StreamLine[];
|
|
18
24
|
liveStatus: string;
|
|
19
25
|
customPrompt: string;
|
|
20
|
-
|
|
21
|
-
|
|
26
|
+
prompts: PromptEntry[];
|
|
27
|
+
workspace: WorkspaceSummary | null;
|
|
28
|
+
onCustomPromptChange: (value: string) => void;
|
|
29
|
+
onSendCommand: (prompt: string, channel: string) => void;
|
|
22
30
|
}
|
|
23
31
|
|
|
32
|
+
type ChannelId =
|
|
33
|
+
| "all"
|
|
34
|
+
| "command"
|
|
35
|
+
| "markets"
|
|
36
|
+
| "feeds"
|
|
37
|
+
| "risk"
|
|
38
|
+
| "execution"
|
|
39
|
+
| "tradingview";
|
|
40
|
+
|
|
41
|
+
type GroupedLine =
|
|
42
|
+
| { kind: "text"; text: string }
|
|
43
|
+
| { kind: "tools"; tools: { tool: StreamLine; result?: StreamLine }[] }
|
|
44
|
+
| { kind: "result"; result: string };
|
|
45
|
+
|
|
46
|
+
type ChatMessage =
|
|
47
|
+
| {
|
|
48
|
+
id: string;
|
|
49
|
+
role: "user";
|
|
50
|
+
channel: ChannelId;
|
|
51
|
+
body: string;
|
|
52
|
+
createdAt: string;
|
|
53
|
+
}
|
|
54
|
+
| {
|
|
55
|
+
id: string;
|
|
56
|
+
role: "assistant" | "result";
|
|
57
|
+
channel: ChannelId;
|
|
58
|
+
body: string;
|
|
59
|
+
createdAt: string;
|
|
60
|
+
}
|
|
61
|
+
| {
|
|
62
|
+
id: string;
|
|
63
|
+
role: "tool";
|
|
64
|
+
channel: ChannelId;
|
|
65
|
+
tools: { tool: StreamLine; result?: StreamLine }[];
|
|
66
|
+
createdAt: string;
|
|
67
|
+
};
|
|
68
|
+
|
|
69
|
+
const CHANNEL_META: Record<
|
|
70
|
+
Exclude<ChannelId, "all">,
|
|
71
|
+
{ label: string; description: string; icon: typeof Terminal }
|
|
72
|
+
> = {
|
|
73
|
+
command: {
|
|
74
|
+
label: "Command",
|
|
75
|
+
description: "Direct the harness and launch missions.",
|
|
76
|
+
icon: Terminal,
|
|
77
|
+
},
|
|
78
|
+
markets: {
|
|
79
|
+
label: "Markets",
|
|
80
|
+
description: "Cross-market comparison and contract selection.",
|
|
81
|
+
icon: TrendingUp,
|
|
82
|
+
},
|
|
83
|
+
feeds: {
|
|
84
|
+
label: "Feeds",
|
|
85
|
+
description: "News and social flow for fresh catalysts.",
|
|
86
|
+
icon: Waves,
|
|
87
|
+
},
|
|
88
|
+
risk: {
|
|
89
|
+
label: "Risk",
|
|
90
|
+
description: "Portfolio review, sizing, and exits.",
|
|
91
|
+
icon: ShieldCheck,
|
|
92
|
+
},
|
|
93
|
+
execution: {
|
|
94
|
+
label: "Execution",
|
|
95
|
+
description: "Trade readiness, routing, and outcomes.",
|
|
96
|
+
icon: CheckCircle2,
|
|
97
|
+
},
|
|
98
|
+
tradingview: {
|
|
99
|
+
label: "TradingView",
|
|
100
|
+
description: "Watchlist and chart-context lane.",
|
|
101
|
+
icon: Link2,
|
|
102
|
+
},
|
|
103
|
+
};
|
|
104
|
+
|
|
24
105
|
export function LiveStream({
|
|
25
106
|
lines,
|
|
26
107
|
liveStatus,
|
|
27
108
|
customPrompt,
|
|
109
|
+
prompts,
|
|
110
|
+
workspace,
|
|
28
111
|
onCustomPromptChange,
|
|
29
112
|
onSendCommand,
|
|
30
113
|
}: LiveStreamProps) {
|
|
31
114
|
const endRef = useRef<HTMLDivElement>(null);
|
|
32
|
-
const
|
|
115
|
+
const [selectedChannel, setSelectedChannel] = useState<ChannelId>("command");
|
|
116
|
+
const showHarnessBoot = liveStatus !== "running" && lines.length < 8 && prompts.length === 0;
|
|
117
|
+
|
|
118
|
+
const availableChannels = useMemo<ChannelId[]>(() => {
|
|
119
|
+
const base: ChannelId[] = ["all", "command", "markets", "feeds", "risk", "execution"];
|
|
120
|
+
if (workspace?.channels.includes("tradingview") || workspace?.enabledMarkets.includes("tradingview")) {
|
|
121
|
+
base.push("tradingview");
|
|
122
|
+
}
|
|
123
|
+
return base;
|
|
124
|
+
}, [workspace]);
|
|
33
125
|
|
|
34
126
|
useEffect(() => {
|
|
35
|
-
if (
|
|
36
|
-
|
|
127
|
+
if (!availableChannels.includes(selectedChannel)) {
|
|
128
|
+
setSelectedChannel("command");
|
|
37
129
|
}
|
|
38
|
-
|
|
39
|
-
}, [lines.length]);
|
|
130
|
+
}, [availableChannels, selectedChannel]);
|
|
40
131
|
|
|
41
|
-
const
|
|
132
|
+
const messages = useMemo(
|
|
133
|
+
() => buildMessages(prompts, lines),
|
|
134
|
+
[prompts, lines]
|
|
135
|
+
);
|
|
136
|
+
|
|
137
|
+
const channelCounts = useMemo(() => {
|
|
138
|
+
const counts = new Map<ChannelId, number>();
|
|
139
|
+
counts.set("all", messages.length);
|
|
140
|
+
for (const channel of availableChannels) {
|
|
141
|
+
if (channel === "all") continue;
|
|
142
|
+
counts.set(channel, messages.filter((message) => message.channel === channel).length);
|
|
143
|
+
}
|
|
144
|
+
return counts;
|
|
145
|
+
}, [availableChannels, messages]);
|
|
146
|
+
|
|
147
|
+
const filteredMessages = useMemo(() => {
|
|
148
|
+
if (selectedChannel === "all") {
|
|
149
|
+
return messages;
|
|
150
|
+
}
|
|
151
|
+
return messages.filter((message) => message.channel === selectedChannel);
|
|
152
|
+
}, [messages, selectedChannel]);
|
|
153
|
+
|
|
154
|
+
const quickPrompts = useMemo(
|
|
155
|
+
() => getQuickPrompts(selectedChannel, workspace),
|
|
156
|
+
[selectedChannel, workspace]
|
|
157
|
+
);
|
|
158
|
+
|
|
159
|
+
useEffect(() => {
|
|
160
|
+
endRef.current?.scrollIntoView({ behavior: "smooth", block: "end" });
|
|
161
|
+
}, [filteredMessages.length, showHarnessBoot]);
|
|
42
162
|
|
|
43
163
|
return (
|
|
44
|
-
<div className="flex flex-col
|
|
45
|
-
<div className="h-
|
|
46
|
-
<
|
|
47
|
-
<
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
164
|
+
<div className="flex h-full min-h-0 flex-col overflow-hidden bg-[linear-gradient(180deg,#120815_0%,#190b17_32%,#120815_100%)] text-[#f7e6ee]">
|
|
165
|
+
<div className="grid min-h-0 flex-1 grid-cols-1 overflow-hidden xl:grid-cols-[240px_minmax(0,1fr)]">
|
|
166
|
+
<aside className="hidden min-h-0 overflow-hidden border-r border-[#4f3042] bg-[#1b0c18]/85 xl:block">
|
|
167
|
+
<div className="flex h-full min-h-0 flex-col">
|
|
168
|
+
<div className="border-b border-[#4f3042] px-4 py-4">
|
|
169
|
+
<p className="font-mono text-[0.68rem] uppercase tracking-[0.28em] text-[#f3a96f]">
|
|
170
|
+
Messaging channels
|
|
171
|
+
</p>
|
|
172
|
+
<p className="mt-2 text-sm leading-6 text-[#e6cfda]/72">
|
|
173
|
+
Route prompts by desk so the assistant knows whether you want markets, feeds,
|
|
174
|
+
risk, execution, or TradingView context.
|
|
175
|
+
</p>
|
|
176
|
+
</div>
|
|
177
|
+
|
|
178
|
+
<ScrollArea className="flex-1 min-h-0">
|
|
179
|
+
<div className="space-y-2 p-3">
|
|
180
|
+
{availableChannels.map((channel) => (
|
|
181
|
+
<ChannelButton
|
|
182
|
+
key={channel}
|
|
183
|
+
channel={channel}
|
|
184
|
+
selected={selectedChannel === channel}
|
|
185
|
+
count={channelCounts.get(channel) || 0}
|
|
186
|
+
onClick={() => setSelectedChannel(channel)}
|
|
187
|
+
/>
|
|
188
|
+
))}
|
|
189
|
+
|
|
190
|
+
<div className="mt-4 rounded-[1.2rem] border border-[#58364c] bg-[#271120]/80 p-3">
|
|
191
|
+
<p className="font-mono text-[0.66rem] uppercase tracking-[0.24em] text-[#f3a96f]">
|
|
192
|
+
Connectors
|
|
193
|
+
</p>
|
|
194
|
+
<div className="mt-3 space-y-2 text-[12px]">
|
|
195
|
+
<ConnectorRow
|
|
196
|
+
label={workspace?.primaryMarket || "kalshi"}
|
|
197
|
+
detail={workspace?.mode === "live" ? "live rail armed" : "paper rail active"}
|
|
198
|
+
/>
|
|
199
|
+
<ConnectorRow
|
|
200
|
+
label="feeds"
|
|
201
|
+
detail={workspace?.integrations.join(", ") || "apify, rss"}
|
|
202
|
+
/>
|
|
203
|
+
{workspace?.tradingview.enabled ? (
|
|
204
|
+
<ConnectorRow
|
|
205
|
+
label="tradingview"
|
|
206
|
+
detail={
|
|
207
|
+
workspace.tradingview.connectorMode === "mcp"
|
|
208
|
+
? workspace.tradingview.configured
|
|
209
|
+
? `${workspace.tradingview.transport} mcp ready`
|
|
210
|
+
: `${workspace.tradingview.transport} mcp needs setup`
|
|
211
|
+
: `watchlist: ${workspace.tradingview.watchlist.join(", ")}`
|
|
212
|
+
}
|
|
213
|
+
/>
|
|
214
|
+
) : null}
|
|
215
|
+
</div>
|
|
216
|
+
</div>
|
|
217
|
+
</div>
|
|
218
|
+
</ScrollArea>
|
|
219
|
+
</div>
|
|
220
|
+
</aside>
|
|
221
|
+
|
|
222
|
+
<div className="flex min-h-0 flex-col overflow-hidden">
|
|
223
|
+
<div className="border-b border-[#4f3042] bg-[#1b0c18]/75 px-4 py-3">
|
|
224
|
+
<div className="flex flex-wrap items-center justify-between gap-3">
|
|
225
|
+
<div>
|
|
226
|
+
<p className="font-mono text-[0.68rem] uppercase tracking-[0.28em] text-[#f3a96f]">
|
|
227
|
+
Chat cockpit
|
|
228
|
+
</p>
|
|
229
|
+
<div className="mt-2 flex items-center gap-2 text-sm text-[#f4dfea]">
|
|
230
|
+
<Radio className={`h-3.5 w-3.5 ${liveStatus === "running" ? "text-emerald-400" : "text-[#f3a96f]"}`} />
|
|
231
|
+
<span>{selectedChannel === "all" ? "All channels" : CHANNEL_META[selectedChannel].label}</span>
|
|
232
|
+
<span className="text-[#8b6578]">/</span>
|
|
233
|
+
<span className="text-[#d4b5c3]/72">
|
|
234
|
+
{selectedChannel === "all"
|
|
235
|
+
? "Full operator conversation"
|
|
236
|
+
: CHANNEL_META[selectedChannel].description}
|
|
237
|
+
</span>
|
|
238
|
+
</div>
|
|
239
|
+
</div>
|
|
240
|
+
<div className="flex flex-wrap items-center gap-2 xl:hidden">
|
|
241
|
+
{availableChannels.map((channel) => (
|
|
242
|
+
<CompactChannelButton
|
|
243
|
+
key={channel}
|
|
244
|
+
channel={channel}
|
|
245
|
+
selected={selectedChannel === channel}
|
|
246
|
+
onClick={() => setSelectedChannel(channel)}
|
|
247
|
+
/>
|
|
248
|
+
))}
|
|
249
|
+
</div>
|
|
250
|
+
</div>
|
|
251
|
+
</div>
|
|
252
|
+
|
|
253
|
+
<ScrollArea className="min-h-0 flex-1">
|
|
254
|
+
<div className="space-y-4 p-4">
|
|
255
|
+
{showHarnessBoot ? (
|
|
256
|
+
<HarnessBootPanel
|
|
257
|
+
workspace={workspace}
|
|
258
|
+
onLaunchMission={(prompt) => onSendCommand(prompt, "command")}
|
|
259
|
+
/>
|
|
260
|
+
) : null}
|
|
261
|
+
|
|
262
|
+
{showHarnessBoot ? (
|
|
263
|
+
<p className="px-1 text-center text-xs text-[#c9abbb]/48">
|
|
264
|
+
Pick a mission above or drop your own prompt into the selected channel.
|
|
265
|
+
</p>
|
|
266
|
+
) : null}
|
|
267
|
+
|
|
268
|
+
{!showHarnessBoot && filteredMessages.length === 0 ? (
|
|
269
|
+
<div className="rounded-[1.2rem] border border-dashed border-[#5c3b4f] bg-[#1b0c18]/45 px-4 py-5 text-sm text-[#d3bac6]/68">
|
|
270
|
+
No messages in this channel yet. Send a prompt to start the conversation.
|
|
271
|
+
</div>
|
|
272
|
+
) : null}
|
|
273
|
+
|
|
274
|
+
{filteredMessages.map((message) => (
|
|
275
|
+
<ChatBlock key={message.id} message={message} />
|
|
276
|
+
))}
|
|
277
|
+
|
|
278
|
+
{liveStatus === "running" && filteredMessages.length === 0 ? (
|
|
279
|
+
<div className="flex items-center justify-center gap-2 py-6">
|
|
280
|
+
<div className="h-1.5 w-1.5 animate-bounce rounded-full bg-emerald-400 [animation-delay:0ms]" />
|
|
281
|
+
<div className="h-1.5 w-1.5 animate-bounce rounded-full bg-emerald-400 [animation-delay:150ms]" />
|
|
282
|
+
<div className="h-1.5 w-1.5 animate-bounce rounded-full bg-emerald-400 [animation-delay:300ms]" />
|
|
283
|
+
</div>
|
|
284
|
+
) : null}
|
|
285
|
+
|
|
286
|
+
<div ref={endRef} />
|
|
287
|
+
</div>
|
|
288
|
+
</ScrollArea>
|
|
289
|
+
|
|
290
|
+
<div className="border-t border-[#4f3042] bg-[#1b0c18]/80 px-4 py-3">
|
|
291
|
+
<div className="mb-3 flex flex-wrap gap-2">
|
|
292
|
+
{quickPrompts.map((prompt) => (
|
|
293
|
+
<button
|
|
294
|
+
key={prompt}
|
|
295
|
+
onClick={() => onSendCommand(prompt, selectedChannel === "all" ? "command" : selectedChannel)}
|
|
296
|
+
className="rounded-full border border-[#654154] bg-[#311425] px-3 py-1.5 text-[11px] text-[#f4dfea]/82 transition-colors hover:border-[#92667f] hover:bg-[#442039]"
|
|
297
|
+
>
|
|
298
|
+
{prompt}
|
|
299
|
+
</button>
|
|
300
|
+
))}
|
|
301
|
+
</div>
|
|
302
|
+
|
|
303
|
+
<div className="flex items-center gap-2 rounded-[1.2rem] border border-[#654154] bg-[#2a1121] px-3 py-2">
|
|
304
|
+
<span className="rounded-full border border-[#84586f] bg-[#3b1730] px-2.5 py-1 font-mono text-[10px] uppercase tracking-[0.2em] text-[#f3a96f]">
|
|
305
|
+
{selectedChannel === "all" ? "command" : selectedChannel}
|
|
306
|
+
</span>
|
|
307
|
+
<input
|
|
308
|
+
type="text"
|
|
309
|
+
value={customPrompt}
|
|
310
|
+
onChange={(event) => onCustomPromptChange(event.target.value)}
|
|
311
|
+
onKeyDown={(event) => {
|
|
312
|
+
if (event.key === "Enter" && customPrompt.trim()) {
|
|
313
|
+
onSendCommand(customPrompt, selectedChannel === "all" ? "command" : selectedChannel);
|
|
314
|
+
onCustomPromptChange("");
|
|
315
|
+
}
|
|
316
|
+
}}
|
|
317
|
+
placeholder={getPlaceholder(selectedChannel, workspace)}
|
|
318
|
+
className="min-w-0 flex-1 bg-transparent text-sm text-white outline-none placeholder:text-[#a88598]"
|
|
319
|
+
/>
|
|
320
|
+
<button
|
|
321
|
+
onClick={() => {
|
|
322
|
+
if (customPrompt.trim()) {
|
|
323
|
+
onSendCommand(customPrompt, selectedChannel === "all" ? "command" : selectedChannel);
|
|
324
|
+
onCustomPromptChange("");
|
|
325
|
+
}
|
|
326
|
+
}}
|
|
327
|
+
className="rounded-full border border-[#84586f] bg-[#3b1730] p-2 text-[#ffd7ae] transition-colors hover:border-[#b47f99] hover:bg-[#53233f]"
|
|
328
|
+
>
|
|
329
|
+
<Send className="h-4 w-4" />
|
|
330
|
+
</button>
|
|
331
|
+
</div>
|
|
332
|
+
</div>
|
|
51
333
|
</div>
|
|
52
|
-
{liveStatus === "running" && (
|
|
53
|
-
<span className="text-[10px] text-primary font-medium flex items-center gap-1.5">
|
|
54
|
-
<span className="w-1.5 h-1.5 rounded-full bg-primary pulse-glow" />
|
|
55
|
-
Streaming
|
|
56
|
-
</span>
|
|
57
|
-
)}
|
|
58
334
|
</div>
|
|
335
|
+
</div>
|
|
336
|
+
);
|
|
337
|
+
}
|
|
59
338
|
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
339
|
+
function ChannelButton({
|
|
340
|
+
channel,
|
|
341
|
+
selected,
|
|
342
|
+
count,
|
|
343
|
+
onClick,
|
|
344
|
+
}: {
|
|
345
|
+
channel: ChannelId;
|
|
346
|
+
selected: boolean;
|
|
347
|
+
count: number;
|
|
348
|
+
onClick: () => void;
|
|
349
|
+
}) {
|
|
350
|
+
if (channel === "all") {
|
|
351
|
+
return (
|
|
352
|
+
<button
|
|
353
|
+
onClick={onClick}
|
|
354
|
+
className={`w-full rounded-[1rem] border px-3 py-3 text-left transition-colors ${
|
|
355
|
+
selected
|
|
356
|
+
? "border-[#95687f] bg-[#3d182f] text-white"
|
|
357
|
+
: "border-[#523345] bg-[#24101d] text-[#dec4d0] hover:border-[#7c5469] hover:bg-[#311425]"
|
|
358
|
+
}`}
|
|
359
|
+
>
|
|
360
|
+
<div className="flex items-center justify-between gap-2">
|
|
361
|
+
<span className="font-mono text-[0.68rem] uppercase tracking-[0.22em]">All channels</span>
|
|
362
|
+
<span className="rounded-full border border-current/20 px-2 py-0.5 text-[10px]">{count}</span>
|
|
78
363
|
</div>
|
|
79
|
-
|
|
364
|
+
<p className="mt-2 text-[12px] text-current/70">Full operator conversation across every lane.</p>
|
|
365
|
+
</button>
|
|
366
|
+
);
|
|
367
|
+
}
|
|
80
368
|
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
369
|
+
const meta = CHANNEL_META[channel];
|
|
370
|
+
const Icon = meta.icon;
|
|
371
|
+
|
|
372
|
+
return (
|
|
373
|
+
<button
|
|
374
|
+
onClick={onClick}
|
|
375
|
+
className={`w-full rounded-[1rem] border px-3 py-3 text-left transition-colors ${
|
|
376
|
+
selected
|
|
377
|
+
? "border-[#95687f] bg-[#3d182f] text-white"
|
|
378
|
+
: "border-[#523345] bg-[#24101d] text-[#dec4d0] hover:border-[#7c5469] hover:bg-[#311425]"
|
|
379
|
+
}`}
|
|
380
|
+
>
|
|
381
|
+
<div className="flex items-center justify-between gap-2">
|
|
382
|
+
<span className="flex items-center gap-2 font-mono text-[0.68rem] uppercase tracking-[0.22em]">
|
|
383
|
+
<Icon className="h-3.5 w-3.5" />
|
|
384
|
+
{meta.label}
|
|
84
385
|
</span>
|
|
85
|
-
<
|
|
86
|
-
type="text"
|
|
87
|
-
value={customPrompt}
|
|
88
|
-
onChange={(e) => onCustomPromptChange(e.target.value)}
|
|
89
|
-
onKeyDown={(e) => {
|
|
90
|
-
if (e.key === "Enter" && customPrompt.trim()) {
|
|
91
|
-
onSendCommand(customPrompt);
|
|
92
|
-
onCustomPromptChange("");
|
|
93
|
-
}
|
|
94
|
-
}}
|
|
95
|
-
placeholder="Send a command to the agent..."
|
|
96
|
-
className="flex-1 bg-transparent text-xs focus:outline-none placeholder:text-muted-foreground/30"
|
|
97
|
-
/>
|
|
98
|
-
<button
|
|
99
|
-
onClick={() => {
|
|
100
|
-
if (customPrompt.trim()) {
|
|
101
|
-
onSendCommand(customPrompt);
|
|
102
|
-
onCustomPromptChange("");
|
|
103
|
-
}
|
|
104
|
-
}}
|
|
105
|
-
className="text-muted-foreground/40 hover:text-primary transition-colors"
|
|
106
|
-
>
|
|
107
|
-
<Send className="h-3.5 w-3.5" />
|
|
108
|
-
</button>
|
|
386
|
+
<span className="rounded-full border border-current/20 px-2 py-0.5 text-[10px]">{count}</span>
|
|
109
387
|
</div>
|
|
388
|
+
<p className="mt-2 text-[12px] text-current/70">{meta.description}</p>
|
|
389
|
+
</button>
|
|
390
|
+
);
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
function CompactChannelButton({
|
|
394
|
+
channel,
|
|
395
|
+
selected,
|
|
396
|
+
onClick,
|
|
397
|
+
}: {
|
|
398
|
+
channel: ChannelId;
|
|
399
|
+
selected: boolean;
|
|
400
|
+
onClick: () => void;
|
|
401
|
+
}) {
|
|
402
|
+
const label = channel === "all" ? "All" : CHANNEL_META[channel].label;
|
|
403
|
+
return (
|
|
404
|
+
<button
|
|
405
|
+
onClick={onClick}
|
|
406
|
+
className={`rounded-full border px-3 py-1.5 text-[11px] uppercase tracking-[0.18em] ${
|
|
407
|
+
selected
|
|
408
|
+
? "border-[#95687f] bg-[#3d182f] text-white"
|
|
409
|
+
: "border-[#523345] bg-[#24101d] text-[#dec4d0]"
|
|
410
|
+
}`}
|
|
411
|
+
>
|
|
412
|
+
{label}
|
|
413
|
+
</button>
|
|
414
|
+
);
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
function ConnectorRow({ label, detail }: { label: string; detail: string }) {
|
|
418
|
+
return (
|
|
419
|
+
<div className="rounded-lg border border-[#4e3143] bg-[#1d0d18] px-3 py-2">
|
|
420
|
+
<p className="font-mono text-[10px] uppercase tracking-[0.18em] text-[#f3a96f]">{label}</p>
|
|
421
|
+
<p className="mt-1 text-[12px] leading-5 text-[#ead4de]/70">{detail}</p>
|
|
110
422
|
</div>
|
|
111
423
|
);
|
|
112
424
|
}
|
|
113
425
|
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
426
|
+
function ChatBlock({ message }: { message: ChatMessage }) {
|
|
427
|
+
if (message.role === "user") {
|
|
428
|
+
return (
|
|
429
|
+
<div className="flex justify-end">
|
|
430
|
+
<div className="max-w-[85%] rounded-[1.2rem] border border-[#8c5e77] bg-[#4a2038] px-4 py-3 shadow-[0_10px_30px_rgba(24,6,14,0.25)]">
|
|
431
|
+
<div className="mb-2 flex items-center gap-2 text-[10px] uppercase tracking-[0.22em] text-[#ffd7ae]">
|
|
432
|
+
<span>Operator</span>
|
|
433
|
+
<span className="text-[#b6859d]">/</span>
|
|
434
|
+
<span>{message.channel}</span>
|
|
435
|
+
</div>
|
|
436
|
+
<p className="text-sm leading-7 text-[#fff3f8]">{message.body}</p>
|
|
437
|
+
</div>
|
|
438
|
+
</div>
|
|
439
|
+
);
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
if (message.role === "tool") {
|
|
443
|
+
return <ToolBlock message={message} />;
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
const title = message.role === "result" ? "Cycle complete" : "Harness";
|
|
447
|
+
const borderColor = message.role === "result" ? "border-emerald-400/30" : "border-[#5c3b4f]";
|
|
448
|
+
const background = message.role === "result" ? "bg-emerald-500/10" : "bg-[#21111b]";
|
|
449
|
+
|
|
450
|
+
return (
|
|
451
|
+
<div className={`max-w-[92%] rounded-[1.2rem] border ${borderColor} ${background} px-4 py-3 shadow-[0_10px_30px_rgba(24,6,14,0.18)]`}>
|
|
452
|
+
<div className="mb-2 flex items-center gap-2 text-[10px] uppercase tracking-[0.22em] text-[#f3a96f]">
|
|
453
|
+
<span>{title}</span>
|
|
454
|
+
<span className="text-[#765268]">/</span>
|
|
455
|
+
<span>{message.channel}</span>
|
|
456
|
+
</div>
|
|
457
|
+
<ReactMarkdown remarkPlugins={[remarkGfm]} components={markdownComponents}>
|
|
458
|
+
{message.body}
|
|
459
|
+
</ReactMarkdown>
|
|
460
|
+
</div>
|
|
461
|
+
);
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
function ToolBlock({
|
|
465
|
+
message,
|
|
466
|
+
}: {
|
|
467
|
+
message: Extract<ChatMessage, { role: "tool" }>;
|
|
468
|
+
}) {
|
|
469
|
+
const [expanded, setExpanded] = useState(false);
|
|
470
|
+
|
|
471
|
+
return (
|
|
472
|
+
<div className="max-w-[92%] rounded-[1.2rem] border border-[#5c3b4f] bg-[#1c0d16] px-4 py-3">
|
|
473
|
+
<button
|
|
474
|
+
onClick={() => setExpanded((value) => !value)}
|
|
475
|
+
className="flex w-full items-start gap-2 text-left"
|
|
476
|
+
>
|
|
477
|
+
<span className="mt-0.5 text-[#b793a5]">
|
|
478
|
+
{expanded ? <ChevronDown className="h-4 w-4" /> : <ChevronRight className="h-4 w-4" />}
|
|
479
|
+
</span>
|
|
480
|
+
<div className="min-w-0 flex-1">
|
|
481
|
+
<div className="flex items-center gap-2 text-[10px] uppercase tracking-[0.22em] text-[#f3a96f]">
|
|
482
|
+
<span>Tool trace</span>
|
|
483
|
+
<span className="text-[#765268]">/</span>
|
|
484
|
+
<span>{message.channel}</span>
|
|
485
|
+
</div>
|
|
486
|
+
<p className="mt-2 text-[12px] leading-6 text-[#e6cfda]/74">
|
|
487
|
+
{message.tools
|
|
488
|
+
.map((item) => item.tool.tool || "tool")
|
|
489
|
+
.filter(Boolean)
|
|
490
|
+
.join(" · ")}
|
|
491
|
+
</p>
|
|
492
|
+
</div>
|
|
493
|
+
</button>
|
|
494
|
+
|
|
495
|
+
{expanded ? (
|
|
496
|
+
<div className="mt-3 space-y-2 border-t border-[#4f3042] pt-3">
|
|
497
|
+
{message.tools.map((item, index) => (
|
|
498
|
+
<div key={`${message.id}-${index}`} className="rounded-lg border border-[#4e3143] bg-[#130913]">
|
|
499
|
+
{item.tool.input ? (
|
|
500
|
+
<pre className="whitespace-pre-wrap break-all px-3 py-2 font-mono text-[11px] leading-6 text-[#e8d4dd]/72">
|
|
501
|
+
{item.tool.input}
|
|
502
|
+
</pre>
|
|
503
|
+
) : null}
|
|
504
|
+
{item.result?.text ? (
|
|
505
|
+
<pre className="max-h-40 overflow-y-auto whitespace-pre-wrap break-all border-t border-[#4e3143] px-3 py-2 font-mono text-[11px] leading-6 text-[#bda1af]">
|
|
506
|
+
{item.result.text}
|
|
507
|
+
</pre>
|
|
508
|
+
) : null}
|
|
509
|
+
</div>
|
|
510
|
+
))}
|
|
511
|
+
</div>
|
|
512
|
+
) : null}
|
|
513
|
+
</div>
|
|
514
|
+
);
|
|
515
|
+
}
|
|
516
|
+
|
|
517
|
+
function buildMessages(prompts: PromptEntry[], lines: StreamLine[]): ChatMessage[] {
|
|
518
|
+
const promptMessages: ChatMessage[] = prompts.map((prompt) => ({
|
|
519
|
+
id: prompt.id,
|
|
520
|
+
role: "user",
|
|
521
|
+
channel: (prompt.channel as ChannelId) || "command",
|
|
522
|
+
body: prompt.text,
|
|
523
|
+
createdAt: prompt.createdAt,
|
|
524
|
+
}));
|
|
525
|
+
|
|
526
|
+
const grouped = groupLines(lines);
|
|
527
|
+
const streamMessages: ChatMessage[] = grouped.map((group, index) => {
|
|
528
|
+
const createdAt = new Date(Date.now() + index).toISOString();
|
|
529
|
+
if (group.kind === "text") {
|
|
530
|
+
return {
|
|
531
|
+
id: `stream-text-${index}`,
|
|
532
|
+
role: "assistant",
|
|
533
|
+
channel: classifyChannel(group.text),
|
|
534
|
+
body: group.text,
|
|
535
|
+
createdAt,
|
|
536
|
+
};
|
|
537
|
+
}
|
|
538
|
+
|
|
539
|
+
if (group.kind === "result") {
|
|
540
|
+
return {
|
|
541
|
+
id: `stream-result-${index}`,
|
|
542
|
+
role: "result",
|
|
543
|
+
channel: classifyChannel(group.result),
|
|
544
|
+
body: group.result,
|
|
545
|
+
createdAt,
|
|
546
|
+
};
|
|
547
|
+
}
|
|
548
|
+
|
|
549
|
+
return {
|
|
550
|
+
id: `stream-tool-${index}`,
|
|
551
|
+
role: "tool",
|
|
552
|
+
channel: classifyToolChannel(group.tools),
|
|
553
|
+
tools: group.tools,
|
|
554
|
+
createdAt,
|
|
555
|
+
};
|
|
556
|
+
});
|
|
557
|
+
|
|
558
|
+
return [...promptMessages, ...streamMessages];
|
|
559
|
+
}
|
|
118
560
|
|
|
119
561
|
function groupLines(lines: StreamLine[]): GroupedLine[] {
|
|
120
562
|
const out: GroupedLine[] = [];
|
|
@@ -135,195 +577,158 @@ function groupLines(lines: StreamLine[]): GroupedLine[] {
|
|
|
135
577
|
}
|
|
136
578
|
};
|
|
137
579
|
|
|
138
|
-
for (let
|
|
139
|
-
const line = lines[
|
|
580
|
+
for (let index = 0; index < lines.length; index += 1) {
|
|
581
|
+
const line = lines[index];
|
|
140
582
|
if (line.type === "text") {
|
|
141
583
|
flushTools();
|
|
142
|
-
textBuffer +=
|
|
143
|
-
|
|
584
|
+
textBuffer += `${textBuffer ? "\n" : ""}${line.text || ""}`;
|
|
585
|
+
continue;
|
|
586
|
+
}
|
|
587
|
+
|
|
588
|
+
if (line.type === "tool_use") {
|
|
144
589
|
flushText();
|
|
145
|
-
const next = lines[
|
|
590
|
+
const next = lines[index + 1];
|
|
146
591
|
const result = next?.type === "tool_result" ? next : undefined;
|
|
147
592
|
toolBuffer.push({ tool: line, result });
|
|
148
|
-
if (result)
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
593
|
+
if (result) index += 1;
|
|
594
|
+
continue;
|
|
595
|
+
}
|
|
596
|
+
|
|
597
|
+
if (line.type === "result") {
|
|
152
598
|
flushText();
|
|
153
599
|
flushTools();
|
|
154
600
|
out.push({ kind: "result", result: line.result || "" });
|
|
155
601
|
}
|
|
156
602
|
}
|
|
603
|
+
|
|
157
604
|
flushText();
|
|
158
605
|
flushTools();
|
|
159
606
|
return out;
|
|
160
607
|
}
|
|
161
608
|
|
|
162
|
-
function
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
return
|
|
167
|
-
}
|
|
168
|
-
|
|
169
|
-
function TextBlock({ text }: { text: string }) {
|
|
170
|
-
return (
|
|
171
|
-
<div className="py-1 px-1">
|
|
172
|
-
<ReactMarkdown
|
|
173
|
-
remarkPlugins={[remarkGfm]}
|
|
174
|
-
components={markdownComponents}
|
|
175
|
-
>
|
|
176
|
-
{text}
|
|
177
|
-
</ReactMarkdown>
|
|
178
|
-
</div>
|
|
179
|
-
);
|
|
609
|
+
function classifyToolChannel(tools: { tool: StreamLine; result?: StreamLine }[]): ChannelId {
|
|
610
|
+
const haystack = tools
|
|
611
|
+
.map((item) => `${item.tool.tool || ""} ${item.tool.input || ""} ${item.result?.text || ""}`.toLowerCase())
|
|
612
|
+
.join(" ");
|
|
613
|
+
return classifyChannel(haystack);
|
|
180
614
|
}
|
|
181
615
|
|
|
182
|
-
function
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
);
|
|
616
|
+
function classifyChannel(text: string): ChannelId {
|
|
617
|
+
const value = text.toLowerCase();
|
|
618
|
+
if (value.includes("tradingview") || value.includes("watchlist") || value.includes("chart")) {
|
|
619
|
+
return "tradingview";
|
|
620
|
+
}
|
|
621
|
+
if (value.includes("risk") || value.includes("portfolio") || value.includes("exposure") || value.includes("kelly") || value.includes("exit")) {
|
|
622
|
+
return "risk";
|
|
623
|
+
}
|
|
624
|
+
if (value.includes("trade") || value.includes("execution") || value.includes("order") || value.includes("fill") || value.includes("contracts")) {
|
|
625
|
+
return "execution";
|
|
626
|
+
}
|
|
627
|
+
if (value.includes("news") || value.includes("rss") || value.includes("twitter") || value.includes("reddit") || value.includes("tiktok") || value.includes("headline") || value.includes("feed")) {
|
|
628
|
+
return "feeds";
|
|
629
|
+
}
|
|
630
|
+
if (value.includes("kalshi") || value.includes("polymarket") || value.includes("market") || value.includes("bid") || value.includes("ask") || value.includes("probability")) {
|
|
631
|
+
return "markets";
|
|
632
|
+
}
|
|
633
|
+
return "command";
|
|
201
634
|
}
|
|
202
635
|
|
|
203
|
-
function
|
|
204
|
-
|
|
205
|
-
}: {
|
|
206
|
-
tools: { tool: StreamLine; result?: StreamLine }[];
|
|
207
|
-
}) {
|
|
208
|
-
const [expanded, setExpanded] = useState(false);
|
|
209
|
-
|
|
210
|
-
const summary = tools
|
|
211
|
-
.map((t) => {
|
|
212
|
-
const name = t.tool.tool || "tool";
|
|
213
|
-
const short = formatToolInput(t.tool.input || "");
|
|
214
|
-
return { name, short };
|
|
215
|
-
});
|
|
216
|
-
|
|
217
|
-
return (
|
|
218
|
-
<div className="my-0.5">
|
|
219
|
-
<button
|
|
220
|
-
onClick={() => setExpanded(!expanded)}
|
|
221
|
-
className="w-full flex items-start gap-1.5 px-1 py-0.5 text-left hover:bg-secondary/20 rounded transition-colors group"
|
|
222
|
-
>
|
|
223
|
-
<span className="text-muted-foreground/30 mt-px shrink-0">
|
|
224
|
-
{expanded ? (
|
|
225
|
-
<ChevronDown className="h-3 w-3" />
|
|
226
|
-
) : (
|
|
227
|
-
<ChevronRight className="h-3 w-3" />
|
|
228
|
-
)}
|
|
229
|
-
</span>
|
|
230
|
-
<div className="flex flex-wrap gap-x-3 gap-y-0 flex-1 min-w-0">
|
|
231
|
-
{summary.map((s, i) => (
|
|
232
|
-
<span key={i} className="flex items-center gap-1 text-[10px] text-muted-foreground/50 font-mono shrink-0">
|
|
233
|
-
<span className="text-primary/50 font-medium">{s.name}</span>
|
|
234
|
-
<span className="truncate max-w-[200px]">{s.short}</span>
|
|
235
|
-
{tools[i].result && (
|
|
236
|
-
<CheckCircle2 className="h-2.5 w-2.5 text-primary/30 shrink-0" />
|
|
237
|
-
)}
|
|
238
|
-
</span>
|
|
239
|
-
))}
|
|
240
|
-
</div>
|
|
241
|
-
</button>
|
|
636
|
+
function getQuickPrompts(channel: ChannelId, workspace: WorkspaceSummary | null) {
|
|
637
|
+
const watchlist = workspace?.tradingview.watchlist.join(", ") || "SPY, QQQ, BTCUSD, NQ1!";
|
|
242
638
|
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
639
|
+
switch (channel) {
|
|
640
|
+
case "markets":
|
|
641
|
+
return [
|
|
642
|
+
"Compare the strongest overlapping Kalshi and Polymarket themes.",
|
|
643
|
+
"Find one liquid macro contract worth deeper work.",
|
|
644
|
+
];
|
|
645
|
+
case "feeds":
|
|
646
|
+
return [
|
|
647
|
+
"Summarize what changed in the last hour.",
|
|
648
|
+
"Separate new information from priced-in narrative.",
|
|
649
|
+
];
|
|
650
|
+
case "risk":
|
|
651
|
+
return [
|
|
652
|
+
"Audit the open book and tell me what should be cut.",
|
|
653
|
+
"List the weakest assumption in each active thesis.",
|
|
654
|
+
];
|
|
655
|
+
case "execution":
|
|
656
|
+
return [
|
|
657
|
+
"Tell me if there is one executable trade right now or a clear pass.",
|
|
658
|
+
"Check supported rails before proposing a trade.",
|
|
659
|
+
];
|
|
660
|
+
case "tradingview":
|
|
661
|
+
return [
|
|
662
|
+
`Scan the TradingView watchlist: ${watchlist}.`,
|
|
663
|
+
"Tell me which symbols deserve a closer look and why.",
|
|
664
|
+
];
|
|
665
|
+
case "all":
|
|
666
|
+
return [
|
|
667
|
+
"Explain the full workspace setup.",
|
|
668
|
+
"Give me the smartest next command.",
|
|
669
|
+
];
|
|
670
|
+
default:
|
|
671
|
+
return [
|
|
672
|
+
"Audit the workspace and tell me what is missing.",
|
|
673
|
+
"Warm boot the harness and propose the best paper trade.",
|
|
674
|
+
];
|
|
675
|
+
}
|
|
252
676
|
}
|
|
253
677
|
|
|
254
|
-
function
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
{resultText && !isNoOutput && (
|
|
273
|
-
<pre className="px-2.5 py-1.5 font-mono text-muted-foreground/40 whitespace-pre-wrap break-all max-h-32 overflow-y-auto leading-relaxed border-t border-border/20">
|
|
274
|
-
{resultText}
|
|
275
|
-
</pre>
|
|
276
|
-
)}
|
|
277
|
-
</div>
|
|
278
|
-
);
|
|
678
|
+
function getPlaceholder(channel: ChannelId, workspace: WorkspaceSummary | null) {
|
|
679
|
+
if (channel === "tradingview") {
|
|
680
|
+
const watchlist = workspace?.tradingview.watchlist.join(", ") || "SPY, QQQ, BTCUSD, NQ1!";
|
|
681
|
+
return `Ask about the TradingView lane, watchlist, or connector setup (${watchlist})`;
|
|
682
|
+
}
|
|
683
|
+
if (channel === "risk") {
|
|
684
|
+
return "Ask for exits, exposure review, weak theses, or sizing discipline";
|
|
685
|
+
}
|
|
686
|
+
if (channel === "execution") {
|
|
687
|
+
return "Ask whether a trade is actually supported, executable, and worth firing";
|
|
688
|
+
}
|
|
689
|
+
if (channel === "feeds") {
|
|
690
|
+
return "Ask what changed in the feeds and what matters now";
|
|
691
|
+
}
|
|
692
|
+
if (channel === "markets") {
|
|
693
|
+
return "Ask for a cross-market scan, compare rails, or find a contract";
|
|
694
|
+
}
|
|
695
|
+
return "Ask the harness what to scan, compare, explain, or trade";
|
|
279
696
|
}
|
|
280
697
|
|
|
281
698
|
const markdownComponents = {
|
|
282
699
|
h1: ({ children }: { children?: React.ReactNode }) => (
|
|
283
|
-
<h1 className="text-[13px] font-bold text-
|
|
284
|
-
{children}
|
|
285
|
-
</h1>
|
|
700
|
+
<h1 className="mb-1 text-[13px] font-bold text-white first:mt-0">{children}</h1>
|
|
286
701
|
),
|
|
287
702
|
h2: ({ children }: { children?: React.ReactNode }) => (
|
|
288
|
-
<h2 className="text-[12px] font-bold text-
|
|
289
|
-
{children}
|
|
290
|
-
</h2>
|
|
703
|
+
<h2 className="mb-1 text-[12px] font-bold text-white first:mt-0">{children}</h2>
|
|
291
704
|
),
|
|
292
705
|
h3: ({ children }: { children?: React.ReactNode }) => (
|
|
293
|
-
<h3 className="text-[11px] font-semibold text-
|
|
294
|
-
{children}
|
|
295
|
-
</h3>
|
|
706
|
+
<h3 className="mb-0.5 text-[11px] font-semibold text-[#fff7fb] first:mt-0">{children}</h3>
|
|
296
707
|
),
|
|
297
708
|
p: ({ children }: { children?: React.ReactNode }) => (
|
|
298
|
-
<p className="text-[12px]
|
|
299
|
-
{children}
|
|
300
|
-
</p>
|
|
709
|
+
<p className="mb-1 text-[12px] leading-7 text-[#f2dce5]/84 last:mb-0">{children}</p>
|
|
301
710
|
),
|
|
302
711
|
strong: ({ children }: { children?: React.ReactNode }) => (
|
|
303
|
-
<strong className="font-semibold text-
|
|
712
|
+
<strong className="font-semibold text-white">{children}</strong>
|
|
304
713
|
),
|
|
305
714
|
em: ({ children }: { children?: React.ReactNode }) => (
|
|
306
|
-
<em className="text-
|
|
715
|
+
<em className="text-[#ffd7ae]">{children}</em>
|
|
307
716
|
),
|
|
308
717
|
ul: ({ children }: { children?: React.ReactNode }) => (
|
|
309
|
-
<ul className="
|
|
310
|
-
{children}
|
|
311
|
-
</ul>
|
|
718
|
+
<ul className="ml-4 mb-1 list-disc space-y-0 text-[12px] text-[#f2dce5]/84">{children}</ul>
|
|
312
719
|
),
|
|
313
720
|
ol: ({ children }: { children?: React.ReactNode }) => (
|
|
314
|
-
<ol className="
|
|
315
|
-
{children}
|
|
316
|
-
</ol>
|
|
721
|
+
<ol className="ml-4 mb-1 list-decimal space-y-0 text-[12px] text-[#f2dce5]/84">{children}</ol>
|
|
317
722
|
),
|
|
318
723
|
li: ({ children }: { children?: React.ReactNode }) => (
|
|
319
|
-
<li className="leading-
|
|
724
|
+
<li className="leading-7">{children}</li>
|
|
320
725
|
),
|
|
321
726
|
a: ({ href, children }: { href?: string; children?: React.ReactNode }) => (
|
|
322
727
|
<a
|
|
323
728
|
href={href}
|
|
324
729
|
target="_blank"
|
|
325
730
|
rel="noopener noreferrer"
|
|
326
|
-
className="text-
|
|
731
|
+
className="text-[#ffd7ae] underline underline-offset-2 transition-colors hover:text-white"
|
|
327
732
|
>
|
|
328
733
|
{children}
|
|
329
734
|
</a>
|
|
@@ -331,64 +736,36 @@ const markdownComponents = {
|
|
|
331
736
|
code: ({ children, className }: { children?: React.ReactNode; className?: string }) => {
|
|
332
737
|
if (className?.includes("language-")) {
|
|
333
738
|
return (
|
|
334
|
-
<code className="block
|
|
739
|
+
<code className="my-1 block overflow-x-auto rounded bg-[#120913] px-2.5 py-1.5 font-mono text-[10px] text-[#f7e6ee]/70">
|
|
335
740
|
{children}
|
|
336
741
|
</code>
|
|
337
742
|
);
|
|
338
743
|
}
|
|
339
744
|
return (
|
|
340
|
-
<code className="bg-
|
|
745
|
+
<code className="rounded bg-[#120913] px-1 py-0.5 font-mono text-[10px] text-[#ffd7ae]">
|
|
341
746
|
{children}
|
|
342
747
|
</code>
|
|
343
748
|
);
|
|
344
749
|
},
|
|
345
|
-
pre: ({ children }: { children?: React.ReactNode }) =>
|
|
346
|
-
<pre className="my-1">{children}</pre>
|
|
347
|
-
),
|
|
750
|
+
pre: ({ children }: { children?: React.ReactNode }) => <pre className="my-1">{children}</pre>,
|
|
348
751
|
blockquote: ({ children }: { children?: React.ReactNode }) => (
|
|
349
|
-
<blockquote className="border-l-2 border-
|
|
752
|
+
<blockquote className="my-1 border-l-2 border-[#8f627a] pl-2.5 italic text-[#d6bdc8]">
|
|
350
753
|
{children}
|
|
351
754
|
</blockquote>
|
|
352
755
|
),
|
|
353
756
|
table: ({ children }: { children?: React.ReactNode }) => (
|
|
354
|
-
<div className="overflow-x-auto
|
|
757
|
+
<div className="my-1.5 overflow-x-auto rounded border border-[#5c3b4f]">
|
|
355
758
|
<table className="w-full text-[10px]">{children}</table>
|
|
356
759
|
</div>
|
|
357
760
|
),
|
|
358
761
|
thead: ({ children }: { children?: React.ReactNode }) => (
|
|
359
|
-
<thead className="
|
|
360
|
-
{children}
|
|
361
|
-
</thead>
|
|
762
|
+
<thead className="border-b border-[#5c3b4f] bg-[#2a1121]">{children}</thead>
|
|
362
763
|
),
|
|
363
764
|
th: ({ children }: { children?: React.ReactNode }) => (
|
|
364
|
-
<th className="
|
|
365
|
-
{children}
|
|
366
|
-
</th>
|
|
765
|
+
<th className="px-2 py-1 text-left font-semibold text-[#fff3f8]/75">{children}</th>
|
|
367
766
|
),
|
|
368
767
|
td: ({ children }: { children?: React.ReactNode }) => (
|
|
369
|
-
<td className="px-2 py-1
|
|
370
|
-
{children}
|
|
371
|
-
</td>
|
|
768
|
+
<td className="border-t border-[#4f3042] px-2 py-1 font-mono text-[#ead4de]/68">{children}</td>
|
|
372
769
|
),
|
|
373
|
-
hr: () => <hr className="
|
|
770
|
+
hr: () => <hr className="my-2 border-[#4f3042]" />,
|
|
374
771
|
};
|
|
375
|
-
|
|
376
|
-
function formatToolInput(raw: string): string {
|
|
377
|
-
try {
|
|
378
|
-
const parsed = JSON.parse(raw);
|
|
379
|
-
if (parsed.command) {
|
|
380
|
-
const cmd = parsed.command as string;
|
|
381
|
-
return cmd.length > 60 ? cmd.slice(0, 60) + "..." : cmd;
|
|
382
|
-
}
|
|
383
|
-
if (parsed.query) return parsed.query;
|
|
384
|
-
if (parsed.file_path) {
|
|
385
|
-
const fp = parsed.file_path as string;
|
|
386
|
-
const parts = fp.split("/");
|
|
387
|
-
return parts.length > 2 ? ".../" + parts.slice(-2).join("/") : fp;
|
|
388
|
-
}
|
|
389
|
-
if (parsed.pattern) return parsed.pattern;
|
|
390
|
-
return raw.length > 60 ? raw.slice(0, 60) + "..." : raw;
|
|
391
|
-
} catch {
|
|
392
|
-
return raw.length > 60 ? raw.slice(0, 60) + "..." : raw;
|
|
393
|
-
}
|
|
394
|
-
}
|