@kirosnn/mosaic 0.0.7
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/.mosaic/mosaic.local.jsonc +0 -0
- package/MOSAIC.md +188 -0
- package/README.md +127 -0
- package/docs/mosaic.png +0 -0
- package/package.json +42 -0
- package/src/agent/Agent.ts +131 -0
- package/src/agent/context.ts +96 -0
- package/src/agent/index.ts +2 -0
- package/src/agent/prompts/systemPrompt.ts +138 -0
- package/src/agent/prompts/toolsPrompt.ts +139 -0
- package/src/agent/provider/anthropic.ts +122 -0
- package/src/agent/provider/google.ts +124 -0
- package/src/agent/provider/mistral.ts +117 -0
- package/src/agent/provider/ollama.ts +531 -0
- package/src/agent/provider/openai.ts +220 -0
- package/src/agent/provider/xai.ts +122 -0
- package/src/agent/tools/bash.ts +20 -0
- package/src/agent/tools/definitions.ts +27 -0
- package/src/agent/tools/edit.ts +23 -0
- package/src/agent/tools/executor.ts +751 -0
- package/src/agent/tools/explore.ts +18 -0
- package/src/agent/tools/exploreExecutor.ts +320 -0
- package/src/agent/tools/glob.ts +16 -0
- package/src/agent/tools/grep.ts +19 -0
- package/src/agent/tools/index.ts +4 -0
- package/src/agent/tools/list.ts +20 -0
- package/src/agent/tools/question.ts +20 -0
- package/src/agent/tools/read.ts +15 -0
- package/src/agent/tools/write.ts +21 -0
- package/src/agent/types.ts +155 -0
- package/src/components/App.tsx +174 -0
- package/src/components/CommandsModal.tsx +77 -0
- package/src/components/CustomInput.tsx +328 -0
- package/src/components/Main.tsx +1112 -0
- package/src/components/Notification.tsx +91 -0
- package/src/components/SelectList.tsx +47 -0
- package/src/components/Setup.tsx +528 -0
- package/src/components/ShortcutsModal.tsx +67 -0
- package/src/components/Welcome.tsx +39 -0
- package/src/components/main/ApprovalPanel.tsx +134 -0
- package/src/components/main/ChatPage.tsx +516 -0
- package/src/components/main/HomePage.tsx +111 -0
- package/src/components/main/QuestionPanel.tsx +85 -0
- package/src/components/main/ThinkingIndicator.tsx +101 -0
- package/src/components/main/types.ts +55 -0
- package/src/components/main/wrapText.ts +41 -0
- package/src/index.tsx +212 -0
- package/src/utils/approvalBridge.ts +129 -0
- package/src/utils/commands/echo.ts +22 -0
- package/src/utils/commands/help.ts +25 -0
- package/src/utils/commands/index.ts +68 -0
- package/src/utils/commands/init.ts +68 -0
- package/src/utils/commands/redo.ts +74 -0
- package/src/utils/commands/registry.ts +29 -0
- package/src/utils/commands/sessions.ts +129 -0
- package/src/utils/commands/types.ts +20 -0
- package/src/utils/commands/undo.ts +75 -0
- package/src/utils/commands/web.ts +77 -0
- package/src/utils/config.ts +357 -0
- package/src/utils/diff.ts +201 -0
- package/src/utils/diffRendering.tsx +62 -0
- package/src/utils/exploreBridge.ts +87 -0
- package/src/utils/fileChangeTracker.ts +98 -0
- package/src/utils/fileChangesBridge.ts +18 -0
- package/src/utils/history.ts +106 -0
- package/src/utils/markdown.tsx +232 -0
- package/src/utils/models.ts +304 -0
- package/src/utils/questionBridge.ts +122 -0
- package/src/utils/terminalUtils.ts +25 -0
- package/src/utils/toolFormatting.ts +384 -0
- package/src/utils/undoRedo.ts +429 -0
- package/src/utils/undoRedoBridge.ts +45 -0
- package/src/utils/undoRedoDb.ts +338 -0
- package/src/utils/uninstall.ts +45 -0
- package/src/utils/version.ts +3 -0
- package/src/web/app.tsx +606 -0
- package/src/web/assets/css/ChatPage.css +212 -0
- package/src/web/assets/css/FileExplorer.css +202 -0
- package/src/web/assets/css/HomePage.css +119 -0
- package/src/web/assets/css/Markdown.css +178 -0
- package/src/web/assets/css/MessageItem.css +160 -0
- package/src/web/assets/css/Sidebar.css +208 -0
- package/src/web/assets/css/SidebarModal.css +137 -0
- package/src/web/assets/css/ThinkingIndicator.css +47 -0
- package/src/web/assets/css/ToolMessage.css +148 -0
- package/src/web/assets/css/global.css +226 -0
- package/src/web/assets/fonts/Geist-Black.woff2 +0 -0
- package/src/web/assets/fonts/Geist-BlackItalic.woff2 +0 -0
- package/src/web/assets/fonts/Geist-Bold.woff2 +0 -0
- package/src/web/assets/fonts/Geist-BoldItalic.woff2 +0 -0
- package/src/web/assets/fonts/Geist-ExtraBold.woff2 +0 -0
- package/src/web/assets/fonts/Geist-ExtraBoldItalic.woff2 +0 -0
- package/src/web/assets/fonts/Geist-ExtraLight.woff2 +0 -0
- package/src/web/assets/fonts/Geist-ExtraLightItalic.woff2 +0 -0
- package/src/web/assets/fonts/Geist-Italic[wght].woff2 +0 -0
- package/src/web/assets/fonts/Geist-Light.woff2 +0 -0
- package/src/web/assets/fonts/Geist-LightItalic.woff2 +0 -0
- package/src/web/assets/fonts/Geist-Medium.woff2 +0 -0
- package/src/web/assets/fonts/Geist-MediumItalic.woff2 +0 -0
- package/src/web/assets/fonts/Geist-Regular.woff2 +0 -0
- package/src/web/assets/fonts/Geist-RegularItalic.woff2 +0 -0
- package/src/web/assets/fonts/Geist-SemiBold.woff2 +0 -0
- package/src/web/assets/fonts/Geist-SemiBoldItalic.woff2 +0 -0
- package/src/web/assets/fonts/Geist-Thin.woff2 +0 -0
- package/src/web/assets/fonts/Geist-ThinItalic.woff2 +0 -0
- package/src/web/assets/fonts/GeistMono-Black.woff2 +0 -0
- package/src/web/assets/fonts/GeistMono-BlackItalic.woff2 +0 -0
- package/src/web/assets/fonts/GeistMono-Bold.woff2 +0 -0
- package/src/web/assets/fonts/GeistMono-BoldItalic.woff2 +0 -0
- package/src/web/assets/fonts/GeistMono-ExtraBold.woff2 +0 -0
- package/src/web/assets/fonts/GeistMono-ExtraBoldItalic.woff2 +0 -0
- package/src/web/assets/fonts/GeistMono-ExtraLight.woff2 +0 -0
- package/src/web/assets/fonts/GeistMono-ExtraLightItalic.woff2 +0 -0
- package/src/web/assets/fonts/GeistMono-Italic.woff2 +0 -0
- package/src/web/assets/fonts/GeistMono-Italic[wght].woff2 +0 -0
- package/src/web/assets/fonts/GeistMono-Light.woff2 +0 -0
- package/src/web/assets/fonts/GeistMono-LightItalic.woff2 +0 -0
- package/src/web/assets/fonts/GeistMono-Medium.woff2 +0 -0
- package/src/web/assets/fonts/GeistMono-MediumItalic.woff2 +0 -0
- package/src/web/assets/fonts/GeistMono-Regular.woff2 +0 -0
- package/src/web/assets/fonts/GeistMono-SemiBold.woff2 +0 -0
- package/src/web/assets/fonts/GeistMono-SemiBoldItalic.woff2 +0 -0
- package/src/web/assets/fonts/GeistMono-Thin.woff2 +0 -0
- package/src/web/assets/fonts/GeistMono-ThinItalic.woff2 +0 -0
- package/src/web/assets/fonts/GeistMono[wght].woff2 +0 -0
- package/src/web/assets/fonts/Geist[wght].woff2 +0 -0
- package/src/web/assets/fonts/blauer-nue-regular.woff2 +0 -0
- package/src/web/assets/fonts/neue-montreal-regular.woff2 +0 -0
- package/src/web/assets/images/favicon-v2.svg +6 -0
- package/src/web/assets/images/favicon.png +0 -0
- package/src/web/assets/images/foruse.svg +5 -0
- package/src/web/assets/images/logo_black.svg +5 -0
- package/src/web/assets/images/logo_white.svg +5 -0
- package/src/web/assets/images/logoblack.png +0 -0
- package/src/web/assets/images/logowhite.png +0 -0
- package/src/web/build.ts +23 -0
- package/src/web/components/ApprovalPanel.tsx +191 -0
- package/src/web/components/ChatPage.tsx +273 -0
- package/src/web/components/FileExplorer.tsx +162 -0
- package/src/web/components/HomePage.tsx +121 -0
- package/src/web/components/MessageItem.tsx +178 -0
- package/src/web/components/Modal.tsx +30 -0
- package/src/web/components/QuestionPanel.tsx +149 -0
- package/src/web/components/Setup.tsx +211 -0
- package/src/web/components/Sidebar.tsx +292 -0
- package/src/web/components/ThinkingIndicator.tsx +85 -0
- package/src/web/logo_black.svg +5 -0
- package/src/web/logo_white.svg +5 -0
- package/src/web/router.ts +46 -0
- package/src/web/server.tsx +662 -0
- package/src/web/storage.ts +92 -0
- package/src/web/types.ts +17 -0
- package/src/web/utils.ts +61 -0
- package/tsconfig.json +33 -0
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
import { notifyFileChanges } from './fileChangesBridge';
|
|
2
|
+
|
|
3
|
+
export interface FileChanges {
|
|
4
|
+
linesAdded: number;
|
|
5
|
+
linesRemoved: number;
|
|
6
|
+
filesModified: number;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
interface FileState {
|
|
10
|
+
path: string;
|
|
11
|
+
lineCount: number;
|
|
12
|
+
existed: boolean;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
let currentChanges: FileChanges = {
|
|
16
|
+
linesAdded: 0,
|
|
17
|
+
linesRemoved: 0,
|
|
18
|
+
filesModified: 0
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
let trackedFiles: Map<string, FileState> = new Map();
|
|
22
|
+
|
|
23
|
+
export function getFileChanges(): FileChanges {
|
|
24
|
+
return { ...currentChanges };
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export function resetFileChanges(): void {
|
|
28
|
+
currentChanges = {
|
|
29
|
+
linesAdded: 0,
|
|
30
|
+
linesRemoved: 0,
|
|
31
|
+
filesModified: 0
|
|
32
|
+
};
|
|
33
|
+
trackedFiles.clear();
|
|
34
|
+
notifyFileChanges(currentChanges);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export function updateFileChangesFromGit(): void {
|
|
38
|
+
notifyFileChanges(currentChanges);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export function trackFileChange(filePath: string, oldContent: string, newContent: string): void {
|
|
42
|
+
const oldLines = oldContent.split('\n');
|
|
43
|
+
const newLines = newContent.split('\n');
|
|
44
|
+
|
|
45
|
+
const oldExisted = oldContent.length > 0;
|
|
46
|
+
const newExists = newContent.length > 0;
|
|
47
|
+
|
|
48
|
+
const tracked = trackedFiles.get(filePath);
|
|
49
|
+
|
|
50
|
+
if (tracked) {
|
|
51
|
+
currentChanges.linesAdded -= Math.max(0, tracked.lineCount);
|
|
52
|
+
currentChanges.linesRemoved -= Math.max(0, tracked.lineCount);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
let added = 0;
|
|
56
|
+
let removed = 0;
|
|
57
|
+
|
|
58
|
+
if (!oldExisted && newExists) {
|
|
59
|
+
added = newLines.length;
|
|
60
|
+
} else if (oldExisted && !newExists) {
|
|
61
|
+
removed = oldLines.length;
|
|
62
|
+
} else {
|
|
63
|
+
const maxLength = Math.max(oldLines.length, newLines.length);
|
|
64
|
+
for (let i = 0; i < maxLength; i++) {
|
|
65
|
+
const oldLine = oldLines[i];
|
|
66
|
+
const newLine = newLines[i];
|
|
67
|
+
|
|
68
|
+
if (oldLine === undefined && newLine !== undefined) {
|
|
69
|
+
added++;
|
|
70
|
+
} else if (oldLine !== undefined && newLine === undefined) {
|
|
71
|
+
removed++;
|
|
72
|
+
} else if (oldLine !== newLine) {
|
|
73
|
+
added++;
|
|
74
|
+
removed++;
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
trackedFiles.set(filePath, {
|
|
80
|
+
path: filePath,
|
|
81
|
+
lineCount: Math.max(added, removed),
|
|
82
|
+
existed: newExists
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
currentChanges.linesAdded += added;
|
|
86
|
+
currentChanges.linesRemoved += removed;
|
|
87
|
+
currentChanges.filesModified = trackedFiles.size;
|
|
88
|
+
|
|
89
|
+
notifyFileChanges(currentChanges);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
export function trackFileCreated(filePath: string, content: string): void {
|
|
93
|
+
trackFileChange(filePath, '', content);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
export function trackFileDeleted(filePath: string, oldContent: string): void {
|
|
97
|
+
trackFileChange(filePath, oldContent, '');
|
|
98
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import type { FileChanges } from './fileChangeTracker';
|
|
2
|
+
|
|
3
|
+
type FileChangesCallback = (changes: FileChanges) => void;
|
|
4
|
+
|
|
5
|
+
let callback: FileChangesCallback | null = null;
|
|
6
|
+
|
|
7
|
+
export function subscribeFileChanges(cb: FileChangesCallback): () => void {
|
|
8
|
+
callback = cb;
|
|
9
|
+
return () => {
|
|
10
|
+
callback = null;
|
|
11
|
+
};
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export function notifyFileChanges(changes: FileChanges): void {
|
|
15
|
+
if (callback) {
|
|
16
|
+
callback(changes);
|
|
17
|
+
}
|
|
18
|
+
}
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
import { existsSync, mkdirSync, writeFileSync, readdirSync, readFileSync } from 'fs';
|
|
2
|
+
import { join } from 'path';
|
|
3
|
+
import { homedir } from 'os';
|
|
4
|
+
|
|
5
|
+
export interface ConversationStep {
|
|
6
|
+
type: 'user' | 'assistant' | 'tool';
|
|
7
|
+
content: string;
|
|
8
|
+
toolName?: string;
|
|
9
|
+
toolArgs?: Record<string, unknown>;
|
|
10
|
+
toolResult?: unknown;
|
|
11
|
+
timestamp: number;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export interface ConversationHistory {
|
|
15
|
+
id: string;
|
|
16
|
+
timestamp: number;
|
|
17
|
+
steps: ConversationStep[];
|
|
18
|
+
totalSteps: number;
|
|
19
|
+
totalTokens?: {
|
|
20
|
+
prompt: number;
|
|
21
|
+
completion: number;
|
|
22
|
+
total: number;
|
|
23
|
+
};
|
|
24
|
+
model?: string;
|
|
25
|
+
provider?: string;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export function getHistoryDir(): string {
|
|
29
|
+
const configDir = join(homedir(), '.mosaic');
|
|
30
|
+
const historyDir = join(configDir, 'history');
|
|
31
|
+
|
|
32
|
+
if (!existsSync(historyDir)) {
|
|
33
|
+
mkdirSync(historyDir, { recursive: true });
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
return historyDir;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export function saveConversation(conversation: ConversationHistory): void {
|
|
40
|
+
const historyDir = getHistoryDir();
|
|
41
|
+
const filename = `${conversation.id}.json`;
|
|
42
|
+
const filepath = join(historyDir, filename);
|
|
43
|
+
|
|
44
|
+
writeFileSync(filepath, JSON.stringify(conversation, null, 2), 'utf-8');
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export function loadConversations(): ConversationHistory[] {
|
|
48
|
+
const historyDir = getHistoryDir();
|
|
49
|
+
|
|
50
|
+
if (!existsSync(historyDir)) {
|
|
51
|
+
return [];
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
const files = readdirSync(historyDir).filter(f => f.endsWith('.json'));
|
|
55
|
+
const conversations: ConversationHistory[] = [];
|
|
56
|
+
|
|
57
|
+
for (const file of files) {
|
|
58
|
+
try {
|
|
59
|
+
const content = readFileSync(join(historyDir, file), 'utf-8');
|
|
60
|
+
conversations.push(JSON.parse(content));
|
|
61
|
+
} catch (error) {
|
|
62
|
+
console.error(`Failed to load ${file}:`, error);
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
return conversations.sort((a, b) => b.timestamp - a.timestamp);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
export function getInputHistory(): string[] {
|
|
70
|
+
const historyDir = getHistoryDir();
|
|
71
|
+
const inputHistoryPath = join(historyDir, 'inputs.json');
|
|
72
|
+
|
|
73
|
+
if (!existsSync(inputHistoryPath)) {
|
|
74
|
+
return [];
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
try {
|
|
78
|
+
const content = readFileSync(inputHistoryPath, 'utf-8');
|
|
79
|
+
return JSON.parse(content);
|
|
80
|
+
} catch (error) {
|
|
81
|
+
return [];
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
export function saveInputHistory(history: string[]): void {
|
|
86
|
+
const historyDir = getHistoryDir();
|
|
87
|
+
const inputHistoryPath = join(historyDir, 'inputs.json');
|
|
88
|
+
|
|
89
|
+
writeFileSync(inputHistoryPath, JSON.stringify(history, null, 2), 'utf-8');
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
export function addInputToHistory(input: string): void {
|
|
93
|
+
if (!input.trim()) return;
|
|
94
|
+
|
|
95
|
+
const history = getInputHistory();
|
|
96
|
+
|
|
97
|
+
if (history[history.length - 1] !== input) {
|
|
98
|
+
history.push(input);
|
|
99
|
+
|
|
100
|
+
if (history.length > 100) {
|
|
101
|
+
history.shift();
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
saveInputHistory(history);
|
|
105
|
+
}
|
|
106
|
+
}
|
|
@@ -0,0 +1,232 @@
|
|
|
1
|
+
import { TextAttributes } from "@opentui/core";
|
|
2
|
+
|
|
3
|
+
export interface MarkdownSegment {
|
|
4
|
+
type: 'text' | 'bold' | 'italic' | 'code' | 'heading' | 'listitem';
|
|
5
|
+
content: string;
|
|
6
|
+
level?: number;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
function parseInline(text: string): MarkdownSegment[] {
|
|
10
|
+
const segments: MarkdownSegment[] = [];
|
|
11
|
+
|
|
12
|
+
let i = 0;
|
|
13
|
+
let buffer = '';
|
|
14
|
+
|
|
15
|
+
const flushText = () => {
|
|
16
|
+
if (buffer) {
|
|
17
|
+
segments.push({ type: 'text', content: buffer });
|
|
18
|
+
buffer = '';
|
|
19
|
+
}
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
while (i < text.length) {
|
|
23
|
+
if (text[i] === '`') {
|
|
24
|
+
const j = text.indexOf('`', i + 1);
|
|
25
|
+
if (j !== -1) {
|
|
26
|
+
flushText();
|
|
27
|
+
segments.push({ type: 'code', content: text.substring(i + 1, j) });
|
|
28
|
+
i = j + 1;
|
|
29
|
+
continue;
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
if (text.substring(i, i + 2) === '**') {
|
|
34
|
+
const j = text.indexOf('**', i + 2);
|
|
35
|
+
if (j !== -1) {
|
|
36
|
+
flushText();
|
|
37
|
+
segments.push({ type: 'bold', content: text.substring(i + 2, j) });
|
|
38
|
+
i = j + 2;
|
|
39
|
+
continue;
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
if (text[i] === '*' && text.substring(i, i + 2) !== '**') {
|
|
44
|
+
const j = text.indexOf('*', i + 1);
|
|
45
|
+
if (j !== -1) {
|
|
46
|
+
flushText();
|
|
47
|
+
segments.push({ type: 'italic', content: text.substring(i + 1, j) });
|
|
48
|
+
i = j + 1;
|
|
49
|
+
continue;
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
buffer += text[i];
|
|
54
|
+
i++;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
flushText();
|
|
58
|
+
return segments.length > 0 ? segments : [{ type: 'text', content: text }];
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export function parseMarkdownLine(line: string): MarkdownSegment[] {
|
|
62
|
+
if (line.match(/^#{1,6}\s/)) {
|
|
63
|
+
const match = line.match(/^(#{1,6})\s+(.+)$/);
|
|
64
|
+
if (match && match[2]) {
|
|
65
|
+
return [{ type: 'heading', content: match[2], level: match[1]?.length || 1 }];
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
if (line.match(/^[-*+]\s/)) {
|
|
70
|
+
const content = line.replace(/^[-*+]\s+/, '');
|
|
71
|
+
return [{ type: 'text', content: '• ' }, ...parseInline(content)];
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
return parseInline(line);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
export function renderMarkdownSegment(segment: MarkdownSegment, key: number) {
|
|
78
|
+
switch (segment.type) {
|
|
79
|
+
case 'bold':
|
|
80
|
+
return <text key={key} fg="white" attributes={TextAttributes.BOLD}>{segment.content}</text>;
|
|
81
|
+
|
|
82
|
+
case 'italic':
|
|
83
|
+
return <text key={key} fg="white" attributes={TextAttributes.DIM}>{segment.content}</text>;
|
|
84
|
+
|
|
85
|
+
case 'code':
|
|
86
|
+
return <text key={key} fg="#ffdd80">{`${segment.content}`}</text>;
|
|
87
|
+
|
|
88
|
+
case 'heading':
|
|
89
|
+
return <text key={key} fg="#ffca38" attributes={TextAttributes.BOLD}>{segment.content}</text>;
|
|
90
|
+
|
|
91
|
+
case 'listitem':
|
|
92
|
+
return (
|
|
93
|
+
<box key={key} flexDirection="row">
|
|
94
|
+
<text fg="#ffca38">• </text>
|
|
95
|
+
<text>{segment.content}</text>
|
|
96
|
+
</box>
|
|
97
|
+
);
|
|
98
|
+
|
|
99
|
+
case 'text':
|
|
100
|
+
default:
|
|
101
|
+
return <text key={key} fg="white">{segment.content}</text>;
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
export interface ParsedMarkdownLine {
|
|
106
|
+
segments: MarkdownSegment[];
|
|
107
|
+
rawLine: string;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
export function parseMarkdownContent(content: string): ParsedMarkdownLine[] {
|
|
111
|
+
const lines = content.split('\n');
|
|
112
|
+
const result: ParsedMarkdownLine[] = [];
|
|
113
|
+
|
|
114
|
+
for (const line of lines) {
|
|
115
|
+
result.push({
|
|
116
|
+
segments: parseMarkdownLine(line),
|
|
117
|
+
rawLine: line
|
|
118
|
+
});
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
return result;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
export function wrapMarkdownText(text: string, maxWidth: number): { text: string; segments: MarkdownSegment[] }[] {
|
|
125
|
+
if (!text || maxWidth <= 0) return [{ text: '', segments: [] }];
|
|
126
|
+
|
|
127
|
+
const segments = parseMarkdownLine(text);
|
|
128
|
+
const lines: { text: string; segments: MarkdownSegment[] }[] = [];
|
|
129
|
+
let currentLine = '';
|
|
130
|
+
let currentSegments: MarkdownSegment[] = [];
|
|
131
|
+
|
|
132
|
+
for (const segment of segments) {
|
|
133
|
+
const content = segment.content;
|
|
134
|
+
const fullText = content;
|
|
135
|
+
|
|
136
|
+
if (!currentLine) {
|
|
137
|
+
if (fullText.length <= maxWidth) {
|
|
138
|
+
currentLine = fullText;
|
|
139
|
+
currentSegments.push(segment);
|
|
140
|
+
} else {
|
|
141
|
+
let remaining = content;
|
|
142
|
+
while (remaining) {
|
|
143
|
+
if (remaining.length <= maxWidth) {
|
|
144
|
+
currentLine = remaining;
|
|
145
|
+
currentSegments.push({ ...segment, content: remaining });
|
|
146
|
+
remaining = '';
|
|
147
|
+
} else {
|
|
148
|
+
const breakPoint = remaining.lastIndexOf(' ', maxWidth);
|
|
149
|
+
if (breakPoint > 0) {
|
|
150
|
+
const chunk = remaining.slice(0, breakPoint);
|
|
151
|
+
currentLine = chunk;
|
|
152
|
+
currentSegments.push({ ...segment, content: chunk });
|
|
153
|
+
lines.push({ text: currentLine, segments: currentSegments });
|
|
154
|
+
currentLine = '';
|
|
155
|
+
currentSegments = [];
|
|
156
|
+
remaining = remaining.slice(breakPoint + 1);
|
|
157
|
+
} else {
|
|
158
|
+
const chunk = remaining.slice(0, maxWidth);
|
|
159
|
+
currentLine = chunk;
|
|
160
|
+
currentSegments.push({ ...segment, content: chunk });
|
|
161
|
+
lines.push({ text: currentLine, segments: currentSegments });
|
|
162
|
+
currentLine = '';
|
|
163
|
+
currentSegments = [];
|
|
164
|
+
remaining = remaining.slice(maxWidth);
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
} else {
|
|
170
|
+
const needsSpace = !currentLine.endsWith(' ') && !fullText.startsWith(' ');
|
|
171
|
+
const separator = needsSpace ? ' ' : '';
|
|
172
|
+
|
|
173
|
+
if ((currentLine + separator + fullText).length <= maxWidth) {
|
|
174
|
+
currentLine += separator + fullText;
|
|
175
|
+
currentSegments.push(segment);
|
|
176
|
+
} else {
|
|
177
|
+
lines.push({ text: currentLine, segments: currentSegments });
|
|
178
|
+
currentLine = fullText;
|
|
179
|
+
currentSegments = [segment];
|
|
180
|
+
|
|
181
|
+
if (fullText.length > maxWidth) {
|
|
182
|
+
let remaining = content;
|
|
183
|
+
while (remaining) {
|
|
184
|
+
if (remaining.length <= maxWidth) {
|
|
185
|
+
currentLine = remaining;
|
|
186
|
+
currentSegments = [{ ...segment, content: remaining }];
|
|
187
|
+
remaining = '';
|
|
188
|
+
} else {
|
|
189
|
+
const breakPoint = remaining.lastIndexOf(' ', maxWidth);
|
|
190
|
+
if (breakPoint > 0) {
|
|
191
|
+
const chunk = remaining.slice(0, breakPoint);
|
|
192
|
+
currentLine = chunk;
|
|
193
|
+
currentSegments = [{ ...segment, content: chunk }];
|
|
194
|
+
lines.push({ text: currentLine, segments: currentSegments });
|
|
195
|
+
currentLine = '';
|
|
196
|
+
currentSegments = [];
|
|
197
|
+
remaining = remaining.slice(breakPoint + 1);
|
|
198
|
+
} else {
|
|
199
|
+
const chunk = remaining.slice(0, maxWidth);
|
|
200
|
+
currentLine = chunk;
|
|
201
|
+
currentSegments = [{ ...segment, content: chunk }];
|
|
202
|
+
lines.push({ text: currentLine, segments: currentSegments });
|
|
203
|
+
currentLine = '';
|
|
204
|
+
currentSegments = [];
|
|
205
|
+
remaining = remaining.slice(maxWidth);
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
if (currentLine) {
|
|
215
|
+
lines.push({ text: currentLine, segments: currentSegments });
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
return lines.length > 0 ? lines : [{ text: '', segments: [] }];
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
export interface WrappedMarkdownBlock {
|
|
222
|
+
type: 'line';
|
|
223
|
+
wrappedLines?: { text: string; segments: MarkdownSegment[] }[];
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
export function parseAndWrapMarkdown(text: string, maxWidth: number): WrappedMarkdownBlock[] {
|
|
227
|
+
const lines = text.split('\n');
|
|
228
|
+
return lines.map((line) => ({
|
|
229
|
+
type: 'line',
|
|
230
|
+
wrappedLines: wrapMarkdownText(line, maxWidth)
|
|
231
|
+
}));
|
|
232
|
+
}
|