protoagent 0.0.4 → 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +99 -19
- package/dist/App.js +602 -0
- package/dist/agentic-loop.js +492 -525
- package/dist/cli.js +39 -0
- package/dist/components/CollapsibleBox.js +26 -0
- package/dist/components/ConfigDialog.js +40 -0
- package/dist/components/ConsolidatedToolMessage.js +41 -0
- package/dist/components/FormattedMessage.js +93 -0
- package/dist/components/Table.js +275 -0
- package/dist/config.js +171 -0
- package/dist/mcp.js +170 -0
- package/dist/providers.js +137 -0
- package/dist/sessions.js +161 -0
- package/dist/skills.js +229 -0
- package/dist/sub-agent.js +103 -0
- package/dist/system-prompt.js +131 -0
- package/dist/tools/bash.js +178 -0
- package/dist/tools/edit-file.js +65 -171
- package/dist/tools/index.js +79 -134
- package/dist/tools/list-directory.js +20 -73
- package/dist/tools/read-file.js +57 -101
- package/dist/tools/search-files.js +74 -162
- package/dist/tools/todo.js +57 -140
- package/dist/tools/webfetch.js +310 -0
- package/dist/tools/write-file.js +44 -135
- package/dist/utils/approval.js +69 -0
- package/dist/utils/compactor.js +87 -0
- package/dist/utils/cost-tracker.js +26 -81
- package/dist/utils/format-message.js +26 -0
- package/dist/utils/logger.js +101 -307
- package/dist/utils/path-validation.js +74 -0
- package/package.json +45 -51
- package/LICENSE +0 -21
- package/dist/config/client.js +0 -315
- package/dist/config/commands.js +0 -223
- package/dist/config/manager.js +0 -117
- package/dist/config/mcp-commands.js +0 -266
- package/dist/config/mcp-manager.js +0 -240
- package/dist/config/mcp-types.js +0 -28
- package/dist/config/providers.js +0 -229
- package/dist/config/setup.js +0 -209
- package/dist/config/system-prompt.js +0 -397
- package/dist/config/types.js +0 -4
- package/dist/index.js +0 -222
- package/dist/tools/create-directory.js +0 -76
- package/dist/tools/directory-operations.js +0 -195
- package/dist/tools/file-operations.js +0 -211
- package/dist/tools/run-shell-command.js +0 -746
- package/dist/tools/search-operations.js +0 -179
- package/dist/tools/shell-operations.js +0 -342
- package/dist/tools/task-complete.js +0 -26
- package/dist/tools/view-directory-tree.js +0 -125
- package/dist/tools.js +0 -2
- package/dist/utils/conversation-compactor.js +0 -140
- package/dist/utils/enhanced-prompt.js +0 -23
- package/dist/utils/file-operations-approval.js +0 -373
- package/dist/utils/interrupt-handler.js +0 -127
- package/dist/utils/user-cancellation.js +0 -34
package/dist/cli.js
ADDED
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
3
|
+
/**
|
|
4
|
+
* CLI entry point for ProtoAgent.
|
|
5
|
+
*
|
|
6
|
+
* Parses command-line flags and launches either the main chat UI
|
|
7
|
+
* or the configuration wizard.
|
|
8
|
+
*/
|
|
9
|
+
import process from 'node:process';
|
|
10
|
+
import path from 'node:path';
|
|
11
|
+
import { fileURLToPath } from 'node:url';
|
|
12
|
+
import { readFileSync } from 'node:fs';
|
|
13
|
+
import { render } from 'ink';
|
|
14
|
+
import { Command } from 'commander';
|
|
15
|
+
import { App } from './App.js';
|
|
16
|
+
import { ConfigureComponent } from './config.js';
|
|
17
|
+
// Get package.json version
|
|
18
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
19
|
+
const __dirname = path.dirname(__filename);
|
|
20
|
+
const packageJson = JSON.parse(readFileSync(path.join(__dirname, '..', 'package.json'), 'utf8'));
|
|
21
|
+
const program = new Command();
|
|
22
|
+
program
|
|
23
|
+
.description('ProtoAgent — a simple, hackable coding agent CLI')
|
|
24
|
+
.version(packageJson.version)
|
|
25
|
+
.option('--dangerously-accept-all', 'Auto-approve all file writes and shell commands')
|
|
26
|
+
.option('--log-level <level>', 'Log level: TRACE, DEBUG, INFO, WARN, ERROR', 'INFO')
|
|
27
|
+
.option('--session <id>', 'Resume a previous session by ID')
|
|
28
|
+
.action((options) => {
|
|
29
|
+
// Default action - start the main app
|
|
30
|
+
render(_jsx(App, { dangerouslyAcceptAll: options.dangerouslyAcceptAll || false, logLevel: options.logLevel, sessionId: options.session }));
|
|
31
|
+
});
|
|
32
|
+
// Configure subcommand
|
|
33
|
+
program
|
|
34
|
+
.command('configure')
|
|
35
|
+
.description('Configure AI model and API key settings')
|
|
36
|
+
.action(() => {
|
|
37
|
+
render(_jsx(ConfigureComponent, {}));
|
|
38
|
+
});
|
|
39
|
+
program.parse(process.argv);
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { Box, Text } from 'ink';
|
|
3
|
+
export const CollapsibleBox = ({ title, content, titleColor, dimColor = false, maxPreviewLines = 3, maxPreviewChars = 500, expanded = false, marginBottom = 0, }) => {
|
|
4
|
+
const lines = content.split('\n');
|
|
5
|
+
const isTooManyLines = lines.length > maxPreviewLines;
|
|
6
|
+
const isTooManyChars = content.length > maxPreviewChars;
|
|
7
|
+
const isLong = isTooManyLines || isTooManyChars;
|
|
8
|
+
// If content is short, always show it
|
|
9
|
+
if (!isLong) {
|
|
10
|
+
return (_jsxs(Box, { flexDirection: "column", marginBottom: marginBottom, borderStyle: "round", borderColor: titleColor || 'white', children: [_jsx(Box, { paddingX: 1, children: _jsx(Text, { color: titleColor, dimColor: dimColor, bold: true, children: title }) }), _jsx(Box, { marginLeft: 2, paddingRight: 1, children: _jsx(Text, { dimColor: dimColor, children: content }) })] }));
|
|
11
|
+
}
|
|
12
|
+
// For long content, show preview or full content
|
|
13
|
+
let preview;
|
|
14
|
+
if (expanded) {
|
|
15
|
+
preview = content;
|
|
16
|
+
}
|
|
17
|
+
else {
|
|
18
|
+
// Truncate by lines first, then by characters
|
|
19
|
+
const linesTruncated = lines.slice(0, maxPreviewLines).join('\n');
|
|
20
|
+
preview = linesTruncated.length > maxPreviewChars
|
|
21
|
+
? linesTruncated.slice(0, maxPreviewChars)
|
|
22
|
+
: linesTruncated;
|
|
23
|
+
}
|
|
24
|
+
const hasMore = !expanded;
|
|
25
|
+
return (_jsxs(Box, { flexDirection: "column", marginBottom: marginBottom, borderStyle: "round", borderColor: titleColor || 'white', children: [_jsx(Box, { paddingX: 1, children: _jsxs(Text, { color: titleColor, dimColor: dimColor, bold: true, children: [expanded ? '▼' : '▶', " ", title] }) }), _jsxs(Box, { flexDirection: "column", marginLeft: 2, paddingRight: 1, children: [_jsx(Text, { dimColor: dimColor, children: preview }), hasMore && _jsx(Text, { dimColor: true, children: "... (use /expand to see all)" })] })] }));
|
|
26
|
+
};
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
/**
|
|
3
|
+
* ConfigDialog — Modal-like dialog for changing config mid-conversation
|
|
4
|
+
*
|
|
5
|
+
* Allows users to update provider, model, or API key without losing chat history.
|
|
6
|
+
*/
|
|
7
|
+
import { useState } from 'react';
|
|
8
|
+
import { Box, Text } from 'ink';
|
|
9
|
+
import { PasswordInput, Select } from '@inkjs/ui';
|
|
10
|
+
import { getProvider, SUPPORTED_MODELS } from '../providers.js';
|
|
11
|
+
export const ConfigDialog = ({ currentConfig, onComplete, onCancel, }) => {
|
|
12
|
+
const [step, setStep] = useState('select_provider');
|
|
13
|
+
const [selectedProviderId, setSelectedProviderId] = useState(currentConfig.provider);
|
|
14
|
+
const [selectedModelId, setSelectedModelId] = useState(currentConfig.model);
|
|
15
|
+
const providerItems = SUPPORTED_MODELS.flatMap((provider) => provider.models.map((model) => ({
|
|
16
|
+
label: `${provider.name} - ${model.name}`,
|
|
17
|
+
value: `${provider.id}:::${model.id}`,
|
|
18
|
+
})));
|
|
19
|
+
const currentProvider = getProvider(currentConfig.provider);
|
|
20
|
+
// Provider selection step
|
|
21
|
+
if (step === 'select_provider') {
|
|
22
|
+
return (_jsxs(Box, { flexDirection: "column", marginTop: 1, borderStyle: "round", borderColor: "green", paddingX: 1, children: [_jsx(Text, { color: "green", bold: true, children: "Change Configuration" }), _jsxs(Text, { dimColor: true, children: ["Current: ", currentProvider?.name, " / ", currentConfig.model] }), _jsx(Text, { dimColor: true, children: "Select a new provider and model:" }), _jsx(Box, { marginTop: 1, children: _jsx(Select, { options: providerItems.map((item) => ({ value: item.value, label: item.label })), onChange: (value) => {
|
|
23
|
+
const [providerId, modelId] = value.split(':::');
|
|
24
|
+
setSelectedProviderId(providerId);
|
|
25
|
+
setSelectedModelId(modelId);
|
|
26
|
+
setStep('enter_api_key');
|
|
27
|
+
} }) })] }));
|
|
28
|
+
}
|
|
29
|
+
// API key entry step
|
|
30
|
+
const provider = getProvider(selectedProviderId);
|
|
31
|
+
return (_jsxs(Box, { flexDirection: "column", marginTop: 1, borderStyle: "round", borderColor: "green", paddingX: 1, children: [_jsx(Text, { color: "green", bold: true, children: "Confirm Configuration" }), _jsxs(Text, { dimColor: true, children: ["Provider: ", provider?.name, " / ", selectedModelId] }), _jsx(Text, { children: "Enter your API key (or leave empty to keep current/env var):" }), _jsx(PasswordInput, { placeholder: `Paste your ${provider?.apiKeyEnvVar || 'API'} key`, onSubmit: (value) => {
|
|
32
|
+
const finalApiKey = value.trim().length > 0 ? value.trim() : currentConfig.apiKey;
|
|
33
|
+
const newConfig = {
|
|
34
|
+
provider: selectedProviderId,
|
|
35
|
+
model: selectedModelId,
|
|
36
|
+
apiKey: finalApiKey,
|
|
37
|
+
};
|
|
38
|
+
onComplete(newConfig);
|
|
39
|
+
} })] }));
|
|
40
|
+
};
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { jsxs as _jsxs, jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import { Box, Text } from 'ink';
|
|
3
|
+
import { Table } from './Table.js';
|
|
4
|
+
import { FormattedMessage } from './FormattedMessage.js';
|
|
5
|
+
export const ConsolidatedToolMessage = ({ toolCalls, toolResults, expanded = false, }) => {
|
|
6
|
+
const toolNames = toolCalls.map((toolCall) => toolCall.name);
|
|
7
|
+
const title = `Called: ${toolNames.join(', ')}`;
|
|
8
|
+
const containsTodoTool = toolCalls.some((toolCall) => toolCall.name === 'todo_read' || toolCall.name === 'todo_write');
|
|
9
|
+
const titleColor = containsTodoTool ? 'green' : 'white';
|
|
10
|
+
const isExpanded = expanded || containsTodoTool;
|
|
11
|
+
if (isExpanded) {
|
|
12
|
+
return (_jsxs(Box, { flexDirection: "column", borderStyle: "round", borderColor: "white", children: [_jsx(Box, { paddingX: 1, children: _jsxs(Text, { color: titleColor, bold: true, children: ["\u25BC ", title] }) }), _jsx(Box, { flexDirection: "column", marginLeft: 2, paddingRight: 1, children: toolCalls.map((toolCall, idx) => {
|
|
13
|
+
const result = toolResults.get(toolCall.id);
|
|
14
|
+
if (!result)
|
|
15
|
+
return null;
|
|
16
|
+
// Try to see if it's JSON that could be a table
|
|
17
|
+
let isJsonTable = false;
|
|
18
|
+
try {
|
|
19
|
+
const parsed = JSON.parse(result.content);
|
|
20
|
+
isJsonTable = typeof parsed === 'object' && parsed !== null;
|
|
21
|
+
}
|
|
22
|
+
catch (e) { }
|
|
23
|
+
return (_jsxs(Box, { flexDirection: "column", children: [_jsxs(Text, { color: "cyan", bold: true, children: ["[", result.name, "]:"] }), isJsonTable ? (_jsx(Table, { data: result.content })) : (_jsx(FormattedMessage, { content: result.content }))] }, idx));
|
|
24
|
+
}) })] }));
|
|
25
|
+
}
|
|
26
|
+
const compactLines = toolCalls.flatMap((toolCall) => {
|
|
27
|
+
const result = toolResults.get(toolCall.id);
|
|
28
|
+
if (!result)
|
|
29
|
+
return [];
|
|
30
|
+
const compactContent = result.content
|
|
31
|
+
.replace(/\s+/g, ' ')
|
|
32
|
+
.trim();
|
|
33
|
+
return [`[${result.name}] ${compactContent}`];
|
|
34
|
+
});
|
|
35
|
+
const compactPreview = compactLines.join(' | ');
|
|
36
|
+
const previewLimit = 180;
|
|
37
|
+
const preview = compactPreview.length > previewLimit
|
|
38
|
+
? `${compactPreview.slice(0, previewLimit).trimEnd()}... (use /expand)`
|
|
39
|
+
: compactPreview;
|
|
40
|
+
return (_jsxs(Box, { flexDirection: "column", borderStyle: "round", borderColor: titleColor || 'white', children: [_jsx(Box, { paddingX: 1, children: _jsxs(Text, { color: titleColor, dimColor: true, bold: true, children: ["\u25B6 ", title] }) }), _jsx(Box, { marginLeft: 2, paddingRight: 1, children: _jsx(Text, { dimColor: true, children: preview }) })] }));
|
|
41
|
+
};
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import { Box, Text } from 'ink';
|
|
3
|
+
import { Table } from './Table.js';
|
|
4
|
+
import { formatMessage } from '../utils/format-message.js';
|
|
5
|
+
/**
|
|
6
|
+
* FormattedMessage component
|
|
7
|
+
*
|
|
8
|
+
* Parses a markdown string and renders:
|
|
9
|
+
* - Standard text with ANSI formatting
|
|
10
|
+
* - Markdown tables using ink-table
|
|
11
|
+
* - Code blocks (rendered in a box)
|
|
12
|
+
*/
|
|
13
|
+
export const FormattedMessage = ({ content }) => {
|
|
14
|
+
if (!content)
|
|
15
|
+
return null;
|
|
16
|
+
const lines = content.split('\n');
|
|
17
|
+
const blocks = [];
|
|
18
|
+
let currentBlockContent = [];
|
|
19
|
+
let currentBlockType = 'text';
|
|
20
|
+
for (let i = 0; i < lines.length; i++) {
|
|
21
|
+
const line = lines[i];
|
|
22
|
+
const trimmedLine = line.trim();
|
|
23
|
+
// 1. Handle Code Blocks
|
|
24
|
+
if (currentBlockType === 'code') {
|
|
25
|
+
currentBlockContent.push(line);
|
|
26
|
+
// Check for end of code block
|
|
27
|
+
if (trimmedLine.startsWith('```')) {
|
|
28
|
+
blocks.push({ type: 'code', content: currentBlockContent.join('\n') });
|
|
29
|
+
currentBlockContent = [];
|
|
30
|
+
currentBlockType = 'text';
|
|
31
|
+
}
|
|
32
|
+
continue;
|
|
33
|
+
}
|
|
34
|
+
// Start Code Block
|
|
35
|
+
if (trimmedLine.startsWith('```')) {
|
|
36
|
+
// Finish pending text block
|
|
37
|
+
if (currentBlockContent.length > 0) {
|
|
38
|
+
blocks.push({ type: 'text', content: currentBlockContent.join('\n') });
|
|
39
|
+
}
|
|
40
|
+
currentBlockContent = [line];
|
|
41
|
+
currentBlockType = 'code';
|
|
42
|
+
continue;
|
|
43
|
+
}
|
|
44
|
+
// 2. Handle Tables
|
|
45
|
+
if (currentBlockType === 'table') {
|
|
46
|
+
if (trimmedLine.startsWith('|')) {
|
|
47
|
+
currentBlockContent.push(line);
|
|
48
|
+
continue;
|
|
49
|
+
}
|
|
50
|
+
else {
|
|
51
|
+
// End of table block found (line doesn't start with |)
|
|
52
|
+
blocks.push({ type: 'table', content: currentBlockContent.join('\n') });
|
|
53
|
+
// Reset to text and fall through to re-process this line
|
|
54
|
+
currentBlockContent = [];
|
|
55
|
+
currentBlockType = 'text';
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
// Start Table Block check
|
|
59
|
+
// A table start requires a pipe AND a subsequent separator line
|
|
60
|
+
const isTableStart = trimmedLine.startsWith('|');
|
|
61
|
+
const nextLine = lines[i + 1];
|
|
62
|
+
const isNextLineSeparator = nextLine && nextLine.trim().startsWith('|') && nextLine.includes('---');
|
|
63
|
+
if (isTableStart && isNextLineSeparator) {
|
|
64
|
+
// Finish pending text block
|
|
65
|
+
if (currentBlockContent.length > 0) {
|
|
66
|
+
blocks.push({ type: 'text', content: currentBlockContent.join('\n') });
|
|
67
|
+
}
|
|
68
|
+
currentBlockContent = [line];
|
|
69
|
+
currentBlockType = 'table';
|
|
70
|
+
continue;
|
|
71
|
+
}
|
|
72
|
+
// 3. Handle Text
|
|
73
|
+
currentBlockContent.push(line);
|
|
74
|
+
}
|
|
75
|
+
// Push final block
|
|
76
|
+
if (currentBlockContent.length > 0) {
|
|
77
|
+
blocks.push({ type: currentBlockType, content: currentBlockContent.join('\n') });
|
|
78
|
+
}
|
|
79
|
+
return (_jsx(Box, { flexDirection: "column", children: blocks.map((block, index) => {
|
|
80
|
+
if (block.type === 'table') {
|
|
81
|
+
if (!block.content.trim())
|
|
82
|
+
return null;
|
|
83
|
+
return _jsx(Table, { data: block.content }, index);
|
|
84
|
+
}
|
|
85
|
+
if (block.type === 'code') {
|
|
86
|
+
return (_jsx(Box, { marginY: 1, paddingX: 1, borderStyle: "round", borderColor: "gray", children: _jsx(Text, { dimColor: true, children: block.content }) }, index));
|
|
87
|
+
}
|
|
88
|
+
// Text Block
|
|
89
|
+
if (!block.content.trim())
|
|
90
|
+
return null;
|
|
91
|
+
return (_jsx(Box, { marginBottom: 0, children: _jsx(Text, { children: formatMessage(block.content) }) }, index));
|
|
92
|
+
}) }));
|
|
93
|
+
};
|
|
@@ -0,0 +1,275 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
/**
|
|
3
|
+
* Table — Simple table renderer using basic ink components
|
|
4
|
+
*
|
|
5
|
+
* No external table library needed. Renders data as aligned columns.
|
|
6
|
+
*/
|
|
7
|
+
import React, { useEffect, useState } from 'react';
|
|
8
|
+
import { Box, Text, useStdout } from 'ink';
|
|
9
|
+
const graphemeSegmenter = typeof Intl !== 'undefined' && 'Segmenter' in Intl
|
|
10
|
+
? new Intl.Segmenter(undefined, { granularity: 'grapheme' })
|
|
11
|
+
: null;
|
|
12
|
+
const COMBINING_MARK_PATTERN = /\p{Mark}/u;
|
|
13
|
+
const ZERO_WIDTH_PATTERN = /[\u200B-\u200D\uFE0E\uFE0F]/u;
|
|
14
|
+
const DOUBLE_WIDTH_PATTERN = /[\u1100-\u115F\u2329\u232A\u2E80-\uA4CF\uAC00-\uD7A3\uF900-\uFAFF\uFE10-\uFE19\uFE30-\uFE6F\uFF00-\uFF60\uFFE0-\uFFE6\u{1F300}-\u{1FAFF}\u{1F900}-\u{1F9FF}\u{1F1E6}-\u{1F1FF}]/u;
|
|
15
|
+
function splitGraphemes(text) {
|
|
16
|
+
if (!text)
|
|
17
|
+
return [];
|
|
18
|
+
if (graphemeSegmenter) {
|
|
19
|
+
return Array.from(graphemeSegmenter.segment(text), (segment) => segment.segment);
|
|
20
|
+
}
|
|
21
|
+
return Array.from(text);
|
|
22
|
+
}
|
|
23
|
+
function getGraphemeWidth(grapheme) {
|
|
24
|
+
if (!grapheme)
|
|
25
|
+
return 0;
|
|
26
|
+
if (ZERO_WIDTH_PATTERN.test(grapheme))
|
|
27
|
+
return 0;
|
|
28
|
+
if (COMBINING_MARK_PATTERN.test(grapheme))
|
|
29
|
+
return 0;
|
|
30
|
+
if (/^[\u0000-\u001F\u007F-\u009F]$/.test(grapheme))
|
|
31
|
+
return 0;
|
|
32
|
+
if (DOUBLE_WIDTH_PATTERN.test(grapheme))
|
|
33
|
+
return 2;
|
|
34
|
+
return 1;
|
|
35
|
+
}
|
|
36
|
+
function getTextWidth(text) {
|
|
37
|
+
return splitGraphemes(text).reduce((width, grapheme) => width + getGraphemeWidth(grapheme), 0);
|
|
38
|
+
}
|
|
39
|
+
function takeByDisplayWidth(text, maxWidth) {
|
|
40
|
+
if (maxWidth <= 0) {
|
|
41
|
+
return { slice: '', remainder: text };
|
|
42
|
+
}
|
|
43
|
+
const graphemes = splitGraphemes(text);
|
|
44
|
+
let consumed = 0;
|
|
45
|
+
let width = 0;
|
|
46
|
+
while (consumed < graphemes.length) {
|
|
47
|
+
const nextWidth = getGraphemeWidth(graphemes[consumed]);
|
|
48
|
+
if (width + nextWidth > maxWidth)
|
|
49
|
+
break;
|
|
50
|
+
width += nextWidth;
|
|
51
|
+
consumed++;
|
|
52
|
+
}
|
|
53
|
+
return {
|
|
54
|
+
slice: graphemes.slice(0, consumed).join(''),
|
|
55
|
+
remainder: graphemes.slice(consumed).join(''),
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
function parseInlineMarkdown(text) {
|
|
59
|
+
const segments = [];
|
|
60
|
+
const pattern = /\*\*\*([\s\S]+?)\*\*\*|\*\*([\s\S]+?)\*\*|\*([\s\S]+?)\*/g;
|
|
61
|
+
let lastIndex = 0;
|
|
62
|
+
let match;
|
|
63
|
+
while ((match = pattern.exec(text)) !== null) {
|
|
64
|
+
if (match.index > lastIndex) {
|
|
65
|
+
segments.push({ text: text.slice(lastIndex, match.index) });
|
|
66
|
+
}
|
|
67
|
+
if (match[1] !== undefined) {
|
|
68
|
+
segments.push({ text: match[1], bold: true, italic: true });
|
|
69
|
+
}
|
|
70
|
+
else if (match[2] !== undefined) {
|
|
71
|
+
segments.push({ text: match[2], bold: true });
|
|
72
|
+
}
|
|
73
|
+
else if (match[3] !== undefined) {
|
|
74
|
+
segments.push({ text: match[3], italic: true });
|
|
75
|
+
}
|
|
76
|
+
lastIndex = pattern.lastIndex;
|
|
77
|
+
}
|
|
78
|
+
if (lastIndex < text.length) {
|
|
79
|
+
segments.push({ text: text.slice(lastIndex) });
|
|
80
|
+
}
|
|
81
|
+
return segments.length > 0 ? segments : [{ text }];
|
|
82
|
+
}
|
|
83
|
+
function getDisplayWidth(text) {
|
|
84
|
+
return text
|
|
85
|
+
.split('\n')
|
|
86
|
+
.reduce((maxWidth, line) => {
|
|
87
|
+
const lineWidth = parseInlineMarkdown(line)
|
|
88
|
+
.reduce((width, segment) => width + getTextWidth(segment.text), 0);
|
|
89
|
+
return Math.max(maxWidth, lineWidth);
|
|
90
|
+
}, 0);
|
|
91
|
+
}
|
|
92
|
+
function wrapStyledText(text, width) {
|
|
93
|
+
if (width <= 0) {
|
|
94
|
+
return [[{ text }]];
|
|
95
|
+
}
|
|
96
|
+
const lines = [];
|
|
97
|
+
const paragraphs = String(text).split('\n');
|
|
98
|
+
for (const paragraph of paragraphs) {
|
|
99
|
+
if (paragraph.trim() === '') {
|
|
100
|
+
lines.push([]);
|
|
101
|
+
continue;
|
|
102
|
+
}
|
|
103
|
+
const words = parseInlineMarkdown(paragraph).flatMap((segment) => segment.text
|
|
104
|
+
.split(/\s+/)
|
|
105
|
+
.filter(Boolean)
|
|
106
|
+
.map((word) => ({
|
|
107
|
+
text: word,
|
|
108
|
+
bold: segment.bold,
|
|
109
|
+
italic: segment.italic,
|
|
110
|
+
})));
|
|
111
|
+
let currentLine = [];
|
|
112
|
+
let currentLength = 0;
|
|
113
|
+
const pushCurrentLine = () => {
|
|
114
|
+
lines.push(currentLine);
|
|
115
|
+
currentLine = [];
|
|
116
|
+
currentLength = 0;
|
|
117
|
+
};
|
|
118
|
+
for (const word of words) {
|
|
119
|
+
let remainingWord = word.text;
|
|
120
|
+
while (remainingWord.length > 0) {
|
|
121
|
+
const availableWidth = currentLength === 0 ? width : width - currentLength - 1;
|
|
122
|
+
const remainingWidth = getTextWidth(remainingWord);
|
|
123
|
+
if (remainingWidth <= availableWidth) {
|
|
124
|
+
if (currentLength > 0) {
|
|
125
|
+
currentLine.push({ text: ' ' });
|
|
126
|
+
currentLength++;
|
|
127
|
+
}
|
|
128
|
+
currentLine.push({ ...word, text: remainingWord });
|
|
129
|
+
currentLength += remainingWidth;
|
|
130
|
+
remainingWord = '';
|
|
131
|
+
continue;
|
|
132
|
+
}
|
|
133
|
+
if (currentLength > 0) {
|
|
134
|
+
pushCurrentLine();
|
|
135
|
+
continue;
|
|
136
|
+
}
|
|
137
|
+
const { slice, remainder } = takeByDisplayWidth(remainingWord, width);
|
|
138
|
+
currentLine.push({ ...word, text: slice });
|
|
139
|
+
remainingWord = remainder;
|
|
140
|
+
pushCurrentLine();
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
if (currentLine.length > 0) {
|
|
144
|
+
pushCurrentLine();
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
return lines.length === 0 ? [[{ text: '' }]] : lines;
|
|
148
|
+
}
|
|
149
|
+
function getLineWidth(segments) {
|
|
150
|
+
return segments.reduce((width, segment) => width + getTextWidth(segment.text), 0);
|
|
151
|
+
}
|
|
152
|
+
/**
|
|
153
|
+
* Simple parser for Markdown tables.
|
|
154
|
+
*/
|
|
155
|
+
function parseMarkdownTable(markdown) {
|
|
156
|
+
const lines = markdown.trim().split('\n');
|
|
157
|
+
if (lines.length < 3)
|
|
158
|
+
return null;
|
|
159
|
+
const hasPipes = lines[0].includes('|');
|
|
160
|
+
const hasSeparator = lines[1].includes('|') && lines[1].includes('-');
|
|
161
|
+
if (!hasPipes || !hasSeparator)
|
|
162
|
+
return null;
|
|
163
|
+
try {
|
|
164
|
+
const parseRow = (row) => row.split('|')
|
|
165
|
+
.map(cell => cell.trim())
|
|
166
|
+
.filter((cell, index, array) => {
|
|
167
|
+
if (index === 0 && cell === '')
|
|
168
|
+
return false;
|
|
169
|
+
if (index === array.length - 1 && cell === '')
|
|
170
|
+
return false;
|
|
171
|
+
return true;
|
|
172
|
+
});
|
|
173
|
+
const headers = parseRow(lines[0]);
|
|
174
|
+
const rows = lines.slice(2).map(parseRow);
|
|
175
|
+
return rows.map(row => {
|
|
176
|
+
const obj = {};
|
|
177
|
+
headers.forEach((header, i) => {
|
|
178
|
+
obj[header || `Column ${i + 1}`] = row[i] || '';
|
|
179
|
+
});
|
|
180
|
+
return obj;
|
|
181
|
+
});
|
|
182
|
+
}
|
|
183
|
+
catch (e) {
|
|
184
|
+
return null;
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
/**
|
|
188
|
+
* Normalize input data into an array of objects for display.
|
|
189
|
+
*/
|
|
190
|
+
function normalizeData(data) {
|
|
191
|
+
let processedData = data;
|
|
192
|
+
if (typeof data === 'string') {
|
|
193
|
+
try {
|
|
194
|
+
const parsed = JSON.parse(data);
|
|
195
|
+
if (typeof parsed === 'object' && parsed !== null) {
|
|
196
|
+
processedData = parsed;
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
catch (e) {
|
|
200
|
+
const parsedMarkdown = parseMarkdownTable(data);
|
|
201
|
+
if (parsedMarkdown) {
|
|
202
|
+
processedData = parsedMarkdown;
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
if (Array.isArray(processedData)) {
|
|
207
|
+
return processedData.map(item => typeof item === 'object' && item !== null ? item : { value: String(item) });
|
|
208
|
+
}
|
|
209
|
+
else if (typeof processedData === 'object' && processedData !== null) {
|
|
210
|
+
return Object.entries(processedData).map(([key, value]) => ({
|
|
211
|
+
property: key,
|
|
212
|
+
value: typeof value === 'object' ? JSON.stringify(value) : String(value),
|
|
213
|
+
}));
|
|
214
|
+
}
|
|
215
|
+
return [{ value: String(processedData) }];
|
|
216
|
+
}
|
|
217
|
+
export const Table = ({ data, title, titleColor = 'cyan' }) => {
|
|
218
|
+
const { stdout } = useStdout();
|
|
219
|
+
const [terminalWidth, setTerminalWidth] = useState(stdout?.columns ?? 80);
|
|
220
|
+
useEffect(() => {
|
|
221
|
+
if (!stdout)
|
|
222
|
+
return;
|
|
223
|
+
const updateWidth = () => {
|
|
224
|
+
setTerminalWidth(stdout.columns ?? 80);
|
|
225
|
+
};
|
|
226
|
+
updateWidth();
|
|
227
|
+
stdout.on('resize', updateWidth);
|
|
228
|
+
return () => {
|
|
229
|
+
stdout.off('resize', updateWidth);
|
|
230
|
+
};
|
|
231
|
+
}, [stdout]);
|
|
232
|
+
const displayData = normalizeData(data);
|
|
233
|
+
if (displayData.length === 0) {
|
|
234
|
+
return (_jsx(Box, { marginY: 1, children: _jsx(Text, { italic: true, dimColor: true, children: "No data to display in table." }) }));
|
|
235
|
+
}
|
|
236
|
+
// Get all column keys
|
|
237
|
+
const columns = Array.from(new Set(displayData.flatMap(row => Object.keys(row))));
|
|
238
|
+
// Calculate column widths
|
|
239
|
+
const colWidths = columns.map(col => {
|
|
240
|
+
const headerLen = getDisplayWidth(String(col));
|
|
241
|
+
const maxCellLen = Math.max(...displayData.map(row => getDisplayWidth(String(row[col] ?? ''))));
|
|
242
|
+
return Math.max(headerLen, Math.min(maxCellLen, 100)) + 2;
|
|
243
|
+
});
|
|
244
|
+
// Adjust widths to fit terminal
|
|
245
|
+
let currentTotal = colWidths.reduce((a, b) => a + b, 0) + columns.length + 1;
|
|
246
|
+
const targetTotal = Math.max(40, terminalWidth - 2);
|
|
247
|
+
if (currentTotal > targetTotal) {
|
|
248
|
+
while (currentTotal > targetTotal) {
|
|
249
|
+
const widestIdx = colWidths.reduce((best, cur, idx) => cur > colWidths[best] ? idx : best, 0);
|
|
250
|
+
if (colWidths[widestIdx] <= 10)
|
|
251
|
+
break;
|
|
252
|
+
colWidths[widestIdx]--;
|
|
253
|
+
currentTotal--;
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
// Box-drawing lines
|
|
257
|
+
const topBorder = '┌' + colWidths.map(w => '─'.repeat(w)).join('┬') + '┐';
|
|
258
|
+
const headerSep = '├' + colWidths.map(w => '─'.repeat(w)).join('┼') + '┤';
|
|
259
|
+
const rowSep = '├' + colWidths.map(w => '─'.repeat(w)).join('┼') + '┤';
|
|
260
|
+
const bottomBorder = '└' + colWidths.map(w => '─'.repeat(w)).join('┴') + '┘';
|
|
261
|
+
const renderRowLines = (cells, rowKey, isHeader = false) => {
|
|
262
|
+
const wrappedCells = cells.map((cell, i) => wrapStyledText(cell, colWidths[i] - 2));
|
|
263
|
+
const maxLines = Math.max(...wrappedCells.map(c => c.length));
|
|
264
|
+
const lines = [];
|
|
265
|
+
for (let lineIdx = 0; lineIdx < maxLines; lineIdx++) {
|
|
266
|
+
lines.push(_jsxs(Text, { bold: isHeader, children: ['│', wrappedCells.map((wrappedCell, i) => {
|
|
267
|
+
const segments = wrappedCell[lineIdx] ?? [];
|
|
268
|
+
const padding = Math.max(0, colWidths[i] - 1 - getLineWidth(segments));
|
|
269
|
+
return (_jsxs(React.Fragment, { children: [' ', segments.map((segment, segmentIdx) => (_jsx(Text, { bold: isHeader || segment.bold, italic: segment.italic, children: segment.text }, `${rowKey}-${lineIdx}-${i}-${segmentIdx}`))), ' '.repeat(padding), '│'] }, `${rowKey}-${lineIdx}-${i}`));
|
|
270
|
+
})] }, `${rowKey}-${lineIdx}`));
|
|
271
|
+
}
|
|
272
|
+
return lines;
|
|
273
|
+
};
|
|
274
|
+
return (_jsxs(Box, { flexDirection: "column", marginY: 1, children: [title && (_jsx(Box, { marginBottom: 1, children: _jsx(Text, { color: titleColor, bold: true, underline: true, children: title }) })), _jsx(Text, { children: topBorder }), renderRowLines(columns.map(String), 'header', true), _jsx(Text, { children: headerSep }), displayData.map((row, rowIdx) => (_jsxs(React.Fragment, { children: [rowIdx > 0 && _jsx(Text, { dimColor: true, children: rowSep }), renderRowLines(columns.map(col => String(row[col] ?? '')), `row-${rowIdx}`)] }, rowIdx))), _jsx(Text, { children: bottomBorder })] }));
|
|
275
|
+
};
|