Compare commits

..

1 Commits

Author SHA1 Message Date
simosmik
f670f552ac fix: mobile viewport 2026-02-20 08:10:22 +00:00
24 changed files with 67 additions and 529 deletions

View File

@@ -21,10 +21,6 @@ PORT=3001
#Frontend port
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"
# CLAUDE_CLI_PATH=claude

View File

@@ -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
View File

@@ -1,12 +1,12 @@
{
"name": "@siteboon/claude-code-ui",
"version": "1.19.0",
"version": "1.18.2",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "@siteboon/claude-code-ui",
"version": "1.19.0",
"version": "1.18.2",
"hasInstallScript": true,
"license": "GPL-3.0",
"dependencies": {
@@ -114,9 +114,9 @@
}
},
"node_modules/@anthropic-ai/claude-agent-sdk": {
"version": "0.1.77",
"resolved": "https://registry.npmjs.org/@anthropic-ai/claude-agent-sdk/-/claude-agent-sdk-0.1.77.tgz",
"integrity": "sha512-ZEjWQtkoB2MEY6K16DWMmF+8OhywAynH0m08V265cerbZ8xPD/2Ng2jPzbbO40mPeFSsMDJboShL+a3aObP0Jg==",
"version": "0.1.71",
"resolved": "https://registry.npmjs.org/@anthropic-ai/claude-agent-sdk/-/claude-agent-sdk-0.1.71.tgz",
"integrity": "sha512-O34SQCEsdU11Z2uy30GaJGRLdRbEwEvaQs8APywHVOW/EdIGE0rS/AE4V6p9j45/j5AFwh2USZWlbz5NTlLnrw==",
"license": "SEE LICENSE IN README.md",
"engines": {
"node": ">=18.0.0"
@@ -132,7 +132,7 @@
"@img/sharp-win32-x64": "^0.33.5"
},
"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": {

View File

@@ -1,6 +1,6 @@
{
"name": "@siteboon/claude-code-ui",
"version": "1.19.0",
"version": "1.18.2",
"description": "A web-based UI for Claude Code CLI",
"type": "module",
"main": "server/index.js",

View File

@@ -250,13 +250,7 @@ function getAllSessions() {
* @returns {Object} Transformed message ready for WebSocket
*/
function transformMessage(sdkMessage) {
// Extract parent_tool_use_id for subagent tool grouping
if (sdkMessage.parent_tool_use_id) {
return {
...sdkMessage,
parentToolUseId: sdkMessage.parent_tool_use_id
};
}
// Pass-through; SDK messages match frontend format.
return sdkMessage;
}

View File

@@ -1886,9 +1886,6 @@ async function getFileTree(dirPath, maxDepth = 3, currentDepth = 0, showHidden =
}
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
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))}`);
}
server.listen(PORT, HOST, async () => {
server.listen(PORT, '0.0.0.0', async () => {
const appInstallPath = path.join(__dirname, '..');
console.log('');
@@ -1916,7 +1913,7 @@ async function startServer() {
console.log(` ${c.bright('Claude Code UI Server - Ready')}`);
console.log(c.dim('═'.repeat(63)));
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.tip('[TIP]')} Run "cloudcli status" for full configuration details`);
console.log('');

View File

@@ -889,80 +889,21 @@ 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
async function getSessionMessages(projectName, sessionId, limit = null, offset = 0) {
const projectDir = path.join(os.homedir(), '.claude', 'projects', projectName);
try {
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 agentFiles = files.filter(file => file.endsWith('.jsonl') && file.startsWith('agent-'));
if (jsonlFiles.length === 0) {
return { messages: [], total: 0, hasMore: false };
}
const messages = [];
// Map of agentId -> tools for subagent tool grouping
const agentToolsCache = new Map();
// Process all JSONL files to find messages for this session
for (const file of jsonlFiles) {
@@ -987,35 +928,6 @@ 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
const sortedMessages = messages.sort((a, b) =>
new Date(a.timestamp || 0) - new Date(b.timestamp || 0)

View File

@@ -63,5 +63,5 @@ export const CODEX_MODELS = {
{ value: 'o4-mini', label: 'O4-mini' }
],
DEFAULT: 'gpt-5.3-codex'
DEFAULT: 'gpt-5.2'
};

View File

@@ -106,7 +106,7 @@ export default function AppContent() {
</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
selectedProject={selectedProject}
selectedSession={selectedSession}

View File

@@ -271,14 +271,13 @@ export function useChatComposerState({
}, [setChatMessages]);
const executeCommand = useCallback(
async (command: SlashCommand, rawInput?: string) => {
async (command: SlashCommand) => {
if (!command || !selectedProject) {
return;
}
try {
const effectiveInput = rawInput ?? input;
const commandMatch = effectiveInput.match(new RegExp(`${escapeRegExp(command.name)}\\s*(.*)`));
const commandMatch = input.match(new RegExp(`${escapeRegExp(command.name)}\\s*(.*)`));
const args =
commandMatch && commandMatch[1] ? commandMatch[1].trim().split(/\s+/) : [];
@@ -352,7 +351,6 @@ export function useChatComposerState({
);
const {
slashCommands,
slashCommandsCount,
filteredCommands,
frequentCommands,
@@ -475,28 +473,6 @@ export function useChatComposerState({
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;
const selectedThinkingMode = thinkingModes.find((mode: { id: string; prefix?: string }) => mode.id === thinkingMode);
if (selectedThinkingMode && selectedThinkingMode.prefix) {
@@ -663,7 +639,6 @@ export function useChatComposerState({
codexModel,
currentSessionId,
cursorModel,
executeCommand,
isLoading,
onSessionActive,
pendingViewSessionRef,
@@ -679,7 +654,6 @@ export function useChatComposerState({
setClaudeStatus,
setIsLoading,
setIsUserScrolledUp,
slashCommands,
thinkingMode,
],
);
@@ -929,11 +903,8 @@ export function useChatComposerState({
[sendMessage, setClaudeStatus, setPendingPermissionRequests],
);
const [isInputFocused, setIsInputFocused] = useState(false);
const handleInputFocusChange = useCallback(
(focused: boolean) => {
setIsInputFocused(focused);
onInputFocusChange?.(focused);
},
[onInputFocusChange],
@@ -982,6 +953,5 @@ export function useChatComposerState({
handlePermissionDecision,
handleGrantToolPermission,
handleInputFocusChange,
isInputFocused,
};
}

View File

@@ -336,43 +336,9 @@ export function useChatRealtimeHandlers({
}
if (structuredMessageData && Array.isArray(structuredMessageData.content)) {
const parentToolUseId = rawStructuredData?.parentToolUseId;
structuredMessageData.content.forEach((part: any) => {
if (part.type === 'tool_use') {
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) => [
...previous,
{
@@ -384,10 +350,6 @@ export function useChatRealtimeHandlers({
toolInput,
toolId: part.id,
toolResult: null,
isSubagentContainer,
subagentState: isSubagentContainer
? { childTools: [], currentToolIndex: -1, isComplete: false }
: undefined,
},
]);
return;
@@ -420,8 +382,6 @@ export function useChatRealtimeHandlers({
}
if (structuredMessageData?.role === 'user' && Array.isArray(structuredMessageData.content)) {
const parentToolUseId = rawStructuredData?.parentToolUseId;
structuredMessageData.content.forEach((part: any) => {
if (part.type !== 'tool_result') {
return;
@@ -429,32 +389,8 @@ export function useChatRealtimeHandlers({
setChatMessages((previous) =>
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) {
const result = {
return {
...message,
toolResult: {
content: part.content,
@@ -462,14 +398,6 @@ export function useChatRealtimeHandlers({
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;
}),

View File

@@ -22,7 +22,7 @@ interface UseSlashCommandsOptions {
input: string;
setInput: Dispatch<SetStateAction<string>>;
textareaRef: RefObject<HTMLTextAreaElement>;
onExecuteCommand: (command: SlashCommand, rawInput?: string) => void | Promise<void>;
onExecuteCommand: (command: SlashCommand) => void | Promise<void>;
}
const getCommandHistoryKey = (projectName: string) => `command_history_${projectName}`;

View File

@@ -1,8 +1,7 @@
import React, { memo, useMemo, useCallback } from 'react';
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 { SubagentChildTool } from '../types/types';
type DiffLine = {
type: string;
@@ -22,12 +21,6 @@ interface ToolRendererProps {
autoExpandTools?: boolean;
showRawParameters?: boolean;
rawToolInput?: string;
isSubagentContainer?: boolean;
subagentState?: {
childTools: SubagentChildTool[];
currentToolIndex: number;
isComplete: boolean;
};
}
function getToolCategory(toolName: string): string {
@@ -57,24 +50,8 @@ export const ToolRenderer: React.FC<ToolRendererProps> = memo(({
selectedProject,
autoExpandTools = false,
showRawParameters = false,
rawToolInput,
isSubagentContainer,
subagentState
rawToolInput
}) => {
// 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 displayConfig: any = mode === 'input' ? config.input : config.result;

View File

@@ -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>
);
};

View File

@@ -2,6 +2,5 @@ export { CollapsibleSection } from './CollapsibleSection';
export { DiffViewer } from './DiffViewer';
export { OneLineDisplay } from './OneLineDisplay';
export { CollapsibleDisplay } from './CollapsibleDisplay';
export { SubagentContainer } from './SubagentContainer';
export * from './ContentRenderers';
export * from './InteractiveRenderers';

View File

@@ -383,7 +383,7 @@ export const TOOL_CONFIGS: Record<string, ToolDisplayConfig> = {
const description = input.description || 'Running task';
return `Subagent / ${subagentType}: ${description}`;
},
defaultOpen: false,
defaultOpen: true,
contentType: 'markdown',
getContentProps: (input) => {
// If only prompt exists (and required fields), show just the prompt
@@ -424,8 +424,14 @@ export const TOOL_CONFIGS: Record<string, ToolDisplayConfig> = {
},
result: {
type: 'collapsible',
title: 'Subagent result',
defaultOpen: false,
title: (result) => {
// 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',
getContentProps: (result) => {
// Handle agent results which may have complex structure

View File

@@ -17,14 +17,6 @@ export interface ToolResult {
[key: string]: unknown;
}
export interface SubagentChildTool {
toolId: string;
toolName: string;
toolInput: unknown;
toolResult?: ToolResult | null;
timestamp: Date;
}
export interface ChatMessage {
type: string;
content?: string;
@@ -40,12 +32,6 @@ export interface ChatMessage {
toolResult?: ToolResult | null;
toolId?: string;
toolCallId?: string;
isSubagentContainer?: boolean;
subagentState?: {
childTools: SubagentChildTool[];
currentToolIndex: number;
isComplete: boolean;
};
[key: string]: unknown;
}

View File

@@ -354,7 +354,7 @@ export const convertSessionMessages = (rawMessages: any[]): ChatMessage[] => {
const converted: ChatMessage[] = [];
const toolResults = new Map<
string,
{ content: unknown; isError: boolean; timestamp: Date; toolUseResult: unknown; subagentTools?: unknown[] }
{ content: unknown; isError: boolean; timestamp: Date; toolUseResult: unknown }
>();
rawMessages.forEach((message) => {
@@ -368,7 +368,6 @@ export const convertSessionMessages = (rawMessages: any[]): ChatMessage[] => {
isError: Boolean(part.is_error),
timestamp: new Date(message.timestamp || Date.now()),
toolUseResult: message.toolUseResult || null,
subagentTools: message.subagentTools,
});
});
}
@@ -485,22 +484,6 @@ export const convertSessionMessages = (rawMessages: any[]): ChatMessage[] => {
if (part.type === 'tool_use') {
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({
type: 'assistant',
content: '',
@@ -508,7 +491,6 @@ export const convertSessionMessages = (rawMessages: any[]): ChatMessage[] => {
isToolUse: true,
toolName: part.name,
toolInput: normalizeToolInput(part.input),
toolId: part.id,
toolResult: toolResult
? {
content:
@@ -521,14 +503,6 @@ export const convertSessionMessages = (rawMessages: any[]): ChatMessage[] => {
: null,
toolError: toolResult?.isError || false,
toolResultTimestamp: toolResult?.timestamp || new Date(),
isSubagentContainer,
subagentState: isSubagentContainer
? {
childTools,
currentToolIndex: childTools.length > 0 ? childTools.length - 1 : -1,
isComplete: Boolean(toolResult),
}
: undefined,
});
}
});

View File

@@ -163,7 +163,6 @@ function ChatInterface({
handlePermissionDecision,
handleGrantToolPermission,
handleInputFocusChange,
isInputFocused,
} = useChatComposerState({
selectedProject,
selectedSession,
@@ -374,7 +373,6 @@ function ChatInterface({
onTextareaScrollSync={syncInputOverlayScroll}
onTextareaInput={handleTextareaInput}
onInputFocusChange={handleInputFocusChange}
isInputFocused={isInputFocused}
placeholder={t('input.placeholder', {
provider:
provider === 'cursor'

View File

@@ -87,7 +87,6 @@ interface ChatComposerProps {
onTextareaScrollSync: (target: HTMLTextAreaElement) => void;
onTextareaInput: (event: FormEvent<HTMLTextAreaElement>) => void;
onInputFocusChange?: (focused: boolean) => void;
isInputFocused?: boolean;
placeholder: string;
isTextareaExpanded: boolean;
sendByCtrlEnter?: boolean;
@@ -144,7 +143,6 @@ export default function ChatComposer({
onTextareaScrollSync,
onTextareaInput,
onInputFocusChange,
isInputFocused,
placeholder,
isTextareaExpanded,
sendByCtrlEnter,
@@ -164,13 +162,8 @@ export default function ChatComposer({
(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 (
<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 && (
<div className="flex-1">
<ClaudeStatus

View File

@@ -184,8 +184,6 @@ const MessageComponent = memo(({ message, index, prevMessage, createDiff, onFile
autoExpandTools={autoExpandTools}
showRawParameters={showRawParameters}
rawToolInput={typeof message.toolInput === 'string' ? message.toolInput : undefined}
isSubagentContainer={message.isSubagentContainer}
subagentState={message.subagentState}
/>
)}

View File

@@ -62,6 +62,9 @@
--safe-area-inset-bottom: env(safe-area-inset-bottom);
--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 */
/* Floating nav: ~52px bar + 8px bottom margin + 12px px-3 top spacing */
--mobile-nav-height: 52px;
@@ -166,12 +169,16 @@
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 */
body.pwa-mode .fixed.inset-0 {
top: var(--header-total-padding);
left: var(--safe-area-inset-left);
right: var(--safe-area-inset-right);
bottom: 0;
}
/* Global transition defaults */

View File

@@ -7,6 +7,19 @@ import 'katex/dist/katex.min.css'
// Initialize i18n
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
if ('serviceWorker' in navigator) {
navigator.serviceWorker.getRegistrations().then(registrations => {

View File

@@ -5,25 +5,19 @@ export default defineConfig(({ command, mode }) => {
// Load env file based on `mode` in the current working directory.
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 {
plugins: [react()],
server: {
host,
port: parseInt(env.VITE_PORT) || 5173,
proxy: {
'/api': `http://${proxyHost}:${port}`,
'/api': `http://localhost:${env.PORT || 3001}`,
'/ws': {
target: `ws://${proxyHost}:${port}`,
target: `ws://localhost:${env.PORT || 3001}`,
ws: true
},
'/shell': {
target: `ws://${proxyHost}:${port}`,
target: `ws://localhost:${env.PORT || 3001}`,
ws: true
}
}