@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.
Files changed (154) hide show
  1. package/.mosaic/mosaic.local.jsonc +0 -0
  2. package/MOSAIC.md +188 -0
  3. package/README.md +127 -0
  4. package/docs/mosaic.png +0 -0
  5. package/package.json +42 -0
  6. package/src/agent/Agent.ts +131 -0
  7. package/src/agent/context.ts +96 -0
  8. package/src/agent/index.ts +2 -0
  9. package/src/agent/prompts/systemPrompt.ts +138 -0
  10. package/src/agent/prompts/toolsPrompt.ts +139 -0
  11. package/src/agent/provider/anthropic.ts +122 -0
  12. package/src/agent/provider/google.ts +124 -0
  13. package/src/agent/provider/mistral.ts +117 -0
  14. package/src/agent/provider/ollama.ts +531 -0
  15. package/src/agent/provider/openai.ts +220 -0
  16. package/src/agent/provider/xai.ts +122 -0
  17. package/src/agent/tools/bash.ts +20 -0
  18. package/src/agent/tools/definitions.ts +27 -0
  19. package/src/agent/tools/edit.ts +23 -0
  20. package/src/agent/tools/executor.ts +751 -0
  21. package/src/agent/tools/explore.ts +18 -0
  22. package/src/agent/tools/exploreExecutor.ts +320 -0
  23. package/src/agent/tools/glob.ts +16 -0
  24. package/src/agent/tools/grep.ts +19 -0
  25. package/src/agent/tools/index.ts +4 -0
  26. package/src/agent/tools/list.ts +20 -0
  27. package/src/agent/tools/question.ts +20 -0
  28. package/src/agent/tools/read.ts +15 -0
  29. package/src/agent/tools/write.ts +21 -0
  30. package/src/agent/types.ts +155 -0
  31. package/src/components/App.tsx +174 -0
  32. package/src/components/CommandsModal.tsx +77 -0
  33. package/src/components/CustomInput.tsx +328 -0
  34. package/src/components/Main.tsx +1112 -0
  35. package/src/components/Notification.tsx +91 -0
  36. package/src/components/SelectList.tsx +47 -0
  37. package/src/components/Setup.tsx +528 -0
  38. package/src/components/ShortcutsModal.tsx +67 -0
  39. package/src/components/Welcome.tsx +39 -0
  40. package/src/components/main/ApprovalPanel.tsx +134 -0
  41. package/src/components/main/ChatPage.tsx +516 -0
  42. package/src/components/main/HomePage.tsx +111 -0
  43. package/src/components/main/QuestionPanel.tsx +85 -0
  44. package/src/components/main/ThinkingIndicator.tsx +101 -0
  45. package/src/components/main/types.ts +55 -0
  46. package/src/components/main/wrapText.ts +41 -0
  47. package/src/index.tsx +212 -0
  48. package/src/utils/approvalBridge.ts +129 -0
  49. package/src/utils/commands/echo.ts +22 -0
  50. package/src/utils/commands/help.ts +25 -0
  51. package/src/utils/commands/index.ts +68 -0
  52. package/src/utils/commands/init.ts +68 -0
  53. package/src/utils/commands/redo.ts +74 -0
  54. package/src/utils/commands/registry.ts +29 -0
  55. package/src/utils/commands/sessions.ts +129 -0
  56. package/src/utils/commands/types.ts +20 -0
  57. package/src/utils/commands/undo.ts +75 -0
  58. package/src/utils/commands/web.ts +77 -0
  59. package/src/utils/config.ts +357 -0
  60. package/src/utils/diff.ts +201 -0
  61. package/src/utils/diffRendering.tsx +62 -0
  62. package/src/utils/exploreBridge.ts +87 -0
  63. package/src/utils/fileChangeTracker.ts +98 -0
  64. package/src/utils/fileChangesBridge.ts +18 -0
  65. package/src/utils/history.ts +106 -0
  66. package/src/utils/markdown.tsx +232 -0
  67. package/src/utils/models.ts +304 -0
  68. package/src/utils/questionBridge.ts +122 -0
  69. package/src/utils/terminalUtils.ts +25 -0
  70. package/src/utils/toolFormatting.ts +384 -0
  71. package/src/utils/undoRedo.ts +429 -0
  72. package/src/utils/undoRedoBridge.ts +45 -0
  73. package/src/utils/undoRedoDb.ts +338 -0
  74. package/src/utils/uninstall.ts +45 -0
  75. package/src/utils/version.ts +3 -0
  76. package/src/web/app.tsx +606 -0
  77. package/src/web/assets/css/ChatPage.css +212 -0
  78. package/src/web/assets/css/FileExplorer.css +202 -0
  79. package/src/web/assets/css/HomePage.css +119 -0
  80. package/src/web/assets/css/Markdown.css +178 -0
  81. package/src/web/assets/css/MessageItem.css +160 -0
  82. package/src/web/assets/css/Sidebar.css +208 -0
  83. package/src/web/assets/css/SidebarModal.css +137 -0
  84. package/src/web/assets/css/ThinkingIndicator.css +47 -0
  85. package/src/web/assets/css/ToolMessage.css +148 -0
  86. package/src/web/assets/css/global.css +226 -0
  87. package/src/web/assets/fonts/Geist-Black.woff2 +0 -0
  88. package/src/web/assets/fonts/Geist-BlackItalic.woff2 +0 -0
  89. package/src/web/assets/fonts/Geist-Bold.woff2 +0 -0
  90. package/src/web/assets/fonts/Geist-BoldItalic.woff2 +0 -0
  91. package/src/web/assets/fonts/Geist-ExtraBold.woff2 +0 -0
  92. package/src/web/assets/fonts/Geist-ExtraBoldItalic.woff2 +0 -0
  93. package/src/web/assets/fonts/Geist-ExtraLight.woff2 +0 -0
  94. package/src/web/assets/fonts/Geist-ExtraLightItalic.woff2 +0 -0
  95. package/src/web/assets/fonts/Geist-Italic[wght].woff2 +0 -0
  96. package/src/web/assets/fonts/Geist-Light.woff2 +0 -0
  97. package/src/web/assets/fonts/Geist-LightItalic.woff2 +0 -0
  98. package/src/web/assets/fonts/Geist-Medium.woff2 +0 -0
  99. package/src/web/assets/fonts/Geist-MediumItalic.woff2 +0 -0
  100. package/src/web/assets/fonts/Geist-Regular.woff2 +0 -0
  101. package/src/web/assets/fonts/Geist-RegularItalic.woff2 +0 -0
  102. package/src/web/assets/fonts/Geist-SemiBold.woff2 +0 -0
  103. package/src/web/assets/fonts/Geist-SemiBoldItalic.woff2 +0 -0
  104. package/src/web/assets/fonts/Geist-Thin.woff2 +0 -0
  105. package/src/web/assets/fonts/Geist-ThinItalic.woff2 +0 -0
  106. package/src/web/assets/fonts/GeistMono-Black.woff2 +0 -0
  107. package/src/web/assets/fonts/GeistMono-BlackItalic.woff2 +0 -0
  108. package/src/web/assets/fonts/GeistMono-Bold.woff2 +0 -0
  109. package/src/web/assets/fonts/GeistMono-BoldItalic.woff2 +0 -0
  110. package/src/web/assets/fonts/GeistMono-ExtraBold.woff2 +0 -0
  111. package/src/web/assets/fonts/GeistMono-ExtraBoldItalic.woff2 +0 -0
  112. package/src/web/assets/fonts/GeistMono-ExtraLight.woff2 +0 -0
  113. package/src/web/assets/fonts/GeistMono-ExtraLightItalic.woff2 +0 -0
  114. package/src/web/assets/fonts/GeistMono-Italic.woff2 +0 -0
  115. package/src/web/assets/fonts/GeistMono-Italic[wght].woff2 +0 -0
  116. package/src/web/assets/fonts/GeistMono-Light.woff2 +0 -0
  117. package/src/web/assets/fonts/GeistMono-LightItalic.woff2 +0 -0
  118. package/src/web/assets/fonts/GeistMono-Medium.woff2 +0 -0
  119. package/src/web/assets/fonts/GeistMono-MediumItalic.woff2 +0 -0
  120. package/src/web/assets/fonts/GeistMono-Regular.woff2 +0 -0
  121. package/src/web/assets/fonts/GeistMono-SemiBold.woff2 +0 -0
  122. package/src/web/assets/fonts/GeistMono-SemiBoldItalic.woff2 +0 -0
  123. package/src/web/assets/fonts/GeistMono-Thin.woff2 +0 -0
  124. package/src/web/assets/fonts/GeistMono-ThinItalic.woff2 +0 -0
  125. package/src/web/assets/fonts/GeistMono[wght].woff2 +0 -0
  126. package/src/web/assets/fonts/Geist[wght].woff2 +0 -0
  127. package/src/web/assets/fonts/blauer-nue-regular.woff2 +0 -0
  128. package/src/web/assets/fonts/neue-montreal-regular.woff2 +0 -0
  129. package/src/web/assets/images/favicon-v2.svg +6 -0
  130. package/src/web/assets/images/favicon.png +0 -0
  131. package/src/web/assets/images/foruse.svg +5 -0
  132. package/src/web/assets/images/logo_black.svg +5 -0
  133. package/src/web/assets/images/logo_white.svg +5 -0
  134. package/src/web/assets/images/logoblack.png +0 -0
  135. package/src/web/assets/images/logowhite.png +0 -0
  136. package/src/web/build.ts +23 -0
  137. package/src/web/components/ApprovalPanel.tsx +191 -0
  138. package/src/web/components/ChatPage.tsx +273 -0
  139. package/src/web/components/FileExplorer.tsx +162 -0
  140. package/src/web/components/HomePage.tsx +121 -0
  141. package/src/web/components/MessageItem.tsx +178 -0
  142. package/src/web/components/Modal.tsx +30 -0
  143. package/src/web/components/QuestionPanel.tsx +149 -0
  144. package/src/web/components/Setup.tsx +211 -0
  145. package/src/web/components/Sidebar.tsx +292 -0
  146. package/src/web/components/ThinkingIndicator.tsx +85 -0
  147. package/src/web/logo_black.svg +5 -0
  148. package/src/web/logo_white.svg +5 -0
  149. package/src/web/router.ts +46 -0
  150. package/src/web/server.tsx +662 -0
  151. package/src/web/storage.ts +92 -0
  152. package/src/web/types.ts +17 -0
  153. package/src/web/utils.ts +61 -0
  154. 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
+ }