@kirosnn/mosaic 0.0.9 → 0.71.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/LICENSE +1 -1
- package/README.md +83 -19
- package/package.json +52 -47
- package/src/agent/prompts/systemPrompt.ts +198 -68
- package/src/agent/prompts/toolsPrompt.ts +217 -135
- package/src/agent/provider/anthropic.ts +19 -15
- package/src/agent/provider/google.ts +21 -17
- package/src/agent/provider/ollama.ts +80 -41
- package/src/agent/provider/openai.ts +107 -67
- package/src/agent/provider/reasoning.ts +29 -0
- package/src/agent/provider/xai.ts +19 -15
- package/src/agent/tools/definitions.ts +9 -5
- package/src/agent/tools/executor.ts +655 -46
- package/src/agent/tools/exploreExecutor.ts +12 -12
- package/src/agent/tools/fetch.ts +58 -0
- package/src/agent/tools/glob.ts +20 -4
- package/src/agent/tools/grep.ts +62 -8
- package/src/agent/tools/plan.ts +27 -0
- package/src/agent/tools/read.ts +2 -0
- package/src/agent/types.ts +6 -6
- package/src/components/App.tsx +67 -25
- package/src/components/CustomInput.tsx +274 -68
- package/src/components/Main.tsx +323 -168
- package/src/components/ShortcutsModal.tsx +11 -8
- package/src/components/main/ChatPage.tsx +217 -58
- package/src/components/main/HomePage.tsx +5 -1
- package/src/components/main/ThinkingIndicator.tsx +11 -1
- package/src/components/main/types.ts +11 -10
- package/src/index.tsx +3 -5
- package/src/utils/approvalBridge.ts +29 -8
- package/src/utils/approvalModeBridge.ts +17 -0
- package/src/utils/commands/approvals.ts +48 -0
- package/src/utils/commands/image.ts +109 -0
- package/src/utils/commands/index.ts +5 -1
- package/src/utils/diffRendering.tsx +13 -14
- package/src/utils/history.ts +82 -40
- package/src/utils/imageBridge.ts +28 -0
- package/src/utils/images.ts +31 -0
- package/src/utils/models.ts +0 -7
- package/src/utils/notificationBridge.ts +23 -0
- package/src/utils/toolFormatting.ts +162 -43
- package/src/web/app.tsx +94 -34
- package/src/web/assets/css/ChatPage.css +102 -30
- package/src/web/assets/css/MessageItem.css +26 -29
- package/src/web/assets/css/ThinkingIndicator.css +44 -6
- package/src/web/assets/css/ToolMessage.css +36 -14
- package/src/web/components/ChatPage.tsx +228 -105
- package/src/web/components/HomePage.tsx +6 -6
- package/src/web/components/MessageItem.tsx +88 -89
- package/src/web/components/Setup.tsx +1 -1
- package/src/web/components/Sidebar.tsx +1 -1
- package/src/web/components/ThinkingIndicator.tsx +40 -21
- package/src/web/router.ts +1 -1
- package/src/web/server.tsx +187 -39
- package/src/web/storage.ts +23 -1
- package/src/web/types.ts +7 -6
|
@@ -59,12 +59,31 @@
|
|
|
59
59
|
background: var(--status-error-dark);
|
|
60
60
|
}
|
|
61
61
|
|
|
62
|
-
.message-content {
|
|
63
|
-
color: var(--text-primary);
|
|
64
|
-
white-space: pre-wrap;
|
|
65
|
-
word-break: break-word;
|
|
66
|
-
flex: 1;
|
|
67
|
-
}
|
|
62
|
+
.message-content {
|
|
63
|
+
color: var(--text-primary);
|
|
64
|
+
white-space: pre-wrap;
|
|
65
|
+
word-break: break-word;
|
|
66
|
+
flex: 1;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
.message-images {
|
|
70
|
+
display: flex;
|
|
71
|
+
gap: 0.5rem;
|
|
72
|
+
flex-wrap: wrap;
|
|
73
|
+
margin-bottom: 0.4rem;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
.message-images img {
|
|
77
|
+
width: 140px;
|
|
78
|
+
height: auto;
|
|
79
|
+
border-radius: 8px;
|
|
80
|
+
border: 1px solid var(--border-code);
|
|
81
|
+
background: var(--bg-code);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
.message-text {
|
|
85
|
+
white-space: pre-wrap;
|
|
86
|
+
}
|
|
68
87
|
|
|
69
88
|
.thinking-section {
|
|
70
89
|
margin-bottom: 0.35rem;
|
|
@@ -95,7 +114,6 @@
|
|
|
95
114
|
font-size: 0.8rem;
|
|
96
115
|
color: var(--text-muted);
|
|
97
116
|
overflow-x: auto;
|
|
98
|
-
line-height: 1.4;
|
|
99
117
|
}
|
|
100
118
|
|
|
101
119
|
.assistant-text {
|
|
@@ -136,25 +154,4 @@
|
|
|
136
154
|
40% {
|
|
137
155
|
transform: scale(1);
|
|
138
156
|
}
|
|
139
|
-
}
|
|
140
|
-
|
|
141
|
-
.blend-indicator {
|
|
142
|
-
display: flex;
|
|
143
|
-
align-items: center;
|
|
144
|
-
gap: 1rem;
|
|
145
|
-
padding: 1rem 1.5rem;
|
|
146
|
-
margin: 0.75rem 0;
|
|
147
|
-
}
|
|
148
|
-
|
|
149
|
-
.blend-icon {
|
|
150
|
-
width: 48px;
|
|
151
|
-
height: 48px;
|
|
152
|
-
fill: var(--text-muted);
|
|
153
|
-
flex-shrink: 0;
|
|
154
|
-
}
|
|
155
|
-
|
|
156
|
-
.blend-text {
|
|
157
|
-
font-size: 1.5rem;
|
|
158
|
-
color: var(--text-muted);
|
|
159
|
-
font-weight: 400;
|
|
160
|
-
}
|
|
157
|
+
}
|
|
@@ -1,8 +1,14 @@
|
|
|
1
|
+
.thinking-block {
|
|
2
|
+
display: flex;
|
|
3
|
+
flex-direction: column;
|
|
4
|
+
gap: 0.15rem;
|
|
5
|
+
}
|
|
6
|
+
|
|
1
7
|
.thinking-indicator {
|
|
2
8
|
display: flex;
|
|
3
9
|
align-items: center;
|
|
4
10
|
gap: 0;
|
|
5
|
-
padding: 0.
|
|
11
|
+
padding: 0.65rem 1.15rem 0.35rem 1.15rem;
|
|
6
12
|
font-family: 'SF Mono', 'Fira Code', 'Consolas', monospace;
|
|
7
13
|
font-size: 0.95rem;
|
|
8
14
|
}
|
|
@@ -31,17 +37,49 @@
|
|
|
31
37
|
.thinking-elapsed {
|
|
32
38
|
color: var(--text-secondary);
|
|
33
39
|
opacity: 0.7;
|
|
34
|
-
margin-left: 0.25rem;
|
|
35
40
|
}
|
|
36
41
|
|
|
37
42
|
.thinking-hint {
|
|
38
43
|
color: var(--text-secondary);
|
|
39
|
-
opacity: 0.
|
|
40
|
-
margin-left: 0.25rem;
|
|
44
|
+
opacity: 0.55;
|
|
41
45
|
}
|
|
42
46
|
|
|
43
47
|
.thinking-tokens {
|
|
44
48
|
color: var(--text-secondary);
|
|
45
49
|
opacity: 0.7;
|
|
46
|
-
|
|
47
|
-
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
.thinking-plan {
|
|
53
|
+
color: var(--text-secondary);
|
|
54
|
+
opacity: 0.7;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
.thinking-sep {
|
|
58
|
+
color: var(--text-secondary);
|
|
59
|
+
opacity: 0.6;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
.thinking-next-line {
|
|
63
|
+
display: flex;
|
|
64
|
+
align-items: center;
|
|
65
|
+
gap: 0.35rem;
|
|
66
|
+
padding: 0 1.15rem 0.6rem 1.15rem;
|
|
67
|
+
font-family: 'SF Mono', 'Fira Code', 'Consolas', monospace;
|
|
68
|
+
font-size: 0.9rem;
|
|
69
|
+
color: var(--text-secondary);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
.thinking-next-icon {
|
|
73
|
+
color: #ffca38;
|
|
74
|
+
font-weight: 700;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
.thinking-next-label {
|
|
78
|
+
font-weight: 700;
|
|
79
|
+
color: #ffca38;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
.thinking-next-step {
|
|
83
|
+
color: var(--text-secondary);
|
|
84
|
+
opacity: 0.75;
|
|
85
|
+
}
|
|
@@ -12,14 +12,24 @@
|
|
|
12
12
|
border: 1px solid var(--border-tool-running);
|
|
13
13
|
}
|
|
14
14
|
|
|
15
|
-
.message.tool.error {
|
|
16
|
-
background: var(--bg-tool-error);
|
|
17
|
-
border: 1px solid var(--border-tool-error);
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
.message.tool.
|
|
21
|
-
|
|
22
|
-
|
|
15
|
+
.message.tool.error {
|
|
16
|
+
background: var(--bg-tool-error);
|
|
17
|
+
border: 1px solid var(--border-tool-error);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
.message.tool.plan-tool,
|
|
21
|
+
.message.tool.plan-tool.running,
|
|
22
|
+
.message.tool.plan-tool.error,
|
|
23
|
+
.message.tool.plan-tool.success {
|
|
24
|
+
background: transparent;
|
|
25
|
+
border: none;
|
|
26
|
+
backdrop-filter: none;
|
|
27
|
+
-webkit-backdrop-filter: none;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
.message.tool.success .message-bar {
|
|
31
|
+
background: var(--status-success);
|
|
32
|
+
}
|
|
23
33
|
|
|
24
34
|
.message.tool.running .message-bar {
|
|
25
35
|
background: var(--status-running);
|
|
@@ -107,11 +117,23 @@
|
|
|
107
117
|
line-height: 1.4;
|
|
108
118
|
}
|
|
109
119
|
|
|
110
|
-
.tool-line {
|
|
111
|
-
padding: 0.1rem 0.5rem;
|
|
112
|
-
white-space: pre-wrap;
|
|
113
|
-
word-break: break-all;
|
|
114
|
-
}
|
|
120
|
+
.tool-line {
|
|
121
|
+
padding: 0.1rem 0.5rem;
|
|
122
|
+
white-space: pre-wrap;
|
|
123
|
+
word-break: break-all;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
.tool-line.plan-line {
|
|
127
|
+
padding-left: 0.9rem;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
.plan-prefix {
|
|
131
|
+
color: #ffca38;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
.plan-bracket.active {
|
|
135
|
+
color: #ffca38;
|
|
136
|
+
}
|
|
115
137
|
|
|
116
138
|
.tool-line.diff-line {
|
|
117
139
|
display: flex;
|
|
@@ -145,4 +167,4 @@
|
|
|
145
167
|
.diff-content {
|
|
146
168
|
flex: 1;
|
|
147
169
|
padding: 0.1rem 0.3rem;
|
|
148
|
-
}
|
|
170
|
+
}
|
|
@@ -1,29 +1,33 @@
|
|
|
1
1
|
/** @jsxImportSource react */
|
|
2
2
|
import React, { useState, useEffect, useRef } from 'react';
|
|
3
|
-
import { Message } from '../types';
|
|
4
|
-
import { MessageItem } from './MessageItem';
|
|
5
|
-
import { Sidebar, SidebarProps } from './Sidebar';
|
|
6
|
-
import { QuestionRequest } from '../../utils/questionBridge';
|
|
7
|
-
import { ApprovalRequest } from '../../utils/approvalBridge';
|
|
8
|
-
import { QuestionPanel } from './QuestionPanel';
|
|
9
|
-
import { ApprovalPanel } from './ApprovalPanel';
|
|
10
|
-
import { ThinkingIndicator } from './ThinkingIndicator';
|
|
11
|
-
import { findModelsDevModelById, modelAcceptsImages } from '../../utils/models';
|
|
12
|
-
import '
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
3
|
+
import { Message } from '../types';
|
|
4
|
+
import { MessageItem } from './MessageItem';
|
|
5
|
+
import { Sidebar, SidebarProps } from './Sidebar';
|
|
6
|
+
import { QuestionRequest } from '../../utils/questionBridge';
|
|
7
|
+
import { ApprovalRequest } from '../../utils/approvalBridge';
|
|
8
|
+
import { QuestionPanel } from './QuestionPanel';
|
|
9
|
+
import { ApprovalPanel } from './ApprovalPanel';
|
|
10
|
+
import { ThinkingIndicator } from './ThinkingIndicator';
|
|
11
|
+
import { findModelsDevModelById, modelAcceptsImages } from '../../utils/models';
|
|
12
|
+
import type { ImageAttachment } from '../../utils/images';
|
|
13
|
+
import { guessImageMimeType, toDataUrl } from '../../utils/images';
|
|
14
|
+
import '../assets/css/global.css'
|
|
15
|
+
|
|
16
|
+
interface ChatPageProps {
|
|
17
|
+
messages: Message[];
|
|
18
|
+
isProcessing: boolean;
|
|
19
|
+
processingStartTime?: number;
|
|
20
|
+
currentTokens?: number;
|
|
21
|
+
onSendMessage: (message: string, images?: ImageAttachment[]) => void;
|
|
22
|
+
onStopAgent?: () => void;
|
|
23
|
+
sidebarProps: SidebarProps;
|
|
24
|
+
currentTitle?: string | null;
|
|
25
|
+
workspace?: string | null;
|
|
26
|
+
questionRequest?: QuestionRequest | null;
|
|
27
|
+
approvalRequest?: ApprovalRequest | null;
|
|
28
|
+
requireApprovals: boolean;
|
|
29
|
+
onToggleApprovals: () => void;
|
|
30
|
+
}
|
|
27
31
|
|
|
28
32
|
function formatWorkspace(path: string | null | undefined): string {
|
|
29
33
|
if (!path) return '';
|
|
@@ -55,11 +59,54 @@ function formatWorkspace(path: string | null | undefined): string {
|
|
|
55
59
|
return normalized;
|
|
56
60
|
}
|
|
57
61
|
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
62
|
+
function getPlanProgress(messages: Message[]): { inProgressStep?: string; nextStep?: string } {
|
|
63
|
+
for (let i = messages.length - 1; i >= 0; i -= 1) {
|
|
64
|
+
const message = messages[i];
|
|
65
|
+
if (!message || message.role !== 'tool' || message.toolName !== 'plan') continue;
|
|
66
|
+
const result = message.toolResult;
|
|
67
|
+
if (!result || typeof result !== 'object') continue;
|
|
68
|
+
const obj = result as Record<string, unknown>;
|
|
69
|
+
const planItems = Array.isArray(obj.plan) ? obj.plan : [];
|
|
70
|
+
const normalized = planItems
|
|
71
|
+
.map((item) => {
|
|
72
|
+
if (!item || typeof item !== 'object') return null;
|
|
73
|
+
const entry = item as Record<string, unknown>;
|
|
74
|
+
const step = typeof entry.step === 'string' ? entry.step.trim() : '';
|
|
75
|
+
const status = typeof entry.status === 'string' ? entry.status : 'pending';
|
|
76
|
+
if (!step) return null;
|
|
77
|
+
return { step, status };
|
|
78
|
+
})
|
|
79
|
+
.filter((item): item is { step: string; status: string } => !!item);
|
|
80
|
+
|
|
81
|
+
if (normalized.length === 0) return {};
|
|
82
|
+
|
|
83
|
+
const inProgressIndex = normalized.findIndex(item => item.status === 'in_progress');
|
|
84
|
+
const inProgressStep = inProgressIndex >= 0 ? normalized[inProgressIndex]?.step : undefined;
|
|
85
|
+
let nextStep: string | undefined;
|
|
86
|
+
|
|
87
|
+
if (inProgressIndex >= 0) {
|
|
88
|
+
const after = normalized.slice(inProgressIndex + 1).find(item => item.status === 'pending');
|
|
89
|
+
nextStep = after?.step;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
if (!nextStep) {
|
|
93
|
+
nextStep = normalized.find(item => item.status === 'pending')?.step;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
return { inProgressStep, nextStep };
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
return {};
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
export function ChatPage({ messages, isProcessing, processingStartTime, currentTokens, onSendMessage, onStopAgent, sidebarProps, currentTitle, workspace, questionRequest, approvalRequest, requireApprovals, onToggleApprovals }: ChatPageProps) {
|
|
103
|
+
const [inputValue, setInputValue] = useState('');
|
|
104
|
+
const [showAttachButton, setShowAttachButton] = useState(false);
|
|
105
|
+
const [pendingImages, setPendingImages] = useState<ImageAttachment[]>([]);
|
|
106
|
+
const messagesEndRef = useRef<HTMLDivElement>(null);
|
|
107
|
+
const inputRef = useRef<HTMLTextAreaElement>(null);
|
|
108
|
+
const fileInputRef = useRef<HTMLInputElement>(null);
|
|
109
|
+
const planProgress = getPlanProgress(messages);
|
|
63
110
|
|
|
64
111
|
useEffect(() => {
|
|
65
112
|
if (messagesEndRef.current) {
|
|
@@ -77,17 +124,23 @@ export function ChatPage({ messages, isProcessing, processingStartTime, currentT
|
|
|
77
124
|
if (inputRef.current) inputRef.current.focus();
|
|
78
125
|
}, []);
|
|
79
126
|
|
|
80
|
-
useEffect(() => {
|
|
81
|
-
const handleKeyDown = (e: KeyboardEvent) => {
|
|
82
|
-
if (e.key === 'Escape' && isProcessing && onStopAgent) {
|
|
83
|
-
e.preventDefault();
|
|
84
|
-
onStopAgent();
|
|
85
|
-
}
|
|
86
|
-
};
|
|
127
|
+
useEffect(() => {
|
|
128
|
+
const handleKeyDown = (e: KeyboardEvent) => {
|
|
129
|
+
if (e.key === 'Escape' && isProcessing && onStopAgent) {
|
|
130
|
+
e.preventDefault();
|
|
131
|
+
onStopAgent();
|
|
132
|
+
}
|
|
133
|
+
};
|
|
87
134
|
|
|
88
135
|
document.addEventListener('keydown', handleKeyDown);
|
|
89
|
-
return () => document.removeEventListener('keydown', handleKeyDown);
|
|
90
|
-
}, [isProcessing, onStopAgent]);
|
|
136
|
+
return () => document.removeEventListener('keydown', handleKeyDown);
|
|
137
|
+
}, [isProcessing, onStopAgent]);
|
|
138
|
+
|
|
139
|
+
useEffect(() => {
|
|
140
|
+
if (!showAttachButton) {
|
|
141
|
+
setPendingImages([]);
|
|
142
|
+
}
|
|
143
|
+
}, [showAttachButton]);
|
|
91
144
|
|
|
92
145
|
useEffect(() => {
|
|
93
146
|
const checkModelSupport = async () => {
|
|
@@ -98,44 +151,82 @@ export function ChatPage({ messages, isProcessing, processingStartTime, currentT
|
|
|
98
151
|
const { model } = await configRes.json();
|
|
99
152
|
|
|
100
153
|
if (model) {
|
|
101
|
-
// Try to find the model using the enhanced fuzzy search
|
|
102
154
|
const result = await findModelsDevModelById(model);
|
|
103
155
|
|
|
104
|
-
if (result && result.model) {
|
|
105
|
-
setShowAttachButton(modelAcceptsImages(result.model));
|
|
106
|
-
} else {
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
lowerId.includes('vision');
|
|
115
|
-
setShowAttachButton(likelySupportsImages);
|
|
116
|
-
}
|
|
117
|
-
}
|
|
118
|
-
} catch (err) {
|
|
119
|
-
console.error('Failed to check model support:', err);
|
|
120
|
-
}
|
|
121
|
-
};
|
|
156
|
+
if (result && result.model) {
|
|
157
|
+
setShowAttachButton(modelAcceptsImages(result.model));
|
|
158
|
+
} else {
|
|
159
|
+
setShowAttachButton(false);
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
} catch (err) {
|
|
163
|
+
console.error('Failed to check model support:', err);
|
|
164
|
+
}
|
|
165
|
+
};
|
|
122
166
|
|
|
123
167
|
checkModelSupport();
|
|
124
168
|
}, []);
|
|
125
169
|
|
|
126
|
-
const handleSubmit = (e?: React.FormEvent) => {
|
|
127
|
-
if (e) e.preventDefault();
|
|
128
|
-
if (!inputValue.trim() || isProcessing) return;
|
|
129
|
-
onSendMessage(inputValue);
|
|
130
|
-
setInputValue('');
|
|
131
|
-
|
|
170
|
+
const handleSubmit = (e?: React.FormEvent) => {
|
|
171
|
+
if (e) e.preventDefault();
|
|
172
|
+
if ((!inputValue.trim() && pendingImages.length === 0) || isProcessing) return;
|
|
173
|
+
onSendMessage(inputValue, showAttachButton ? pendingImages : []);
|
|
174
|
+
setInputValue('');
|
|
175
|
+
setPendingImages([]);
|
|
176
|
+
};
|
|
132
177
|
|
|
133
|
-
const handleKeyDown = (e: React.KeyboardEvent<HTMLTextAreaElement>) => {
|
|
134
|
-
if (e.key === 'Enter' && !e.shiftKey) {
|
|
135
|
-
e.preventDefault();
|
|
136
|
-
handleSubmit();
|
|
137
|
-
}
|
|
138
|
-
};
|
|
178
|
+
const handleKeyDown = (e: React.KeyboardEvent<HTMLTextAreaElement>) => {
|
|
179
|
+
if (e.key === 'Enter' && !e.shiftKey) {
|
|
180
|
+
e.preventDefault();
|
|
181
|
+
handleSubmit();
|
|
182
|
+
}
|
|
183
|
+
};
|
|
184
|
+
|
|
185
|
+
const toBase64 = async (file: File): Promise<string> => {
|
|
186
|
+
const buffer = await file.arrayBuffer();
|
|
187
|
+
const bytes = new Uint8Array(buffer);
|
|
188
|
+
let binary = '';
|
|
189
|
+
const chunkSize = 0x8000;
|
|
190
|
+
for (let i = 0; i < bytes.length; i += chunkSize) {
|
|
191
|
+
const chunk = bytes.subarray(i, i + chunkSize);
|
|
192
|
+
binary += String.fromCharCode(...Array.from(chunk));
|
|
193
|
+
}
|
|
194
|
+
return btoa(binary);
|
|
195
|
+
};
|
|
196
|
+
|
|
197
|
+
const handleAttachClick = () => {
|
|
198
|
+
fileInputRef.current?.click();
|
|
199
|
+
};
|
|
200
|
+
|
|
201
|
+
const handleFilesSelected = async (e: React.ChangeEvent<HTMLInputElement>) => {
|
|
202
|
+
if (!showAttachButton) {
|
|
203
|
+
e.target.value = '';
|
|
204
|
+
return;
|
|
205
|
+
}
|
|
206
|
+
const files = Array.from(e.target.files || []);
|
|
207
|
+
if (files.length === 0) return;
|
|
208
|
+
const attachments: ImageAttachment[] = [];
|
|
209
|
+
for (const file of files) {
|
|
210
|
+
const mimeType = file.type || guessImageMimeType(file.name);
|
|
211
|
+
if (!mimeType.startsWith('image/')) continue;
|
|
212
|
+
const data = await toBase64(file);
|
|
213
|
+
attachments.push({
|
|
214
|
+
id: `${Date.now()}-${Math.random().toString(36).slice(2, 8)}`,
|
|
215
|
+
name: file.name,
|
|
216
|
+
mimeType,
|
|
217
|
+
data,
|
|
218
|
+
size: file.size
|
|
219
|
+
});
|
|
220
|
+
}
|
|
221
|
+
if (attachments.length > 0) {
|
|
222
|
+
setPendingImages((prev) => [...prev, ...attachments]);
|
|
223
|
+
}
|
|
224
|
+
e.target.value = '';
|
|
225
|
+
};
|
|
226
|
+
|
|
227
|
+
const handleRemovePendingImage = (id: string) => {
|
|
228
|
+
setPendingImages((prev) => prev.filter((img) => img.id !== id));
|
|
229
|
+
};
|
|
139
230
|
|
|
140
231
|
const handleQuestionAnswer = async (index: number, customText?: string) => {
|
|
141
232
|
try {
|
|
@@ -169,16 +260,24 @@ export function ChatPage({ messages, isProcessing, processingStartTime, currentT
|
|
|
169
260
|
|
|
170
261
|
<div className="main-content" style={{ padding: 0 }}>
|
|
171
262
|
<div className="chat-page">
|
|
172
|
-
|
|
173
|
-
<
|
|
174
|
-
|
|
175
|
-
{formattedWorkspace && (
|
|
176
|
-
<span className="chat-workspace" title={workspace || ''}>
|
|
177
|
-
{formattedWorkspace}
|
|
178
|
-
</span>
|
|
179
|
-
)}
|
|
180
|
-
|
|
181
|
-
|
|
263
|
+
<div className="chat-title-bar">
|
|
264
|
+
<span className="chat-title">{currentTitle || ''}</span>
|
|
265
|
+
<div className="chat-title-actions">
|
|
266
|
+
{formattedWorkspace && (
|
|
267
|
+
<span className="chat-workspace" title={workspace || ''}>
|
|
268
|
+
{formattedWorkspace}
|
|
269
|
+
</span>
|
|
270
|
+
)}
|
|
271
|
+
<button
|
|
272
|
+
type="button"
|
|
273
|
+
className={`approval-toggle ${requireApprovals ? '' : 'active'}`}
|
|
274
|
+
onClick={onToggleApprovals}
|
|
275
|
+
title={requireApprovals ? 'Enable auto-approve' : 'Disable auto-approve'}
|
|
276
|
+
>
|
|
277
|
+
{requireApprovals ? 'Approvals on' : 'Auto-approve'}
|
|
278
|
+
</button>
|
|
279
|
+
</div>
|
|
280
|
+
</div>
|
|
182
281
|
<div className="chat-container">
|
|
183
282
|
<div className="messages">
|
|
184
283
|
{messages.map((msg) => (
|
|
@@ -187,7 +286,12 @@ export function ChatPage({ messages, isProcessing, processingStartTime, currentT
|
|
|
187
286
|
{isProcessing && !questionRequest && !approvalRequest && (
|
|
188
287
|
<div className="message assistant">
|
|
189
288
|
<div className="message-content">
|
|
190
|
-
<ThinkingIndicator
|
|
289
|
+
<ThinkingIndicator
|
|
290
|
+
startTime={processingStartTime}
|
|
291
|
+
tokens={currentTokens}
|
|
292
|
+
inProgressStep={planProgress.inProgressStep}
|
|
293
|
+
nextStep={planProgress.nextStep}
|
|
294
|
+
/>
|
|
191
295
|
</div>
|
|
192
296
|
</div>
|
|
193
297
|
)}
|
|
@@ -217,25 +321,44 @@ export function ChatPage({ messages, isProcessing, processingStartTime, currentT
|
|
|
217
321
|
<div ref={messagesEndRef} />
|
|
218
322
|
</div>
|
|
219
323
|
|
|
220
|
-
<form onSubmit={handleSubmit} className="input-area">
|
|
221
|
-
<
|
|
222
|
-
ref={
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
<
|
|
235
|
-
|
|
236
|
-
</
|
|
237
|
-
</
|
|
238
|
-
)}
|
|
324
|
+
<form onSubmit={handleSubmit} className="input-area">
|
|
325
|
+
<input
|
|
326
|
+
ref={fileInputRef}
|
|
327
|
+
type="file"
|
|
328
|
+
accept="image/*"
|
|
329
|
+
multiple
|
|
330
|
+
style={{ display: 'none' }}
|
|
331
|
+
onChange={handleFilesSelected}
|
|
332
|
+
/>
|
|
333
|
+
{pendingImages.length > 0 && (
|
|
334
|
+
<div className="attachment-strip">
|
|
335
|
+
{pendingImages.map((img) => (
|
|
336
|
+
<div key={img.id} className="attachment-item">
|
|
337
|
+
<img src={toDataUrl(img)} alt={img.name} />
|
|
338
|
+
<button type="button" onClick={() => handleRemovePendingImage(img.id)} title="Remove">
|
|
339
|
+
x
|
|
340
|
+
</button>
|
|
341
|
+
</div>
|
|
342
|
+
))}
|
|
343
|
+
</div>
|
|
344
|
+
)}
|
|
345
|
+
<textarea
|
|
346
|
+
ref={inputRef}
|
|
347
|
+
value={inputValue}
|
|
348
|
+
onChange={(e) => setInputValue(e.target.value)}
|
|
349
|
+
onKeyDown={handleKeyDown}
|
|
350
|
+
placeholder="Type your message..."
|
|
351
|
+
rows={2}
|
|
352
|
+
/>
|
|
353
|
+
<div className="input-actions">
|
|
354
|
+
<div className="input-actions-left">
|
|
355
|
+
{showAttachButton && (
|
|
356
|
+
<button type="button" className="send-btn" title="Attach image" onClick={handleAttachClick}>
|
|
357
|
+
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" style={{ transform: 'rotate(-45deg)' }}>
|
|
358
|
+
<path d="M21.44 11.05l-9.19 9.19a6 6 0 0 1-8.49-8.49l9.19-9.19a4 4 0 0 1 5.66 5.66l-9.2 9.19a2 2 0 0 1-2.83-2.83l8.49-8.48"></path>
|
|
359
|
+
</svg>
|
|
360
|
+
</button>
|
|
361
|
+
)}
|
|
239
362
|
</div>
|
|
240
363
|
<div className="input-actions-right">
|
|
241
364
|
{isProcessing ? (
|
|
@@ -250,12 +373,12 @@ export function ChatPage({ messages, isProcessing, processingStartTime, currentT
|
|
|
250
373
|
</svg>
|
|
251
374
|
</button>
|
|
252
375
|
) : (
|
|
253
|
-
<button
|
|
254
|
-
type="submit"
|
|
255
|
-
className="send-btn"
|
|
256
|
-
disabled={!inputValue.trim() || !!questionRequest || !!approvalRequest}
|
|
257
|
-
title="Send"
|
|
258
|
-
>
|
|
376
|
+
<button
|
|
377
|
+
type="submit"
|
|
378
|
+
className="send-btn"
|
|
379
|
+
disabled={(!inputValue.trim() && pendingImages.length === 0) || !!questionRequest || !!approvalRequest}
|
|
380
|
+
title="Send"
|
|
381
|
+
>
|
|
259
382
|
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.5" strokeLinecap="round" strokeLinejoin="round">
|
|
260
383
|
<line x1="12" y1="19" x2="12" y2="5"></line>
|
|
261
384
|
<polyline points="5 12 12 5 19 12"></polyline>
|
|
@@ -11,11 +11,11 @@ interface RecentProject {
|
|
|
11
11
|
lastOpened: number;
|
|
12
12
|
}
|
|
13
13
|
|
|
14
|
-
interface HomePageProps {
|
|
15
|
-
onStartChat: (message: string) => void;
|
|
16
|
-
onOpenProject: (path: string) => void;
|
|
17
|
-
sidebarProps: SidebarProps;
|
|
18
|
-
}
|
|
14
|
+
interface HomePageProps {
|
|
15
|
+
onStartChat: (message: string, images?: import("../../utils/images").ImageAttachment[]) => void;
|
|
16
|
+
onOpenProject: (path: string) => void;
|
|
17
|
+
sidebarProps: SidebarProps;
|
|
18
|
+
}
|
|
19
19
|
|
|
20
20
|
function formatRelativeTime(timestamp: number): string {
|
|
21
21
|
const now = Date.now();
|
|
@@ -118,4 +118,4 @@ export function HomePage({ onStartChat, onOpenProject, sidebarProps }: HomePageP
|
|
|
118
118
|
</Modal>
|
|
119
119
|
</div>
|
|
120
120
|
);
|
|
121
|
-
}
|
|
121
|
+
}
|