@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,516 @@
1
+ import { useEffect, useState } from "react";
2
+ import { TextAttributes } from "@opentui/core";
3
+ import { renderMarkdownSegment, parseAndWrapMarkdown } from "../../utils/markdown";
4
+ import { getToolParagraphIndent, getToolWrapTarget, getToolWrapWidth } from "../../utils/toolFormatting";
5
+ import { subscribeQuestion, answerQuestion, type QuestionRequest } from "../../utils/questionBridge";
6
+ import { subscribeApproval, respondApproval, type ApprovalRequest } from "../../utils/approvalBridge";
7
+ import { subscribeFileChanges } from "../../utils/fileChangesBridge";
8
+ import type { FileChanges } from "../../utils/fileChangeTracker";
9
+ import { CustomInput } from "../CustomInput";
10
+ import type { Message } from "./types";
11
+ import { wrapText } from "./wrapText";
12
+ import { QuestionPanel } from "./QuestionPanel";
13
+ import { ApprovalPanel } from "./ApprovalPanel";
14
+ import { ThinkingIndicatorBlock, getBottomReservedLinesForInputBar, getInputBarBaseLines, getInputAreaTotalLines, formatElapsedTime } from "./ThinkingIndicator";
15
+ import { renderInlineDiffLine, getDiffLineBackground } from "../../utils/diffRendering";
16
+
17
+ function renderToolText(content: string, paragraphIndex: number, indent: number) {
18
+ if (paragraphIndex === 0) {
19
+ const match = content.match(/^(.+?)\s*\((.+)\)$/);
20
+ if (match) {
21
+ const [, toolName, toolInfo] = match;
22
+ return (
23
+ <>
24
+ <text fg="white">{toolName}</text>
25
+ <text fg="white" attributes={TextAttributes.DIM}> ({toolInfo})</text>
26
+ </>
27
+ );
28
+ }
29
+ }
30
+
31
+ const diffLineRender = renderInlineDiffLine(content);
32
+ if (diffLineRender) {
33
+ return diffLineRender;
34
+ }
35
+
36
+ return <text fg="white">{`${' '.repeat(indent)}${content || ' '}`}</text>;
37
+ }
38
+
39
+ interface ChatPageProps {
40
+ messages: Message[];
41
+ isProcessing: boolean;
42
+ processingStartTime: number | null;
43
+ currentTokens: number;
44
+ scrollOffset: number;
45
+ terminalHeight: number;
46
+ terminalWidth: number;
47
+ pasteRequestId: number;
48
+ shortcutsOpen: boolean;
49
+ onSubmit: (value: string, meta?: import("../CustomInput").InputSubmitMeta) => void;
50
+ }
51
+
52
+ export function ChatPage({
53
+ messages,
54
+ isProcessing,
55
+ processingStartTime,
56
+ currentTokens,
57
+ scrollOffset,
58
+ terminalHeight,
59
+ terminalWidth,
60
+ pasteRequestId,
61
+ shortcutsOpen,
62
+ onSubmit,
63
+ }: ChatPageProps) {
64
+ const maxWidth = Math.max(20, terminalWidth - 6);
65
+ const [questionRequest, setQuestionRequest] = useState<QuestionRequest | null>(null);
66
+ const [approvalRequest, setApprovalRequest] = useState<ApprovalRequest | null>(null);
67
+ const [fileChanges, setFileChanges] = useState<FileChanges>({ linesAdded: 0, linesRemoved: 0, filesModified: 0 });
68
+ const [, setTimerTick] = useState(0);
69
+
70
+ useEffect(() => {
71
+ return subscribeQuestion(setQuestionRequest);
72
+ }, []);
73
+
74
+ useEffect(() => {
75
+ return subscribeApproval(setApprovalRequest);
76
+ }, []);
77
+
78
+ useEffect(() => {
79
+ return subscribeFileChanges(setFileChanges);
80
+ }, []);
81
+
82
+ useEffect(() => {
83
+ const interval = setInterval(() => {
84
+ const hasRunning = messages.some(m => m.isRunning);
85
+ if (hasRunning) {
86
+ setTimerTick(tick => tick + 1);
87
+ }
88
+ }, 1000);
89
+ return () => clearInterval(interval);
90
+ }, [messages]);
91
+
92
+ const bottomReservedLines = getBottomReservedLinesForInputBar({
93
+ isProcessing,
94
+ hasQuestion: Boolean(questionRequest) || Boolean(approvalRequest),
95
+ });
96
+ const viewportHeight = Math.max(5, terminalHeight - (bottomReservedLines + 2));
97
+
98
+ interface RenderItem {
99
+ key: string;
100
+ type: 'line' | 'question' | 'approval' | 'blend';
101
+ content?: string;
102
+ role: "user" | "assistant" | "tool" | "slash";
103
+ isFirst: boolean;
104
+ indent?: number;
105
+ paragraphIndex?: number;
106
+ segments?: import("../../utils/markdown").MarkdownSegment[];
107
+ success?: boolean;
108
+ isError?: boolean;
109
+ isSpacer?: boolean;
110
+ questionRequest?: QuestionRequest;
111
+ approvalRequest?: ApprovalRequest;
112
+ visualLines: number;
113
+ blendDuration?: number;
114
+ blendWord?: string;
115
+ isRunning?: boolean;
116
+ runningStartTime?: number;
117
+ }
118
+
119
+ const allItems: RenderItem[] = [];
120
+ let pendingBlend: { key: string; blendDuration: number; blendWord: string } | null = null;
121
+
122
+ for (let messageIndex = 0; messageIndex < messages.length; messageIndex++) {
123
+ const message = messages[messageIndex]!;
124
+ const messageKey = message.id || `m-${messageIndex}`;
125
+ const messageRole = message.displayRole ?? message.role;
126
+
127
+ if (messageRole === 'user' && pendingBlend) {
128
+ allItems.push({
129
+ key: pendingBlend.key,
130
+ type: 'blend',
131
+ role: 'assistant',
132
+ isFirst: false,
133
+ visualLines: 1,
134
+ blendDuration: pendingBlend.blendDuration,
135
+ blendWord: pendingBlend.blendWord
136
+ });
137
+ pendingBlend = null;
138
+ }
139
+
140
+ if (messageRole === 'assistant') {
141
+ const blocks = parseAndWrapMarkdown(message.content, maxWidth);
142
+ let isFirstContent = true;
143
+
144
+ for (let blockIndex = 0; blockIndex < blocks.length; blockIndex++) {
145
+ const block = blocks[blockIndex]!;
146
+ if (block.type !== 'line' || !block.wrappedLines) continue;
147
+
148
+ for (let j = 0; j < block.wrappedLines.length; j++) {
149
+ const wrapped = block.wrappedLines[j];
150
+ if (wrapped) {
151
+ allItems.push({
152
+ key: `${messageKey}-line-${blockIndex}-${j}`,
153
+ type: 'line',
154
+ content: wrapped.text || '',
155
+ role: messageRole,
156
+ isFirst: isFirstContent && j === 0,
157
+ segments: wrapped.segments,
158
+ isError: message.isError,
159
+ visualLines: 1
160
+ });
161
+ if (wrapped.text && wrapped.text.trim()) {
162
+ isFirstContent = false;
163
+ }
164
+ }
165
+ }
166
+ }
167
+ } else {
168
+ const messageText = message.displayContent ?? message.content;
169
+ const paragraphs = messageText.split('\n');
170
+ let isFirstContent = true;
171
+
172
+ for (let i = 0; i < paragraphs.length; i++) {
173
+ const paragraph = paragraphs[i] ?? '';
174
+ if (paragraph === '') {
175
+ allItems.push({
176
+ key: `${messageKey}-paragraph-${i}-empty`,
177
+ type: 'line',
178
+ content: '',
179
+ role: messageRole,
180
+ isFirst: false,
181
+ indent: messageRole === 'tool' ? getToolParagraphIndent(i) : 0,
182
+ paragraphIndex: i,
183
+ success: (messageRole === 'tool' || messageRole === 'slash') ? message.success : undefined,
184
+ isSpacer: messageRole !== 'tool' && messageRole !== 'slash',
185
+ visualLines: 1,
186
+ isRunning: message.isRunning,
187
+ runningStartTime: message.runningStartTime
188
+ });
189
+ } else {
190
+ const indent = messageRole === 'tool' ? getToolParagraphIndent(i) : 0;
191
+ const wrapTarget = messageRole === 'tool' ? getToolWrapTarget(paragraph, i) : paragraph;
192
+ const wrapWidth = messageRole === 'tool' ? getToolWrapWidth(maxWidth, i) : maxWidth;
193
+ const wrappedLines = wrapText(wrapTarget, wrapWidth);
194
+ for (let j = 0; j < wrappedLines.length; j++) {
195
+ allItems.push({
196
+ key: `${messageKey}-paragraph-${i}-line-${j}`,
197
+ type: 'line',
198
+ content: wrappedLines[j] || '',
199
+ role: messageRole,
200
+ isFirst: isFirstContent && i === 0 && j === 0,
201
+ indent,
202
+ paragraphIndex: i,
203
+ success: (messageRole === 'tool' || messageRole === 'slash') ? message.success : undefined,
204
+ isSpacer: false,
205
+ visualLines: 1,
206
+ isRunning: message.isRunning,
207
+ runningStartTime: message.runningStartTime
208
+ });
209
+ }
210
+ isFirstContent = false;
211
+ }
212
+ }
213
+ }
214
+
215
+ if (message.isRunning && message.runningStartTime && messageRole === 'tool' && message.toolName !== 'explore') {
216
+ allItems.push({
217
+ key: `${messageKey}-running`,
218
+ type: 'line',
219
+ content: '',
220
+ role: messageRole,
221
+ isFirst: false,
222
+ indent: 2,
223
+ paragraphIndex: 1,
224
+ success: message.success,
225
+ isSpacer: false,
226
+ visualLines: 1,
227
+ isRunning: true,
228
+ runningStartTime: message.runningStartTime
229
+ });
230
+ }
231
+
232
+ if (message.responseDuration && messageRole === 'assistant' && message.responseDuration > 60000) {
233
+ pendingBlend = {
234
+ key: `${messageKey}-blend`,
235
+ blendDuration: message.responseDuration,
236
+ blendWord: message.blendWord || 'Blended'
237
+ };
238
+ }
239
+
240
+ allItems.push({
241
+ key: `${messageKey}-spacer`,
242
+ type: 'line',
243
+ content: '',
244
+ role: messageRole,
245
+ isFirst: false,
246
+ isSpacer: true,
247
+ visualLines: 1
248
+ });
249
+ }
250
+
251
+ if (pendingBlend) {
252
+ allItems.push({
253
+ key: pendingBlend.key,
254
+ type: 'blend',
255
+ role: 'assistant',
256
+ isFirst: false,
257
+ visualLines: 1,
258
+ blendDuration: pendingBlend.blendDuration,
259
+ blendWord: pendingBlend.blendWord
260
+ });
261
+ }
262
+
263
+ if (questionRequest) {
264
+ const questionPanelLines = Math.max(6, 5 + questionRequest.options.length);
265
+ const currentTotalLines = allItems.reduce((sum, item) => sum + item.visualLines, 0);
266
+ const linesFromBottom = currentTotalLines % viewportHeight;
267
+ const spaceNeeded = viewportHeight - linesFromBottom;
268
+
269
+ if (linesFromBottom > 0 && questionPanelLines + 2 > spaceNeeded) {
270
+ allItems.push({
271
+ key: `question-${questionRequest.id}-pagebreak`,
272
+ type: 'line',
273
+ content: '',
274
+ role: 'assistant',
275
+ isFirst: false,
276
+ isSpacer: true,
277
+ visualLines: spaceNeeded,
278
+ });
279
+ }
280
+
281
+ allItems.push({
282
+ key: `question-${questionRequest.id}`,
283
+ type: 'question',
284
+ role: 'assistant',
285
+ isFirst: true,
286
+ questionRequest,
287
+ visualLines: questionPanelLines,
288
+ });
289
+ allItems.push({
290
+ key: `question-${questionRequest.id}-spacer`,
291
+ type: 'line',
292
+ content: '',
293
+ role: 'assistant',
294
+ isFirst: false,
295
+ isSpacer: true,
296
+ visualLines: 1,
297
+ });
298
+ }
299
+
300
+ if (approvalRequest) {
301
+ const previewLines = approvalRequest.preview.content.split('\n').length;
302
+ const maxVisibleLines = Math.min(previewLines, viewportHeight - 10);
303
+ const approvalPanelLines = Math.max(8, 6 + maxVisibleLines);
304
+ const currentTotalLines = allItems.reduce((sum, item) => sum + item.visualLines, 0);
305
+ const linesFromBottom = currentTotalLines % viewportHeight;
306
+ const spaceNeeded = viewportHeight - linesFromBottom;
307
+
308
+ if (linesFromBottom > 0) {
309
+ allItems.push({
310
+ key: `approval-${approvalRequest.id}-pagebreak`,
311
+ type: 'line',
312
+ content: '',
313
+ role: 'assistant',
314
+ isFirst: false,
315
+ isSpacer: true,
316
+ visualLines: spaceNeeded,
317
+ });
318
+ }
319
+
320
+ allItems.push({
321
+ key: `approval-${approvalRequest.id}`,
322
+ type: 'approval',
323
+ role: 'assistant',
324
+ isFirst: true,
325
+ approvalRequest,
326
+ visualLines: approvalPanelLines,
327
+ });
328
+ allItems.push({
329
+ key: `approval-${approvalRequest.id}-spacer`,
330
+ type: 'line',
331
+ content: '',
332
+ role: 'assistant',
333
+ isFirst: false,
334
+ isSpacer: true,
335
+ visualLines: 1,
336
+ });
337
+ }
338
+
339
+ const totalVisualLines = allItems.reduce((sum, item) => sum + item.visualLines, 0);
340
+ const maxScrollOffset = Math.max(0, totalVisualLines - viewportHeight);
341
+ const clampedScrollOffset = Math.max(0, Math.min(scrollOffset, maxScrollOffset));
342
+
343
+ let visibleLines: RenderItem[] = [];
344
+
345
+ if (allItems.length === 0) {
346
+ visibleLines = [];
347
+ } else {
348
+ const targetEndLine = totalVisualLines - clampedScrollOffset;
349
+ const targetStartLine = Math.max(0, targetEndLine - viewportHeight);
350
+
351
+ let currentLine = 0;
352
+ let startIdx = -1;
353
+ let endIdx = -1;
354
+
355
+ for (let i = 0; i < allItems.length; i++) {
356
+ const item = allItems[i]!;
357
+ const itemEndLine = currentLine + item.visualLines;
358
+
359
+ if (startIdx === -1 && itemEndLine > targetStartLine) {
360
+ startIdx = i;
361
+ }
362
+
363
+ if (endIdx === -1 && itemEndLine >= targetEndLine) {
364
+ endIdx = i + 1;
365
+ break;
366
+ }
367
+
368
+ currentLine = itemEndLine;
369
+ }
370
+
371
+ if (startIdx === -1) startIdx = 0;
372
+ if (endIdx === -1) endIdx = allItems.length;
373
+
374
+ visibleLines = allItems.slice(startIdx, endIdx);
375
+ }
376
+
377
+ return (
378
+ <box flexDirection="column" width="100%" height="100%" position="relative">
379
+ <box flexGrow={1} flexDirection="column" width="100%" paddingLeft={1} paddingRight={1} paddingTop={1} paddingBottom={bottomReservedLines}>
380
+ {visibleLines.map((item) => {
381
+ if (item.type === 'question') {
382
+ const req = item.questionRequest;
383
+ if (!req) return null;
384
+ return (
385
+ <box key={item.key} flexDirection="column" width="100%">
386
+ <QuestionPanel
387
+ request={req}
388
+ disabled={shortcutsOpen}
389
+ onAnswer={(index, customText) => answerQuestion(index, customText)}
390
+ />
391
+ </box>
392
+ );
393
+ }
394
+
395
+ if (item.type === 'approval') {
396
+ const req = item.approvalRequest;
397
+ if (!req) return null;
398
+ return (
399
+ <box key={item.key} flexDirection="column" width="100%">
400
+ <ApprovalPanel
401
+ request={req}
402
+ disabled={shortcutsOpen}
403
+ onRespond={(approved, customResponse) => respondApproval(approved, customResponse)}
404
+ />
405
+ </box>
406
+ );
407
+ }
408
+
409
+ if (item.type === 'blend') {
410
+ if (item.blendDuration && item.blendDuration > 60000) {
411
+ const timeStr = formatElapsedTime(item.blendDuration, false);
412
+ return (
413
+ <box key={item.key} flexDirection="row" width="100%" paddingLeft={1}>
414
+ <text attributes={TextAttributes.DIM}>⁘ {item.blendWord} for {timeStr}</text>
415
+ </box>
416
+ );
417
+ }
418
+ return null;
419
+ }
420
+
421
+ const showErrorBar = item.role === "assistant" && item.isError && item.isFirst && item.content;
422
+ const showToolBar = item.role === "tool" && item.isSpacer === false;
423
+ const showSlashBar = item.role === "slash" && item.isSpacer === false;
424
+ const showToolBackground = item.role === "tool" && item.isSpacer === false;
425
+ const showSlashBackground = item.role === "slash" && item.isSpacer === false;
426
+ const isRunningTool = item.isRunning && item.runningStartTime;
427
+
428
+ const diffBackground = getDiffLineBackground(item.content || '');
429
+
430
+ const runningBackground = isRunningTool ? "#2a2a2a" : (diffBackground || (((item.role === "user" && item.content) || showToolBackground || showSlashBackground || showErrorBar) ? "#1a1a1a" : "transparent"));
431
+
432
+ return (
433
+ <box
434
+ key={item.key}
435
+ flexDirection="row"
436
+ width="100%"
437
+ backgroundColor={runningBackground}
438
+ paddingRight={((item.role === "user" && item.content) || showToolBackground || showSlashBackground || showErrorBar || isRunningTool) ? 1 : 0}
439
+ >
440
+ {item.role === "user" && item.content && (
441
+ <text fg="#ffca38">▎ </text>
442
+ )}
443
+ {showToolBar && !isRunningTool && (
444
+ <text fg={item.success ? "#1a3a1a" : "#3a1a1a"}>▎ </text>
445
+ )}
446
+ {showToolBar && isRunningTool && (
447
+ <text fg="#808080">▎ </text>
448
+ )}
449
+ {showSlashBar && (
450
+ <text fg="white">▎ </text>
451
+ )}
452
+ {showErrorBar && (
453
+ <text fg="#ff3838">▎ </text>
454
+ )}
455
+ {item.role === "tool" ? (
456
+ isRunningTool && item.runningStartTime && item.paragraphIndex === 1 ? (
457
+ <text fg="#ffffff" attributes={TextAttributes.DIM}> Running... {Math.floor((Date.now() - item.runningStartTime) / 1000)}s</text>
458
+ ) : (
459
+ renderToolText(item.content || ' ', item.paragraphIndex || 0, item.indent || 0)
460
+ )
461
+ ) : item.role === "user" || item.role === "slash" ? (
462
+ <text fg="white">{`${' '.repeat(item.indent || 0)}${item.content || ' '}`}</text>
463
+ ) : item.segments && item.segments.length > 0 ? (
464
+ <>
465
+ {item.segments.map((segment, segIndex) => renderMarkdownSegment(segment, segIndex))}
466
+ </>
467
+ ) : (
468
+ <text fg={item.isError ? "#ff3838" : "white"}>{item.content || ' '}</text>
469
+ )}
470
+ </box>
471
+ );
472
+ })}
473
+ </box>
474
+
475
+ <box
476
+ position="absolute"
477
+ bottom={1.4}
478
+ left={0}
479
+ right={0}
480
+ flexDirection="column"
481
+ backgroundColor="#1a1a1a"
482
+ paddingLeft={1}
483
+ paddingRight={1}
484
+ paddingTop={0}
485
+ paddingBottom={0}
486
+ flexShrink={0}
487
+ minHeight={getInputBarBaseLines()}
488
+ minWidth="100%"
489
+ >
490
+ <box flexDirection="row" alignItems="center" width="100%" flexGrow={1} minWidth={0}>
491
+ <box flexGrow={1} flexShrink={1} minWidth={0}>
492
+ <CustomInput
493
+ onSubmit={onSubmit}
494
+ placeholder="Type your message..."
495
+ focused={!isProcessing && !shortcutsOpen && !questionRequest && !approvalRequest}
496
+ pasteRequestId={shortcutsOpen ? 0 : pasteRequestId}
497
+ />
498
+ </box>
499
+ </box>
500
+ </box>
501
+
502
+ <box position="absolute" bottom={0} left={0} right={0} flexDirection="row" paddingLeft={1} paddingRight={1} justifyContent="space-between">
503
+ <box flexDirection="row" gap={1}>
504
+ <text>—</text>
505
+ <text fg="#4d8f29">+{fileChanges.linesAdded}</text>
506
+ <text fg="#d73a49">-{fileChanges.linesRemoved}</text>
507
+ </box>
508
+ <text attributes={TextAttributes.DIM}>ctrl+o to see commands — ctrl+p to view shortcuts</text>
509
+ </box>
510
+
511
+ <box position="absolute" bottom={getInputBarBaseLines() + 1} left={0} right={0} flexDirection="column" paddingLeft={1} paddingRight={1}>
512
+ <ThinkingIndicatorBlock isProcessing={isProcessing} hasQuestion={Boolean(questionRequest) || Boolean(approvalRequest)} startTime={processingStartTime} tokens={currentTokens} />
513
+ </box>
514
+ </box>
515
+ );
516
+ }
@@ -0,0 +1,111 @@
1
+ import { useState, useEffect } from "react";
2
+ import { TextAttributes } from "@opentui/core";
3
+ import { VERSION } from "../../utils/version";
4
+ import { CustomInput } from "../CustomInput";
5
+
6
+ interface HomePageProps {
7
+ onSubmit: (value: string, meta?: import("../CustomInput").InputSubmitMeta) => void;
8
+ pasteRequestId: number;
9
+ shortcutsOpen: boolean;
10
+ }
11
+
12
+ const TIPS = [
13
+ "Press Alt + P to view all available shortcuts.",
14
+ "Use the Up and Down arrows to navigate through message history.",
15
+ "Press Esc to cancel the current action or close popups.",
16
+ "Paste text using Ctrl + V (or Cmd + V on macOS).",
17
+ "Use /clear to reset the current chat session.",
18
+ "Use Tab to autocomplete commands and arguments.",
19
+ "Use Ctrl + K to clear the current input line.",
20
+ "Use /help to display the list of available commands.",
21
+ ];
22
+
23
+ export function HomePage({ onSubmit, pasteRequestId, shortcutsOpen }: HomePageProps) {
24
+ const [currentTipIndex, setCurrentTipIndex] = useState(0);
25
+ const [displayedText, setDisplayedText] = useState("");
26
+ const [isDeleting, setIsDeleting] = useState(false);
27
+ const [isWaiting, setIsWaiting] = useState(false);
28
+ const [cursorVisible, setCursorVisible] = useState(true);
29
+
30
+ useEffect(() => {
31
+ const cursorInterval = setInterval(() => {
32
+ setCursorVisible((v) => !v);
33
+ }, 500);
34
+ return () => clearInterval(cursorInterval);
35
+ }, []);
36
+
37
+ useEffect(() => {
38
+ if (isWaiting) {
39
+ const timeout = setTimeout(() => {
40
+ setIsWaiting(false);
41
+ setIsDeleting(true);
42
+ }, 5000);
43
+ return () => clearTimeout(timeout);
44
+ }
45
+
46
+ const currentTip = TIPS[currentTipIndex] || "";
47
+
48
+ if (isDeleting) {
49
+ if (displayedText.length === 0) {
50
+ setIsDeleting(false);
51
+ let nextIndex = Math.floor(Math.random() * TIPS.length);
52
+ while (nextIndex === currentTipIndex && TIPS.length > 1) {
53
+ nextIndex = Math.floor(Math.random() * TIPS.length);
54
+ }
55
+ setCurrentTipIndex(nextIndex);
56
+ } else {
57
+ const timeout = setTimeout(() => {
58
+ setDisplayedText((prev) => prev.slice(0, -1));
59
+ }, 30);
60
+ return () => clearTimeout(timeout);
61
+ }
62
+ } else {
63
+ if (displayedText === currentTip) {
64
+ setIsWaiting(true);
65
+ } else {
66
+ const timeout = setTimeout(() => {
67
+ setDisplayedText(currentTip.slice(0, displayedText.length + 1));
68
+ }, 60);
69
+ return () => clearTimeout(timeout);
70
+ }
71
+ }
72
+ }, [displayedText, isDeleting, isWaiting, currentTipIndex]);
73
+
74
+ return (
75
+ <box flexDirection="column" width="100%" height="100%" justifyContent="center" alignItems="center">
76
+ <box flexDirection="column" alignItems="center" marginBottom={2}>
77
+ <text fg="#ffca38" attributes={TextAttributes.BOLD}>███╗ ███╗</text>
78
+ <text fg="#ffca38" attributes={TextAttributes.BOLD}>████╗ ████║</text>
79
+ <text fg="#ffca38" attributes={TextAttributes.BOLD}>███╔████╔███║</text>
80
+ </box>
81
+
82
+ <box width="80%" maxWidth={80}>
83
+ <box
84
+ flexDirection="row"
85
+ backgroundColor="#1a1a1a"
86
+ paddingLeft={2}
87
+ paddingRight={2}
88
+ paddingTop={1}
89
+ paddingBottom={1}
90
+ >
91
+ <CustomInput
92
+ onSubmit={onSubmit}
93
+ placeholder="Ask anything..."
94
+ focused={!shortcutsOpen}
95
+ pasteRequestId={shortcutsOpen ? 0 : pasteRequestId}
96
+ />
97
+ </box>
98
+ </box>
99
+
100
+ <box width="80%" maxWidth={80} marginTop={3} flexDirection="row" justifyContent="center">
101
+ <text fg="#ffca38" attributes={TextAttributes.BOLD}>ⓘ TIPS: </text>
102
+ <text fg="gray">{displayedText}</text>
103
+ <text fg="#ffca38">{cursorVisible ? "█" : " "}</text>
104
+ </box>
105
+
106
+ <box position="absolute" bottom={1} right={2}>
107
+ <text fg="gray" attributes={TextAttributes.DIM}>v{VERSION}</text>
108
+ </box>
109
+ </box>
110
+ );
111
+ }