subagent thinking accordion; indicator fixes; script details & edit
This commit is contained in:
113
gateway/docs/details-edit-protocol.md
Normal file
113
gateway/docs/details-edit-protocol.md
Normal file
@@ -0,0 +1,113 @@
|
|||||||
|
# Details Edit Protocol
|
||||||
|
|
||||||
|
This document describes the WebSocket message protocol for reading and editing the `details` field of category items (indicators, strategies, research scripts) from the web client.
|
||||||
|
|
||||||
|
## Background
|
||||||
|
|
||||||
|
Every category item stored in the sandbox has a `details` field: a full markdown description of the implementation with enough detail that another coding agent could reproduce the code from it alone. The web client can display this field, allow the user to edit it in plain text, and submit the revised version — the gateway then diffs the old vs new details and instructs the appropriate subagent to update the Python code accordingly.
|
||||||
|
|
||||||
|
The `details` field is intentionally **filtered out of the workspace `_types` stores** (see `mcp-tool-wrapper.ts:filterTypeStoreState`) because it can be several kilobytes of markdown. The read/update protocol below provides direct, on-demand access.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Message Flow
|
||||||
|
|
||||||
|
### 1. Read Details
|
||||||
|
|
||||||
|
**Client → Server**
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"type": "read_details",
|
||||||
|
"category": "indicator" | "strategy" | "research",
|
||||||
|
"name": "My Indicator Name"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Server → Client (success)**
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"type": "details_data",
|
||||||
|
"category": "indicator",
|
||||||
|
"name": "My Indicator Name",
|
||||||
|
"details": "## My Indicator\n\nFull markdown description..."
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Server → Client (error)**
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"type": "details_error",
|
||||||
|
"category": "indicator",
|
||||||
|
"name": "My Indicator Name",
|
||||||
|
"error": "Item not found or has no details"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 2. Submit Updated Details
|
||||||
|
|
||||||
|
**Client → Server**
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"type": "update_details",
|
||||||
|
"category": "indicator" | "strategy" | "research",
|
||||||
|
"name": "My Indicator Name",
|
||||||
|
"details": "## My Indicator\n\nRevised full markdown description..."
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
The gateway will:
|
||||||
|
1. Read the current `details` from the sandbox via `python_read`
|
||||||
|
2. Compute a unified diff between the old and new text
|
||||||
|
3. If no changes are detected, reply immediately with `details_updated` (success)
|
||||||
|
4. Otherwise, invoke the appropriate subagent (indicator / strategy / research) with instructions to update the Python code according to the diff, and also persist the new `details` text
|
||||||
|
|
||||||
|
**While the subagent is running**, the server streams progress events using the same event types as normal agent interactions:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{ "type": "subagent_chunk", "agentName": "indicator", "content": "Reading current implementation..." }
|
||||||
|
{ "type": "subagent_tool_call", "agentName": "indicator", "toolName": "python_read", "label": "python_read" }
|
||||||
|
{ "type": "subagent_tool_call", "agentName": "indicator", "toolName": "python_edit", "label": "python_edit" }
|
||||||
|
{ "type": "subagent_chunk", "agentName": "indicator", "content": "Applied patch. Validation passed." }
|
||||||
|
```
|
||||||
|
|
||||||
|
**Server → Client (completion)**
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"type": "details_updated",
|
||||||
|
"category": "indicator",
|
||||||
|
"name": "My Indicator Name",
|
||||||
|
"success": true
|
||||||
|
}
|
||||||
|
```
|
||||||
|
or on failure:
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"type": "details_updated",
|
||||||
|
"category": "indicator",
|
||||||
|
"name": "My Indicator Name",
|
||||||
|
"success": false,
|
||||||
|
"error": "Failed to update details"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Workspace Sync After Update
|
||||||
|
|
||||||
|
When the subagent calls `python_edit`, the sandbox returns a `_workspace_sync` payload in the MCP response. The gateway automatically applies this to the `{category}_types` workspace store and sends a WebSocket `patch` message to the client (the normal workspace sync path). The client should listen for these patches to refresh any UI that displays list metadata (name, description).
|
||||||
|
|
||||||
|
The `details` field itself is **not** in the workspace store — the client must call `read_details` again if it needs the refreshed details text after an update.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Implementation Notes
|
||||||
|
|
||||||
|
| Component | File | Responsibility |
|
||||||
|
|---|---|---|
|
||||||
|
| WebSocket routing | `src/channels/websocket-handler.ts` | Parse `read_details` / `update_details`, stream subagent events, send `details_data` / `details_updated` |
|
||||||
|
| Harness methods | `src/harness/agent-harness.ts` | `readDetails()`, `streamDetailsUpdate()` |
|
||||||
|
| Diff utility | `src/harness/agent-harness.ts` | `buildUnifiedDiff()`, `computeLCS()` (module-level helpers) |
|
||||||
|
| Instruction builder | `src/harness/agent-harness.ts` | `buildDetailsUpdateInstruction()` |
|
||||||
|
| Details filter | `src/tools/mcp/mcp-tool-wrapper.ts` | `filterTypeStoreState()` — strips `details` before workspace sync |
|
||||||
@@ -381,9 +381,75 @@ export class WebSocketHandler {
|
|||||||
// Workspace sync: patch message
|
// Workspace sync: patch message
|
||||||
logger.debug({ store: payload.store, seq: payload.seq }, 'Handling workspace patch');
|
logger.debug({ store: payload.store, seq: payload.seq }, 'Handling workspace patch');
|
||||||
await workspace!.handlePatch(payload.store, payload.seq, payload.patch || []);
|
await workspace!.handlePatch(payload.store, payload.seq, payload.patch || []);
|
||||||
|
} else if (payload.type === 'client_log') {
|
||||||
|
const level: string = payload.level ?? 'log';
|
||||||
|
const msg = `[client:${authContext.sessionId}] ${payload.message ?? ''}`;
|
||||||
|
const logMeta = { source: 'client', sessionId: authContext.sessionId };
|
||||||
|
if (level === 'error') logger.error(logMeta, msg);
|
||||||
|
else if (level === 'warn') logger.warn(logMeta, msg);
|
||||||
|
else if (level === 'debug') logger.debug(logMeta, msg);
|
||||||
|
else logger.info(logMeta, msg);
|
||||||
} else if (payload.type === 'agent_stop') {
|
} else if (payload.type === 'agent_stop') {
|
||||||
logger.info('Agent stop requested');
|
logger.info('Agent stop requested');
|
||||||
harness?.interrupt();
|
harness?.interrupt();
|
||||||
|
} else if (payload.type === 'read_details') {
|
||||||
|
// Read the details field for a category item
|
||||||
|
const { category, name } = payload;
|
||||||
|
if (!harness) {
|
||||||
|
socket.send(JSON.stringify({ type: 'details_error', category, name, error: 'Session not ready' }));
|
||||||
|
} else {
|
||||||
|
try {
|
||||||
|
const details = await harness.readDetails(category, name);
|
||||||
|
if (details === null) {
|
||||||
|
socket.send(JSON.stringify({ type: 'details_error', category, name, error: 'Item not found or has no details' }));
|
||||||
|
} else {
|
||||||
|
socket.send(JSON.stringify({ type: 'details_data', category, name, details }));
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
logger.error({ error, category, name }, 'Error reading details');
|
||||||
|
socket.send(JSON.stringify({ type: 'details_error', category, name, error: 'Failed to read details' }));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else if (payload.type === 'update_details') {
|
||||||
|
// User submitted a revised details string — diff and invoke the appropriate subagent
|
||||||
|
const { category, name, details: newDetails } = payload;
|
||||||
|
if (!harness) {
|
||||||
|
socket.send(JSON.stringify({ type: 'details_updated', category, name, success: false, error: 'Session not ready' }));
|
||||||
|
} else {
|
||||||
|
try {
|
||||||
|
let hadError = false;
|
||||||
|
for await (const event of harness.streamDetailsUpdate(category, name, newDetails)) {
|
||||||
|
const e = event as HarnessEvent;
|
||||||
|
switch (e.type) {
|
||||||
|
case 'chunk':
|
||||||
|
socket.send(JSON.stringify({ type: 'subagent_chunk', agentName: category, content: e.content }));
|
||||||
|
break;
|
||||||
|
case 'subagent_chunk':
|
||||||
|
socket.send(JSON.stringify({ type: 'subagent_chunk', agentName: e.agentName, content: e.content }));
|
||||||
|
break;
|
||||||
|
case 'subagent_tool_call':
|
||||||
|
socket.send(JSON.stringify({ type: 'subagent_tool_call', agentName: e.agentName, toolName: e.toolName, label: e.label }));
|
||||||
|
break;
|
||||||
|
case 'tool_call':
|
||||||
|
socket.send(JSON.stringify({ type: 'agent_tool_call', toolName: e.toolName, label: e.label }));
|
||||||
|
break;
|
||||||
|
case 'image':
|
||||||
|
socket.send(JSON.stringify({ type: 'image', data: e.data, mimeType: e.mimeType, caption: e.caption }));
|
||||||
|
break;
|
||||||
|
case 'error':
|
||||||
|
hadError = true;
|
||||||
|
socket.send(JSON.stringify({ type: 'subagent_chunk', agentName: category, content: `Error in ${e.source}` }));
|
||||||
|
break;
|
||||||
|
case 'done':
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
socket.send(JSON.stringify({ type: 'details_updated', category, name, success: !hadError }));
|
||||||
|
} catch (error) {
|
||||||
|
logger.error({ error, category, name }, 'Error updating details');
|
||||||
|
socket.send(JSON.stringify({ type: 'details_updated', category, name, success: false, error: 'Failed to update details' }));
|
||||||
|
}
|
||||||
|
}
|
||||||
} else if (this.isDatafeedMessage(payload)) {
|
} else if (this.isDatafeedMessage(payload)) {
|
||||||
// Historical data request - send to OHLC service
|
// Historical data request - send to OHLC service
|
||||||
logger.info({ type: payload.type }, 'Routing to datafeed handler');
|
logger.info({ type: payload.type }, 'Routing to datafeed handler');
|
||||||
|
|||||||
@@ -733,6 +733,106 @@ export class AgentHarness {
|
|||||||
return this.mcpClient.callTool(name, args);
|
return this.mcpClient.callTool(name, args);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Read the `details` field for a category item directly from the sandbox.
|
||||||
|
* Returns null if the item doesn't exist or has no details.
|
||||||
|
*/
|
||||||
|
async readDetails(category: string, name: string): Promise<string | null> {
|
||||||
|
try {
|
||||||
|
const raw = await this.mcpClient.callTool('python_read', { category, name });
|
||||||
|
const content = (raw as any)?.content;
|
||||||
|
if (!Array.isArray(content)) return null;
|
||||||
|
for (const item of content) {
|
||||||
|
if (item.type === 'text' && item.text) {
|
||||||
|
try {
|
||||||
|
const parsed = JSON.parse(item.text);
|
||||||
|
if (parsed?.exists && parsed?.metadata?.details !== undefined) {
|
||||||
|
return parsed.metadata.details as string;
|
||||||
|
}
|
||||||
|
} catch { /* ignore */ }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
} catch {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Stream a details-driven code update for a category item.
|
||||||
|
*
|
||||||
|
* Computes a unified diff between the stored details and `newDetails`,
|
||||||
|
* then instructs the appropriate subagent to update the Python code to
|
||||||
|
* match the revised specification. Yields HarnessEvents from the subagent
|
||||||
|
* so the WebSocket handler can stream progress to the web client.
|
||||||
|
*/
|
||||||
|
async *streamDetailsUpdate(
|
||||||
|
category: string,
|
||||||
|
name: string,
|
||||||
|
newDetails: string,
|
||||||
|
signal?: AbortSignal,
|
||||||
|
): AsyncGenerator<HarnessEvent> {
|
||||||
|
const logger = this.config.logger;
|
||||||
|
|
||||||
|
// 1. Read current details
|
||||||
|
const oldDetails = await this.readDetails(category, name) ?? '';
|
||||||
|
|
||||||
|
// 2. Compute a simple unified diff
|
||||||
|
const diff = buildUnifiedDiff(oldDetails, newDetails, `${category}/${name} details`);
|
||||||
|
|
||||||
|
if (!diff.trim()) {
|
||||||
|
// No change — nothing to do
|
||||||
|
yield { type: 'done', content: 'No changes detected in the details.' };
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3. Build instruction for the subagent
|
||||||
|
const instruction = buildDetailsUpdateInstruction(category, name, newDetails, diff);
|
||||||
|
|
||||||
|
// 4. Build a minimal subagent context
|
||||||
|
const context = {
|
||||||
|
userContext: createUserContext({
|
||||||
|
userId: this.config.userId,
|
||||||
|
sessionId: this.config.sessionId,
|
||||||
|
license: this.config.license,
|
||||||
|
channelType: this.config.channelType ?? ChannelType.WEBSOCKET,
|
||||||
|
channelUserId: this.config.channelUserId ?? this.config.userId,
|
||||||
|
}),
|
||||||
|
};
|
||||||
|
|
||||||
|
// 5. Ensure the right subagent is ready and invoke it
|
||||||
|
if (category === 'indicator') {
|
||||||
|
if (!this.indicatorSubagent) await this.initializeIndicatorSubagent();
|
||||||
|
if (!this.indicatorSubagent) {
|
||||||
|
yield { type: 'error', source: 'indicator', fatal: false };
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
logger.info({ category, name }, 'Streaming indicator details update');
|
||||||
|
yield* this.indicatorSubagent.streamEvents(context, instruction, signal);
|
||||||
|
|
||||||
|
} else if (category === 'strategy') {
|
||||||
|
if (!this.strategySubagent) await this.initializeStrategySubagent();
|
||||||
|
if (!this.strategySubagent) {
|
||||||
|
yield { type: 'error', source: 'strategy', fatal: false };
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
logger.info({ category, name }, 'Streaming strategy details update');
|
||||||
|
yield* this.strategySubagent.streamEvents(context, instruction, signal);
|
||||||
|
|
||||||
|
} else if (category === 'research') {
|
||||||
|
if (!this.researchSubagent) await this.initializeResearchSubagent();
|
||||||
|
if (!this.researchSubagent) {
|
||||||
|
yield { type: 'error', source: 'research', fatal: false };
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
logger.info({ category, name }, 'Streaming research details update');
|
||||||
|
yield* this.researchSubagent.streamEvents(context, instruction, signal);
|
||||||
|
|
||||||
|
} else {
|
||||||
|
yield { type: 'error', source: 'harness', fatal: false };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Expose MCP client so channel handlers can wire ContainerSync after harness init.
|
* Expose MCP client so channel handlers can wire ContainerSync after harness init.
|
||||||
*/
|
*/
|
||||||
@@ -1138,3 +1238,95 @@ export class AgentHarness {
|
|||||||
await this.mcpClient.disconnect();
|
await this.mcpClient.disconnect();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// =============================================================================
|
||||||
|
// Details update helpers (module-level, no class dependency)
|
||||||
|
// =============================================================================
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Produce a minimal unified diff between two strings, suitable for passing to
|
||||||
|
* an LLM as a change description. Returns an empty string when there is no diff.
|
||||||
|
*/
|
||||||
|
function buildUnifiedDiff(oldText: string, newText: string, label: string): string {
|
||||||
|
const oldLines = oldText.split('\n');
|
||||||
|
const newLines = newText.split('\n');
|
||||||
|
|
||||||
|
if (oldLines.join('\n') === newLines.join('\n')) return '';
|
||||||
|
|
||||||
|
const lines: string[] = [];
|
||||||
|
lines.push(`--- a/${label}`);
|
||||||
|
lines.push(`+++ b/${label}`);
|
||||||
|
|
||||||
|
// Simple LCS-based diff — sufficient for the LLM to understand structural changes
|
||||||
|
const lcs = computeLCS(oldLines, newLines);
|
||||||
|
let oi = 0, ni = 0, li = 0;
|
||||||
|
|
||||||
|
while (oi < oldLines.length || ni < newLines.length) {
|
||||||
|
if (li < lcs.length && oi < oldLines.length && ni < newLines.length &&
|
||||||
|
oldLines[oi] === lcs[li] && newLines[ni] === lcs[li]) {
|
||||||
|
lines.push(` ${oldLines[oi]}`);
|
||||||
|
oi++; ni++; li++;
|
||||||
|
} else if (ni < newLines.length && (li >= lcs.length || newLines[ni] !== lcs[li])) {
|
||||||
|
lines.push(`+${newLines[ni]}`);
|
||||||
|
ni++;
|
||||||
|
} else {
|
||||||
|
lines.push(`-${oldLines[oi]}`);
|
||||||
|
oi++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return lines.join('\n');
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Compute longest common subsequence of two string arrays. */
|
||||||
|
function computeLCS(a: string[], b: string[]): string[] {
|
||||||
|
const m = a.length, n = b.length;
|
||||||
|
const dp: number[][] = Array.from({ length: m + 1 }, () => new Array(n + 1).fill(0));
|
||||||
|
for (let i = 1; i <= m; i++) {
|
||||||
|
for (let j = 1; j <= n; j++) {
|
||||||
|
dp[i][j] = a[i - 1] === b[j - 1] ? dp[i - 1][j - 1] + 1 : Math.max(dp[i - 1][j], dp[i][j - 1]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const result: string[] = [];
|
||||||
|
let i = m, j = n;
|
||||||
|
while (i > 0 && j > 0) {
|
||||||
|
if (a[i - 1] === b[j - 1]) { result.unshift(a[i - 1]); i--; j--; }
|
||||||
|
else if (dp[i - 1][j] > dp[i][j - 1]) { i--; } else { j--; }
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Build the instruction string passed to the subagent when the user edits
|
||||||
|
* the details field of a category item.
|
||||||
|
*/
|
||||||
|
function buildDetailsUpdateInstruction(
|
||||||
|
category: string,
|
||||||
|
name: string,
|
||||||
|
newDetails: string,
|
||||||
|
diff: string,
|
||||||
|
): string {
|
||||||
|
const categoryLabel = category === 'indicator' ? 'custom indicator'
|
||||||
|
: category === 'strategy' ? 'trading strategy'
|
||||||
|
: 'research script';
|
||||||
|
|
||||||
|
return `The user has edited the specification (details) for the ${categoryLabel} named "${name}".
|
||||||
|
|
||||||
|
Your task: update the Python implementation to match the revised specification. Use \`python_edit\` with targeted patches — make only the changes implied by the diff below. Also update the \`details\` field via the \`details\` parameter on \`python_edit\` to store the new specification text.
|
||||||
|
|
||||||
|
## Revised specification
|
||||||
|
|
||||||
|
${newDetails}
|
||||||
|
|
||||||
|
## What changed (unified diff of the details text)
|
||||||
|
|
||||||
|
\`\`\`diff
|
||||||
|
${diff}
|
||||||
|
\`\`\`
|
||||||
|
|
||||||
|
Instructions:
|
||||||
|
- Read the current implementation first with \`python_read(category="${category}", name="${name}")\` to understand what exists.
|
||||||
|
- Apply only the changes described by the diff above — do not rewrite unrelated parts of the code.
|
||||||
|
- Pass \`details\` as the full revised specification text shown above.
|
||||||
|
- After editing, confirm the change was applied and validation passed.`;
|
||||||
|
}
|
||||||
|
|||||||
@@ -208,9 +208,13 @@ workspace_patch("indicators", [
|
|||||||
|
|
||||||
Custom indicators are Python scripts in the `indicator` category. Use `python_write` / `python_edit` / `python_read` / `python_list` exactly as you would for research scripts, but with `category="indicator"`.
|
Custom indicators are Python scripts in the `indicator` category. Use `python_write` / `python_edit` / `python_read` / `python_list` exactly as you would for research scripts, but with `category="indicator"`.
|
||||||
|
|
||||||
|
`python_write` requires `category`, `name`, `description`, `details`, and `code`. The `details` field must be a complete markdown description of the indicator — formula, algorithm, all parameters and their semantics, input series, output columns, and any non-obvious implementation choices — with enough detail that another agent could reproduce the code from it alone.
|
||||||
|
|
||||||
### Writing a Custom Indicator Script
|
### Writing a Custom Indicator Script
|
||||||
|
|
||||||
A custom indicator must define a **top-level function whose name exactly matches the sanitized directory name** (the name you passed to `python_write`, after sanitization). It receives the OHLC columns it needs as positional arguments, matching `input_series` in the metadata. It must return a `pd.Series` (single output) or `pd.DataFrame` (multi-output, column names must match `output_columns`).
|
A custom indicator must define a **top-level function whose name is the lowercase, snake_case form of the `name` passed to `python_write`**: take `name`, lowercase it, replace spaces and hyphens with underscores. For example, `name="TrendFlex"` → function `def trendflex(...)`, `name="VW RSI"` → function `def vw_rsi(...)`.
|
||||||
|
|
||||||
|
The function receives the OHLC columns it needs as positional arguments, matching `input_series` in the metadata. It must return a `pd.Series` (single output) or `pd.DataFrame` (multi-output, column names must match `output_columns`).
|
||||||
|
|
||||||
```python
|
```python
|
||||||
# Example: volume-weighted RSI (function name = "vw_rsi", directory name = "vw_rsi")
|
# Example: volume-weighted RSI (function name = "vw_rsi", directory name = "vw_rsi")
|
||||||
@@ -243,6 +247,15 @@ After writing a custom indicator with `python_write`, add it to the workspace us
|
|||||||
|
|
||||||
When writing a custom indicator you **must** supply complete metadata so the web client can auto-construct the TradingView plotter. Pass these fields in the `metadata` argument to `python_write`:
|
When writing a custom indicator you **must** supply complete metadata so the web client can auto-construct the TradingView plotter. Pass these fields in the `metadata` argument to `python_write`:
|
||||||
|
|
||||||
|
**Top-level required fields** (not inside `metadata`):
|
||||||
|
|
||||||
|
| Field | Required | Description |
|
||||||
|
|---|---|---|
|
||||||
|
| `description` | yes | One-sentence summary |
|
||||||
|
| `details` | yes | Full markdown description — formula, algorithm, all parameters and their semantics, input series, output columns, and any non-obvious choices. Enough detail for another agent to reproduce the code. |
|
||||||
|
|
||||||
|
**`metadata` fields:**
|
||||||
|
|
||||||
| Field | Type | Required | Description |
|
| Field | Type | Required | Description |
|
||||||
|---|---|---|---|
|
|---|---|---|---|
|
||||||
| `parameters` | dict | yes | Parameter schema: `{param_name: {type, default, description?, min?, max?}}` |
|
| `parameters` | dict | yes | Parameter schema: `{param_name: {type, default, description?, min?, max?}}` |
|
||||||
@@ -322,6 +335,15 @@ python_write(
|
|||||||
category="indicator",
|
category="indicator",
|
||||||
name="vw_rsi",
|
name="vw_rsi",
|
||||||
description="RSI weighted by relative volume.",
|
description="RSI weighted by relative volume.",
|
||||||
|
details="""## Volume-Weighted RSI
|
||||||
|
|
||||||
|
Computes RSI(length) on close prices, then scales it by relative volume (current volume divided by its rolling mean over the same period), and applies a 3-bar smoothing average.
|
||||||
|
|
||||||
|
**Formula:** `(rsi * (volume / volume.rolling(length).mean())).rolling(3).mean()`
|
||||||
|
|
||||||
|
**Inputs:** close (Series), volume (Series)
|
||||||
|
**Output:** single Series named "value" — the smoothed volume-weighted RSI, plotted in a separate pane.
|
||||||
|
**Parameters:** length (int, default 14, range 2–200) — lookback period for both RSI and the volume mean.""",
|
||||||
code="""
|
code="""
|
||||||
import pandas as pd
|
import pandas as pd
|
||||||
import pandas_ta as ta
|
import pandas_ta as ta
|
||||||
@@ -351,6 +373,15 @@ python_write(
|
|||||||
category="indicator",
|
category="indicator",
|
||||||
name="my_bbands",
|
name="my_bbands",
|
||||||
description="Custom Bollinger Bands.",
|
description="Custom Bollinger Bands.",
|
||||||
|
details="""## Custom Bollinger Bands
|
||||||
|
|
||||||
|
Standard Bollinger Bands computed via pandas-ta on close prices.
|
||||||
|
|
||||||
|
**Formula:** upper = SMA(length) + std * σ(length); lower = SMA(length) - std * σ(length); mid = SMA(length)
|
||||||
|
|
||||||
|
**Inputs:** close (Series)
|
||||||
|
**Outputs:** upper, mid, lower — three Series plotted on the price pane with a shaded fill between upper and lower.
|
||||||
|
**Parameters:** length (int, default 20, range 5–500), std (float, default 2.0, range 0.5–5.0)""",
|
||||||
code="""
|
code="""
|
||||||
import pandas as pd
|
import pandas as pd
|
||||||
import pandas_ta as ta
|
import pandas_ta as ta
|
||||||
@@ -439,13 +470,15 @@ Use `evaluate_indicator` to test any indicator (standard or custom) before addin
|
|||||||
evaluate_indicator(
|
evaluate_indicator(
|
||||||
symbol="BTC/USDT.BINANCE",
|
symbol="BTC/USDT.BINANCE",
|
||||||
from_time="30 days ago",
|
from_time="30 days ago",
|
||||||
to_time="now",
|
to_time="0 minutes ago",
|
||||||
period_seconds=3600,
|
period_seconds=3600,
|
||||||
pandas_ta_name="custom_vw_rsi",
|
pandas_ta_name="custom_vw_rsi",
|
||||||
parameters={"length": 14}
|
parameters={"length": 14}
|
||||||
)
|
)
|
||||||
```
|
```
|
||||||
|
|
||||||
|
**Time format for `from_time`/`to_time`**: Use a relative string like `"30 days ago"` / `"1 minute ago"` (format: `"N unit(s) ago"` where unit is second/minute/hour/day/week/month/year), an ISO date string like `"2024-04-20"`, or a Unix timestamp integer. Do **not** use `"now"` — it is not a valid value; use `"0 minutes ago"` instead.
|
||||||
|
|
||||||
Returns a structured array of `{timestamp, value}` (or multiple value columns for multi-output indicators like MACD, BBands). Use the results to confirm the indicator is computing as expected before patching the workspace.
|
Returns a structured array of `{timestamp, value}` (or multiple value columns for multi-output indicators like MACD, BBands). Use the results to confirm the indicator is computing as expected before patching the workspace.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|||||||
@@ -42,7 +42,7 @@ Quick reference — approximate bars per resolution at various windows:
|
|||||||
You have direct access to these MCP tools:
|
You have direct access to these MCP tools:
|
||||||
|
|
||||||
- **python_write**: Create a new script (research, strategy, or indicator category)
|
- **python_write**: Create a new script (research, strategy, or indicator category)
|
||||||
- Required: category, name, description, code
|
- Required: category, name, description, details, code
|
||||||
- Optional: metadata (category-specific fields — see below)
|
- Optional: metadata (category-specific fields — see below)
|
||||||
- **For research**: fully executes the script and returns all output (stdout, stderr) and captured chart images. The response IS the execution result — **do not call `execute_research` afterward**.
|
- **For research**: fully executes the script and returns all output (stdout, stderr) and captured chart images. The response IS the execution result — **do not call `execute_research` afterward**.
|
||||||
- **For indicator/strategy**: runs against synthetic test data to catch compile/runtime errors; no chart images are generated.
|
- **For indicator/strategy**: runs against synthetic test data to catch compile/runtime errors; no chart images are generated.
|
||||||
@@ -50,7 +50,7 @@ You have direct access to these MCP tools:
|
|||||||
|
|
||||||
- **python_edit**: Update an existing script
|
- **python_edit**: Update an existing script
|
||||||
- Required: category, name
|
- Required: category, name
|
||||||
- Optional: code, description, metadata
|
- Optional: code, patches, description, details (full replacement), detail_patches (targeted text replacements in details), metadata
|
||||||
- **For research**: re-executes the script when code is changed and returns all output and images. **Do not call `execute_research` afterward**.
|
- **For research**: re-executes the script when code is changed and returns all output and images. **Do not call `execute_research` afterward**.
|
||||||
- **For indicator/strategy**: re-runs the validation test only.
|
- **For indicator/strategy**: re-runs the validation test only.
|
||||||
- Returns validation results and execution output
|
- Returns validation results and execution output
|
||||||
@@ -100,6 +100,7 @@ When a user requests analysis:
|
|||||||
- Write clean, well-commented Python code
|
- Write clean, well-commented Python code
|
||||||
- Include proper error handling
|
- Include proper error handling
|
||||||
- Use appropriate ticker symbols, time ranges, and periods
|
- Use appropriate ticker symbols, time ranges, and periods
|
||||||
|
- Always supply `details`: a complete markdown description of what the script does — algorithms, data sources, parameters, and any non-obvious implementation choices — with enough detail that another agent could reproduce the code from it alone
|
||||||
- The script will auto-execute after writing
|
- The script will auto-execute after writing
|
||||||
|
|
||||||
4. **Check execution results**: The tool returns the execution result directly — this is the script's actual output:
|
4. **Check execution results**: The tool returns the execution result directly — this is the script's actual output:
|
||||||
@@ -164,6 +165,7 @@ You:
|
|||||||
3. Call `python_write` with:
|
3. Call `python_write` with:
|
||||||
- name: "BTC ETH Price Correlation"
|
- name: "BTC ETH Price Correlation"
|
||||||
- description: "Rolling correlation of BTC/USDT and ETH/USDT daily returns using 5 years of 1h data"
|
- description: "Rolling correlation of BTC/USDT and ETH/USDT daily returns using 5 years of 1h data"
|
||||||
|
- details: "Fetches 5 years of 1h OHLC for BTC/USDT.BINANCE and ETH/USDT.BINANCE. Computes log daily returns from close prices. Calculates a 30-day rolling Pearson correlation between the two return series. Plots the correlation over time with a horizontal zero line. Prints bar count and date range after each fetch."
|
||||||
- code: (Python script fetching 5yr of 1h OHLC for both tickers and plotting rolling correlation)
|
- code: (Python script fetching 5yr of 1h OHLC for both tickers and plotting rolling correlation)
|
||||||
4. Check execution results
|
4. Check execution results
|
||||||
5. If successful, respond with a brief summary of what the script does
|
5. If successful, respond with a brief summary of what the script does
|
||||||
|
|||||||
@@ -103,6 +103,21 @@ python_write(
|
|||||||
category="strategy",
|
category="strategy",
|
||||||
name="RSI Mean Reversion",
|
name="RSI Mean Reversion",
|
||||||
description="Buy oversold, sell overbought based on RSI(14) on BTC/USDT 1h bars.",
|
description="Buy oversold, sell overbought based on RSI(14) on BTC/USDT 1h bars.",
|
||||||
|
details="""## RSI Mean Reversion
|
||||||
|
|
||||||
|
Trades BTC/USDT on 5-minute bars using RSI(14) as the signal.
|
||||||
|
|
||||||
|
**Entry logic:**
|
||||||
|
- Buy when RSI crosses below `oversold` (default 30) — mean-reversion long
|
||||||
|
- Sell when RSI crosses above `overbought` (default 70) — mean-reversion short
|
||||||
|
|
||||||
|
**Position sizing:** `trade_qty` (default 0.01 BTC) per trade, fixed quantity.
|
||||||
|
|
||||||
|
**Parameters:** rsi_length (14), oversold (30), overbought (70), trade_qty (0.01)
|
||||||
|
|
||||||
|
**Data:** BTC/USDT.BINANCE 5-minute OHLCV bars. Requires at least `rsi_length + 1` bars before trading.
|
||||||
|
|
||||||
|
**No stop-loss or take-profit** — exits only on the opposite RSI signal.""",
|
||||||
code="""...""",
|
code="""...""",
|
||||||
metadata={
|
metadata={
|
||||||
"data_feeds": [
|
"data_feeds": [
|
||||||
@@ -113,12 +128,18 @@ python_write(
|
|||||||
"oversold": {"default": 30, "description": "RSI oversold threshold"},
|
"oversold": {"default": 30, "description": "RSI oversold threshold"},
|
||||||
"overbought": {"default": 70, "description": "RSI overbought threshold"},
|
"overbought": {"default": 70, "description": "RSI overbought threshold"},
|
||||||
"trade_qty": {"default": 0.01, "description": "Trade quantity in BTC"}
|
"trade_qty": {"default": 0.01, "description": "Trade quantity in BTC"}
|
||||||
},
|
}
|
||||||
"conda_packages": []
|
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
```
|
```
|
||||||
|
|
||||||
|
### Top-level fields
|
||||||
|
|
||||||
|
| Field | Required | Description |
|
||||||
|
|-------|----------|-------------|
|
||||||
|
| `description` | yes | One-sentence summary of the strategy |
|
||||||
|
| `details` | yes | Full markdown description — algorithm, entry/exit logic, parameters, data feeds, position sizing, and any non-obvious implementation choices. Must be detailed enough that another agent could reproduce the code from it alone. |
|
||||||
|
|
||||||
### Metadata fields
|
### Metadata fields
|
||||||
|
|
||||||
| Field | Required | Description |
|
| Field | Required | Description |
|
||||||
@@ -297,8 +318,9 @@ class VolumeBreakout(PandasStrategy):
|
|||||||
|
|
||||||
2. **Write the strategy**:
|
2. **Write the strategy**:
|
||||||
```
|
```
|
||||||
python_write(category="strategy", name="...", description="...", code="...", metadata={...})
|
python_write(category="strategy", name="...", description="...", details="...", code="...", metadata={...})
|
||||||
```
|
```
|
||||||
|
Always include `details`: a complete markdown description covering algorithm, entry/exit logic, all parameters, data feeds, and position sizing — enough detail for another agent to reproduce the code.
|
||||||
After writing, the system automatically runs the strategy against synthetic data. If validation fails, fix the reported error before proceeding.
|
After writing, the system automatically runs the strategy against synthetic data. If validation fails, fix the reported error before proceeding.
|
||||||
|
|
||||||
3. **Run a backtest** — choose the window to target 100k–200k bars at the strategy's resolution (max 5 years):
|
3. **Run a backtest** — choose the window to target 100k–200k bars at the strategy's resolution (max 5 years):
|
||||||
|
|||||||
@@ -20,6 +20,31 @@ export interface MCPToolInfo {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Strip the `details` field from all entries in a `_types` workspace store before
|
||||||
|
* syncing to clients. `details` is a long markdown blob intended for agent consumption
|
||||||
|
* only and should not be included in the compact workspace state sent to the web client.
|
||||||
|
*/
|
||||||
|
function filterTypeStoreState(storeName: string, state: unknown): unknown {
|
||||||
|
if (!storeName.endsWith('_types') || typeof state !== 'object' || state === null) {
|
||||||
|
return state;
|
||||||
|
}
|
||||||
|
const typed = state as Record<string, unknown>;
|
||||||
|
if (typeof typed['types'] !== 'object' || typed['types'] === null) {
|
||||||
|
return state;
|
||||||
|
}
|
||||||
|
const filteredTypes: Record<string, unknown> = {};
|
||||||
|
for (const [key, entry] of Object.entries(typed['types'] as Record<string, unknown>)) {
|
||||||
|
if (typeof entry === 'object' && entry !== null) {
|
||||||
|
const { details: _details, ...rest } = entry as Record<string, unknown>;
|
||||||
|
filteredTypes[key] = rest;
|
||||||
|
} else {
|
||||||
|
filteredTypes[key] = entry;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return { ...typed, types: filteredTypes };
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Create a LangChain tool from an MCP tool definition
|
* Create a LangChain tool from an MCP tool definition
|
||||||
*/
|
*/
|
||||||
@@ -57,12 +82,14 @@ export function createMCPToolWrapper(
|
|||||||
(toolInfo.name === 'workspace_patch' || toolInfo.name === 'workspace_write') &&
|
(toolInfo.name === 'workspace_patch' || toolInfo.name === 'workspace_write') &&
|
||||||
parsed?.success && parsed?.data !== undefined
|
parsed?.success && parsed?.data !== undefined
|
||||||
) {
|
) {
|
||||||
onWorkspaceMutation((input as any).store_name as string, parsed.data);
|
const storeName = (input as any).store_name as string;
|
||||||
|
onWorkspaceMutation(storeName, filterTypeStoreState(storeName, parsed.data));
|
||||||
}
|
}
|
||||||
// python_write / python_edit / python_delete / python_revert:
|
// python_write / python_edit / python_delete / python_revert:
|
||||||
// {"_workspace_sync": {"store": <name>, "data": <state>}}
|
// {"_workspace_sync": {"store": <name>, "data": <state>}}
|
||||||
if (parsed?._workspace_sync?.store && parsed._workspace_sync.data !== undefined) {
|
if (parsed?._workspace_sync?.store && parsed._workspace_sync.data !== undefined) {
|
||||||
onWorkspaceMutation(parsed._workspace_sync.store, parsed._workspace_sync.data);
|
const storeName = parsed._workspace_sync.store as string;
|
||||||
|
onWorkspaceMutation(storeName, filterTypeStoreState(storeName, parsed._workspace_sync.data));
|
||||||
}
|
}
|
||||||
} catch { /* ignore parse errors */ }
|
} catch { /* ignore parse errors */ }
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -101,17 +101,17 @@ export const DEFAULT_STORES: StoreConfig[] = [
|
|||||||
{
|
{
|
||||||
name: 'indicator_types',
|
name: 'indicator_types',
|
||||||
persistent: true,
|
persistent: true,
|
||||||
initialState: () => ({}),
|
initialState: () => ({ types: {} }),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'strategy_types',
|
name: 'strategy_types',
|
||||||
persistent: true,
|
persistent: true,
|
||||||
initialState: () => ({}),
|
initialState: () => ({ types: {} }),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'research_types',
|
name: 'research_types',
|
||||||
persistent: true,
|
persistent: true,
|
||||||
initialState: () => ({}),
|
initialState: () => ({ types: {} }),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'channelState',
|
name: 'channelState',
|
||||||
|
|||||||
@@ -97,6 +97,15 @@ export class WorkspaceManager {
|
|||||||
this.dirtyStores.add(storeName); // persist migrated format immediately
|
this.dirtyStores.add(storeName); // persist migrated format immediately
|
||||||
this.logger.info({ store: storeName }, 'Migrated shapes store to wrapped format');
|
this.logger.info({ store: storeName }, 'Migrated shapes store to wrapped format');
|
||||||
}
|
}
|
||||||
|
// Migrate *_types stores from old flat format { key: {...} } to wrapped { types: { key: {...} } }
|
||||||
|
if (
|
||||||
|
(storeName === 'indicator_types' || storeName === 'strategy_types' || storeName === 'research_types') &&
|
||||||
|
state && typeof state === 'object' && !('types' in (state as object))
|
||||||
|
) {
|
||||||
|
migratedState = { types: state };
|
||||||
|
this.dirtyStores.add(storeName); // persist migrated format immediately
|
||||||
|
this.logger.info({ store: storeName }, 'Migrated types store to wrapped format');
|
||||||
|
}
|
||||||
this.registry.setState(storeName, migratedState);
|
this.registry.setState(storeName, migratedState);
|
||||||
this.logger.debug({ store: storeName }, 'Loaded persistent store');
|
this.logger.debug({ store: storeName }, 'Loaded persistent store');
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -144,10 +144,25 @@ async def evaluate_indicator(
|
|||||||
)
|
)
|
||||||
}))]
|
}))]
|
||||||
|
|
||||||
# Get input_series from the indicator's metadata
|
# Get input_series from the indicator's metadata.
|
||||||
indicator_name = pandas_ta_name[len("custom_"):]
|
# Look up by the stored pandas_ta_name field (written at creation time),
|
||||||
|
# which is the reliable reverse mapping from ta_name → directory.
|
||||||
|
# Fall back to display-name matching for indicators created before this
|
||||||
|
# field was added.
|
||||||
mgr = get_category_manager()
|
mgr = get_category_manager()
|
||||||
read_result = mgr.read("indicator", indicator_name)
|
all_items = mgr.list_items("indicator")
|
||||||
|
read_result = {"exists": False}
|
||||||
|
for item in all_items.get("items", []):
|
||||||
|
meta = item.get("metadata", {})
|
||||||
|
# Primary: match stored pandas_ta_name field (exact, set at write time)
|
||||||
|
if meta.get("pandas_ta_name") == name_lower:
|
||||||
|
read_result = mgr.read("indicator", item.get("name", ""))
|
||||||
|
break
|
||||||
|
# Fallback: infer from display name (legacy indicators without the field)
|
||||||
|
item_name = item.get("name", "")
|
||||||
|
if "custom_" + item_name.lower().replace("-", "_").replace(" ", "_") == name_lower:
|
||||||
|
read_result = mgr.read("indicator", item_name)
|
||||||
|
break
|
||||||
if read_result.get("exists") and read_result.get("metadata"):
|
if read_result.get("exists") and read_result.get("metadata"):
|
||||||
raw_series = read_result["metadata"].get("input_series") or ["close"]
|
raw_series = read_result["metadata"].get("input_series") or ["close"]
|
||||||
input_cols = tuple(raw_series)
|
input_cols = tuple(raw_series)
|
||||||
|
|||||||
@@ -62,6 +62,12 @@ class BaseMetadata:
|
|||||||
"""Base metadata for all categories."""
|
"""Base metadata for all categories."""
|
||||||
name: str # Display name (can have special chars)
|
name: str # Display name (can have special chars)
|
||||||
description: str # LLM-generated description
|
description: str # LLM-generated description
|
||||||
|
details: str = "" # Full markdown description with enough detail to reproduce the code
|
||||||
|
conda_packages: list[str] = None # Additional conda packages required
|
||||||
|
|
||||||
|
def __post_init__(self):
|
||||||
|
if self.conda_packages is None:
|
||||||
|
self.conda_packages = []
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
@@ -69,21 +75,21 @@ class StrategyMetadata(BaseMetadata):
|
|||||||
"""Metadata for trading strategies."""
|
"""Metadata for trading strategies."""
|
||||||
data_feeds: list[dict] = None # Required data feeds: [{"symbol": "BTC/USDT.BINANCE", "period_seconds": 3600, "description": "..."}]
|
data_feeds: list[dict] = None # Required data feeds: [{"symbol": "BTC/USDT.BINANCE", "period_seconds": 3600, "description": "..."}]
|
||||||
parameters: dict = None # Strategy parameters: {"param_name": {"default": value, "description": "..."}}
|
parameters: dict = None # Strategy parameters: {"param_name": {"default": value, "description": "..."}}
|
||||||
conda_packages: list[str] = None # Additional conda packages required
|
|
||||||
|
|
||||||
def __post_init__(self):
|
def __post_init__(self):
|
||||||
|
super().__post_init__()
|
||||||
if self.data_feeds is None:
|
if self.data_feeds is None:
|
||||||
self.data_feeds = []
|
self.data_feeds = []
|
||||||
if self.parameters is None:
|
if self.parameters is None:
|
||||||
self.parameters = {}
|
self.parameters = {}
|
||||||
if self.conda_packages is None:
|
|
||||||
self.conda_packages = []
|
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
class IndicatorMetadata(BaseMetadata):
|
class IndicatorMetadata(BaseMetadata):
|
||||||
"""Metadata for technical indicators."""
|
"""Metadata for technical indicators."""
|
||||||
conda_packages: list[str] = None # Additional conda packages required
|
# Canonical pandas-ta name, e.g. "custom_trendflex". Set automatically
|
||||||
|
# by CategoryFileManager.write() — do not pass manually.
|
||||||
|
pandas_ta_name: str = ""
|
||||||
|
|
||||||
# Fields for TradingView custom study auto-construction:
|
# Fields for TradingView custom study auto-construction:
|
||||||
parameters: dict = None
|
parameters: dict = None
|
||||||
@@ -143,8 +149,7 @@ class IndicatorMetadata(BaseMetadata):
|
|||||||
# Example (RSI levels): [{"id": "ob", "value": 70}, {"id": "os", "value": 30}]
|
# Example (RSI levels): [{"id": "ob", "value": 70}, {"id": "os", "value": 30}]
|
||||||
|
|
||||||
def __post_init__(self):
|
def __post_init__(self):
|
||||||
if self.conda_packages is None:
|
super().__post_init__()
|
||||||
self.conda_packages = []
|
|
||||||
if self.input_series is None:
|
if self.input_series is None:
|
||||||
self.input_series = ["close"]
|
self.input_series = ["close"]
|
||||||
if self.output_columns is None:
|
if self.output_columns is None:
|
||||||
@@ -160,11 +165,7 @@ class IndicatorMetadata(BaseMetadata):
|
|||||||
@dataclass
|
@dataclass
|
||||||
class ResearchMetadata(BaseMetadata):
|
class ResearchMetadata(BaseMetadata):
|
||||||
"""Metadata for research scripts."""
|
"""Metadata for research scripts."""
|
||||||
conda_packages: list[str] = None # Additional conda packages required
|
pass
|
||||||
|
|
||||||
def __post_init__(self):
|
|
||||||
if self.conda_packages is None:
|
|
||||||
self.conda_packages = []
|
|
||||||
|
|
||||||
|
|
||||||
# Metadata class registry
|
# Metadata class registry
|
||||||
@@ -503,6 +504,7 @@ class CategoryFileManager:
|
|||||||
category: str,
|
category: str,
|
||||||
name: str,
|
name: str,
|
||||||
description: str,
|
description: str,
|
||||||
|
details: str,
|
||||||
code: str,
|
code: str,
|
||||||
metadata: Optional[dict] = None
|
metadata: Optional[dict] = None
|
||||||
) -> dict[str, Any]:
|
) -> dict[str, Any]:
|
||||||
@@ -513,6 +515,7 @@ class CategoryFileManager:
|
|||||||
category: Category name (strategy, indicator, research)
|
category: Category name (strategy, indicator, research)
|
||||||
name: Display name for the item
|
name: Display name for the item
|
||||||
description: LLM-generated description (required)
|
description: LLM-generated description (required)
|
||||||
|
details: Full markdown description with enough detail to reproduce the code (required)
|
||||||
code: Python implementation code
|
code: Python implementation code
|
||||||
metadata: Additional category-specific metadata fields
|
metadata: Additional category-specific metadata fields
|
||||||
|
|
||||||
@@ -547,6 +550,13 @@ class CategoryFileManager:
|
|||||||
meta_dict = metadata or {}
|
meta_dict = metadata or {}
|
||||||
meta_dict["name"] = name
|
meta_dict["name"] = name
|
||||||
meta_dict["description"] = description
|
meta_dict["description"] = description
|
||||||
|
meta_dict["details"] = details
|
||||||
|
|
||||||
|
# For indicators, store the canonical pandas_ta_name so the reverse
|
||||||
|
# mapping (ta_name → directory) is reliable regardless of name casing.
|
||||||
|
if cat == Category.INDICATOR:
|
||||||
|
sanitized = sanitize_name(name).lower()
|
||||||
|
meta_dict["pandas_ta_name"] = f"custom_{sanitized}"
|
||||||
|
|
||||||
# Validate and write metadata
|
# Validate and write metadata
|
||||||
try:
|
try:
|
||||||
@@ -592,6 +602,8 @@ class CategoryFileManager:
|
|||||||
code: Optional[str] = None,
|
code: Optional[str] = None,
|
||||||
patches: Optional[list[dict]] = None,
|
patches: Optional[list[dict]] = None,
|
||||||
description: Optional[str] = None,
|
description: Optional[str] = None,
|
||||||
|
details: Optional[str] = None,
|
||||||
|
detail_patches: Optional[list[dict]] = None,
|
||||||
metadata: Optional[dict] = None
|
metadata: Optional[dict] = None
|
||||||
) -> dict[str, Any]:
|
) -> dict[str, Any]:
|
||||||
"""
|
"""
|
||||||
@@ -603,6 +615,8 @@ class CategoryFileManager:
|
|||||||
code: Full Python implementation code to replace existing (optional)
|
code: Full Python implementation code to replace existing (optional)
|
||||||
patches: List of {old_string, new_string} replacements (optional, preferred for small changes)
|
patches: List of {old_string, new_string} replacements (optional, preferred for small changes)
|
||||||
description: Updated description (optional, omit to keep existing)
|
description: Updated description (optional, omit to keep existing)
|
||||||
|
details: Full replacement for the details field (optional, mutually exclusive with detail_patches)
|
||||||
|
detail_patches: List of {old_string, new_string} replacements applied to the details field (optional)
|
||||||
metadata: Additional metadata updates (optional)
|
metadata: Additional metadata updates (optional)
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
@@ -614,6 +628,8 @@ class CategoryFileManager:
|
|||||||
"""
|
"""
|
||||||
if code is not None and patches is not None:
|
if code is not None and patches is not None:
|
||||||
return {"success": False, "error": "Provide either 'code' or 'patches', not both"}
|
return {"success": False, "error": "Provide either 'code' or 'patches', not both"}
|
||||||
|
if details is not None and detail_patches is not None:
|
||||||
|
return {"success": False, "error": "Provide either 'details' or 'detail_patches', not both"}
|
||||||
|
|
||||||
try:
|
try:
|
||||||
cat = Category(category)
|
cat = Category(category)
|
||||||
@@ -664,10 +680,25 @@ class CategoryFileManager:
|
|||||||
except Exception as e:
|
except Exception as e:
|
||||||
return {"success": False, "error": f"Failed to write implementation: {e}"}
|
return {"success": False, "error": f"Failed to write implementation: {e}"}
|
||||||
|
|
||||||
|
# Apply text-replacement patches to details field if provided
|
||||||
|
if detail_patches is not None:
|
||||||
|
current_details = existing_meta.get("details", "")
|
||||||
|
for i, patch in enumerate(detail_patches):
|
||||||
|
old = patch.get("old_string", "")
|
||||||
|
new = patch.get("new_string", "")
|
||||||
|
if old not in current_details:
|
||||||
|
return {"success": False, "error": f"Detail patch {i}: old_string not found in details"}
|
||||||
|
if current_details.count(old) > 1:
|
||||||
|
return {"success": False, "error": f"Detail patch {i}: old_string is not unique — add more surrounding context"}
|
||||||
|
current_details = current_details.replace(old, new, 1)
|
||||||
|
details = current_details
|
||||||
|
|
||||||
# Update metadata
|
# Update metadata
|
||||||
updated_meta = existing_meta.copy()
|
updated_meta = existing_meta.copy()
|
||||||
if description is not None:
|
if description is not None:
|
||||||
updated_meta["description"] = description
|
updated_meta["description"] = description
|
||||||
|
if details is not None:
|
||||||
|
updated_meta["details"] = details
|
||||||
if metadata:
|
if metadata:
|
||||||
updated_meta.update(metadata)
|
updated_meta.update(metadata)
|
||||||
|
|
||||||
|
|||||||
@@ -197,26 +197,6 @@ def _get_env_yml() -> Optional[Path]:
|
|||||||
return p if p.exists() else None
|
return p if p.exists() else None
|
||||||
|
|
||||||
|
|
||||||
def _populate_indicator_types_from_disk(workspace_store, category_manager) -> None:
|
|
||||||
"""Scan existing indicators and add any missing entries to indicator_types store."""
|
|
||||||
existing = workspace_store.read('indicator_types')
|
|
||||||
existing_types = (existing.get('data') or {}).get('types') or {}
|
|
||||||
|
|
||||||
list_result = category_manager.list_items('indicator')
|
|
||||||
items = list_result.get('items', [])
|
|
||||||
added = 0
|
|
||||||
for item in items:
|
|
||||||
item_name = item.get('name', '')
|
|
||||||
if not item_name:
|
|
||||||
continue
|
|
||||||
pandas_ta_name = f"custom_{sanitize_name(item_name).lower()}"
|
|
||||||
if pandas_ta_name not in existing_types:
|
|
||||||
_upsert_indicator_type(workspace_store, category_manager, item_name)
|
|
||||||
added += 1
|
|
||||||
|
|
||||||
if added > 0:
|
|
||||||
logging.info(f"Populated {added} indicator type(s) from disk into indicator_types store")
|
|
||||||
|
|
||||||
|
|
||||||
# =============================================================================
|
# =============================================================================
|
||||||
# Configuration
|
# Configuration
|
||||||
@@ -436,6 +416,15 @@ def create_mcp_server(config: Config, event_publisher: EventPublisher) -> Server
|
|||||||
"type": "string",
|
"type": "string",
|
||||||
"description": "LLM-generated description of what this does (required)"
|
"description": "LLM-generated description of what this does (required)"
|
||||||
},
|
},
|
||||||
|
"details": {
|
||||||
|
"type": "string",
|
||||||
|
"description": (
|
||||||
|
"Full markdown description of the code with sufficient detail that another coding agent "
|
||||||
|
"could functionally reproduce the implementation from this field alone. "
|
||||||
|
"Include: purpose, algorithm, all parameters and their semantics, data feed usage, "
|
||||||
|
"formulas, edge cases, and any non-obvious implementation choices (required)."
|
||||||
|
)
|
||||||
|
},
|
||||||
"code": {
|
"code": {
|
||||||
"type": "string",
|
"type": "string",
|
||||||
"description": "Python implementation code"
|
"description": "Python implementation code"
|
||||||
@@ -453,7 +442,7 @@ def create_mcp_server(config: Config, event_publisher: EventPublisher) -> Server
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"required": ["category", "name", "description", "code"]
|
"required": ["category", "name", "description", "details", "code"]
|
||||||
}
|
}
|
||||||
),
|
),
|
||||||
Tool(
|
Tool(
|
||||||
@@ -500,6 +489,27 @@ def create_mcp_server(config: Config, event_publisher: EventPublisher) -> Server
|
|||||||
"type": "string",
|
"type": "string",
|
||||||
"description": "Updated description (optional, omit to keep existing)"
|
"description": "Updated description (optional, omit to keep existing)"
|
||||||
},
|
},
|
||||||
|
"details": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "Full replacement for the details field. Use only when rewriting the entire description; prefer 'detail_patches' for targeted edits."
|
||||||
|
},
|
||||||
|
"detail_patches": {
|
||||||
|
"type": "array",
|
||||||
|
"description": (
|
||||||
|
"Targeted edits to the details field as old/new string pairs. Preferred over 'details' for small changes. "
|
||||||
|
"Each patch: {\"old_string\": \"exact text to find\", \"new_string\": \"replacement text\"}. "
|
||||||
|
"old_string must be unique in the details field (add surrounding context if needed). "
|
||||||
|
"Patches are applied in order. Mutually exclusive with 'details'."
|
||||||
|
),
|
||||||
|
"items": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"old_string": {"type": "string"},
|
||||||
|
"new_string": {"type": "string"}
|
||||||
|
},
|
||||||
|
"required": ["old_string", "new_string"]
|
||||||
|
}
|
||||||
|
},
|
||||||
"metadata": {
|
"metadata": {
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"description": (
|
"description": (
|
||||||
@@ -912,6 +922,7 @@ def create_mcp_server(config: Config, event_publisher: EventPublisher) -> Server
|
|||||||
category=arguments.get("category", ""),
|
category=arguments.get("category", ""),
|
||||||
name=arguments.get("name", ""),
|
name=arguments.get("name", ""),
|
||||||
description=arguments.get("description", ""),
|
description=arguments.get("description", ""),
|
||||||
|
details=arguments.get("details", ""),
|
||||||
code=arguments.get("code", ""),
|
code=arguments.get("code", ""),
|
||||||
metadata=arguments.get("metadata")
|
metadata=arguments.get("metadata")
|
||||||
)
|
)
|
||||||
@@ -947,6 +958,8 @@ def create_mcp_server(config: Config, event_publisher: EventPublisher) -> Server
|
|||||||
code=arguments.get("code"),
|
code=arguments.get("code"),
|
||||||
patches=arguments.get("patches"),
|
patches=arguments.get("patches"),
|
||||||
description=arguments.get("description"),
|
description=arguments.get("description"),
|
||||||
|
details=arguments.get("details"),
|
||||||
|
detail_patches=arguments.get("detail_patches"),
|
||||||
metadata=arguments.get("metadata")
|
metadata=arguments.get("metadata")
|
||||||
)
|
)
|
||||||
content = []
|
content = []
|
||||||
|
|||||||
759
web/package-lock.json
generated
759
web/package-lock.json
generated
@@ -9,10 +9,13 @@
|
|||||||
"version": "0.0.0",
|
"version": "0.0.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@primevue/themes": "^4.5.4",
|
"@primevue/themes": "^4.5.4",
|
||||||
|
"@tiptap/starter-kit": "^3.22.4",
|
||||||
|
"@tiptap/vue-3": "^3.22.4",
|
||||||
"fast-json-patch": "^3.1.1",
|
"fast-json-patch": "^3.1.1",
|
||||||
"pinia": "^3.0.4",
|
"pinia": "^3.0.4",
|
||||||
"primeicons": "^7.0.0",
|
"primeicons": "^7.0.0",
|
||||||
"primevue": "^4.5.4",
|
"primevue": "^4.5.4",
|
||||||
|
"tiptap-markdown": "^0.9.0",
|
||||||
"vue": "^3.5.29",
|
"vue": "^3.5.29",
|
||||||
"vue-advanced-chat": "^2.0.4",
|
"vue-advanced-chat": "^2.0.4",
|
||||||
"vue-router": "^5.0.3"
|
"vue-router": "^5.0.3"
|
||||||
@@ -133,6 +136,7 @@
|
|||||||
"integrity": "sha512-CGOfOJqWjg2qW/Mb6zNsDm+u5vFQ8DxXfbM09z69p5Z6+mE1ikP2jUXw+j42Pf1XTYED2Rni5f95npYeuwMDQA==",
|
"integrity": "sha512-CGOfOJqWjg2qW/Mb6zNsDm+u5vFQ8DxXfbM09z69p5Z6+mE1ikP2jUXw+j42Pf1XTYED2Rni5f95npYeuwMDQA==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
"peer": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@babel/code-frame": "^7.29.0",
|
"@babel/code-frame": "^7.29.0",
|
||||||
"@babel/generator": "^7.29.0",
|
"@babel/generator": "^7.29.0",
|
||||||
@@ -657,6 +661,7 @@
|
|||||||
}
|
}
|
||||||
],
|
],
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
"peer": true,
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=20.19.0"
|
"node": ">=20.19.0"
|
||||||
},
|
},
|
||||||
@@ -697,6 +702,7 @@
|
|||||||
}
|
}
|
||||||
],
|
],
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
"peer": true,
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=20.19.0"
|
"node": ">=20.19.0"
|
||||||
}
|
}
|
||||||
@@ -1161,6 +1167,32 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@floating-ui/core": {
|
||||||
|
"version": "1.7.5",
|
||||||
|
"resolved": "https://registry.npmjs.org/@floating-ui/core/-/core-1.7.5.tgz",
|
||||||
|
"integrity": "sha512-1Ih4WTWyw0+lKyFMcBHGbb5U5FtuHJuujoyyr5zTaWS5EYMeT6Jb2AuDeftsCsEuchO+mM2ij5+q9crhydzLhQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@floating-ui/utils": "^0.2.11"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@floating-ui/dom": {
|
||||||
|
"version": "1.7.6",
|
||||||
|
"resolved": "https://registry.npmjs.org/@floating-ui/dom/-/dom-1.7.6.tgz",
|
||||||
|
"integrity": "sha512-9gZSAI5XM36880PPMm//9dfiEngYoC6Am2izES1FF406YFsjvyBMmeJ2g4SAju3xWwtuynNRFL2s9hgxpLI5SQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"peer": true,
|
||||||
|
"dependencies": {
|
||||||
|
"@floating-ui/core": "^1.7.5",
|
||||||
|
"@floating-ui/utils": "^0.2.11"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@floating-ui/utils": {
|
||||||
|
"version": "0.2.11",
|
||||||
|
"resolved": "https://registry.npmjs.org/@floating-ui/utils/-/utils-0.2.11.tgz",
|
||||||
|
"integrity": "sha512-RiB/yIh78pcIxl6lLMG0CgBXAZ2Y0eVHqMPYugu+9U0AeT6YBeiJpf7lbdJNIugFP5SIjwNRgo4DhR1Qxi26Gg==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/@isaacs/cliui": {
|
"node_modules/@isaacs/cliui": {
|
||||||
"version": "8.0.2",
|
"version": "8.0.2",
|
||||||
"resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz",
|
"resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz",
|
||||||
@@ -1695,6 +1727,431 @@
|
|||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/@tiptap/core": {
|
||||||
|
"version": "3.22.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/@tiptap/core/-/core-3.22.4.tgz",
|
||||||
|
"integrity": "sha512-vGIGm/HpqLg8EAAQXQ+koV+/S828OEpzocfWcPOwo1u2QUVf9dQG47Yy6JJ8zFFaJwfv4dBcOXli+7BrJwsxDQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"peer": true,
|
||||||
|
"funding": {
|
||||||
|
"type": "github",
|
||||||
|
"url": "https://github.com/sponsors/ueberdosis"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@tiptap/pm": "3.22.4"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@tiptap/extension-blockquote": {
|
||||||
|
"version": "3.22.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/@tiptap/extension-blockquote/-/extension-blockquote-3.22.4.tgz",
|
||||||
|
"integrity": "sha512-7/61kNPbGFhMgM//zMknD0pSb69rGdRIkpulXOWS1JBrFHkH6hjZDfrOETNzgKkO+NlmzVl9rXSTv0xauS3lzA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"funding": {
|
||||||
|
"type": "github",
|
||||||
|
"url": "https://github.com/sponsors/ueberdosis"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@tiptap/core": "3.22.4"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@tiptap/extension-bold": {
|
||||||
|
"version": "3.22.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/@tiptap/extension-bold/-/extension-bold-3.22.4.tgz",
|
||||||
|
"integrity": "sha512-jIaPKfNOQu2lhpbLDvtwlQqM+mjF+Kk+auHpzYjBnsuwUli1Cl5ZOau7RH+rru/SQvZe1DtpQlANujDywugZAA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"funding": {
|
||||||
|
"type": "github",
|
||||||
|
"url": "https://github.com/sponsors/ueberdosis"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@tiptap/core": "3.22.4"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@tiptap/extension-bubble-menu": {
|
||||||
|
"version": "3.22.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/@tiptap/extension-bubble-menu/-/extension-bubble-menu-3.22.4.tgz",
|
||||||
|
"integrity": "sha512-v4pux5Ql3THAEjaLMY4ldtdy/Xy2qU7PJLBkq8ugLp8qicaKC+tpqxp6sGif4vLIjz7Ap5hurRbTNbXzszyyHA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"dependencies": {
|
||||||
|
"@floating-ui/dom": "^1.0.0"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"type": "github",
|
||||||
|
"url": "https://github.com/sponsors/ueberdosis"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@tiptap/core": "3.22.4",
|
||||||
|
"@tiptap/pm": "3.22.4"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@tiptap/extension-bullet-list": {
|
||||||
|
"version": "3.22.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/@tiptap/extension-bullet-list/-/extension-bullet-list-3.22.4.tgz",
|
||||||
|
"integrity": "sha512-TB+d3fGcTixYjO7coKqTr1mGTJuqr8hjDCPUFgzuvKyJnBhqWITmBzQ/8CLq4rr6mihgGURbD3N+xkQuPAKFiw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"funding": {
|
||||||
|
"type": "github",
|
||||||
|
"url": "https://github.com/sponsors/ueberdosis"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@tiptap/extension-list": "3.22.4"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@tiptap/extension-code": {
|
||||||
|
"version": "3.22.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/@tiptap/extension-code/-/extension-code-3.22.4.tgz",
|
||||||
|
"integrity": "sha512-cnbxmVhAcc7X3G81QUYEmKP0ve2hRmvAiFXBuuv9RUtQlBiRnzmhHoJOMgkX0CsMR7+8kMRpTfeDUYq2xp5s5w==",
|
||||||
|
"license": "MIT",
|
||||||
|
"funding": {
|
||||||
|
"type": "github",
|
||||||
|
"url": "https://github.com/sponsors/ueberdosis"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@tiptap/core": "3.22.4"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@tiptap/extension-code-block": {
|
||||||
|
"version": "3.22.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/@tiptap/extension-code-block/-/extension-code-block-3.22.4.tgz",
|
||||||
|
"integrity": "sha512-MEurzNXfMET3rhjpoPJYUgMfxTdTqbzT9+ToFrqNGAHocdXVm6m1hhO2frVC7fEtHPnxXKsn0Z3NUbCRkRTLuA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"funding": {
|
||||||
|
"type": "github",
|
||||||
|
"url": "https://github.com/sponsors/ueberdosis"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@tiptap/core": "3.22.4",
|
||||||
|
"@tiptap/pm": "3.22.4"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@tiptap/extension-document": {
|
||||||
|
"version": "3.22.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/@tiptap/extension-document/-/extension-document-3.22.4.tgz",
|
||||||
|
"integrity": "sha512-XQKla1+703FqQJC48tPDVgt9ucGiFbIEmQdOg5L5o07z9a6/NzuaZAc+1zJ7NxcUZzy+z6wBn1PrVMTiqiSXlw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"funding": {
|
||||||
|
"type": "github",
|
||||||
|
"url": "https://github.com/sponsors/ueberdosis"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@tiptap/core": "3.22.4"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@tiptap/extension-dropcursor": {
|
||||||
|
"version": "3.22.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/@tiptap/extension-dropcursor/-/extension-dropcursor-3.22.4.tgz",
|
||||||
|
"integrity": "sha512-N9/yMDC35jJp0V/naL0+6gi4gUDUIcPpWEzFdCDWUSYBA8mt41c1kI1ZU7UTKYIBzTClenhYHRc2XKZxxx0+LQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"funding": {
|
||||||
|
"type": "github",
|
||||||
|
"url": "https://github.com/sponsors/ueberdosis"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@tiptap/extensions": "3.22.4"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@tiptap/extension-floating-menu": {
|
||||||
|
"version": "3.22.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/@tiptap/extension-floating-menu/-/extension-floating-menu-3.22.4.tgz",
|
||||||
|
"integrity": "sha512-DFuyYxgaZPgxum5z1yvJPbfYCvDdO8geXsdyqt0qYYdiat3aGE4ncJhiLRIFDhSHBhaZg5eCgu/YPYAN6jZnrA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"funding": {
|
||||||
|
"type": "github",
|
||||||
|
"url": "https://github.com/sponsors/ueberdosis"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@floating-ui/dom": "^1.0.0",
|
||||||
|
"@tiptap/core": "3.22.4",
|
||||||
|
"@tiptap/pm": "3.22.4"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@tiptap/extension-gapcursor": {
|
||||||
|
"version": "3.22.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/@tiptap/extension-gapcursor/-/extension-gapcursor-3.22.4.tgz",
|
||||||
|
"integrity": "sha512-UYBEUj3SFpKINIE7AdzcyeS3xICK+ee+YLBbuqNXyHStYChjJOohzJehqiqhjR16A88KQQ+ZjgyDcItKGygSog==",
|
||||||
|
"license": "MIT",
|
||||||
|
"funding": {
|
||||||
|
"type": "github",
|
||||||
|
"url": "https://github.com/sponsors/ueberdosis"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@tiptap/extensions": "3.22.4"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@tiptap/extension-hard-break": {
|
||||||
|
"version": "3.22.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/@tiptap/extension-hard-break/-/extension-hard-break-3.22.4.tgz",
|
||||||
|
"integrity": "sha512-xq+a4dE7T6VwApCkh/yU3p30gn3F8g8Arb9CyEZm58/WIJUIGvHSTjDdHmvU16+kiWSBg+wOOsaFHhYjJjxcKA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"funding": {
|
||||||
|
"type": "github",
|
||||||
|
"url": "https://github.com/sponsors/ueberdosis"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@tiptap/core": "3.22.4"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@tiptap/extension-heading": {
|
||||||
|
"version": "3.22.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/@tiptap/extension-heading/-/extension-heading-3.22.4.tgz",
|
||||||
|
"integrity": "sha512-TUaj5f0Ir5qy9HKKt2ocnwfXKpZDYeHgbbP9gshKFzdq5PLe1RbIgkjfy6bnoI865cYjmPYWRjcT7XsKyIcb9Q==",
|
||||||
|
"license": "MIT",
|
||||||
|
"funding": {
|
||||||
|
"type": "github",
|
||||||
|
"url": "https://github.com/sponsors/ueberdosis"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@tiptap/core": "3.22.4"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@tiptap/extension-horizontal-rule": {
|
||||||
|
"version": "3.22.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/@tiptap/extension-horizontal-rule/-/extension-horizontal-rule-3.22.4.tgz",
|
||||||
|
"integrity": "sha512-cCI1HekGQwhY/MbgaKQ0R/7HcH5ZM1oFAyI/J72QGLC0XnF403S/OXoHMuBWr1mCu8hNiQWCzeNRJUty0iytNw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"funding": {
|
||||||
|
"type": "github",
|
||||||
|
"url": "https://github.com/sponsors/ueberdosis"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@tiptap/core": "3.22.4",
|
||||||
|
"@tiptap/pm": "3.22.4"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@tiptap/extension-italic": {
|
||||||
|
"version": "3.22.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/@tiptap/extension-italic/-/extension-italic-3.22.4.tgz",
|
||||||
|
"integrity": "sha512-fVSDx5AYXgDI3v2zZIqb7V8EewthwM2NJ/ZCX+XaxRsqNEpnjVhgHs7UlvDqK1wj2OJ6zmUNjPtVlAFRxwT+HQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"funding": {
|
||||||
|
"type": "github",
|
||||||
|
"url": "https://github.com/sponsors/ueberdosis"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@tiptap/core": "3.22.4"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@tiptap/extension-link": {
|
||||||
|
"version": "3.22.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/@tiptap/extension-link/-/extension-link-3.22.4.tgz",
|
||||||
|
"integrity": "sha512-uoP3yus02uwGPVzW2QaEPJWVIrUb/r5nKm6c8DiJv9fNSX1+gykZZMg42c6GwRFLZ/vyfWjVCbAE03VMUqafgA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"linkifyjs": "^4.3.2"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"type": "github",
|
||||||
|
"url": "https://github.com/sponsors/ueberdosis"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@tiptap/core": "3.22.4",
|
||||||
|
"@tiptap/pm": "3.22.4"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@tiptap/extension-list": {
|
||||||
|
"version": "3.22.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/@tiptap/extension-list/-/extension-list-3.22.4.tgz",
|
||||||
|
"integrity": "sha512-Xe8UFvvHmyp/c/TJsFwlwU9CWACYbBirNsluJ3U1+H8BTu1wqdrT/AXR5uIXeyCl5kiWKgX5q71eHWbYFOrqrg==",
|
||||||
|
"license": "MIT",
|
||||||
|
"peer": true,
|
||||||
|
"funding": {
|
||||||
|
"type": "github",
|
||||||
|
"url": "https://github.com/sponsors/ueberdosis"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@tiptap/core": "3.22.4",
|
||||||
|
"@tiptap/pm": "3.22.4"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@tiptap/extension-list-item": {
|
||||||
|
"version": "3.22.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/@tiptap/extension-list-item/-/extension-list-item-3.22.4.tgz",
|
||||||
|
"integrity": "sha512-H659KXTvggSypIDWSOJBZ37jh9pKjQriDDvYPYvOZCdfij0D0hsDXN/wXoypArneUkoBdgruHfTtMkFOaQlgkw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"funding": {
|
||||||
|
"type": "github",
|
||||||
|
"url": "https://github.com/sponsors/ueberdosis"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@tiptap/extension-list": "3.22.4"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@tiptap/extension-list-keymap": {
|
||||||
|
"version": "3.22.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/@tiptap/extension-list-keymap/-/extension-list-keymap-3.22.4.tgz",
|
||||||
|
"integrity": "sha512-t/zhker4oIS78AIGYDdFFfZC6zSBlszfD7z/zqFLGCg5PHNNgkZK5hKj6Vyix6D2SapRn/ajnx+8mhbKIUH5eA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"funding": {
|
||||||
|
"type": "github",
|
||||||
|
"url": "https://github.com/sponsors/ueberdosis"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@tiptap/extension-list": "3.22.4"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@tiptap/extension-ordered-list": {
|
||||||
|
"version": "3.22.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/@tiptap/extension-ordered-list/-/extension-ordered-list-3.22.4.tgz",
|
||||||
|
"integrity": "sha512-w77hPVf7pcHt97vfrybg/l0t5CimCd4y75OJKuHuo3CfgM5xbUP/gaPNMDyLLe7MYole/UHi/XvG3XjgzqTzAw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"funding": {
|
||||||
|
"type": "github",
|
||||||
|
"url": "https://github.com/sponsors/ueberdosis"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@tiptap/extension-list": "3.22.4"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@tiptap/extension-paragraph": {
|
||||||
|
"version": "3.22.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/@tiptap/extension-paragraph/-/extension-paragraph-3.22.4.tgz",
|
||||||
|
"integrity": "sha512-de6dFkIhigiENESY6rNJ3yTVS/337ybfP30dNPudTwGe9oAu9ZCS+04j6QCvXSjhlI3ULiv7wiSHqrP26Gd+Hw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"funding": {
|
||||||
|
"type": "github",
|
||||||
|
"url": "https://github.com/sponsors/ueberdosis"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@tiptap/core": "3.22.4"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@tiptap/extension-strike": {
|
||||||
|
"version": "3.22.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/@tiptap/extension-strike/-/extension-strike-3.22.4.tgz",
|
||||||
|
"integrity": "sha512-aRHWQj42HiailXSC9LkKYM3jWMcSeGwOjbqM4PiuxQZmHVDRFmeHkfJItOdn2cSHaO0vuEVK+TvrWUWsBFi3pg==",
|
||||||
|
"license": "MIT",
|
||||||
|
"funding": {
|
||||||
|
"type": "github",
|
||||||
|
"url": "https://github.com/sponsors/ueberdosis"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@tiptap/core": "3.22.4"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@tiptap/extension-text": {
|
||||||
|
"version": "3.22.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/@tiptap/extension-text/-/extension-text-3.22.4.tgz",
|
||||||
|
"integrity": "sha512-mM69uUW5cSxIhyEpWXi/YcfyupcJMDLCPEfYi62awH0iOP/LRoCv/nHjJq4Hyj/KxRJbe8HKwIUnqaCUf7m5Pg==",
|
||||||
|
"license": "MIT",
|
||||||
|
"funding": {
|
||||||
|
"type": "github",
|
||||||
|
"url": "https://github.com/sponsors/ueberdosis"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@tiptap/core": "3.22.4"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@tiptap/extension-underline": {
|
||||||
|
"version": "3.22.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/@tiptap/extension-underline/-/extension-underline-3.22.4.tgz",
|
||||||
|
"integrity": "sha512-08kGdbhIrA6h10GWXqOkqIveaBj5tmxclK208/nUIAlonI9hPd739vu7fmVtpnmqCnSSNpoRtU4u6Gj5at0ZpA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"funding": {
|
||||||
|
"type": "github",
|
||||||
|
"url": "https://github.com/sponsors/ueberdosis"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@tiptap/core": "3.22.4"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@tiptap/extensions": {
|
||||||
|
"version": "3.22.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/@tiptap/extensions/-/extensions-3.22.4.tgz",
|
||||||
|
"integrity": "sha512-fOe8VptJvLPs32bNdUYo8SRyljwqKNQVXWW056VoXIc5en/59OdJlJQVeHI0jRRciH3MtrqODi/gfJR0VHNZ8A==",
|
||||||
|
"license": "MIT",
|
||||||
|
"peer": true,
|
||||||
|
"funding": {
|
||||||
|
"type": "github",
|
||||||
|
"url": "https://github.com/sponsors/ueberdosis"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@tiptap/core": "3.22.4",
|
||||||
|
"@tiptap/pm": "3.22.4"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@tiptap/pm": {
|
||||||
|
"version": "3.22.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/@tiptap/pm/-/pm-3.22.4.tgz",
|
||||||
|
"integrity": "sha512-hj8Qka6WcHRllHUdeSjDnq2XaisUo4KsoGJc1WcFpoa1Yd+OeD861zUMnV7DFVGdZRy45Obht0CUYJpXQ4yA4w==",
|
||||||
|
"license": "MIT",
|
||||||
|
"peer": true,
|
||||||
|
"dependencies": {
|
||||||
|
"prosemirror-changeset": "^2.3.0",
|
||||||
|
"prosemirror-commands": "^1.6.2",
|
||||||
|
"prosemirror-dropcursor": "^1.8.1",
|
||||||
|
"prosemirror-gapcursor": "^1.3.2",
|
||||||
|
"prosemirror-history": "^1.4.1",
|
||||||
|
"prosemirror-keymap": "^1.2.2",
|
||||||
|
"prosemirror-model": "^1.24.1",
|
||||||
|
"prosemirror-schema-list": "^1.5.0",
|
||||||
|
"prosemirror-state": "^1.4.3",
|
||||||
|
"prosemirror-tables": "^1.6.4",
|
||||||
|
"prosemirror-transform": "^1.10.2",
|
||||||
|
"prosemirror-view": "^1.38.1"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"type": "github",
|
||||||
|
"url": "https://github.com/sponsors/ueberdosis"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@tiptap/starter-kit": {
|
||||||
|
"version": "3.22.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/@tiptap/starter-kit/-/starter-kit-3.22.4.tgz",
|
||||||
|
"integrity": "sha512-qWjw+vfdin1rzMRpRU4cC5tLTwMJtUpXeQukv+6mOqqvhptuwuZBjUHImVEJaSPoHXS7+1ut+nTnrLyWyEuE5Q==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@tiptap/core": "^3.22.4",
|
||||||
|
"@tiptap/extension-blockquote": "^3.22.4",
|
||||||
|
"@tiptap/extension-bold": "^3.22.4",
|
||||||
|
"@tiptap/extension-bullet-list": "^3.22.4",
|
||||||
|
"@tiptap/extension-code": "^3.22.4",
|
||||||
|
"@tiptap/extension-code-block": "^3.22.4",
|
||||||
|
"@tiptap/extension-document": "^3.22.4",
|
||||||
|
"@tiptap/extension-dropcursor": "^3.22.4",
|
||||||
|
"@tiptap/extension-gapcursor": "^3.22.4",
|
||||||
|
"@tiptap/extension-hard-break": "^3.22.4",
|
||||||
|
"@tiptap/extension-heading": "^3.22.4",
|
||||||
|
"@tiptap/extension-horizontal-rule": "^3.22.4",
|
||||||
|
"@tiptap/extension-italic": "^3.22.4",
|
||||||
|
"@tiptap/extension-link": "^3.22.4",
|
||||||
|
"@tiptap/extension-list": "^3.22.4",
|
||||||
|
"@tiptap/extension-list-item": "^3.22.4",
|
||||||
|
"@tiptap/extension-list-keymap": "^3.22.4",
|
||||||
|
"@tiptap/extension-ordered-list": "^3.22.4",
|
||||||
|
"@tiptap/extension-paragraph": "^3.22.4",
|
||||||
|
"@tiptap/extension-strike": "^3.22.4",
|
||||||
|
"@tiptap/extension-text": "^3.22.4",
|
||||||
|
"@tiptap/extension-underline": "^3.22.4",
|
||||||
|
"@tiptap/extensions": "^3.22.4",
|
||||||
|
"@tiptap/pm": "^3.22.4"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"type": "github",
|
||||||
|
"url": "https://github.com/sponsors/ueberdosis"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@tiptap/vue-3": {
|
||||||
|
"version": "3.22.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/@tiptap/vue-3/-/vue-3-3.22.4.tgz",
|
||||||
|
"integrity": "sha512-fcqUWt6LlA5PbcFaDXyV1apWwAs8j80m0kWwoL5+DgKdkGxsB5LgDZU1pTWle0zvR5zmGvJ7LmB6EGAYIBjdmQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"funding": {
|
||||||
|
"type": "github",
|
||||||
|
"url": "https://github.com/sponsors/ueberdosis"
|
||||||
|
},
|
||||||
|
"optionalDependencies": {
|
||||||
|
"@tiptap/extension-bubble-menu": "^3.22.4",
|
||||||
|
"@tiptap/extension-floating-menu": "^3.22.4"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@floating-ui/dom": "^1.0.0",
|
||||||
|
"@tiptap/core": "3.22.4",
|
||||||
|
"@tiptap/pm": "3.22.4",
|
||||||
|
"vue": "^3.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@tsconfig/node24": {
|
"node_modules/@tsconfig/node24": {
|
||||||
"version": "24.0.4",
|
"version": "24.0.4",
|
||||||
"resolved": "https://registry.npmjs.org/@tsconfig/node24/-/node24-24.0.4.tgz",
|
"resolved": "https://registry.npmjs.org/@tsconfig/node24/-/node24-24.0.4.tgz",
|
||||||
@@ -1749,6 +2206,28 @@
|
|||||||
"undici-types": "^7.21.0"
|
"undici-types": "^7.21.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@types/linkify-it": {
|
||||||
|
"version": "3.0.5",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/linkify-it/-/linkify-it-3.0.5.tgz",
|
||||||
|
"integrity": "sha512-yg6E+u0/+Zjva+buc3EIb+29XEg4wltq7cSmd4Uc2EE/1nUVmxyzpX6gUXD0V8jIrG0r7YeOGVIbYRkxeooCtw==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
|
"node_modules/@types/markdown-it": {
|
||||||
|
"version": "13.0.9",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/markdown-it/-/markdown-it-13.0.9.tgz",
|
||||||
|
"integrity": "sha512-1XPwR0+MgXLWfTn9gCsZ55AHOKW1WN+P9vr0PaQh5aerR9LLQXUbjfEAFhjmEmyoYFWAyuN2Mqkn40MZ4ukjBw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@types/linkify-it": "^3",
|
||||||
|
"@types/mdurl": "^1"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@types/mdurl": {
|
||||||
|
"version": "1.0.5",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/mdurl/-/mdurl-1.0.5.tgz",
|
||||||
|
"integrity": "sha512-6L6VymKTzYSrEf4Nev4Xa1LCHKrlTlYCBMTlQKFuddo1CvQcE52I0mwfOJayueUC7MJuXOeHTcIU683lzd0cUA==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/@types/ms": {
|
"node_modules/@types/ms": {
|
||||||
"version": "2.1.0",
|
"version": "2.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/@types/ms/-/ms-2.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/@types/ms/-/ms-2.1.0.tgz",
|
||||||
@@ -2371,6 +2850,12 @@
|
|||||||
"node": ">=14"
|
"node": ">=14"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/argparse": {
|
||||||
|
"version": "2.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz",
|
||||||
|
"integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==",
|
||||||
|
"license": "Python-2.0"
|
||||||
|
},
|
||||||
"node_modules/assertion-error": {
|
"node_modules/assertion-error": {
|
||||||
"version": "2.0.1",
|
"version": "2.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-2.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-2.0.1.tgz",
|
||||||
@@ -2482,6 +2967,7 @@
|
|||||||
}
|
}
|
||||||
],
|
],
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
"peer": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"baseline-browser-mapping": "^2.9.0",
|
"baseline-browser-mapping": "^2.9.0",
|
||||||
"caniuse-lite": "^1.0.30001759",
|
"caniuse-lite": "^1.0.30001759",
|
||||||
@@ -3411,6 +3897,21 @@
|
|||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/linkify-it": {
|
||||||
|
"version": "5.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/linkify-it/-/linkify-it-5.0.0.tgz",
|
||||||
|
"integrity": "sha512-5aHCbzQRADcdP+ATqnDuhhJ/MRIqDkZX5pyjFHRRysS8vZ5AbqGEoFIb6pYHPZ+L/OC2Lc+xT8uHVVR5CAK/wQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"uc.micro": "^2.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/linkifyjs": {
|
||||||
|
"version": "4.3.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/linkifyjs/-/linkifyjs-4.3.2.tgz",
|
||||||
|
"integrity": "sha512-NT1CJtq3hHIreOianA8aSXn6Cw0JzYOuDQbOrSPe7gqFnCpKP++MQe3ODgO3oh2GJFORkAAdqredOa60z63GbA==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/local-pkg": {
|
"node_modules/local-pkg": {
|
||||||
"version": "1.1.2",
|
"version": "1.1.2",
|
||||||
"resolved": "https://registry.npmjs.org/local-pkg/-/local-pkg-1.1.2.tgz",
|
"resolved": "https://registry.npmjs.org/local-pkg/-/local-pkg-1.1.2.tgz",
|
||||||
@@ -3462,6 +3963,41 @@
|
|||||||
"url": "https://github.com/sponsors/sxzz"
|
"url": "https://github.com/sponsors/sxzz"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/markdown-it": {
|
||||||
|
"version": "14.1.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/markdown-it/-/markdown-it-14.1.1.tgz",
|
||||||
|
"integrity": "sha512-BuU2qnTti9YKgK5N+IeMubp14ZUKUUw7yeJbkjtosvHiP0AZ5c8IAgEMk79D0eC8F23r4Ac/q8cAIFdm2FtyoA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"argparse": "^2.0.1",
|
||||||
|
"entities": "^4.4.0",
|
||||||
|
"linkify-it": "^5.0.0",
|
||||||
|
"mdurl": "^2.0.0",
|
||||||
|
"punycode.js": "^2.3.1",
|
||||||
|
"uc.micro": "^2.1.0"
|
||||||
|
},
|
||||||
|
"bin": {
|
||||||
|
"markdown-it": "bin/markdown-it.mjs"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/markdown-it-task-lists": {
|
||||||
|
"version": "2.1.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/markdown-it-task-lists/-/markdown-it-task-lists-2.1.1.tgz",
|
||||||
|
"integrity": "sha512-TxFAc76Jnhb2OUu+n3yz9RMu4CwGfaT788br6HhEDlvWfdeJcLUsxk1Hgw2yJio0OXsxv7pyIPmvECY7bMbluA==",
|
||||||
|
"license": "ISC"
|
||||||
|
},
|
||||||
|
"node_modules/markdown-it/node_modules/entities": {
|
||||||
|
"version": "4.5.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz",
|
||||||
|
"integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==",
|
||||||
|
"license": "BSD-2-Clause",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=0.12"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/fb55/entities?sponsor=1"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/mdn-data": {
|
"node_modules/mdn-data": {
|
||||||
"version": "2.12.2",
|
"version": "2.12.2",
|
||||||
"resolved": "https://registry.npmjs.org/mdn-data/-/mdn-data-2.12.2.tgz",
|
"resolved": "https://registry.npmjs.org/mdn-data/-/mdn-data-2.12.2.tgz",
|
||||||
@@ -3469,6 +4005,12 @@
|
|||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "CC0-1.0"
|
"license": "CC0-1.0"
|
||||||
},
|
},
|
||||||
|
"node_modules/mdurl": {
|
||||||
|
"version": "2.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/mdurl/-/mdurl-2.0.0.tgz",
|
||||||
|
"integrity": "sha512-Lf+9+2r+Tdp5wXDXC4PcIBjTDtq4UKjCPMQhKIuzpJNW0b96kVqSwW0bT7FhRSfmAiFYgP+SCRvdrDozfh0U5w==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/memorystream": {
|
"node_modules/memorystream": {
|
||||||
"version": "0.3.1",
|
"version": "0.3.1",
|
||||||
"resolved": "https://registry.npmjs.org/memorystream/-/memorystream-0.3.1.tgz",
|
"resolved": "https://registry.npmjs.org/memorystream/-/memorystream-0.3.1.tgz",
|
||||||
@@ -4252,6 +4794,12 @@
|
|||||||
"url": "https://github.com/sponsors/sindresorhus"
|
"url": "https://github.com/sponsors/sindresorhus"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/orderedmap": {
|
||||||
|
"version": "2.1.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/orderedmap/-/orderedmap-2.1.1.tgz",
|
||||||
|
"integrity": "sha512-TvAWxi0nDe1j/rtMcWcIj94+Ffe6n7zhow33h40SKxmsmozs6dz/e+EajymfoFcHd7sxNn8yHM8839uixMOV6g==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/package-json-from-dist": {
|
"node_modules/package-json-from-dist": {
|
||||||
"version": "1.0.1",
|
"version": "1.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/package-json-from-dist/-/package-json-from-dist-1.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/package-json-from-dist/-/package-json-from-dist-1.0.1.tgz",
|
||||||
@@ -4374,6 +4922,7 @@
|
|||||||
"resolved": "https://registry.npmjs.org/pinia/-/pinia-3.0.4.tgz",
|
"resolved": "https://registry.npmjs.org/pinia/-/pinia-3.0.4.tgz",
|
||||||
"integrity": "sha512-l7pqLUFTI/+ESXn6k3nu30ZIzW5E2WZF/LaHJEpoq6ElcLD+wduZoB2kBN19du6K/4FDpPMazY2wJr+IndBtQw==",
|
"integrity": "sha512-l7pqLUFTI/+ESXn6k3nu30ZIzW5E2WZF/LaHJEpoq6ElcLD+wduZoB2kBN19du6K/4FDpPMazY2wJr+IndBtQw==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
"peer": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@vue/devtools-api": "^7.7.7"
|
"@vue/devtools-api": "^7.7.7"
|
||||||
},
|
},
|
||||||
@@ -4451,6 +5000,168 @@
|
|||||||
"node": ">=12.11.0"
|
"node": ">=12.11.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/prosemirror-changeset": {
|
||||||
|
"version": "2.4.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/prosemirror-changeset/-/prosemirror-changeset-2.4.1.tgz",
|
||||||
|
"integrity": "sha512-96WBLhOaYhJ+kPhLg3uW359Tz6I/MfcrQfL4EGv4SrcqKEMC1gmoGrXHecPE8eOwTVCJ4IwgfzM8fFad25wNfw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"prosemirror-transform": "^1.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/prosemirror-commands": {
|
||||||
|
"version": "1.7.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/prosemirror-commands/-/prosemirror-commands-1.7.1.tgz",
|
||||||
|
"integrity": "sha512-rT7qZnQtx5c0/y/KlYaGvtG411S97UaL6gdp6RIZ23DLHanMYLyfGBV5DtSnZdthQql7W+lEVbpSfwtO8T+L2w==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"prosemirror-model": "^1.0.0",
|
||||||
|
"prosemirror-state": "^1.0.0",
|
||||||
|
"prosemirror-transform": "^1.10.2"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/prosemirror-dropcursor": {
|
||||||
|
"version": "1.8.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/prosemirror-dropcursor/-/prosemirror-dropcursor-1.8.2.tgz",
|
||||||
|
"integrity": "sha512-CCk6Gyx9+Tt2sbYk5NK0nB1ukHi2ryaRgadV/LvyNuO3ena1payM2z6Cg0vO1ebK8cxbzo41ku2DE5Axj1Zuiw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"prosemirror-state": "^1.0.0",
|
||||||
|
"prosemirror-transform": "^1.1.0",
|
||||||
|
"prosemirror-view": "^1.1.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/prosemirror-gapcursor": {
|
||||||
|
"version": "1.4.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/prosemirror-gapcursor/-/prosemirror-gapcursor-1.4.1.tgz",
|
||||||
|
"integrity": "sha512-pMdYaEnjNMSwl11yjEGtgTmLkR08m/Vl+Jj443167p9eB3HVQKhYCc4gmHVDsLPODfZfjr/MmirsdyZziXbQKw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"prosemirror-keymap": "^1.0.0",
|
||||||
|
"prosemirror-model": "^1.0.0",
|
||||||
|
"prosemirror-state": "^1.0.0",
|
||||||
|
"prosemirror-view": "^1.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/prosemirror-history": {
|
||||||
|
"version": "1.5.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/prosemirror-history/-/prosemirror-history-1.5.0.tgz",
|
||||||
|
"integrity": "sha512-zlzTiH01eKA55UAf1MEjtssJeHnGxO0j4K4Dpx+gnmX9n+SHNlDqI2oO1Kv1iPN5B1dm5fsljCfqKF9nFL6HRg==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"prosemirror-state": "^1.2.2",
|
||||||
|
"prosemirror-transform": "^1.0.0",
|
||||||
|
"prosemirror-view": "^1.31.0",
|
||||||
|
"rope-sequence": "^1.3.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/prosemirror-keymap": {
|
||||||
|
"version": "1.2.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/prosemirror-keymap/-/prosemirror-keymap-1.2.3.tgz",
|
||||||
|
"integrity": "sha512-4HucRlpiLd1IPQQXNqeo81BGtkY8Ai5smHhKW9jjPKRc2wQIxksg7Hl1tTI2IfT2B/LgX6bfYvXxEpJl7aKYKw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"prosemirror-state": "^1.0.0",
|
||||||
|
"w3c-keyname": "^2.2.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/prosemirror-markdown": {
|
||||||
|
"version": "1.13.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/prosemirror-markdown/-/prosemirror-markdown-1.13.4.tgz",
|
||||||
|
"integrity": "sha512-D98dm4cQ3Hs6EmjK500TdAOew4Z03EV71ajEFiWra3Upr7diytJsjF4mPV2dW+eK5uNectiRj0xFxYI9NLXDbw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@types/markdown-it": "^14.0.0",
|
||||||
|
"markdown-it": "^14.0.0",
|
||||||
|
"prosemirror-model": "^1.25.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/prosemirror-markdown/node_modules/@types/linkify-it": {
|
||||||
|
"version": "5.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/linkify-it/-/linkify-it-5.0.0.tgz",
|
||||||
|
"integrity": "sha512-sVDA58zAw4eWAffKOaQH5/5j3XeayukzDk+ewSsnv3p4yJEZHCCzMDiZM8e0OUrRvmpGZ85jf4yDHkHsgBNr9Q==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
|
"node_modules/prosemirror-markdown/node_modules/@types/markdown-it": {
|
||||||
|
"version": "14.1.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/markdown-it/-/markdown-it-14.1.2.tgz",
|
||||||
|
"integrity": "sha512-promo4eFwuiW+TfGxhi+0x3czqTYJkG8qB17ZUJiVF10Xm7NLVRSLUsfRTU/6h1e24VvRnXCx+hG7li58lkzog==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@types/linkify-it": "^5",
|
||||||
|
"@types/mdurl": "^2"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/prosemirror-markdown/node_modules/@types/mdurl": {
|
||||||
|
"version": "2.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/mdurl/-/mdurl-2.0.0.tgz",
|
||||||
|
"integrity": "sha512-RGdgjQUZba5p6QEFAVx2OGb8rQDL/cPRG7GiedRzMcJ1tYnUANBncjbSB1NRGwbvjcPeikRABz2nshyPk1bhWg==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
|
"node_modules/prosemirror-model": {
|
||||||
|
"version": "1.25.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/prosemirror-model/-/prosemirror-model-1.25.4.tgz",
|
||||||
|
"integrity": "sha512-PIM7E43PBxKce8OQeezAs9j4TP+5yDpZVbuurd1h5phUxEKIu+G2a+EUZzIC5nS1mJktDJWzbqS23n1tsAf5QA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"orderedmap": "^2.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/prosemirror-schema-list": {
|
||||||
|
"version": "1.5.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/prosemirror-schema-list/-/prosemirror-schema-list-1.5.1.tgz",
|
||||||
|
"integrity": "sha512-927lFx/uwyQaGwJxLWCZRkjXG0p48KpMj6ueoYiu4JX05GGuGcgzAy62dfiV8eFZftgyBUvLx76RsMe20fJl+Q==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"prosemirror-model": "^1.0.0",
|
||||||
|
"prosemirror-state": "^1.0.0",
|
||||||
|
"prosemirror-transform": "^1.7.3"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/prosemirror-state": {
|
||||||
|
"version": "1.4.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/prosemirror-state/-/prosemirror-state-1.4.4.tgz",
|
||||||
|
"integrity": "sha512-6jiYHH2CIGbCfnxdHbXZ12gySFY/fz/ulZE333G6bPqIZ4F+TXo9ifiR86nAHpWnfoNjOb3o5ESi7J8Uz1jXHw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"prosemirror-model": "^1.0.0",
|
||||||
|
"prosemirror-transform": "^1.0.0",
|
||||||
|
"prosemirror-view": "^1.27.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/prosemirror-tables": {
|
||||||
|
"version": "1.8.5",
|
||||||
|
"resolved": "https://registry.npmjs.org/prosemirror-tables/-/prosemirror-tables-1.8.5.tgz",
|
||||||
|
"integrity": "sha512-V/0cDCsHKHe/tfWkeCmthNUcEp1IVO3p6vwN8XtwE9PZQLAZJigbw3QoraAdfJPir4NKJtNvOB8oYGKRl+t0Dw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"prosemirror-keymap": "^1.2.3",
|
||||||
|
"prosemirror-model": "^1.25.4",
|
||||||
|
"prosemirror-state": "^1.4.4",
|
||||||
|
"prosemirror-transform": "^1.10.5",
|
||||||
|
"prosemirror-view": "^1.41.4"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/prosemirror-transform": {
|
||||||
|
"version": "1.12.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/prosemirror-transform/-/prosemirror-transform-1.12.0.tgz",
|
||||||
|
"integrity": "sha512-GxboyN4AMIsoHNtz5uf2r2Ru551i5hWeCMD6E2Ib4Eogqoub0NflniaBPVQ4MrGE5yZ8JV9tUHg9qcZTTrcN4w==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"prosemirror-model": "^1.21.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/prosemirror-view": {
|
||||||
|
"version": "1.41.8",
|
||||||
|
"resolved": "https://registry.npmjs.org/prosemirror-view/-/prosemirror-view-1.41.8.tgz",
|
||||||
|
"integrity": "sha512-TnKDdohEatgyZNGCDWIdccOHXhYloJwbwU+phw/a23KBvJIR9lWQWW7WHHK3vBdOLDNuF7TaX98GObUZOWkOnA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"prosemirror-model": "^1.20.0",
|
||||||
|
"prosemirror-state": "^1.0.0",
|
||||||
|
"prosemirror-transform": "^1.1.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/proto-list": {
|
"node_modules/proto-list": {
|
||||||
"version": "1.2.4",
|
"version": "1.2.4",
|
||||||
"resolved": "https://registry.npmjs.org/proto-list/-/proto-list-1.2.4.tgz",
|
"resolved": "https://registry.npmjs.org/proto-list/-/proto-list-1.2.4.tgz",
|
||||||
@@ -4468,6 +5179,15 @@
|
|||||||
"node": ">=6"
|
"node": ">=6"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/punycode.js": {
|
||||||
|
"version": "2.3.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/punycode.js/-/punycode.js-2.3.1.tgz",
|
||||||
|
"integrity": "sha512-uxFIHU0YlHYhDQtV4R9J6a52SLx28BCjT+4ieh7IGbgwVJWO+km431c4yRlREUAsAmt/uMjQUyQHNEPf0M39CA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=6"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/quansync": {
|
"node_modules/quansync": {
|
||||||
"version": "0.2.11",
|
"version": "0.2.11",
|
||||||
"resolved": "https://registry.npmjs.org/quansync/-/quansync-0.2.11.tgz",
|
"resolved": "https://registry.npmjs.org/quansync/-/quansync-0.2.11.tgz",
|
||||||
@@ -4572,6 +5292,12 @@
|
|||||||
"fsevents": "~2.3.2"
|
"fsevents": "~2.3.2"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/rope-sequence": {
|
||||||
|
"version": "1.3.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/rope-sequence/-/rope-sequence-1.3.4.tgz",
|
||||||
|
"integrity": "sha512-UT5EDe2cu2E/6O4igUr5PSFs23nvvukicWHx6GnOPlHAiiYbzNuCRQCuiUdHJQcqKalLKlrYJnjY0ySGsXNQXQ==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/run-applescript": {
|
"node_modules/run-applescript": {
|
||||||
"version": "7.1.0",
|
"version": "7.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/run-applescript/-/run-applescript-7.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/run-applescript/-/run-applescript-7.1.0.tgz",
|
||||||
@@ -4895,6 +5621,24 @@
|
|||||||
"node": ">=14.0.0"
|
"node": ">=14.0.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/tiptap-markdown": {
|
||||||
|
"version": "0.9.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/tiptap-markdown/-/tiptap-markdown-0.9.0.tgz",
|
||||||
|
"integrity": "sha512-dKLQ9iiuGNgrlGVjrNauF/UBzWu4LYOx5pkD0jNkmQt/GOwfCJsBuzZTsf1jZ204ANHOm572mZ9PYvGh1S7tpQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"workspaces": [
|
||||||
|
"example"
|
||||||
|
],
|
||||||
|
"dependencies": {
|
||||||
|
"@types/markdown-it": "^13.0.7",
|
||||||
|
"markdown-it": "^14.1.0",
|
||||||
|
"markdown-it-task-lists": "^2.1.1",
|
||||||
|
"prosemirror-markdown": "^1.11.1"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@tiptap/core": "^3.0.1"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/tldts": {
|
"node_modules/tldts": {
|
||||||
"version": "7.0.23",
|
"version": "7.0.23",
|
||||||
"resolved": "https://registry.npmjs.org/tldts/-/tldts-7.0.23.tgz",
|
"resolved": "https://registry.npmjs.org/tldts/-/tldts-7.0.23.tgz",
|
||||||
@@ -4957,6 +5701,7 @@
|
|||||||
"integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==",
|
"integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==",
|
||||||
"devOptional": true,
|
"devOptional": true,
|
||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
|
"peer": true,
|
||||||
"bin": {
|
"bin": {
|
||||||
"tsc": "bin/tsc",
|
"tsc": "bin/tsc",
|
||||||
"tsserver": "bin/tsserver"
|
"tsserver": "bin/tsserver"
|
||||||
@@ -4965,6 +5710,12 @@
|
|||||||
"node": ">=14.17"
|
"node": ">=14.17"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/uc.micro": {
|
||||||
|
"version": "2.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/uc.micro/-/uc.micro-2.1.0.tgz",
|
||||||
|
"integrity": "sha512-ARDJmphmdvUk6Glw7y9DQ2bFkKBHwQHLi2lsaH6PPmz/Ka9sFOBsBluozhDltWmnv9u/cF6Rt87znRTPV+yp/A==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/ufo": {
|
"node_modules/ufo": {
|
||||||
"version": "1.6.3",
|
"version": "1.6.3",
|
||||||
"resolved": "https://registry.npmjs.org/ufo/-/ufo-1.6.3.tgz",
|
"resolved": "https://registry.npmjs.org/ufo/-/ufo-1.6.3.tgz",
|
||||||
@@ -5073,6 +5824,7 @@
|
|||||||
"integrity": "sha512-w+N7Hifpc3gRjZ63vYBXA56dvvRlNWRczTdmCBBa+CotUzAPf5b7YMdMR/8CQoeYE5LX3W4wj6RYTgonm1b9DA==",
|
"integrity": "sha512-w+N7Hifpc3gRjZ63vYBXA56dvvRlNWRczTdmCBBa+CotUzAPf5b7YMdMR/8CQoeYE5LX3W4wj6RYTgonm1b9DA==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
"peer": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"esbuild": "^0.27.0",
|
"esbuild": "^0.27.0",
|
||||||
"fdir": "^6.5.0",
|
"fdir": "^6.5.0",
|
||||||
@@ -5429,6 +6181,7 @@
|
|||||||
"resolved": "https://registry.npmjs.org/vue/-/vue-3.5.29.tgz",
|
"resolved": "https://registry.npmjs.org/vue/-/vue-3.5.29.tgz",
|
||||||
"integrity": "sha512-BZqN4Ze6mDQVNAni0IHeMJ5mwr8VAJ3MQC9FmprRhcBYENw+wOAAjRj8jfmN6FLl0j96OXbR+CjWhmAmM+QGnA==",
|
"integrity": "sha512-BZqN4Ze6mDQVNAni0IHeMJ5mwr8VAJ3MQC9FmprRhcBYENw+wOAAjRj8jfmN6FLl0j96OXbR+CjWhmAmM+QGnA==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
"peer": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@vue/compiler-dom": "3.5.29",
|
"@vue/compiler-dom": "3.5.29",
|
||||||
"@vue/compiler-sfc": "3.5.29",
|
"@vue/compiler-sfc": "3.5.29",
|
||||||
@@ -5564,6 +6317,12 @@
|
|||||||
"typescript": ">=5.0.0"
|
"typescript": ">=5.0.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/w3c-keyname": {
|
||||||
|
"version": "2.2.8",
|
||||||
|
"resolved": "https://registry.npmjs.org/w3c-keyname/-/w3c-keyname-2.2.8.tgz",
|
||||||
|
"integrity": "sha512-dpojBhNsCNN7T82Tm7k26A6G9ML3NkhDsnw9n/eoxSRlVBB4CEtIQ/KTCLI2Fwf3ataSXRhYFkQi3SlnFwPvPQ==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/w3c-xmlserializer": {
|
"node_modules/w3c-xmlserializer": {
|
||||||
"version": "5.0.0",
|
"version": "5.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/w3c-xmlserializer/-/w3c-xmlserializer-5.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/w3c-xmlserializer/-/w3c-xmlserializer-5.0.0.tgz",
|
||||||
|
|||||||
@@ -13,10 +13,13 @@
|
|||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@primevue/themes": "^4.5.4",
|
"@primevue/themes": "^4.5.4",
|
||||||
|
"@tiptap/starter-kit": "^3.22.4",
|
||||||
|
"@tiptap/vue-3": "^3.22.4",
|
||||||
"fast-json-patch": "^3.1.1",
|
"fast-json-patch": "^3.1.1",
|
||||||
"pinia": "^3.0.4",
|
"pinia": "^3.0.4",
|
||||||
"primeicons": "^7.0.0",
|
"primeicons": "^7.0.0",
|
||||||
"primevue": "^4.5.4",
|
"primevue": "^4.5.4",
|
||||||
|
"tiptap-markdown": "^0.9.0",
|
||||||
"vue": "^3.5.29",
|
"vue": "^3.5.29",
|
||||||
"vue-advanced-chat": "^2.0.4",
|
"vue-advanced-chat": "^2.0.4",
|
||||||
"vue-router": "^5.0.3"
|
"vue-router": "^5.0.3"
|
||||||
|
|||||||
@@ -8,6 +8,8 @@ import TabPanel from 'primevue/tabpanel'
|
|||||||
import OrdersTab from './tabs/OrdersTab.vue'
|
import OrdersTab from './tabs/OrdersTab.vue'
|
||||||
import PlaceholderTab from './tabs/PlaceholderTab.vue'
|
import PlaceholderTab from './tabs/PlaceholderTab.vue'
|
||||||
import ResearchTab from './tabs/ResearchTab.vue'
|
import ResearchTab from './tabs/ResearchTab.vue'
|
||||||
|
import StrategiesTab from './tabs/StrategiesTab.vue'
|
||||||
|
import IndicatorsTab from './tabs/IndicatorsTab.vue'
|
||||||
|
|
||||||
interface TempTab {
|
interface TempTab {
|
||||||
id: string
|
id: string
|
||||||
@@ -84,10 +86,11 @@ defineExpose({
|
|||||||
<div v-if="isExpanded" class="tray-resize-handle" @pointerdown="startResize" @pointermove="onResizeMove" />
|
<div v-if="isExpanded" class="tray-resize-handle" @pointerdown="startResize" @pointermove="onResizeMove" />
|
||||||
<Tabs :value="isExpanded ? activeTab : null" class="tray-tabs">
|
<Tabs :value="isExpanded ? activeTab : null" class="tray-tabs">
|
||||||
<TabList class="tray-tab-list">
|
<TabList class="tray-tab-list">
|
||||||
|
<Tab value="positions" @click="onTabClick('positions')">Positions</Tab>
|
||||||
<Tab value="orders" @click="onTabClick('orders')">Orders</Tab>
|
<Tab value="orders" @click="onTabClick('orders')">Orders</Tab>
|
||||||
|
<Tab value="indicators" @click="onTabClick('indicators')">Indicators</Tab>
|
||||||
<Tab value="research" @click="onTabClick('research')">Research</Tab>
|
<Tab value="research" @click="onTabClick('research')">Research</Tab>
|
||||||
<Tab value="strategies" @click="onTabClick('strategies')">Strategies</Tab>
|
<Tab value="strategies" @click="onTabClick('strategies')">Strategies</Tab>
|
||||||
<Tab value="positions" @click="onTabClick('positions')">Positions</Tab>
|
|
||||||
<Tab
|
<Tab
|
||||||
v-for="tab in tempTabs"
|
v-for="tab in tempTabs"
|
||||||
:key="tab.id"
|
:key="tab.id"
|
||||||
@@ -106,8 +109,9 @@ defineExpose({
|
|||||||
<TabPanels v-if="isExpanded" class="tray-panels">
|
<TabPanels v-if="isExpanded" class="tray-panels">
|
||||||
<TabPanel value="positions" class="tray-panel"><PlaceholderTab label="Positions" /></TabPanel>
|
<TabPanel value="positions" class="tray-panel"><PlaceholderTab label="Positions" /></TabPanel>
|
||||||
<TabPanel value="orders" class="tray-panel"><OrdersTab /></TabPanel>
|
<TabPanel value="orders" class="tray-panel"><OrdersTab /></TabPanel>
|
||||||
<TabPanel value="strategies" class="tray-panel"><PlaceholderTab label="Strategies" /></TabPanel>
|
|
||||||
<TabPanel value="research" class="tray-panel"><ResearchTab /></TabPanel>
|
<TabPanel value="research" class="tray-panel"><ResearchTab /></TabPanel>
|
||||||
|
<TabPanel value="indicators" class="tray-panel"><IndicatorsTab /></TabPanel>
|
||||||
|
<TabPanel value="strategies" class="tray-panel"><StrategiesTab /></TabPanel>
|
||||||
<TabPanel
|
<TabPanel
|
||||||
v-for="tab in tempTabs"
|
v-for="tab in tempTabs"
|
||||||
:key="tab.id"
|
:key="tab.id"
|
||||||
|
|||||||
107
web/src/components/CategoryItemList.vue
Normal file
107
web/src/components/CategoryItemList.vue
Normal file
@@ -0,0 +1,107 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { ref } from 'vue'
|
||||||
|
import DetailsEditDialog from './DetailsEditDialog.vue'
|
||||||
|
|
||||||
|
const props = defineProps<{
|
||||||
|
category: 'indicator' | 'strategy' | 'research'
|
||||||
|
rows: Array<{ id: string; display_name: string; description?: string }>
|
||||||
|
}>()
|
||||||
|
|
||||||
|
const dialogVisible = ref(false)
|
||||||
|
const editingName = ref('')
|
||||||
|
|
||||||
|
function openEdit(name: string) {
|
||||||
|
editingName.value = name
|
||||||
|
dialogVisible.value = true
|
||||||
|
}
|
||||||
|
|
||||||
|
function onUpdated(_payload: { category: string; name: string; success: boolean; error?: string }) {
|
||||||
|
// Hook for handling the details_updated response — add logic here as needed
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="category-list">
|
||||||
|
<div v-if="rows.length === 0" class="empty">No items</div>
|
||||||
|
<div v-for="row in rows" :key="row.id" class="item-row">
|
||||||
|
<span class="item-name">{{ row.display_name }}</span>
|
||||||
|
<span class="item-desc">{{ row.description ?? '' }}</span>
|
||||||
|
<button class="edit-btn" @click="openEdit(row.display_name)">Edit</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<DetailsEditDialog
|
||||||
|
v-model:visible="dialogVisible"
|
||||||
|
:category="category"
|
||||||
|
:name="editingName"
|
||||||
|
@updated="onUpdated"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.category-list {
|
||||||
|
flex: 1;
|
||||||
|
overflow-y: auto;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
.empty {
|
||||||
|
color: #555;
|
||||||
|
text-align: center;
|
||||||
|
padding: 16px;
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.item-row {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 10px;
|
||||||
|
padding: 5px 10px;
|
||||||
|
border-bottom: 1px solid #1e1e1e;
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.item-row:hover {
|
||||||
|
background: #1a1a1a;
|
||||||
|
}
|
||||||
|
|
||||||
|
.item-name {
|
||||||
|
font-size: 12px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #dbdbdb;
|
||||||
|
white-space: nowrap;
|
||||||
|
flex-shrink: 0;
|
||||||
|
max-width: 180px;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
}
|
||||||
|
|
||||||
|
.item-desc {
|
||||||
|
flex: 1;
|
||||||
|
min-width: 0;
|
||||||
|
font-size: 12px;
|
||||||
|
color: #777;
|
||||||
|
white-space: nowrap;
|
||||||
|
overflow: hidden;
|
||||||
|
-webkit-mask-image: linear-gradient(to right, black calc(100% - 48px), transparent 100%);
|
||||||
|
mask-image: linear-gradient(to right, black calc(100% - 48px), transparent 100%);
|
||||||
|
}
|
||||||
|
|
||||||
|
.edit-btn {
|
||||||
|
flex-shrink: 0;
|
||||||
|
background: none;
|
||||||
|
border: 1px solid #3d3d3d;
|
||||||
|
color: #888;
|
||||||
|
cursor: pointer;
|
||||||
|
font-size: 11px;
|
||||||
|
padding: 2px 8px;
|
||||||
|
border-radius: 3px;
|
||||||
|
line-height: 18px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.edit-btn:hover {
|
||||||
|
border-color: #089981;
|
||||||
|
color: #089981;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -32,19 +32,10 @@ let symbolWatcher: WatchStopHandle | null = null
|
|||||||
|
|
||||||
const maybeInitChart = () => {
|
const maybeInitChart = () => {
|
||||||
if (chartInitialized || !chartContainer.value) return
|
if (chartInitialized || !chartContainer.value) return
|
||||||
if (!chartStore.symbol) {
|
// If the workspace persisted null values (bad state), reset to defaults immediately
|
||||||
// Defer until backend provides a symbol
|
// rather than deferring forever. This also syncs the reset back to the container.
|
||||||
if (!symbolWatcher) {
|
if (!chartStore.symbol) chartStore.symbol = 'BTC/USDT.BINANCE'
|
||||||
symbolWatcher = watch(() => chartStore.symbol, (sym) => {
|
if (!chartStore.period) chartStore.period = 900
|
||||||
if (sym) {
|
|
||||||
symbolWatcher?.()
|
|
||||||
symbolWatcher = null
|
|
||||||
maybeInitChart()
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
return
|
|
||||||
}
|
|
||||||
chartInitialized = true
|
chartInitialized = true
|
||||||
initChart()
|
initChart()
|
||||||
}
|
}
|
||||||
@@ -73,7 +64,7 @@ function initChart() {
|
|||||||
tvWidget = new window.TradingView.widget({
|
tvWidget = new window.TradingView.widget({
|
||||||
symbol: chartStore.symbol, // Use symbol from store
|
symbol: chartStore.symbol, // Use symbol from store
|
||||||
datafeed: datafeed,
|
datafeed: datafeed,
|
||||||
interval: secondsToInterval(chartStore.period) as any,
|
interval: secondsToInterval(chartStore.period || 900) as any,
|
||||||
container: chartContainer.value!,
|
container: chartContainer.value!,
|
||||||
library_path: '/charting_library/',
|
library_path: '/charting_library/',
|
||||||
locale: 'en',
|
locale: 'en',
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import Badge from 'primevue/badge'
|
|||||||
import { wsManager } from '../composables/useWebSocket'
|
import { wsManager } from '../composables/useWebSocket'
|
||||||
import type { WebSocketMessage } from '../composables/useWebSocket'
|
import type { WebSocketMessage } from '../composables/useWebSocket'
|
||||||
import { useChannelStore } from '../stores/channel'
|
import { useChannelStore } from '../stores/channel'
|
||||||
|
import ToolCallWithSubagents from './ToolCallWithSubagents.vue'
|
||||||
|
|
||||||
register()
|
register()
|
||||||
|
|
||||||
@@ -57,6 +58,7 @@ const rooms = computed(() => [{
|
|||||||
let currentStreamingMessageId: string | null = null
|
let currentStreamingMessageId: string | null = null
|
||||||
let toolCallMessageId: string | null = null
|
let toolCallMessageId: string | null = null
|
||||||
let lastSentMessageId: string | null = null
|
let lastSentMessageId: string | null = null
|
||||||
|
let subagentContentMap = new Map<string, number>() // agentName → index in subagentBlocks
|
||||||
let streamingBuffer = ''
|
let streamingBuffer = ''
|
||||||
const isAgentProcessing = ref(false)
|
const isAgentProcessing = ref(false)
|
||||||
const isStopHovered = ref(false)
|
const isStopHovered = ref(false)
|
||||||
@@ -64,6 +66,7 @@ const isStopPressed = ref(false)
|
|||||||
|
|
||||||
const addToolCallBubble = (label: string) => {
|
const addToolCallBubble = (label: string) => {
|
||||||
removeToolCallBubble()
|
removeToolCallBubble()
|
||||||
|
subagentContentMap = new Map()
|
||||||
toolCallMessageId = `tool-call-${Date.now()}`
|
toolCallMessageId = `tool-call-${Date.now()}`
|
||||||
const timestamp = new Date().toTimeString().split(' ')[0].slice(0, 5)
|
const timestamp = new Date().toTimeString().split(' ')[0].slice(0, 5)
|
||||||
messages.value = [...messages.value, {
|
messages.value = [...messages.value, {
|
||||||
@@ -76,7 +79,9 @@ const addToolCallBubble = (label: string) => {
|
|||||||
distributed: false,
|
distributed: false,
|
||||||
seen: false,
|
seen: false,
|
||||||
files: [],
|
files: [],
|
||||||
toolCall: true
|
toolCall: true,
|
||||||
|
subagentBlocks: [],
|
||||||
|
collapsed: false
|
||||||
}]
|
}]
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -92,10 +97,49 @@ const appendToolCallStatus = (status: string) => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const removeToolCallBubble = () => {
|
const removeToolCallBubble = (force = false) => {
|
||||||
if (toolCallMessageId) {
|
if (!toolCallMessageId) return
|
||||||
|
if (!force) {
|
||||||
|
const idx = messages.value.findIndex(m => m._id === toolCallMessageId)
|
||||||
|
if (idx !== -1 && messages.value[idx].subagentBlocks?.length) {
|
||||||
|
// Has subagent content — fold and keep for the user to re-expand
|
||||||
|
messages.value[idx] = {
|
||||||
|
...messages.value[idx],
|
||||||
|
collapsed: true,
|
||||||
|
subagentBlocks: messages.value[idx].subagentBlocks.map((b: any) => ({ ...b, isActive: false }))
|
||||||
|
}
|
||||||
|
messages.value = [...messages.value]
|
||||||
|
toolCallMessageId = null
|
||||||
|
subagentContentMap = new Map()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
messages.value = messages.value.filter(m => m._id !== toolCallMessageId)
|
messages.value = messages.value.filter(m => m._id !== toolCallMessageId)
|
||||||
toolCallMessageId = null
|
toolCallMessageId = null
|
||||||
|
subagentContentMap = new Map()
|
||||||
|
}
|
||||||
|
|
||||||
|
const appendSubagentChunk = (agentName: string, content: string) => {
|
||||||
|
if (!toolCallMessageId) return
|
||||||
|
const idx = messages.value.findIndex(m => m._id === toolCallMessageId)
|
||||||
|
if (idx === -1) return
|
||||||
|
const blocks = [...(messages.value[idx].subagentBlocks ?? [])]
|
||||||
|
if (subagentContentMap.has(agentName)) {
|
||||||
|
const blockIdx = subagentContentMap.get(agentName)!
|
||||||
|
blocks[blockIdx] = { ...blocks[blockIdx], content: blocks[blockIdx].content + content }
|
||||||
|
} else {
|
||||||
|
subagentContentMap.set(agentName, blocks.length)
|
||||||
|
blocks.push({ agentName, content, isActive: true })
|
||||||
|
}
|
||||||
|
messages.value[idx] = { ...messages.value[idx], subagentBlocks: blocks }
|
||||||
|
messages.value = [...messages.value]
|
||||||
|
}
|
||||||
|
|
||||||
|
const toggleCollapsed = (messageId: string) => {
|
||||||
|
const idx = messages.value.findIndex(m => m._id === messageId)
|
||||||
|
if (idx !== -1) {
|
||||||
|
messages.value[idx] = { ...messages.value[idx], collapsed: !messages.value[idx].collapsed }
|
||||||
|
messages.value = [...messages.value]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -146,7 +190,7 @@ const handleMessage = (data: WebSocketMessage) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (data.type === 'subagent_chunk') {
|
if (data.type === 'subagent_chunk') {
|
||||||
// Subagent final text — not shown separately; the main agent will incorporate it in its response
|
appendSubagentChunk(data.agentName, data.content)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -189,13 +233,12 @@ const handleMessage = (data: WebSocketMessage) => {
|
|||||||
}
|
}
|
||||||
} else if (data.type === 'agent_chunk') {
|
} else if (data.type === 'agent_chunk') {
|
||||||
console.log('[ChatPanel] Processing agent_chunk, content:', data.content, 'done:', data.done)
|
console.log('[ChatPanel] Processing agent_chunk, content:', data.content, 'done:', data.done)
|
||||||
// Always remove any tool-call bubble when the agent sends text, whether this
|
|
||||||
// is a new message or a continuation of an existing one (e.g. after a retry).
|
|
||||||
removeToolCallBubble()
|
|
||||||
const timestamp = new Date().toTimeString().split(' ')[0].slice(0, 5)
|
const timestamp = new Date().toTimeString().split(' ')[0].slice(0, 5)
|
||||||
|
|
||||||
if (!currentStreamingMessageId) {
|
if (!currentStreamingMessageId) {
|
||||||
console.log('[ChatPanel] Starting new streaming message')
|
console.log('[ChatPanel] Starting new streaming message')
|
||||||
|
// Fold tool call bubble (keeps it if it has subagent content, removes if empty)
|
||||||
|
removeToolCallBubble()
|
||||||
// Set up streaming state and mark user message as seen
|
// Set up streaming state and mark user message as seen
|
||||||
isAgentProcessing.value = true
|
isAgentProcessing.value = true
|
||||||
currentStreamingMessageId = generateMessageId()
|
currentStreamingMessageId = generateMessageId()
|
||||||
@@ -310,7 +353,7 @@ const handleMessage = (data: WebSocketMessage) => {
|
|||||||
const stopAgent = () => {
|
const stopAgent = () => {
|
||||||
wsManager.send({ type: 'agent_stop', session_id: SESSION_ID })
|
wsManager.send({ type: 'agent_stop', session_id: SESSION_ID })
|
||||||
isAgentProcessing.value = false
|
isAgentProcessing.value = false
|
||||||
removeToolCallBubble()
|
removeToolCallBubble(true)
|
||||||
lastSentMessageId = null
|
lastSentMessageId = null
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -651,6 +694,14 @@ onUnmounted(() => {
|
|||||||
@fetch-messages="fetchMessages"
|
@fetch-messages="fetchMessages"
|
||||||
@open-file="openFile"
|
@open-file="openFile"
|
||||||
>
|
>
|
||||||
|
<div
|
||||||
|
v-for="msg in messages.filter((m: any) => m.toolCall && m.subagentBlocks?.length)"
|
||||||
|
:key="'slot-' + msg._id"
|
||||||
|
:slot="'message_' + msg._id"
|
||||||
|
>
|
||||||
|
<ToolCallWithSubagents :message="msg" @toggle="toggleCollapsed(msg._id)" />
|
||||||
|
</div>
|
||||||
|
|
||||||
<div
|
<div
|
||||||
v-if="isAgentProcessing"
|
v-if="isAgentProcessing"
|
||||||
slot="send-icon"
|
slot="send-icon"
|
||||||
|
|||||||
294
web/src/components/DetailsEditDialog.vue
Normal file
294
web/src/components/DetailsEditDialog.vue
Normal file
@@ -0,0 +1,294 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { ref, watch, computed, onUnmounted } from 'vue'
|
||||||
|
import Dialog from 'primevue/dialog'
|
||||||
|
import Button from 'primevue/button'
|
||||||
|
import { useEditor, EditorContent } from '@tiptap/vue-3'
|
||||||
|
import StarterKit from '@tiptap/starter-kit'
|
||||||
|
import { Markdown } from 'tiptap-markdown'
|
||||||
|
import { wsManager, type WebSocketMessage } from '../composables/useWebSocket'
|
||||||
|
|
||||||
|
const props = defineProps<{
|
||||||
|
visible: boolean
|
||||||
|
category: 'indicator' | 'strategy' | 'research'
|
||||||
|
name: string
|
||||||
|
}>()
|
||||||
|
|
||||||
|
const emit = defineEmits<{
|
||||||
|
'update:visible': [value: boolean]
|
||||||
|
'updated': [payload: { category: string; name: string; success: boolean; error?: string }]
|
||||||
|
}>()
|
||||||
|
|
||||||
|
type LoadState = 'idle' | 'loading' | 'ready' | 'error'
|
||||||
|
|
||||||
|
const loadState = ref<LoadState>('idle')
|
||||||
|
const loadError = ref('')
|
||||||
|
const saving = ref(false)
|
||||||
|
const showConfirmCancel = ref(false)
|
||||||
|
|
||||||
|
const editor = useEditor({
|
||||||
|
extensions: [
|
||||||
|
StarterKit,
|
||||||
|
Markdown.configure({ html: false, transformPastedText: true }),
|
||||||
|
],
|
||||||
|
content: '',
|
||||||
|
})
|
||||||
|
|
||||||
|
const originalContent = ref('')
|
||||||
|
|
||||||
|
const isDirty = computed(() => {
|
||||||
|
if (!editor.value || loadState.value !== 'ready') return false
|
||||||
|
return (editor.value.storage as any).markdown.getMarkdown() !== originalContent.value
|
||||||
|
})
|
||||||
|
|
||||||
|
watch(() => props.visible, (v) => {
|
||||||
|
if (v) {
|
||||||
|
loadState.value = 'loading'
|
||||||
|
loadError.value = ''
|
||||||
|
saving.value = false
|
||||||
|
showConfirmCancel.value = false
|
||||||
|
wsManager.send({ type: 'read_details', category: props.category, name: props.name })
|
||||||
|
} else {
|
||||||
|
loadState.value = 'idle'
|
||||||
|
originalContent.value = ''
|
||||||
|
editor.value?.commands.setContent('')
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
const messageHandler = (msg: WebSocketMessage) => {
|
||||||
|
if (msg.category !== props.category || msg.name !== props.name) return
|
||||||
|
|
||||||
|
if (msg.type === 'details_data') {
|
||||||
|
originalContent.value = msg.details ?? ''
|
||||||
|
editor.value?.commands.setContent(msg.details ?? '')
|
||||||
|
loadState.value = 'ready'
|
||||||
|
} else if (msg.type === 'details_error') {
|
||||||
|
loadError.value = msg.error ?? 'Failed to load details'
|
||||||
|
loadState.value = 'error'
|
||||||
|
} else if (msg.type === 'details_updated') {
|
||||||
|
saving.value = false
|
||||||
|
emit('updated', {
|
||||||
|
category: props.category,
|
||||||
|
name: props.name,
|
||||||
|
success: msg.success,
|
||||||
|
error: msg.error,
|
||||||
|
})
|
||||||
|
if (msg.success) {
|
||||||
|
close()
|
||||||
|
} else {
|
||||||
|
loadError.value = msg.error ?? 'Update failed'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
wsManager.addHandler(messageHandler)
|
||||||
|
onUnmounted(() => wsManager.removeHandler(messageHandler))
|
||||||
|
|
||||||
|
function save() {
|
||||||
|
if (!editor.value || saving.value) return
|
||||||
|
saving.value = true
|
||||||
|
loadError.value = ''
|
||||||
|
wsManager.send({
|
||||||
|
type: 'update_details',
|
||||||
|
category: props.category,
|
||||||
|
name: props.name,
|
||||||
|
details: (editor.value.storage as any).markdown.getMarkdown(),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
function requestClose() {
|
||||||
|
if (isDirty.value) {
|
||||||
|
showConfirmCancel.value = true
|
||||||
|
} else {
|
||||||
|
close()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function close() {
|
||||||
|
showConfirmCancel.value = false
|
||||||
|
emit('update:visible', false)
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<Dialog
|
||||||
|
:visible="visible"
|
||||||
|
:header="`Edit Details — ${name}`"
|
||||||
|
:modal="true"
|
||||||
|
:closable="true"
|
||||||
|
:style="{ width: '720px', maxWidth: '95vw' }"
|
||||||
|
class="details-dialog"
|
||||||
|
@update:visible="requestClose"
|
||||||
|
>
|
||||||
|
<div class="dialog-body">
|
||||||
|
<div v-if="loadState === 'loading'" class="state-msg">
|
||||||
|
<i class="pi pi-spin pi-spinner" /> Loading details…
|
||||||
|
</div>
|
||||||
|
<div v-else-if="loadState === 'error'" class="state-msg error">
|
||||||
|
<i class="pi pi-exclamation-triangle" /> {{ loadError }}
|
||||||
|
</div>
|
||||||
|
<template v-else>
|
||||||
|
<div v-if="loadError" class="save-error">
|
||||||
|
<i class="pi pi-exclamation-triangle" /> {{ loadError }}
|
||||||
|
</div>
|
||||||
|
<div class="editor-wrap">
|
||||||
|
<EditorContent :editor="editor" class="tiptap-editor" />
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Confirm-cancel overlay -->
|
||||||
|
<div v-if="showConfirmCancel" class="confirm-overlay">
|
||||||
|
<div class="confirm-box">
|
||||||
|
<p>Discard unsaved changes?</p>
|
||||||
|
<div class="confirm-actions">
|
||||||
|
<Button label="Keep editing" size="small" outlined @click="showConfirmCancel = false" />
|
||||||
|
<Button label="Discard" size="small" severity="danger" @click="close" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<template #footer>
|
||||||
|
<Button label="Cancel" size="small" outlined :disabled="saving" @click="requestClose" />
|
||||||
|
<Button
|
||||||
|
label="Save"
|
||||||
|
size="small"
|
||||||
|
:loading="saving"
|
||||||
|
:disabled="loadState !== 'ready'"
|
||||||
|
@click="save"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
</Dialog>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.dialog-body {
|
||||||
|
min-height: 300px;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.state-msg {
|
||||||
|
flex: 1;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
gap: 8px;
|
||||||
|
color: #8a8a8a;
|
||||||
|
font-size: 13px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.state-msg.error {
|
||||||
|
color: #e06c6c;
|
||||||
|
}
|
||||||
|
|
||||||
|
.save-error {
|
||||||
|
color: #e06c6c;
|
||||||
|
font-size: 12px;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.editor-wrap {
|
||||||
|
flex: 1;
|
||||||
|
border: 1px solid #2e2e2e;
|
||||||
|
border-radius: 4px;
|
||||||
|
overflow: auto;
|
||||||
|
background: #0d0d0d;
|
||||||
|
min-height: 300px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.confirm-overlay {
|
||||||
|
position: absolute;
|
||||||
|
inset: 0;
|
||||||
|
background: rgba(0, 0, 0, 0.7);
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
border-radius: 4px;
|
||||||
|
z-index: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.confirm-box {
|
||||||
|
background: #1a1a1a;
|
||||||
|
border: 1px solid #3d3d3d;
|
||||||
|
border-radius: 6px;
|
||||||
|
padding: 20px 24px;
|
||||||
|
text-align: center;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.confirm-box p {
|
||||||
|
color: #dbdbdb;
|
||||||
|
font-size: 13px;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.confirm-actions {
|
||||||
|
display: flex;
|
||||||
|
gap: 8px;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
/* Global (unscoped) so TipTap's .ProseMirror gets styled */
|
||||||
|
.details-dialog .tiptap-editor .ProseMirror {
|
||||||
|
padding: 12px 14px;
|
||||||
|
outline: none;
|
||||||
|
min-height: 280px;
|
||||||
|
font-size: 13px;
|
||||||
|
line-height: 1.6;
|
||||||
|
color: #dbdbdb;
|
||||||
|
font-family: 'JetBrains Mono', 'Fira Code', 'Cascadia Code', monospace;
|
||||||
|
}
|
||||||
|
|
||||||
|
.details-dialog .tiptap-editor .ProseMirror h1,
|
||||||
|
.details-dialog .tiptap-editor .ProseMirror h2,
|
||||||
|
.details-dialog .tiptap-editor .ProseMirror h3 {
|
||||||
|
color: #dbdbdb;
|
||||||
|
margin: 0.8em 0 0.4em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.details-dialog .tiptap-editor .ProseMirror p { margin: 0 0 0.6em; }
|
||||||
|
|
||||||
|
.details-dialog .tiptap-editor .ProseMirror code {
|
||||||
|
background: #1a1a1a;
|
||||||
|
border-radius: 3px;
|
||||||
|
padding: 1px 5px;
|
||||||
|
font-size: 0.9em;
|
||||||
|
color: #89d4e0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.details-dialog .tiptap-editor .ProseMirror pre {
|
||||||
|
background: #141414;
|
||||||
|
border: 1px solid #2e2e2e;
|
||||||
|
border-radius: 4px;
|
||||||
|
padding: 10px 14px;
|
||||||
|
overflow-x: auto;
|
||||||
|
margin: 0.6em 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.details-dialog .tiptap-editor .ProseMirror pre code {
|
||||||
|
background: none;
|
||||||
|
padding: 0;
|
||||||
|
color: #dbdbdb;
|
||||||
|
}
|
||||||
|
|
||||||
|
.details-dialog .tiptap-editor .ProseMirror ul,
|
||||||
|
.details-dialog .tiptap-editor .ProseMirror ol {
|
||||||
|
padding-left: 1.4em;
|
||||||
|
margin: 0.4em 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.details-dialog .tiptap-editor .ProseMirror blockquote {
|
||||||
|
border-left: 3px solid #3d3d3d;
|
||||||
|
margin: 0.6em 0;
|
||||||
|
padding-left: 12px;
|
||||||
|
color: #8a8a8a;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
152
web/src/components/ToolCallWithSubagents.vue
Normal file
152
web/src/components/ToolCallWithSubagents.vue
Normal file
@@ -0,0 +1,152 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { computed } from 'vue'
|
||||||
|
|
||||||
|
interface SubagentBlock {
|
||||||
|
agentName: string
|
||||||
|
content: string
|
||||||
|
isActive: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
const props = defineProps<{
|
||||||
|
message: {
|
||||||
|
_id: string
|
||||||
|
content: string
|
||||||
|
subagentBlocks: SubagentBlock[]
|
||||||
|
collapsed: boolean
|
||||||
|
}
|
||||||
|
}>()
|
||||||
|
|
||||||
|
const emit = defineEmits<{ toggle: [messageId: string] }>()
|
||||||
|
|
||||||
|
const agentIcons: Record<string, string> = {
|
||||||
|
research: '🔬',
|
||||||
|
indicator: '📊',
|
||||||
|
strategy: '📈',
|
||||||
|
}
|
||||||
|
|
||||||
|
const headerLabel = computed(() => {
|
||||||
|
const firstLine = props.message.content.split('\n')[0]
|
||||||
|
return firstLine.replace(/^⚙\s*/, '') || 'Processing...'
|
||||||
|
})
|
||||||
|
|
||||||
|
const isActive = computed(() => props.message.subagentBlocks.some(b => b.isActive))
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="tc-bubble">
|
||||||
|
<div class="tc-header" @click="emit('toggle', message._id)">
|
||||||
|
<span class="tc-icon">⚙</span>
|
||||||
|
<span class="tc-label">{{ headerLabel }}</span>
|
||||||
|
<div v-if="isActive" class="tc-spinner"></div>
|
||||||
|
<span v-else class="tc-done">✓</span>
|
||||||
|
<span class="tc-chevron">{{ message.collapsed ? '▼' : '▲' }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="tc-body" :class="{ 'tc-body--collapsed': message.collapsed }">
|
||||||
|
<div v-for="block in message.subagentBlocks" :key="block.agentName" class="tc-agent-block">
|
||||||
|
<div class="tc-agent-name">
|
||||||
|
{{ agentIcons[block.agentName] ?? '🤖' }} {{ block.agentName }}
|
||||||
|
</div>
|
||||||
|
<div class="tc-agent-content">{{ block.content }}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.tc-bubble {
|
||||||
|
background: #141414;
|
||||||
|
color: #dbdbdb;
|
||||||
|
border-radius: 8px;
|
||||||
|
border: 1px solid #2e2e2e;
|
||||||
|
font-size: 13px;
|
||||||
|
min-width: 200px;
|
||||||
|
max-width: 100%;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tc-header {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 6px;
|
||||||
|
padding: 7px 10px;
|
||||||
|
cursor: pointer;
|
||||||
|
user-select: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tc-header:hover {
|
||||||
|
background: rgba(255, 255, 255, 0.04);
|
||||||
|
}
|
||||||
|
|
||||||
|
.tc-icon {
|
||||||
|
font-size: 13px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tc-label {
|
||||||
|
flex: 1;
|
||||||
|
font-size: 13px;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tc-spinner {
|
||||||
|
width: 10px;
|
||||||
|
height: 10px;
|
||||||
|
border: 2px solid rgba(8, 153, 129, 0.3);
|
||||||
|
border-top-color: #089981;
|
||||||
|
border-radius: 50%;
|
||||||
|
animation: tc-spin 0.7s linear infinite;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes tc-spin {
|
||||||
|
to { transform: rotate(360deg); }
|
||||||
|
}
|
||||||
|
|
||||||
|
.tc-done {
|
||||||
|
color: #089981;
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tc-chevron {
|
||||||
|
font-size: 10px;
|
||||||
|
opacity: 0.5;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tc-body {
|
||||||
|
overflow: hidden;
|
||||||
|
max-height: 300px;
|
||||||
|
overflow-y: auto;
|
||||||
|
border-top: 1px solid #2e2e2e;
|
||||||
|
transition: max-height 0.25s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tc-body--collapsed {
|
||||||
|
max-height: 0 !important;
|
||||||
|
border-top-color: transparent;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tc-agent-block {
|
||||||
|
padding: 6px 10px 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tc-agent-block + .tc-agent-block {
|
||||||
|
border-top: 1px solid rgba(46, 46, 46, 0.5);
|
||||||
|
}
|
||||||
|
|
||||||
|
.tc-agent-name {
|
||||||
|
font-size: 11px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #8a8a8a;
|
||||||
|
margin-bottom: 4px;
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 0.04em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tc-agent-content {
|
||||||
|
font-size: 12px;
|
||||||
|
font-family: monospace;
|
||||||
|
color: #b8b8b8;
|
||||||
|
white-space: pre-wrap;
|
||||||
|
word-break: break-word;
|
||||||
|
line-height: 1.5;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
17
web/src/components/tabs/IndicatorsTab.vue
Normal file
17
web/src/components/tabs/IndicatorsTab.vue
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { computed } from 'vue'
|
||||||
|
import { storeToRefs } from 'pinia'
|
||||||
|
import { useIndicatorTypesStore } from '../../stores/indicatorTypes'
|
||||||
|
import CategoryItemList from '../CategoryItemList.vue'
|
||||||
|
|
||||||
|
const store = useIndicatorTypesStore()
|
||||||
|
const { types } = storeToRefs(store)
|
||||||
|
|
||||||
|
const rows = computed(() =>
|
||||||
|
Object.entries(types.value).map(([id, t]) => ({ id, display_name: t.display_name, description: t.description }))
|
||||||
|
)
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<CategoryItemList category="indicator" :rows="rows" />
|
||||||
|
</template>
|
||||||
@@ -1,108 +1,17 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ref, computed } from 'vue'
|
import { computed } from 'vue'
|
||||||
import { storeToRefs } from 'pinia'
|
import { storeToRefs } from 'pinia'
|
||||||
import { useResearchTypesStore } from '../../stores/researchTypes'
|
import { useResearchTypesStore } from '../../stores/researchTypes'
|
||||||
|
import CategoryItemList from '../CategoryItemList.vue'
|
||||||
|
|
||||||
const store = useResearchTypesStore()
|
const store = useResearchTypesStore()
|
||||||
const { types } = storeToRefs(store)
|
const { types } = storeToRefs(store)
|
||||||
|
|
||||||
const expanded = ref<Set<string>>(new Set())
|
|
||||||
|
|
||||||
const rows = computed(() =>
|
const rows = computed(() =>
|
||||||
Object.entries(types.value).map(([id, t]) => ({ id, ...t }))
|
Object.entries(types.value).map(([id, t]) => ({ id, display_name: t.display_name, description: t.description }))
|
||||||
)
|
)
|
||||||
|
|
||||||
function toggle(id: string) {
|
|
||||||
if (expanded.value.has(id)) {
|
|
||||||
expanded.value.delete(id)
|
|
||||||
} else {
|
|
||||||
expanded.value.add(id)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div class="research-tab">
|
<CategoryItemList category="research" :rows="rows" />
|
||||||
<div v-if="rows.length === 0" class="empty">No research items</div>
|
|
||||||
<div v-for="row in rows" :key="row.id" class="research-row">
|
|
||||||
<button class="row-header" @click="toggle(row.id)">
|
|
||||||
<i class="pi" :class="expanded.has(row.id) ? 'pi-chevron-down' : 'pi-chevron-right'" />
|
|
||||||
<span class="row-name">{{ row.display_name }}</span>
|
|
||||||
<span class="row-id">{{ row.id }}</span>
|
|
||||||
</button>
|
|
||||||
<div v-if="expanded.has(row.id)" class="row-body">
|
|
||||||
<span v-if="row.description">{{ row.description }}</span>
|
|
||||||
<span v-else class="no-desc">No description</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<style scoped>
|
|
||||||
.research-tab {
|
|
||||||
flex: 1;
|
|
||||||
overflow-y: auto;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
}
|
|
||||||
|
|
||||||
.empty {
|
|
||||||
color: #555;
|
|
||||||
text-align: center;
|
|
||||||
padding: 16px;
|
|
||||||
font-size: 12px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.research-row {
|
|
||||||
border-bottom: 1px solid #1e1e1e;
|
|
||||||
}
|
|
||||||
|
|
||||||
.row-header {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 6px;
|
|
||||||
width: 100%;
|
|
||||||
background: none;
|
|
||||||
border: none;
|
|
||||||
padding: 5px 10px;
|
|
||||||
cursor: pointer;
|
|
||||||
text-align: left;
|
|
||||||
color: #dbdbdb;
|
|
||||||
font-size: 12px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.row-header:hover {
|
|
||||||
background: #1a1a1a;
|
|
||||||
}
|
|
||||||
|
|
||||||
.row-header .pi {
|
|
||||||
color: #666;
|
|
||||||
font-size: 10px;
|
|
||||||
flex-shrink: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.row-name {
|
|
||||||
flex: 1;
|
|
||||||
font-weight: 500;
|
|
||||||
}
|
|
||||||
|
|
||||||
.row-id {
|
|
||||||
color: #555;
|
|
||||||
font-size: 11px;
|
|
||||||
font-family: monospace;
|
|
||||||
}
|
|
||||||
|
|
||||||
.row-body {
|
|
||||||
padding: 6px 26px 8px;
|
|
||||||
font-size: 12px;
|
|
||||||
color: #aaa;
|
|
||||||
line-height: 1.5;
|
|
||||||
background: #0d0d0d;
|
|
||||||
white-space: pre-wrap;
|
|
||||||
}
|
|
||||||
|
|
||||||
.no-desc {
|
|
||||||
color: #444;
|
|
||||||
font-style: italic;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
|
|||||||
17
web/src/components/tabs/StrategiesTab.vue
Normal file
17
web/src/components/tabs/StrategiesTab.vue
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { computed } from 'vue'
|
||||||
|
import { storeToRefs } from 'pinia'
|
||||||
|
import { useStrategyTypesStore } from '../../stores/strategyTypes'
|
||||||
|
import CategoryItemList from '../CategoryItemList.vue'
|
||||||
|
|
||||||
|
const store = useStrategyTypesStore()
|
||||||
|
const { types } = storeToRefs(store)
|
||||||
|
|
||||||
|
const rows = computed(() =>
|
||||||
|
Object.entries(types.value).map(([id, t]) => ({ id, display_name: t.display_name, description: t.description }))
|
||||||
|
)
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<CategoryItemList category="strategy" :rows="rows" />
|
||||||
|
</template>
|
||||||
@@ -159,6 +159,10 @@ const fetchedRanges = new Map<string, { fromTime: number; toTime: number }>()
|
|||||||
// constructor can query the current visible range.
|
// constructor can query the current visible range.
|
||||||
let _tvWidget: any = null
|
let _tvWidget: any = null
|
||||||
|
|
||||||
|
// Set of pandas_ta_names that were registered as named TV studies at widget init.
|
||||||
|
// Types that arrive after init are NOT in TV's registry, so must use the generic fallback.
|
||||||
|
const _registeredStudyNames = new Set<string>()
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
// Generic study design constants
|
// Generic study design constants
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
@@ -443,7 +447,16 @@ export function getCustomIndicatorsGetter(
|
|||||||
const typeKeys = Object.keys(types)
|
const typeKeys = Object.keys(types)
|
||||||
console.log('[CustomIndicators] custom_indicators_getter called, types in store:', typeKeys)
|
console.log('[CustomIndicators] custom_indicators_getter called, types in store:', typeKeys)
|
||||||
|
|
||||||
const namedStudies = Object.values(types).map(makeNamedStudy)
|
_registeredStudyNames.clear()
|
||||||
|
const namedStudies = Object.values(types).flatMap(type => {
|
||||||
|
try {
|
||||||
|
_registeredStudyNames.add(type.pandas_ta_name)
|
||||||
|
return [makeNamedStudy(type)]
|
||||||
|
} catch (err) {
|
||||||
|
console.error('[CustomIndicators] Skipping malformed indicator type:', type?.pandas_ta_name, err)
|
||||||
|
return []
|
||||||
|
}
|
||||||
|
})
|
||||||
const testStudy = makeTestStudy(PineJS)
|
const testStudy = makeTestStudy(PineJS)
|
||||||
const studies = [
|
const studies = [
|
||||||
makeGenericStudy('dxo_customstudy_overlay', true),
|
makeGenericStudy('dxo_customstudy_overlay', true),
|
||||||
@@ -526,11 +539,12 @@ export function useCustomIndicators(tvWidget: any) {
|
|||||||
// Resolve the study type name to use when creating a new TV study
|
// Resolve the study type name to use when creating a new TV study
|
||||||
// ------------------------------------------------------------------
|
// ------------------------------------------------------------------
|
||||||
function resolveStudyTypeName(pandasTaName: string, pane: string): string {
|
function resolveStudyTypeName(pandasTaName: string, pane: string): string {
|
||||||
// TV's createStudy() matches by the `description` field in metainfo, not the internal `name`.
|
// Only use the named study if it was registered in TV at widget init time.
|
||||||
// Named studies have description = display_name (e.g. "TrendFlex"), not "dxo_ind_*".
|
// Types that arrived after init are not in TV's registry — fall back to generic.
|
||||||
|
if (_registeredStudyNames.has(pandasTaName)) {
|
||||||
const typeEntry = indicatorTypesStore.types[pandasTaName]
|
const typeEntry = indicatorTypesStore.types[pandasTaName]
|
||||||
if (typeEntry) return typeEntry.metadata.display_name
|
if (typeEntry) return typeEntry.metadata.display_name
|
||||||
// Generic fallbacks have name === description, so either works.
|
}
|
||||||
return pane === 'price' ? 'dxo_customstudy_overlay' : 'dxo_customstudy_pane'
|
return pane === 'price' ? 'dxo_customstudy_overlay' : 'dxo_customstudy_pane'
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -23,6 +23,8 @@ class WebSocketManager {
|
|||||||
private maxReconnectAttempts = Infinity // Keep trying indefinitely
|
private maxReconnectAttempts = Infinity // Keep trying indefinitely
|
||||||
private reconnectDelay = 1000 // Start with 1 second
|
private reconnectDelay = 1000 // Start with 1 second
|
||||||
private maxReconnectDelay = 50000 // Max 50 seconds
|
private maxReconnectDelay = 50000 // Max 50 seconds
|
||||||
|
private consolePatchInstalled = false
|
||||||
|
private forwardingLog = false // prevent re-entrancy
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Connect to WebSocket with JWT token for authentication
|
* Connect to WebSocket with JWT token for authentication
|
||||||
@@ -101,6 +103,7 @@ class WebSocketManager {
|
|||||||
this.isAuthenticated.value = true
|
this.isAuthenticated.value = true
|
||||||
this.sessionStatus.value = 'ready'
|
this.sessionStatus.value = 'ready'
|
||||||
this.statusMessage.value = ''
|
this.statusMessage.value = ''
|
||||||
|
this.installConsoleForwarding(message.sessionId)
|
||||||
// Flush any queued messages now that we're authenticated
|
// Flush any queued messages now that we're authenticated
|
||||||
this.flushMessageQueue()
|
this.flushMessageQueue()
|
||||||
} else if (message.type === 'error') {
|
} else if (message.type === 'error') {
|
||||||
@@ -210,6 +213,29 @@ class WebSocketManager {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private installConsoleForwarding(sessionId: string) {
|
||||||
|
if (this.consolePatchInstalled) return
|
||||||
|
this.consolePatchInstalled = true
|
||||||
|
|
||||||
|
const levels = ['log', 'info', 'warn', 'error', 'debug'] as const
|
||||||
|
for (const level of levels) {
|
||||||
|
const original = (console[level] as (...args: any[]) => void).bind(console)
|
||||||
|
console[level] = (...args: any[]) => {
|
||||||
|
original(...args)
|
||||||
|
if (!this.forwardingLog && this.ws?.readyState === WebSocket.OPEN) {
|
||||||
|
this.forwardingLog = true
|
||||||
|
try {
|
||||||
|
const message = args.map(a =>
|
||||||
|
typeof a === 'string' ? a : (() => { try { return JSON.stringify(a) } catch { return String(a) } })()
|
||||||
|
).join(' ')
|
||||||
|
this.ws!.send(JSON.stringify({ type: 'client_log', level, message }))
|
||||||
|
} catch { /* ignore send errors */ }
|
||||||
|
this.forwardingLog = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
disconnect() {
|
disconnect() {
|
||||||
if (this.reconnectTimeout) {
|
if (this.reconnectTimeout) {
|
||||||
clearTimeout(this.reconnectTimeout)
|
clearTimeout(this.reconnectTimeout)
|
||||||
|
|||||||
Reference in New Issue
Block a user