mirror of
https://github.com/siteboon/claudecodeui.git
synced 2026-02-23 17:17:41 +00:00
Compare commits
1 Commits
main
...
fix/mobile
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f670f552ac |
@@ -21,10 +21,6 @@ PORT=3001
|
|||||||
#Frontend port
|
#Frontend port
|
||||||
VITE_PORT=5173
|
VITE_PORT=5173
|
||||||
|
|
||||||
# Host/IP to bind servers to (default: 0.0.0.0 for all interfaces)
|
|
||||||
# Use 127.0.0.1 to restrict to localhost only
|
|
||||||
HOST=0.0.0.0
|
|
||||||
|
|
||||||
# Uncomment the following line if you have a custom claude cli path other than the default "claude"
|
# Uncomment the following line if you have a custom claude cli path other than the default "claude"
|
||||||
# CLAUDE_CLI_PATH=claude
|
# CLAUDE_CLI_PATH=claude
|
||||||
|
|
||||||
|
|||||||
24
CHANGELOG.md
24
CHANGELOG.md
@@ -1,24 +0,0 @@
|
|||||||
# Changelog
|
|
||||||
|
|
||||||
All notable changes to CloudCLI UI will be documented in this file.
|
|
||||||
|
|
||||||
|
|
||||||
## [1.19.0](https://github.com/siteboon/claudecodeui/compare/v1.18.2...v1.19.0) (2026-02-23)
|
|
||||||
|
|
||||||
### New Features
|
|
||||||
|
|
||||||
* add HOST environment variable for configurable bind address ([#360](https://github.com/siteboon/claudecodeui/issues/360)) ([cccd915](https://github.com/siteboon/claudecodeui/commit/cccd915c336192216b6e6f68e2b5f3ece0ccf966))
|
|
||||||
* subagent tool grouping ([#398](https://github.com/siteboon/claudecodeui/issues/398)) ([0207a1f](https://github.com/siteboon/claudecodeui/commit/0207a1f3a3c87f1c6c1aee8213be999b23289386))
|
|
||||||
|
|
||||||
### Bug Fixes
|
|
||||||
|
|
||||||
* **macos:** fix node-pty posix_spawnp error with postinstall script ([#347](https://github.com/siteboon/claudecodeui/issues/347)) ([38a593c](https://github.com/siteboon/claudecodeui/commit/38a593c97fdb2bb7f051e09e8e99c16035448655)), closes [#284](https://github.com/siteboon/claudecodeui/issues/284)
|
|
||||||
* slash commands with arguments bypass command execution ([#392](https://github.com/siteboon/claudecodeui/issues/392)) ([597e9c5](https://github.com/siteboon/claudecodeui/commit/597e9c54b76e7c6cd1947299c668c78d24019cab))
|
|
||||||
|
|
||||||
### Refactoring
|
|
||||||
|
|
||||||
* **releases:** Create a contributing guide and proper release notes using a release-it plugin ([fc369d0](https://github.com/siteboon/claudecodeui/commit/fc369d047e13cba9443fe36c0b6bb2ce3beaf61c))
|
|
||||||
|
|
||||||
### Maintenance
|
|
||||||
|
|
||||||
* update @anthropic-ai/claude-agent-sdk to version 0.1.77 in package-lock.json ([#410](https://github.com/siteboon/claudecodeui/issues/410)) ([7ccbc8d](https://github.com/siteboon/claudecodeui/commit/7ccbc8d92d440e18c157b656c9ea2635044a64f6))
|
|
||||||
12
package-lock.json
generated
12
package-lock.json
generated
@@ -1,12 +1,12 @@
|
|||||||
{
|
{
|
||||||
"name": "@siteboon/claude-code-ui",
|
"name": "@siteboon/claude-code-ui",
|
||||||
"version": "1.19.0",
|
"version": "1.18.2",
|
||||||
"lockfileVersion": 3,
|
"lockfileVersion": 3,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "@siteboon/claude-code-ui",
|
"name": "@siteboon/claude-code-ui",
|
||||||
"version": "1.19.0",
|
"version": "1.18.2",
|
||||||
"hasInstallScript": true,
|
"hasInstallScript": true,
|
||||||
"license": "GPL-3.0",
|
"license": "GPL-3.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
@@ -114,9 +114,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@anthropic-ai/claude-agent-sdk": {
|
"node_modules/@anthropic-ai/claude-agent-sdk": {
|
||||||
"version": "0.1.77",
|
"version": "0.1.71",
|
||||||
"resolved": "https://registry.npmjs.org/@anthropic-ai/claude-agent-sdk/-/claude-agent-sdk-0.1.77.tgz",
|
"resolved": "https://registry.npmjs.org/@anthropic-ai/claude-agent-sdk/-/claude-agent-sdk-0.1.71.tgz",
|
||||||
"integrity": "sha512-ZEjWQtkoB2MEY6K16DWMmF+8OhywAynH0m08V265cerbZ8xPD/2Ng2jPzbbO40mPeFSsMDJboShL+a3aObP0Jg==",
|
"integrity": "sha512-O34SQCEsdU11Z2uy30GaJGRLdRbEwEvaQs8APywHVOW/EdIGE0rS/AE4V6p9j45/j5AFwh2USZWlbz5NTlLnrw==",
|
||||||
"license": "SEE LICENSE IN README.md",
|
"license": "SEE LICENSE IN README.md",
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=18.0.0"
|
"node": ">=18.0.0"
|
||||||
@@ -132,7 +132,7 @@
|
|||||||
"@img/sharp-win32-x64": "^0.33.5"
|
"@img/sharp-win32-x64": "^0.33.5"
|
||||||
},
|
},
|
||||||
"peerDependencies": {
|
"peerDependencies": {
|
||||||
"zod": "^3.25.0 || ^4.0.0"
|
"zod": "^3.24.1 || ^4.0.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@anthropic-ai/claude-agent-sdk/node_modules/@img/sharp-libvips-linuxmusl-arm64": {
|
"node_modules/@anthropic-ai/claude-agent-sdk/node_modules/@img/sharp-libvips-linuxmusl-arm64": {
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@siteboon/claude-code-ui",
|
"name": "@siteboon/claude-code-ui",
|
||||||
"version": "1.19.0",
|
"version": "1.18.2",
|
||||||
"description": "A web-based UI for Claude Code CLI",
|
"description": "A web-based UI for Claude Code CLI",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"main": "server/index.js",
|
"main": "server/index.js",
|
||||||
|
|||||||
@@ -250,13 +250,7 @@ function getAllSessions() {
|
|||||||
* @returns {Object} Transformed message ready for WebSocket
|
* @returns {Object} Transformed message ready for WebSocket
|
||||||
*/
|
*/
|
||||||
function transformMessage(sdkMessage) {
|
function transformMessage(sdkMessage) {
|
||||||
// Extract parent_tool_use_id for subagent tool grouping
|
// Pass-through; SDK messages match frontend format.
|
||||||
if (sdkMessage.parent_tool_use_id) {
|
|
||||||
return {
|
|
||||||
...sdkMessage,
|
|
||||||
parentToolUseId: sdkMessage.parent_tool_use_id
|
|
||||||
};
|
|
||||||
}
|
|
||||||
return sdkMessage;
|
return sdkMessage;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1886,9 +1886,6 @@ async function getFileTree(dirPath, maxDepth = 3, currentDepth = 0, showHidden =
|
|||||||
}
|
}
|
||||||
|
|
||||||
const PORT = process.env.PORT || 3001;
|
const PORT = process.env.PORT || 3001;
|
||||||
const HOST = process.env.HOST || '0.0.0.0';
|
|
||||||
// Show localhost in URL when binding to all interfaces (0.0.0.0 isn't a connectable address)
|
|
||||||
const DISPLAY_HOST = HOST === '0.0.0.0' ? 'localhost' : HOST;
|
|
||||||
|
|
||||||
// Initialize database and start server
|
// Initialize database and start server
|
||||||
async function startServer() {
|
async function startServer() {
|
||||||
@@ -1908,7 +1905,7 @@ async function startServer() {
|
|||||||
console.log(`${c.warn('[WARN]')} Note: Requests will be proxied to Vite dev server at ${c.dim('http://localhost:' + (process.env.VITE_PORT || 5173))}`);
|
console.log(`${c.warn('[WARN]')} Note: Requests will be proxied to Vite dev server at ${c.dim('http://localhost:' + (process.env.VITE_PORT || 5173))}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
server.listen(PORT, HOST, async () => {
|
server.listen(PORT, '0.0.0.0', async () => {
|
||||||
const appInstallPath = path.join(__dirname, '..');
|
const appInstallPath = path.join(__dirname, '..');
|
||||||
|
|
||||||
console.log('');
|
console.log('');
|
||||||
@@ -1916,7 +1913,7 @@ async function startServer() {
|
|||||||
console.log(` ${c.bright('Claude Code UI Server - Ready')}`);
|
console.log(` ${c.bright('Claude Code UI Server - Ready')}`);
|
||||||
console.log(c.dim('═'.repeat(63)));
|
console.log(c.dim('═'.repeat(63)));
|
||||||
console.log('');
|
console.log('');
|
||||||
console.log(`${c.info('[INFO]')} Server URL: ${c.bright('http://' + DISPLAY_HOST + ':' + PORT)}`);
|
console.log(`${c.info('[INFO]')} Server URL: ${c.bright('http://0.0.0.0:' + PORT)}`);
|
||||||
console.log(`${c.info('[INFO]')} Installed at: ${c.dim(appInstallPath)}`);
|
console.log(`${c.info('[INFO]')} Installed at: ${c.dim(appInstallPath)}`);
|
||||||
console.log(`${c.tip('[TIP]')} Run "cloudcli status" for full configuration details`);
|
console.log(`${c.tip('[TIP]')} Run "cloudcli status" for full configuration details`);
|
||||||
console.log('');
|
console.log('');
|
||||||
|
|||||||
@@ -889,81 +889,22 @@ async function parseJsonlSessions(filePath) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Parse an agent JSONL file and extract tool uses
|
|
||||||
async function parseAgentTools(filePath) {
|
|
||||||
const tools = [];
|
|
||||||
|
|
||||||
try {
|
|
||||||
const fileStream = fsSync.createReadStream(filePath);
|
|
||||||
const rl = readline.createInterface({
|
|
||||||
input: fileStream,
|
|
||||||
crlfDelay: Infinity
|
|
||||||
});
|
|
||||||
|
|
||||||
for await (const line of rl) {
|
|
||||||
if (line.trim()) {
|
|
||||||
try {
|
|
||||||
const entry = JSON.parse(line);
|
|
||||||
// Look for assistant messages with tool_use
|
|
||||||
if (entry.message?.role === 'assistant' && Array.isArray(entry.message?.content)) {
|
|
||||||
for (const part of entry.message.content) {
|
|
||||||
if (part.type === 'tool_use') {
|
|
||||||
tools.push({
|
|
||||||
toolId: part.id,
|
|
||||||
toolName: part.name,
|
|
||||||
toolInput: part.input,
|
|
||||||
timestamp: entry.timestamp
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// Look for tool results
|
|
||||||
if (entry.message?.role === 'user' && Array.isArray(entry.message?.content)) {
|
|
||||||
for (const part of entry.message.content) {
|
|
||||||
if (part.type === 'tool_result') {
|
|
||||||
// Find the matching tool and add result
|
|
||||||
const tool = tools.find(t => t.toolId === part.tool_use_id);
|
|
||||||
if (tool) {
|
|
||||||
tool.toolResult = {
|
|
||||||
content: typeof part.content === 'string' ? part.content :
|
|
||||||
Array.isArray(part.content) ? part.content.map(c => c.text || '').join('\n') :
|
|
||||||
JSON.stringify(part.content),
|
|
||||||
isError: Boolean(part.is_error)
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch (parseError) {
|
|
||||||
// Skip malformed lines
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.warn(`Error parsing agent file ${filePath}:`, error.message);
|
|
||||||
}
|
|
||||||
|
|
||||||
return tools;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Get messages for a specific session with pagination support
|
// Get messages for a specific session with pagination support
|
||||||
async function getSessionMessages(projectName, sessionId, limit = null, offset = 0) {
|
async function getSessionMessages(projectName, sessionId, limit = null, offset = 0) {
|
||||||
const projectDir = path.join(os.homedir(), '.claude', 'projects', projectName);
|
const projectDir = path.join(os.homedir(), '.claude', 'projects', projectName);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const files = await fs.readdir(projectDir);
|
const files = await fs.readdir(projectDir);
|
||||||
// agent-*.jsonl files contain subagent tool history - we'll process them separately
|
// agent-*.jsonl files contain session start data at this point. This needs to be revisited
|
||||||
|
// periodically to make sure only accurate data is there and no new functionality is added there
|
||||||
const jsonlFiles = files.filter(file => file.endsWith('.jsonl') && !file.startsWith('agent-'));
|
const jsonlFiles = files.filter(file => file.endsWith('.jsonl') && !file.startsWith('agent-'));
|
||||||
const agentFiles = files.filter(file => file.endsWith('.jsonl') && file.startsWith('agent-'));
|
|
||||||
|
|
||||||
if (jsonlFiles.length === 0) {
|
if (jsonlFiles.length === 0) {
|
||||||
return { messages: [], total: 0, hasMore: false };
|
return { messages: [], total: 0, hasMore: false };
|
||||||
}
|
}
|
||||||
|
|
||||||
const messages = [];
|
const messages = [];
|
||||||
// Map of agentId -> tools for subagent tool grouping
|
|
||||||
const agentToolsCache = new Map();
|
|
||||||
|
|
||||||
// Process all JSONL files to find messages for this session
|
// Process all JSONL files to find messages for this session
|
||||||
for (const file of jsonlFiles) {
|
for (const file of jsonlFiles) {
|
||||||
const jsonlFile = path.join(projectDir, file);
|
const jsonlFile = path.join(projectDir, file);
|
||||||
@@ -972,7 +913,7 @@ async function getSessionMessages(projectName, sessionId, limit = null, offset =
|
|||||||
input: fileStream,
|
input: fileStream,
|
||||||
crlfDelay: Infinity
|
crlfDelay: Infinity
|
||||||
});
|
});
|
||||||
|
|
||||||
for await (const line of rl) {
|
for await (const line of rl) {
|
||||||
if (line.trim()) {
|
if (line.trim()) {
|
||||||
try {
|
try {
|
||||||
@@ -986,55 +927,26 @@ async function getSessionMessages(projectName, sessionId, limit = null, offset =
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Collect agentIds from Task tool results
|
|
||||||
const agentIds = new Set();
|
|
||||||
for (const message of messages) {
|
|
||||||
if (message.toolUseResult?.agentId) {
|
|
||||||
agentIds.add(message.toolUseResult.agentId);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Load agent tools for each agentId found
|
|
||||||
for (const agentId of agentIds) {
|
|
||||||
const agentFileName = `agent-${agentId}.jsonl`;
|
|
||||||
if (agentFiles.includes(agentFileName)) {
|
|
||||||
const agentFilePath = path.join(projectDir, agentFileName);
|
|
||||||
const tools = await parseAgentTools(agentFilePath);
|
|
||||||
agentToolsCache.set(agentId, tools);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Attach agent tools to their parent Task messages
|
|
||||||
for (const message of messages) {
|
|
||||||
if (message.toolUseResult?.agentId) {
|
|
||||||
const agentId = message.toolUseResult.agentId;
|
|
||||||
const agentTools = agentToolsCache.get(agentId);
|
|
||||||
if (agentTools && agentTools.length > 0) {
|
|
||||||
message.subagentTools = agentTools;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Sort messages by timestamp
|
// Sort messages by timestamp
|
||||||
const sortedMessages = messages.sort((a, b) =>
|
const sortedMessages = messages.sort((a, b) =>
|
||||||
new Date(a.timestamp || 0) - new Date(b.timestamp || 0)
|
new Date(a.timestamp || 0) - new Date(b.timestamp || 0)
|
||||||
);
|
);
|
||||||
|
|
||||||
const total = sortedMessages.length;
|
const total = sortedMessages.length;
|
||||||
|
|
||||||
// If no limit is specified, return all messages (backward compatibility)
|
// If no limit is specified, return all messages (backward compatibility)
|
||||||
if (limit === null) {
|
if (limit === null) {
|
||||||
return sortedMessages;
|
return sortedMessages;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Apply pagination - for recent messages, we need to slice from the end
|
// Apply pagination - for recent messages, we need to slice from the end
|
||||||
// offset 0 should give us the most recent messages
|
// offset 0 should give us the most recent messages
|
||||||
const startIndex = Math.max(0, total - offset - limit);
|
const startIndex = Math.max(0, total - offset - limit);
|
||||||
const endIndex = total - offset;
|
const endIndex = total - offset;
|
||||||
const paginatedMessages = sortedMessages.slice(startIndex, endIndex);
|
const paginatedMessages = sortedMessages.slice(startIndex, endIndex);
|
||||||
const hasMore = startIndex > 0;
|
const hasMore = startIndex > 0;
|
||||||
|
|
||||||
return {
|
return {
|
||||||
messages: paginatedMessages,
|
messages: paginatedMessages,
|
||||||
total,
|
total,
|
||||||
|
|||||||
@@ -63,5 +63,5 @@ export const CODEX_MODELS = {
|
|||||||
{ value: 'o4-mini', label: 'O4-mini' }
|
{ value: 'o4-mini', label: 'O4-mini' }
|
||||||
],
|
],
|
||||||
|
|
||||||
DEFAULT: 'gpt-5.3-codex'
|
DEFAULT: 'gpt-5.2'
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -106,7 +106,7 @@ export default function AppContent() {
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<div className={`flex-1 flex flex-col min-w-0 ${isMobile ? 'pb-mobile-nav' : ''}`}>
|
<div className={`flex-1 flex flex-col min-w-0 ${isMobile && !isInputFocused ? 'pb-mobile-nav' : ''}`}>
|
||||||
<MainContent
|
<MainContent
|
||||||
selectedProject={selectedProject}
|
selectedProject={selectedProject}
|
||||||
selectedSession={selectedSession}
|
selectedSession={selectedSession}
|
||||||
|
|||||||
@@ -271,14 +271,13 @@ export function useChatComposerState({
|
|||||||
}, [setChatMessages]);
|
}, [setChatMessages]);
|
||||||
|
|
||||||
const executeCommand = useCallback(
|
const executeCommand = useCallback(
|
||||||
async (command: SlashCommand, rawInput?: string) => {
|
async (command: SlashCommand) => {
|
||||||
if (!command || !selectedProject) {
|
if (!command || !selectedProject) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const effectiveInput = rawInput ?? input;
|
const commandMatch = input.match(new RegExp(`${escapeRegExp(command.name)}\\s*(.*)`));
|
||||||
const commandMatch = effectiveInput.match(new RegExp(`${escapeRegExp(command.name)}\\s*(.*)`));
|
|
||||||
const args =
|
const args =
|
||||||
commandMatch && commandMatch[1] ? commandMatch[1].trim().split(/\s+/) : [];
|
commandMatch && commandMatch[1] ? commandMatch[1].trim().split(/\s+/) : [];
|
||||||
|
|
||||||
@@ -352,7 +351,6 @@ export function useChatComposerState({
|
|||||||
);
|
);
|
||||||
|
|
||||||
const {
|
const {
|
||||||
slashCommands,
|
|
||||||
slashCommandsCount,
|
slashCommandsCount,
|
||||||
filteredCommands,
|
filteredCommands,
|
||||||
frequentCommands,
|
frequentCommands,
|
||||||
@@ -475,28 +473,6 @@ export function useChatComposerState({
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Intercept slash commands: if input starts with /commandName, execute as command with args
|
|
||||||
const trimmedInput = currentInput.trim();
|
|
||||||
if (trimmedInput.startsWith('/')) {
|
|
||||||
const firstSpace = trimmedInput.indexOf(' ');
|
|
||||||
const commandName = firstSpace > 0 ? trimmedInput.slice(0, firstSpace) : trimmedInput;
|
|
||||||
const matchedCommand = slashCommands.find((cmd: SlashCommand) => cmd.name === commandName);
|
|
||||||
if (matchedCommand) {
|
|
||||||
executeCommand(matchedCommand, trimmedInput);
|
|
||||||
setInput('');
|
|
||||||
inputValueRef.current = '';
|
|
||||||
setAttachedImages([]);
|
|
||||||
setUploadingImages(new Map());
|
|
||||||
setImageErrors(new Map());
|
|
||||||
resetCommandMenuState();
|
|
||||||
setIsTextareaExpanded(false);
|
|
||||||
if (textareaRef.current) {
|
|
||||||
textareaRef.current.style.height = 'auto';
|
|
||||||
}
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
let messageContent = currentInput;
|
let messageContent = currentInput;
|
||||||
const selectedThinkingMode = thinkingModes.find((mode: { id: string; prefix?: string }) => mode.id === thinkingMode);
|
const selectedThinkingMode = thinkingModes.find((mode: { id: string; prefix?: string }) => mode.id === thinkingMode);
|
||||||
if (selectedThinkingMode && selectedThinkingMode.prefix) {
|
if (selectedThinkingMode && selectedThinkingMode.prefix) {
|
||||||
@@ -663,7 +639,6 @@ export function useChatComposerState({
|
|||||||
codexModel,
|
codexModel,
|
||||||
currentSessionId,
|
currentSessionId,
|
||||||
cursorModel,
|
cursorModel,
|
||||||
executeCommand,
|
|
||||||
isLoading,
|
isLoading,
|
||||||
onSessionActive,
|
onSessionActive,
|
||||||
pendingViewSessionRef,
|
pendingViewSessionRef,
|
||||||
@@ -679,7 +654,6 @@ export function useChatComposerState({
|
|||||||
setClaudeStatus,
|
setClaudeStatus,
|
||||||
setIsLoading,
|
setIsLoading,
|
||||||
setIsUserScrolledUp,
|
setIsUserScrolledUp,
|
||||||
slashCommands,
|
|
||||||
thinkingMode,
|
thinkingMode,
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
@@ -929,11 +903,8 @@ export function useChatComposerState({
|
|||||||
[sendMessage, setClaudeStatus, setPendingPermissionRequests],
|
[sendMessage, setClaudeStatus, setPendingPermissionRequests],
|
||||||
);
|
);
|
||||||
|
|
||||||
const [isInputFocused, setIsInputFocused] = useState(false);
|
|
||||||
|
|
||||||
const handleInputFocusChange = useCallback(
|
const handleInputFocusChange = useCallback(
|
||||||
(focused: boolean) => {
|
(focused: boolean) => {
|
||||||
setIsInputFocused(focused);
|
|
||||||
onInputFocusChange?.(focused);
|
onInputFocusChange?.(focused);
|
||||||
},
|
},
|
||||||
[onInputFocusChange],
|
[onInputFocusChange],
|
||||||
@@ -982,6 +953,5 @@ export function useChatComposerState({
|
|||||||
handlePermissionDecision,
|
handlePermissionDecision,
|
||||||
handleGrantToolPermission,
|
handleGrantToolPermission,
|
||||||
handleInputFocusChange,
|
handleInputFocusChange,
|
||||||
isInputFocused,
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -336,43 +336,9 @@ export function useChatRealtimeHandlers({
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (structuredMessageData && Array.isArray(structuredMessageData.content)) {
|
if (structuredMessageData && Array.isArray(structuredMessageData.content)) {
|
||||||
const parentToolUseId = rawStructuredData?.parentToolUseId;
|
|
||||||
|
|
||||||
structuredMessageData.content.forEach((part: any) => {
|
structuredMessageData.content.forEach((part: any) => {
|
||||||
if (part.type === 'tool_use') {
|
if (part.type === 'tool_use') {
|
||||||
const toolInput = part.input ? JSON.stringify(part.input, null, 2) : '';
|
const toolInput = part.input ? JSON.stringify(part.input, null, 2) : '';
|
||||||
|
|
||||||
// Check if this is a child tool from a subagent
|
|
||||||
if (parentToolUseId) {
|
|
||||||
setChatMessages((previous) =>
|
|
||||||
previous.map((message) => {
|
|
||||||
if (message.toolId === parentToolUseId && message.isSubagentContainer) {
|
|
||||||
const childTool = {
|
|
||||||
toolId: part.id,
|
|
||||||
toolName: part.name,
|
|
||||||
toolInput: part.input,
|
|
||||||
toolResult: null,
|
|
||||||
timestamp: new Date(),
|
|
||||||
};
|
|
||||||
const existingChildren = message.subagentState?.childTools || [];
|
|
||||||
return {
|
|
||||||
...message,
|
|
||||||
subagentState: {
|
|
||||||
childTools: [...existingChildren, childTool],
|
|
||||||
currentToolIndex: existingChildren.length,
|
|
||||||
isComplete: false,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
}
|
|
||||||
return message;
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check if this is a Task tool (subagent container)
|
|
||||||
const isSubagentContainer = part.name === 'Task';
|
|
||||||
|
|
||||||
setChatMessages((previous) => [
|
setChatMessages((previous) => [
|
||||||
...previous,
|
...previous,
|
||||||
{
|
{
|
||||||
@@ -384,10 +350,6 @@ export function useChatRealtimeHandlers({
|
|||||||
toolInput,
|
toolInput,
|
||||||
toolId: part.id,
|
toolId: part.id,
|
||||||
toolResult: null,
|
toolResult: null,
|
||||||
isSubagentContainer,
|
|
||||||
subagentState: isSubagentContainer
|
|
||||||
? { childTools: [], currentToolIndex: -1, isComplete: false }
|
|
||||||
: undefined,
|
|
||||||
},
|
},
|
||||||
]);
|
]);
|
||||||
return;
|
return;
|
||||||
@@ -420,8 +382,6 @@ export function useChatRealtimeHandlers({
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (structuredMessageData?.role === 'user' && Array.isArray(structuredMessageData.content)) {
|
if (structuredMessageData?.role === 'user' && Array.isArray(structuredMessageData.content)) {
|
||||||
const parentToolUseId = rawStructuredData?.parentToolUseId;
|
|
||||||
|
|
||||||
structuredMessageData.content.forEach((part: any) => {
|
structuredMessageData.content.forEach((part: any) => {
|
||||||
if (part.type !== 'tool_result') {
|
if (part.type !== 'tool_result') {
|
||||||
return;
|
return;
|
||||||
@@ -429,32 +389,8 @@ export function useChatRealtimeHandlers({
|
|||||||
|
|
||||||
setChatMessages((previous) =>
|
setChatMessages((previous) =>
|
||||||
previous.map((message) => {
|
previous.map((message) => {
|
||||||
// Handle child tool results (route to parent's subagentState)
|
|
||||||
if (parentToolUseId && message.toolId === parentToolUseId && message.isSubagentContainer) {
|
|
||||||
return {
|
|
||||||
...message,
|
|
||||||
subagentState: {
|
|
||||||
...message.subagentState!,
|
|
||||||
childTools: message.subagentState!.childTools.map((child) => {
|
|
||||||
if (child.toolId === part.tool_use_id) {
|
|
||||||
return {
|
|
||||||
...child,
|
|
||||||
toolResult: {
|
|
||||||
content: part.content,
|
|
||||||
isError: part.is_error,
|
|
||||||
timestamp: new Date(),
|
|
||||||
},
|
|
||||||
};
|
|
||||||
}
|
|
||||||
return child;
|
|
||||||
}),
|
|
||||||
},
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
// Handle normal tool results (including parent Task tool completion)
|
|
||||||
if (message.isToolUse && message.toolId === part.tool_use_id) {
|
if (message.isToolUse && message.toolId === part.tool_use_id) {
|
||||||
const result = {
|
return {
|
||||||
...message,
|
...message,
|
||||||
toolResult: {
|
toolResult: {
|
||||||
content: part.content,
|
content: part.content,
|
||||||
@@ -462,14 +398,6 @@ export function useChatRealtimeHandlers({
|
|||||||
timestamp: new Date(),
|
timestamp: new Date(),
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
// Mark subagent as complete when parent Task receives its result
|
|
||||||
if (message.isSubagentContainer && message.subagentState) {
|
|
||||||
result.subagentState = {
|
|
||||||
...message.subagentState,
|
|
||||||
isComplete: true,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
return result;
|
|
||||||
}
|
}
|
||||||
return message;
|
return message;
|
||||||
}),
|
}),
|
||||||
|
|||||||
@@ -22,7 +22,7 @@ interface UseSlashCommandsOptions {
|
|||||||
input: string;
|
input: string;
|
||||||
setInput: Dispatch<SetStateAction<string>>;
|
setInput: Dispatch<SetStateAction<string>>;
|
||||||
textareaRef: RefObject<HTMLTextAreaElement>;
|
textareaRef: RefObject<HTMLTextAreaElement>;
|
||||||
onExecuteCommand: (command: SlashCommand, rawInput?: string) => void | Promise<void>;
|
onExecuteCommand: (command: SlashCommand) => void | Promise<void>;
|
||||||
}
|
}
|
||||||
|
|
||||||
const getCommandHistoryKey = (projectName: string) => `command_history_${projectName}`;
|
const getCommandHistoryKey = (projectName: string) => `command_history_${projectName}`;
|
||||||
|
|||||||
@@ -1,8 +1,7 @@
|
|||||||
import React, { memo, useMemo, useCallback } from 'react';
|
import React, { memo, useMemo, useCallback } from 'react';
|
||||||
import { getToolConfig } from './configs/toolConfigs';
|
import { getToolConfig } from './configs/toolConfigs';
|
||||||
import { OneLineDisplay, CollapsibleDisplay, DiffViewer, MarkdownContent, FileListContent, TodoListContent, TaskListContent, TextContent, QuestionAnswerContent, SubagentContainer } from './components';
|
import { OneLineDisplay, CollapsibleDisplay, DiffViewer, MarkdownContent, FileListContent, TodoListContent, TaskListContent, TextContent, QuestionAnswerContent } from './components';
|
||||||
import type { Project } from '../../../types/app';
|
import type { Project } from '../../../types/app';
|
||||||
import type { SubagentChildTool } from '../types/types';
|
|
||||||
|
|
||||||
type DiffLine = {
|
type DiffLine = {
|
||||||
type: string;
|
type: string;
|
||||||
@@ -22,12 +21,6 @@ interface ToolRendererProps {
|
|||||||
autoExpandTools?: boolean;
|
autoExpandTools?: boolean;
|
||||||
showRawParameters?: boolean;
|
showRawParameters?: boolean;
|
||||||
rawToolInput?: string;
|
rawToolInput?: string;
|
||||||
isSubagentContainer?: boolean;
|
|
||||||
subagentState?: {
|
|
||||||
childTools: SubagentChildTool[];
|
|
||||||
currentToolIndex: number;
|
|
||||||
isComplete: boolean;
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function getToolCategory(toolName: string): string {
|
function getToolCategory(toolName: string): string {
|
||||||
@@ -57,24 +50,8 @@ export const ToolRenderer: React.FC<ToolRendererProps> = memo(({
|
|||||||
selectedProject,
|
selectedProject,
|
||||||
autoExpandTools = false,
|
autoExpandTools = false,
|
||||||
showRawParameters = false,
|
showRawParameters = false,
|
||||||
rawToolInput,
|
rawToolInput
|
||||||
isSubagentContainer,
|
|
||||||
subagentState
|
|
||||||
}) => {
|
}) => {
|
||||||
// Route subagent containers to dedicated component
|
|
||||||
if (isSubagentContainer && subagentState) {
|
|
||||||
if (mode === 'result') {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
return (
|
|
||||||
<SubagentContainer
|
|
||||||
toolInput={toolInput}
|
|
||||||
toolResult={toolResult}
|
|
||||||
subagentState={subagentState}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
const config = getToolConfig(toolName);
|
const config = getToolConfig(toolName);
|
||||||
const displayConfig: any = mode === 'input' ? config.input : config.result;
|
const displayConfig: any = mode === 'input' ? config.input : config.result;
|
||||||
|
|
||||||
|
|||||||
@@ -1,180 +0,0 @@
|
|||||||
import React from 'react';
|
|
||||||
import { CollapsibleSection } from './CollapsibleSection';
|
|
||||||
import type { SubagentChildTool } from '../../types/types';
|
|
||||||
|
|
||||||
interface SubagentContainerProps {
|
|
||||||
toolInput: unknown;
|
|
||||||
toolResult?: { content?: unknown; isError?: boolean } | null;
|
|
||||||
subagentState: {
|
|
||||||
childTools: SubagentChildTool[];
|
|
||||||
currentToolIndex: number;
|
|
||||||
isComplete: boolean;
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
const getCompactToolDisplay = (toolName: string, toolInput: unknown): string => {
|
|
||||||
const input = typeof toolInput === 'string' ? (() => {
|
|
||||||
try { return JSON.parse(toolInput); } catch { return {}; }
|
|
||||||
})() : (toolInput || {});
|
|
||||||
|
|
||||||
switch (toolName) {
|
|
||||||
case 'Read':
|
|
||||||
case 'Write':
|
|
||||||
case 'Edit':
|
|
||||||
case 'ApplyPatch':
|
|
||||||
return input.file_path?.split('/').pop() || input.file_path || '';
|
|
||||||
case 'Grep':
|
|
||||||
case 'Glob':
|
|
||||||
return input.pattern || '';
|
|
||||||
case 'Bash':
|
|
||||||
const cmd = input.command || '';
|
|
||||||
return cmd.length > 40 ? `${cmd.slice(0, 40)}...` : cmd;
|
|
||||||
case 'Task':
|
|
||||||
return input.description || input.subagent_type || '';
|
|
||||||
case 'WebFetch':
|
|
||||||
case 'WebSearch':
|
|
||||||
return input.url || input.query || '';
|
|
||||||
default:
|
|
||||||
return '';
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
export const SubagentContainer: React.FC<SubagentContainerProps> = ({
|
|
||||||
toolInput,
|
|
||||||
toolResult,
|
|
||||||
subagentState,
|
|
||||||
}) => {
|
|
||||||
const parsedInput = typeof toolInput === 'string' ? (() => {
|
|
||||||
try { return JSON.parse(toolInput); } catch { return {}; }
|
|
||||||
})() : (toolInput || {});
|
|
||||||
|
|
||||||
const subagentType = parsedInput?.subagent_type || 'Agent';
|
|
||||||
const description = parsedInput?.description || 'Running task';
|
|
||||||
const prompt = parsedInput?.prompt || '';
|
|
||||||
const { childTools, currentToolIndex, isComplete } = subagentState;
|
|
||||||
const currentTool = currentToolIndex >= 0 ? childTools[currentToolIndex] : null;
|
|
||||||
|
|
||||||
const title = `Subagent / ${subagentType}: ${description}`;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="border-l-2 border-l-purple-500 dark:border-l-purple-400 pl-3 py-0.5 my-1">
|
|
||||||
<CollapsibleSection
|
|
||||||
title={title}
|
|
||||||
toolName="Task"
|
|
||||||
open={false}
|
|
||||||
>
|
|
||||||
{/* Prompt/request to the subagent */}
|
|
||||||
{prompt && (
|
|
||||||
<div className="text-xs text-gray-600 dark:text-gray-400 mb-2 whitespace-pre-wrap break-words line-clamp-4">
|
|
||||||
{prompt}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{/* Current tool indicator (while running) */}
|
|
||||||
{currentTool && !isComplete && (
|
|
||||||
<div className="flex items-center gap-1.5 text-xs text-gray-500 dark:text-gray-400 mt-1">
|
|
||||||
<span className="animate-pulse w-1.5 h-1.5 rounded-full bg-purple-500 dark:bg-purple-400 flex-shrink-0" />
|
|
||||||
<span className="text-gray-400 dark:text-gray-500">Currently:</span>
|
|
||||||
<span className="font-medium text-gray-600 dark:text-gray-300">{currentTool.toolName}</span>
|
|
||||||
{getCompactToolDisplay(currentTool.toolName, currentTool.toolInput) && (
|
|
||||||
<>
|
|
||||||
<span className="text-gray-300 dark:text-gray-600">/</span>
|
|
||||||
<span className="font-mono truncate text-gray-500 dark:text-gray-400">
|
|
||||||
{getCompactToolDisplay(currentTool.toolName, currentTool.toolInput)}
|
|
||||||
</span>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{/* Completion status */}
|
|
||||||
{isComplete && (
|
|
||||||
<div className="flex items-center gap-1.5 text-xs text-green-600 dark:text-green-400 mt-1">
|
|
||||||
<svg className="w-3 h-3 flex-shrink-0" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
||||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M5 13l4 4L19 7" />
|
|
||||||
</svg>
|
|
||||||
<span>Completed ({childTools.length} {childTools.length === 1 ? 'tool' : 'tools'})</span>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{/* Tool history (collapsed) */}
|
|
||||||
{childTools.length > 0 && (
|
|
||||||
<details className="mt-2 group/history">
|
|
||||||
<summary className="cursor-pointer text-[11px] text-gray-400 dark:text-gray-500 hover:text-gray-600 dark:hover:text-gray-300 flex items-center gap-1">
|
|
||||||
<svg
|
|
||||||
className="w-2.5 h-2.5 transition-transform duration-150 group-open/history:rotate-90 flex-shrink-0"
|
|
||||||
fill="none"
|
|
||||||
stroke="currentColor"
|
|
||||||
viewBox="0 0 24 24"
|
|
||||||
>
|
|
||||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 5l7 7-7 7" />
|
|
||||||
</svg>
|
|
||||||
<span>View tool history ({childTools.length})</span>
|
|
||||||
</summary>
|
|
||||||
<div className="mt-1 pl-3 border-l border-gray-200 dark:border-gray-700 space-y-0.5">
|
|
||||||
{childTools.map((child, index) => (
|
|
||||||
<div key={child.toolId} className="flex items-center gap-1.5 text-[11px] text-gray-500 dark:text-gray-400">
|
|
||||||
<span className="text-gray-400 dark:text-gray-500 w-4 text-right flex-shrink-0">{index + 1}.</span>
|
|
||||||
<span className="font-medium">{child.toolName}</span>
|
|
||||||
{getCompactToolDisplay(child.toolName, child.toolInput) && (
|
|
||||||
<span className="font-mono truncate text-gray-400 dark:text-gray-500">
|
|
||||||
{getCompactToolDisplay(child.toolName, child.toolInput)}
|
|
||||||
</span>
|
|
||||||
)}
|
|
||||||
{child.toolResult?.isError && (
|
|
||||||
<span className="text-red-500 flex-shrink-0">(error)</span>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</details>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{/* Final result */}
|
|
||||||
{isComplete && toolResult && (
|
|
||||||
<div className="mt-2 text-xs text-gray-600 dark:text-gray-400">
|
|
||||||
{(() => {
|
|
||||||
let content = toolResult.content;
|
|
||||||
|
|
||||||
// Handle JSON string that needs parsing
|
|
||||||
if (typeof content === 'string') {
|
|
||||||
try {
|
|
||||||
const parsed = JSON.parse(content);
|
|
||||||
if (Array.isArray(parsed)) {
|
|
||||||
// Extract text from array format like [{"type":"text","text":"..."}]
|
|
||||||
const textParts = parsed
|
|
||||||
.filter((p: any) => p.type === 'text' && p.text)
|
|
||||||
.map((p: any) => p.text);
|
|
||||||
if (textParts.length > 0) {
|
|
||||||
content = textParts.join('\n');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch {
|
|
||||||
// Not JSON, use as-is
|
|
||||||
}
|
|
||||||
} else if (Array.isArray(content)) {
|
|
||||||
// Direct array format
|
|
||||||
const textParts = content
|
|
||||||
.filter((p: any) => p.type === 'text' && p.text)
|
|
||||||
.map((p: any) => p.text);
|
|
||||||
if (textParts.length > 0) {
|
|
||||||
content = textParts.join('\n');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return typeof content === 'string' ? (
|
|
||||||
<div className="whitespace-pre-wrap break-words line-clamp-6">
|
|
||||||
{content}
|
|
||||||
</div>
|
|
||||||
) : content ? (
|
|
||||||
<pre className="whitespace-pre-wrap break-words line-clamp-6 font-mono text-[11px]">
|
|
||||||
{JSON.stringify(content, null, 2)}
|
|
||||||
</pre>
|
|
||||||
) : null;
|
|
||||||
})()}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</CollapsibleSection>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
@@ -2,6 +2,5 @@ export { CollapsibleSection } from './CollapsibleSection';
|
|||||||
export { DiffViewer } from './DiffViewer';
|
export { DiffViewer } from './DiffViewer';
|
||||||
export { OneLineDisplay } from './OneLineDisplay';
|
export { OneLineDisplay } from './OneLineDisplay';
|
||||||
export { CollapsibleDisplay } from './CollapsibleDisplay';
|
export { CollapsibleDisplay } from './CollapsibleDisplay';
|
||||||
export { SubagentContainer } from './SubagentContainer';
|
|
||||||
export * from './ContentRenderers';
|
export * from './ContentRenderers';
|
||||||
export * from './InteractiveRenderers';
|
export * from './InteractiveRenderers';
|
||||||
|
|||||||
@@ -383,7 +383,7 @@ export const TOOL_CONFIGS: Record<string, ToolDisplayConfig> = {
|
|||||||
const description = input.description || 'Running task';
|
const description = input.description || 'Running task';
|
||||||
return `Subagent / ${subagentType}: ${description}`;
|
return `Subagent / ${subagentType}: ${description}`;
|
||||||
},
|
},
|
||||||
defaultOpen: false,
|
defaultOpen: true,
|
||||||
contentType: 'markdown',
|
contentType: 'markdown',
|
||||||
getContentProps: (input) => {
|
getContentProps: (input) => {
|
||||||
// If only prompt exists (and required fields), show just the prompt
|
// If only prompt exists (and required fields), show just the prompt
|
||||||
@@ -424,8 +424,14 @@ export const TOOL_CONFIGS: Record<string, ToolDisplayConfig> = {
|
|||||||
},
|
},
|
||||||
result: {
|
result: {
|
||||||
type: 'collapsible',
|
type: 'collapsible',
|
||||||
title: 'Subagent result',
|
title: (result) => {
|
||||||
defaultOpen: false,
|
// Check if result has content with type array (agent results often have this structure)
|
||||||
|
if (result && result.content && Array.isArray(result.content)) {
|
||||||
|
return 'Subagent Response';
|
||||||
|
}
|
||||||
|
return 'Subagent Result';
|
||||||
|
},
|
||||||
|
defaultOpen: true,
|
||||||
contentType: 'markdown',
|
contentType: 'markdown',
|
||||||
getContentProps: (result) => {
|
getContentProps: (result) => {
|
||||||
// Handle agent results which may have complex structure
|
// Handle agent results which may have complex structure
|
||||||
|
|||||||
@@ -17,14 +17,6 @@ export interface ToolResult {
|
|||||||
[key: string]: unknown;
|
[key: string]: unknown;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface SubagentChildTool {
|
|
||||||
toolId: string;
|
|
||||||
toolName: string;
|
|
||||||
toolInput: unknown;
|
|
||||||
toolResult?: ToolResult | null;
|
|
||||||
timestamp: Date;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface ChatMessage {
|
export interface ChatMessage {
|
||||||
type: string;
|
type: string;
|
||||||
content?: string;
|
content?: string;
|
||||||
@@ -40,12 +32,6 @@ export interface ChatMessage {
|
|||||||
toolResult?: ToolResult | null;
|
toolResult?: ToolResult | null;
|
||||||
toolId?: string;
|
toolId?: string;
|
||||||
toolCallId?: string;
|
toolCallId?: string;
|
||||||
isSubagentContainer?: boolean;
|
|
||||||
subagentState?: {
|
|
||||||
childTools: SubagentChildTool[];
|
|
||||||
currentToolIndex: number;
|
|
||||||
isComplete: boolean;
|
|
||||||
};
|
|
||||||
[key: string]: unknown;
|
[key: string]: unknown;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -354,7 +354,7 @@ export const convertSessionMessages = (rawMessages: any[]): ChatMessage[] => {
|
|||||||
const converted: ChatMessage[] = [];
|
const converted: ChatMessage[] = [];
|
||||||
const toolResults = new Map<
|
const toolResults = new Map<
|
||||||
string,
|
string,
|
||||||
{ content: unknown; isError: boolean; timestamp: Date; toolUseResult: unknown; subagentTools?: unknown[] }
|
{ content: unknown; isError: boolean; timestamp: Date; toolUseResult: unknown }
|
||||||
>();
|
>();
|
||||||
|
|
||||||
rawMessages.forEach((message) => {
|
rawMessages.forEach((message) => {
|
||||||
@@ -368,7 +368,6 @@ export const convertSessionMessages = (rawMessages: any[]): ChatMessage[] => {
|
|||||||
isError: Boolean(part.is_error),
|
isError: Boolean(part.is_error),
|
||||||
timestamp: new Date(message.timestamp || Date.now()),
|
timestamp: new Date(message.timestamp || Date.now()),
|
||||||
toolUseResult: message.toolUseResult || null,
|
toolUseResult: message.toolUseResult || null,
|
||||||
subagentTools: message.subagentTools,
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -485,22 +484,6 @@ export const convertSessionMessages = (rawMessages: any[]): ChatMessage[] => {
|
|||||||
|
|
||||||
if (part.type === 'tool_use') {
|
if (part.type === 'tool_use') {
|
||||||
const toolResult = toolResults.get(part.id);
|
const toolResult = toolResults.get(part.id);
|
||||||
const isSubagentContainer = part.name === 'Task';
|
|
||||||
|
|
||||||
// Build child tools from server-provided subagentTools data
|
|
||||||
const childTools: import('../types/types').SubagentChildTool[] = [];
|
|
||||||
if (isSubagentContainer && toolResult?.subagentTools && Array.isArray(toolResult.subagentTools)) {
|
|
||||||
for (const tool of toolResult.subagentTools as any[]) {
|
|
||||||
childTools.push({
|
|
||||||
toolId: tool.toolId,
|
|
||||||
toolName: tool.toolName,
|
|
||||||
toolInput: tool.toolInput,
|
|
||||||
toolResult: tool.toolResult || null,
|
|
||||||
timestamp: new Date(tool.timestamp || Date.now()),
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
converted.push({
|
converted.push({
|
||||||
type: 'assistant',
|
type: 'assistant',
|
||||||
content: '',
|
content: '',
|
||||||
@@ -508,7 +491,6 @@ export const convertSessionMessages = (rawMessages: any[]): ChatMessage[] => {
|
|||||||
isToolUse: true,
|
isToolUse: true,
|
||||||
toolName: part.name,
|
toolName: part.name,
|
||||||
toolInput: normalizeToolInput(part.input),
|
toolInput: normalizeToolInput(part.input),
|
||||||
toolId: part.id,
|
|
||||||
toolResult: toolResult
|
toolResult: toolResult
|
||||||
? {
|
? {
|
||||||
content:
|
content:
|
||||||
@@ -521,14 +503,6 @@ export const convertSessionMessages = (rawMessages: any[]): ChatMessage[] => {
|
|||||||
: null,
|
: null,
|
||||||
toolError: toolResult?.isError || false,
|
toolError: toolResult?.isError || false,
|
||||||
toolResultTimestamp: toolResult?.timestamp || new Date(),
|
toolResultTimestamp: toolResult?.timestamp || new Date(),
|
||||||
isSubagentContainer,
|
|
||||||
subagentState: isSubagentContainer
|
|
||||||
? {
|
|
||||||
childTools,
|
|
||||||
currentToolIndex: childTools.length > 0 ? childTools.length - 1 : -1,
|
|
||||||
isComplete: Boolean(toolResult),
|
|
||||||
}
|
|
||||||
: undefined,
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -163,7 +163,6 @@ function ChatInterface({
|
|||||||
handlePermissionDecision,
|
handlePermissionDecision,
|
||||||
handleGrantToolPermission,
|
handleGrantToolPermission,
|
||||||
handleInputFocusChange,
|
handleInputFocusChange,
|
||||||
isInputFocused,
|
|
||||||
} = useChatComposerState({
|
} = useChatComposerState({
|
||||||
selectedProject,
|
selectedProject,
|
||||||
selectedSession,
|
selectedSession,
|
||||||
@@ -374,7 +373,6 @@ function ChatInterface({
|
|||||||
onTextareaScrollSync={syncInputOverlayScroll}
|
onTextareaScrollSync={syncInputOverlayScroll}
|
||||||
onTextareaInput={handleTextareaInput}
|
onTextareaInput={handleTextareaInput}
|
||||||
onInputFocusChange={handleInputFocusChange}
|
onInputFocusChange={handleInputFocusChange}
|
||||||
isInputFocused={isInputFocused}
|
|
||||||
placeholder={t('input.placeholder', {
|
placeholder={t('input.placeholder', {
|
||||||
provider:
|
provider:
|
||||||
provider === 'cursor'
|
provider === 'cursor'
|
||||||
|
|||||||
@@ -87,7 +87,6 @@ interface ChatComposerProps {
|
|||||||
onTextareaScrollSync: (target: HTMLTextAreaElement) => void;
|
onTextareaScrollSync: (target: HTMLTextAreaElement) => void;
|
||||||
onTextareaInput: (event: FormEvent<HTMLTextAreaElement>) => void;
|
onTextareaInput: (event: FormEvent<HTMLTextAreaElement>) => void;
|
||||||
onInputFocusChange?: (focused: boolean) => void;
|
onInputFocusChange?: (focused: boolean) => void;
|
||||||
isInputFocused?: boolean;
|
|
||||||
placeholder: string;
|
placeholder: string;
|
||||||
isTextareaExpanded: boolean;
|
isTextareaExpanded: boolean;
|
||||||
sendByCtrlEnter?: boolean;
|
sendByCtrlEnter?: boolean;
|
||||||
@@ -144,7 +143,6 @@ export default function ChatComposer({
|
|||||||
onTextareaScrollSync,
|
onTextareaScrollSync,
|
||||||
onTextareaInput,
|
onTextareaInput,
|
||||||
onInputFocusChange,
|
onInputFocusChange,
|
||||||
isInputFocused,
|
|
||||||
placeholder,
|
placeholder,
|
||||||
isTextareaExpanded,
|
isTextareaExpanded,
|
||||||
sendByCtrlEnter,
|
sendByCtrlEnter,
|
||||||
@@ -164,13 +162,8 @@ export default function ChatComposer({
|
|||||||
(r) => r.toolName === 'AskUserQuestion'
|
(r) => r.toolName === 'AskUserQuestion'
|
||||||
);
|
);
|
||||||
|
|
||||||
// On mobile, when input is focused, float the input box at the bottom
|
|
||||||
const mobileFloatingClass = isInputFocused
|
|
||||||
? 'max-sm:fixed max-sm:bottom-0 max-sm:left-0 max-sm:right-0 max-sm:z-50 max-sm:bg-background max-sm:shadow-[0_-4px_20px_rgba(0,0,0,0.15)]'
|
|
||||||
: '';
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={`p-2 sm:p-4 md:p-4 flex-shrink-0 pb-2 sm:pb-4 md:pb-6 ${mobileFloatingClass}`}>
|
<div className="p-2 sm:p-4 md:p-4 flex-shrink-0 pb-2 sm:pb-4 md:pb-6">
|
||||||
{!hasQuestionPanel && (
|
{!hasQuestionPanel && (
|
||||||
<div className="flex-1">
|
<div className="flex-1">
|
||||||
<ClaudeStatus
|
<ClaudeStatus
|
||||||
|
|||||||
@@ -184,8 +184,6 @@ const MessageComponent = memo(({ message, index, prevMessage, createDiff, onFile
|
|||||||
autoExpandTools={autoExpandTools}
|
autoExpandTools={autoExpandTools}
|
||||||
showRawParameters={showRawParameters}
|
showRawParameters={showRawParameters}
|
||||||
rawToolInput={typeof message.toolInput === 'string' ? message.toolInput : undefined}
|
rawToolInput={typeof message.toolInput === 'string' ? message.toolInput : undefined}
|
||||||
isSubagentContainer={message.isSubagentContainer}
|
|
||||||
subagentState={message.subagentState}
|
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
|||||||
@@ -62,6 +62,9 @@
|
|||||||
--safe-area-inset-bottom: env(safe-area-inset-bottom);
|
--safe-area-inset-bottom: env(safe-area-inset-bottom);
|
||||||
--safe-area-inset-left: env(safe-area-inset-left);
|
--safe-area-inset-left: env(safe-area-inset-left);
|
||||||
|
|
||||||
|
/* Virtual keyboard height (set by JS for iOS/Safari fallback) */
|
||||||
|
--keyboard-height: 0px;
|
||||||
|
|
||||||
/* Mobile navigation dimensions - Single source of truth */
|
/* Mobile navigation dimensions - Single source of truth */
|
||||||
/* Floating nav: ~52px bar + 8px bottom margin + 12px px-3 top spacing */
|
/* Floating nav: ~52px bar + 8px bottom margin + 12px px-3 top spacing */
|
||||||
--mobile-nav-height: 52px;
|
--mobile-nav-height: 52px;
|
||||||
@@ -166,12 +169,16 @@
|
|||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Virtual keyboard offset — works in both PWA and regular mobile browsers */
|
||||||
|
.fixed.inset-0 {
|
||||||
|
bottom: max(env(keyboard-inset-bottom, 0px), var(--keyboard-height, 0px));
|
||||||
|
}
|
||||||
|
|
||||||
/* Adjust fixed inset positioning in PWA mode */
|
/* Adjust fixed inset positioning in PWA mode */
|
||||||
body.pwa-mode .fixed.inset-0 {
|
body.pwa-mode .fixed.inset-0 {
|
||||||
top: var(--header-total-padding);
|
top: var(--header-total-padding);
|
||||||
left: var(--safe-area-inset-left);
|
left: var(--safe-area-inset-left);
|
||||||
right: var(--safe-area-inset-right);
|
right: var(--safe-area-inset-right);
|
||||||
bottom: 0;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Global transition defaults */
|
/* Global transition defaults */
|
||||||
|
|||||||
13
src/main.jsx
13
src/main.jsx
@@ -7,6 +7,19 @@ import 'katex/dist/katex.min.css'
|
|||||||
// Initialize i18n
|
// Initialize i18n
|
||||||
import './i18n/config.js'
|
import './i18n/config.js'
|
||||||
|
|
||||||
|
// Tell the browser to overlay the virtual keyboard instead of resizing the viewport (PWA)
|
||||||
|
if ('virtualKeyboard' in navigator) {
|
||||||
|
navigator.virtualKeyboard.overlaysContent = true;
|
||||||
|
} else if (window.visualViewport) {
|
||||||
|
// iOS/Safari fallback: track keyboard height via visualViewport
|
||||||
|
const viewport = window.visualViewport;
|
||||||
|
const updateKeyboardHeight = () => {
|
||||||
|
const keyboardHeight = Math.max(0, window.innerHeight - viewport.height);
|
||||||
|
document.documentElement.style.setProperty('--keyboard-height', `${keyboardHeight}px`);
|
||||||
|
};
|
||||||
|
viewport.addEventListener('resize', updateKeyboardHeight);
|
||||||
|
}
|
||||||
|
|
||||||
// Clean up stale service workers on app load to prevent caching issues after builds
|
// Clean up stale service workers on app load to prevent caching issues after builds
|
||||||
if ('serviceWorker' in navigator) {
|
if ('serviceWorker' in navigator) {
|
||||||
navigator.serviceWorker.getRegistrations().then(registrations => {
|
navigator.serviceWorker.getRegistrations().then(registrations => {
|
||||||
|
|||||||
@@ -4,26 +4,20 @@ import react from '@vitejs/plugin-react'
|
|||||||
export default defineConfig(({ command, mode }) => {
|
export default defineConfig(({ command, mode }) => {
|
||||||
// Load env file based on `mode` in the current working directory.
|
// Load env file based on `mode` in the current working directory.
|
||||||
const env = loadEnv(mode, process.cwd(), '')
|
const env = loadEnv(mode, process.cwd(), '')
|
||||||
|
|
||||||
const host = env.HOST || '0.0.0.0'
|
|
||||||
// When binding to all interfaces (0.0.0.0), proxy should connect to localhost
|
|
||||||
// Otherwise, proxy to the specific host the backend is bound to
|
|
||||||
const proxyHost = host === '0.0.0.0' ? 'localhost' : host
|
|
||||||
const port = env.PORT || 3001
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
plugins: [react()],
|
plugins: [react()],
|
||||||
server: {
|
server: {
|
||||||
host,
|
|
||||||
port: parseInt(env.VITE_PORT) || 5173,
|
port: parseInt(env.VITE_PORT) || 5173,
|
||||||
proxy: {
|
proxy: {
|
||||||
'/api': `http://${proxyHost}:${port}`,
|
'/api': `http://localhost:${env.PORT || 3001}`,
|
||||||
'/ws': {
|
'/ws': {
|
||||||
target: `ws://${proxyHost}:${port}`,
|
target: `ws://localhost:${env.PORT || 3001}`,
|
||||||
ws: true
|
ws: true
|
||||||
},
|
},
|
||||||
'/shell': {
|
'/shell': {
|
||||||
target: `ws://${proxyHost}:${port}`,
|
target: `ws://localhost:${env.PORT || 3001}`,
|
||||||
ws: true
|
ws: true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user