mirror of
https://github.com/siteboon/claudecodeui.git
synced 2026-02-24 01:27:42 +00:00
Compare commits
13 Commits
fix/mobile
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
23801e9cc1 | ||
|
|
4f6ff9260d | ||
|
|
49061bc7a3 | ||
|
|
50e097d4ac | ||
|
|
f986004319 | ||
|
|
f488a346ef | ||
|
|
82efac4704 | ||
|
|
81697d0e73 | ||
|
|
27bf09b0c1 | ||
|
|
7ccbc8d92d | ||
|
|
597e9c54b7 | ||
|
|
cccd915c33 | ||
|
|
0207a1f3a3 |
@@ -21,6 +21,10 @@ 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
|
||||||
|
|
||||||
|
|||||||
37
CHANGELOG.md
Normal file
37
CHANGELOG.md
Normal file
@@ -0,0 +1,37 @@
|
|||||||
|
# Changelog
|
||||||
|
|
||||||
|
All notable changes to CloudCLI UI will be documented in this file.
|
||||||
|
|
||||||
|
|
||||||
|
## [1.20.1](https://github.com/siteboon/claudecodeui/compare/v1.19.1...v1.20.1) (2026-02-23)
|
||||||
|
|
||||||
|
### New Features
|
||||||
|
|
||||||
|
* implement install mode detection and update commands in version upgrade process ([f986004](https://github.com/siteboon/claudecodeui/commit/f986004319207b068431f9f6adf338a8ce8decfc))
|
||||||
|
* migrate legacy database to new location and improve last login update handling ([50e097d](https://github.com/siteboon/claudecodeui/commit/50e097d4ac498aa9f1803ef3564843721833dc19))
|
||||||
|
|
||||||
|
## [1.19.1](https://github.com/siteboon/claudecodeui/compare/v1.19.0...v1.19.1) (2026-02-23)
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
* add prepublishOnly script to build before publishing ([82efac4](https://github.com/siteboon/claudecodeui/commit/82efac4704cab11ed8d1a05fe84f41312140b223))
|
||||||
|
|
||||||
|
## [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.18.2",
|
"version": "1.20.1",
|
||||||
"lockfileVersion": 3,
|
"lockfileVersion": 3,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "@siteboon/claude-code-ui",
|
"name": "@siteboon/claude-code-ui",
|
||||||
"version": "1.18.2",
|
"version": "1.20.1",
|
||||||
"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.71",
|
"version": "0.1.77",
|
||||||
"resolved": "https://registry.npmjs.org/@anthropic-ai/claude-agent-sdk/-/claude-agent-sdk-0.1.71.tgz",
|
"resolved": "https://registry.npmjs.org/@anthropic-ai/claude-agent-sdk/-/claude-agent-sdk-0.1.77.tgz",
|
||||||
"integrity": "sha512-O34SQCEsdU11Z2uy30GaJGRLdRbEwEvaQs8APywHVOW/EdIGE0rS/AE4V6p9j45/j5AFwh2USZWlbz5NTlLnrw==",
|
"integrity": "sha512-ZEjWQtkoB2MEY6K16DWMmF+8OhywAynH0m08V265cerbZ8xPD/2Ng2jPzbbO40mPeFSsMDJboShL+a3aObP0Jg==",
|
||||||
"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.24.1 || ^4.0.0"
|
"zod": "^3.25.0 || ^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.18.2",
|
"version": "1.20.1",
|
||||||
"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",
|
||||||
@@ -32,6 +32,7 @@
|
|||||||
"typecheck": "tsc --noEmit -p tsconfig.json",
|
"typecheck": "tsc --noEmit -p tsconfig.json",
|
||||||
"start": "npm run build && npm run server",
|
"start": "npm run build && npm run server",
|
||||||
"release": "./release.sh",
|
"release": "./release.sh",
|
||||||
|
"prepublishOnly": "npm run build",
|
||||||
"postinstall": "node scripts/fix-node-pty.js"
|
"postinstall": "node scripts/fix-node-pty.js"
|
||||||
},
|
},
|
||||||
"keywords": [
|
"keywords": [
|
||||||
|
|||||||
@@ -250,7 +250,13 @@ function getAllSessions() {
|
|||||||
* @returns {Object} Transformed message ready for WebSocket
|
* @returns {Object} Transformed message ready for WebSocket
|
||||||
*/
|
*/
|
||||||
function transformMessage(sdkMessage) {
|
function transformMessage(sdkMessage) {
|
||||||
// Pass-through; SDK messages match frontend format.
|
// Extract parent_tool_use_id for subagent tool grouping
|
||||||
|
if (sdkMessage.parent_tool_use_id) {
|
||||||
|
return {
|
||||||
|
...sdkMessage,
|
||||||
|
parentToolUseId: sdkMessage.parent_tool_use_id
|
||||||
|
};
|
||||||
|
}
|
||||||
return sdkMessage;
|
return sdkMessage;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -40,6 +40,22 @@ if (process.env.DATABASE_PATH) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// As part of 1.19.2 we are introducing a new location for auth.db. The below handles exisitng moving legacy database from install directory to new location
|
||||||
|
const LEGACY_DB_PATH = path.join(__dirname, 'auth.db');
|
||||||
|
if (DB_PATH !== LEGACY_DB_PATH && !fs.existsSync(DB_PATH) && fs.existsSync(LEGACY_DB_PATH)) {
|
||||||
|
try {
|
||||||
|
fs.copyFileSync(LEGACY_DB_PATH, DB_PATH);
|
||||||
|
console.log(`[MIGRATION] Copied database from ${LEGACY_DB_PATH} to ${DB_PATH}`);
|
||||||
|
for (const suffix of ['-wal', '-shm']) {
|
||||||
|
if (fs.existsSync(LEGACY_DB_PATH + suffix)) {
|
||||||
|
fs.copyFileSync(LEGACY_DB_PATH + suffix, DB_PATH + suffix);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
console.warn(`[MIGRATION] Could not copy legacy database: ${err.message}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Create database connection
|
// Create database connection
|
||||||
const db = new Database(DB_PATH);
|
const db = new Database(DB_PATH);
|
||||||
|
|
||||||
@@ -128,12 +144,12 @@ const userDb = {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
// Update last login time
|
// Update last login time (non-fatal — logged but not thrown)
|
||||||
updateLastLogin: (userId) => {
|
updateLastLogin: (userId) => {
|
||||||
try {
|
try {
|
||||||
db.prepare('UPDATE users SET last_login = CURRENT_TIMESTAMP WHERE id = ?').run(userId);
|
db.prepare('UPDATE users SET last_login = CURRENT_TIMESTAMP WHERE id = ?').run(userId);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
throw err;
|
console.warn('Failed to update last login:', err.message);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|||||||
@@ -9,6 +9,8 @@ import { dirname } from 'path';
|
|||||||
const __filename = fileURLToPath(import.meta.url);
|
const __filename = fileURLToPath(import.meta.url);
|
||||||
const __dirname = dirname(__filename);
|
const __dirname = dirname(__filename);
|
||||||
|
|
||||||
|
const installMode = fs.existsSync(path.join(__dirname, '..', '.git')) ? 'git' : 'npm';
|
||||||
|
|
||||||
// ANSI color codes for terminal output
|
// ANSI color codes for terminal output
|
||||||
const colors = {
|
const colors = {
|
||||||
reset: '\x1b[0m',
|
reset: '\x1b[0m',
|
||||||
@@ -333,7 +335,8 @@ app.use(express.urlencoded({ limit: '50mb', extended: true }));
|
|||||||
app.get('/health', (req, res) => {
|
app.get('/health', (req, res) => {
|
||||||
res.json({
|
res.json({
|
||||||
status: 'ok',
|
status: 'ok',
|
||||||
timestamp: new Date().toISOString()
|
timestamp: new Date().toISOString(),
|
||||||
|
installMode
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -410,11 +413,13 @@ app.post('/api/system/update', authenticateToken, async (req, res) => {
|
|||||||
|
|
||||||
console.log('Starting system update from directory:', projectRoot);
|
console.log('Starting system update from directory:', projectRoot);
|
||||||
|
|
||||||
// Run the update command
|
// Run the update command based on install mode
|
||||||
const updateCommand = 'git checkout main && git pull && npm install';
|
const updateCommand = installMode === 'git'
|
||||||
|
? 'git checkout main && git pull && npm install'
|
||||||
|
: 'npm install -g @siteboon/claude-code-ui@latest';
|
||||||
|
|
||||||
const child = spawn('sh', ['-c', updateCommand], {
|
const child = spawn('sh', ['-c', updateCommand], {
|
||||||
cwd: projectRoot,
|
cwd: installMode === 'git' ? projectRoot : os.homedir(),
|
||||||
env: process.env
|
env: process.env
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -1133,7 +1138,7 @@ function handleShellConnection(ws) {
|
|||||||
if (isPlainShell) {
|
if (isPlainShell) {
|
||||||
welcomeMsg = `\x1b[36mStarting terminal in: ${projectPath}\x1b[0m\r\n`;
|
welcomeMsg = `\x1b[36mStarting terminal in: ${projectPath}\x1b[0m\r\n`;
|
||||||
} else {
|
} else {
|
||||||
const providerName = provider === 'cursor' ? 'Cursor' : 'Claude';
|
const providerName = provider === 'cursor' ? 'Cursor' : provider === 'codex' ? 'Codex' : 'Claude';
|
||||||
welcomeMsg = hasSession ?
|
welcomeMsg = hasSession ?
|
||||||
`\x1b[36mResuming ${providerName} session ${sessionId} in: ${projectPath}\x1b[0m\r\n` :
|
`\x1b[36mResuming ${providerName} session ${sessionId} in: ${projectPath}\x1b[0m\r\n` :
|
||||||
`\x1b[36mStarting new ${providerName} session in: ${projectPath}\x1b[0m\r\n`;
|
`\x1b[36mStarting new ${providerName} session in: ${projectPath}\x1b[0m\r\n`;
|
||||||
@@ -1169,6 +1174,23 @@ function handleShellConnection(ws) {
|
|||||||
shellCommand = `cd "${projectPath}" && cursor-agent`;
|
shellCommand = `cd "${projectPath}" && cursor-agent`;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
} else if (provider === 'codex') {
|
||||||
|
// Use codex command
|
||||||
|
if (os.platform() === 'win32') {
|
||||||
|
if (hasSession && sessionId) {
|
||||||
|
// Try to resume session, but with fallback to a new session if it fails
|
||||||
|
shellCommand = `Set-Location -Path "${projectPath}"; codex resume "${sessionId}"; if ($LASTEXITCODE -ne 0) { codex }`;
|
||||||
|
} else {
|
||||||
|
shellCommand = `Set-Location -Path "${projectPath}"; codex`;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (hasSession && sessionId) {
|
||||||
|
// Try to resume session, but with fallback to a new session if it fails
|
||||||
|
shellCommand = `cd "${projectPath}" && codex resume "${sessionId}" || codex`;
|
||||||
|
} else {
|
||||||
|
shellCommand = `cd "${projectPath}" && codex`;
|
||||||
|
}
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
// Use claude command (default) or initialCommand if provided
|
// Use claude command (default) or initialCommand if provided
|
||||||
const command = initialCommand || 'claude';
|
const command = initialCommand || 'claude';
|
||||||
@@ -1886,6 +1908,9 @@ 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() {
|
||||||
@@ -1905,7 +1930,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, '0.0.0.0', async () => {
|
server.listen(PORT, HOST, async () => {
|
||||||
const appInstallPath = path.join(__dirname, '..');
|
const appInstallPath = path.join(__dirname, '..');
|
||||||
|
|
||||||
console.log('');
|
console.log('');
|
||||||
@@ -1913,7 +1938,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://0.0.0.0:' + PORT)}`);
|
console.log(`${c.info('[INFO]')} Server URL: ${c.bright('http://' + DISPLAY_HOST + ':' + 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('');
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
// Load environment variables from .env before other imports execute.
|
// Load environment variables from .env before other imports execute.
|
||||||
import fs from 'fs';
|
import fs from 'fs';
|
||||||
|
import os from 'os';
|
||||||
import path from 'path';
|
import path from 'path';
|
||||||
import { fileURLToPath } from 'url';
|
import { fileURLToPath } from 'url';
|
||||||
import { dirname } from 'path';
|
import { dirname } from 'path';
|
||||||
@@ -22,3 +23,7 @@ try {
|
|||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.log('No .env file found or error reading it:', e.message);
|
console.log('No .env file found or error reading it:', e.message);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!process.env.DATABASE_PATH) {
|
||||||
|
process.env.DATABASE_PATH = path.join(os.homedir(), '.cloudcli', 'auth.db');
|
||||||
|
}
|
||||||
|
|||||||
@@ -889,21 +889,80 @@ 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 session start data at this point. This needs to be revisited
|
// agent-*.jsonl files contain subagent tool history - we'll process them separately
|
||||||
// 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) {
|
||||||
@@ -928,6 +987,35 @@ 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)
|
||||||
|
|||||||
@@ -53,11 +53,11 @@ router.post('/register', async (req, res) => {
|
|||||||
// Generate token
|
// Generate token
|
||||||
const token = generateToken(user);
|
const token = generateToken(user);
|
||||||
|
|
||||||
// Update last login
|
|
||||||
userDb.updateLastLogin(user.id);
|
|
||||||
|
|
||||||
db.prepare('COMMIT').run();
|
db.prepare('COMMIT').run();
|
||||||
|
|
||||||
|
// Update last login (non-fatal, outside transaction)
|
||||||
|
userDb.updateLastLogin(user.id);
|
||||||
|
|
||||||
res.json({
|
res.json({
|
||||||
success: true,
|
success: true,
|
||||||
user: { id: user.id, username: user.username },
|
user: { id: user.id, username: user.username },
|
||||||
|
|||||||
@@ -63,5 +63,5 @@ export const CODEX_MODELS = {
|
|||||||
{ value: 'o4-mini', label: 'O4-mini' }
|
{ value: 'o4-mini', label: 'O4-mini' }
|
||||||
],
|
],
|
||||||
|
|
||||||
DEFAULT: 'gpt-5.2'
|
DEFAULT: 'gpt-5.3-codex'
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -162,7 +162,7 @@ function Shell({ selectedProject, selectedSession, initialCommand, isPlainShell
|
|||||||
projectPath: selectedProjectRef.current.fullPath || selectedProjectRef.current.path,
|
projectPath: selectedProjectRef.current.fullPath || selectedProjectRef.current.path,
|
||||||
sessionId: isPlainShellRef.current ? null : selectedSessionRef.current?.id,
|
sessionId: isPlainShellRef.current ? null : selectedSessionRef.current?.id,
|
||||||
hasSession: isPlainShellRef.current ? false : !!selectedSessionRef.current,
|
hasSession: isPlainShellRef.current ? false : !!selectedSessionRef.current,
|
||||||
provider: isPlainShellRef.current ? 'plain-shell' : (selectedSessionRef.current?.__provider || 'claude'),
|
provider: isPlainShellRef.current ? 'plain-shell' : (selectedSessionRef.current?.__provider || localStorage.getItem('selected-provider') || 'claude'),
|
||||||
cols: terminal.current.cols,
|
cols: terminal.current.cols,
|
||||||
rows: terminal.current.rows,
|
rows: terminal.current.rows,
|
||||||
initialCommand: initialCommandRef.current,
|
initialCommand: initialCommandRef.current,
|
||||||
|
|||||||
@@ -106,7 +106,7 @@ export default function AppContent() {
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<div className={`flex-1 flex flex-col min-w-0 ${isMobile && !isInputFocused ? 'pb-mobile-nav' : ''}`}>
|
<div className={`flex-1 flex flex-col min-w-0 ${isMobile ? 'pb-mobile-nav' : ''}`}>
|
||||||
<MainContent
|
<MainContent
|
||||||
selectedProject={selectedProject}
|
selectedProject={selectedProject}
|
||||||
selectedSession={selectedSession}
|
selectedSession={selectedSession}
|
||||||
|
|||||||
@@ -271,13 +271,14 @@ export function useChatComposerState({
|
|||||||
}, [setChatMessages]);
|
}, [setChatMessages]);
|
||||||
|
|
||||||
const executeCommand = useCallback(
|
const executeCommand = useCallback(
|
||||||
async (command: SlashCommand) => {
|
async (command: SlashCommand, rawInput?: string) => {
|
||||||
if (!command || !selectedProject) {
|
if (!command || !selectedProject) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const commandMatch = input.match(new RegExp(`${escapeRegExp(command.name)}\\s*(.*)`));
|
const effectiveInput = rawInput ?? input;
|
||||||
|
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+/) : [];
|
||||||
|
|
||||||
@@ -351,6 +352,7 @@ export function useChatComposerState({
|
|||||||
);
|
);
|
||||||
|
|
||||||
const {
|
const {
|
||||||
|
slashCommands,
|
||||||
slashCommandsCount,
|
slashCommandsCount,
|
||||||
filteredCommands,
|
filteredCommands,
|
||||||
frequentCommands,
|
frequentCommands,
|
||||||
@@ -473,6 +475,28 @@ 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) {
|
||||||
@@ -639,6 +663,7 @@ export function useChatComposerState({
|
|||||||
codexModel,
|
codexModel,
|
||||||
currentSessionId,
|
currentSessionId,
|
||||||
cursorModel,
|
cursorModel,
|
||||||
|
executeCommand,
|
||||||
isLoading,
|
isLoading,
|
||||||
onSessionActive,
|
onSessionActive,
|
||||||
pendingViewSessionRef,
|
pendingViewSessionRef,
|
||||||
@@ -654,6 +679,7 @@ export function useChatComposerState({
|
|||||||
setClaudeStatus,
|
setClaudeStatus,
|
||||||
setIsLoading,
|
setIsLoading,
|
||||||
setIsUserScrolledUp,
|
setIsUserScrolledUp,
|
||||||
|
slashCommands,
|
||||||
thinkingMode,
|
thinkingMode,
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
@@ -903,8 +929,11 @@ 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],
|
||||||
@@ -953,5 +982,6 @@ export function useChatComposerState({
|
|||||||
handlePermissionDecision,
|
handlePermissionDecision,
|
||||||
handleGrantToolPermission,
|
handleGrantToolPermission,
|
||||||
handleInputFocusChange,
|
handleInputFocusChange,
|
||||||
|
isInputFocused,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -336,9 +336,43 @@ 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,
|
||||||
{
|
{
|
||||||
@@ -350,6 +384,10 @@ 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;
|
||||||
@@ -382,6 +420,8 @@ 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;
|
||||||
@@ -389,8 +429,32 @@ export function useChatRealtimeHandlers({
|
|||||||
|
|
||||||
setChatMessages((previous) =>
|
setChatMessages((previous) =>
|
||||||
previous.map((message) => {
|
previous.map((message) => {
|
||||||
if (message.isToolUse && message.toolId === part.tool_use_id) {
|
// Handle child tool results (route to parent's subagentState)
|
||||||
|
if (parentToolUseId && message.toolId === parentToolUseId && message.isSubagentContainer) {
|
||||||
return {
|
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 = {
|
||||||
...message,
|
...message,
|
||||||
toolResult: {
|
toolResult: {
|
||||||
content: part.content,
|
content: part.content,
|
||||||
@@ -398,6 +462,14 @@ 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) => void | Promise<void>;
|
onExecuteCommand: (command: SlashCommand, rawInput?: string) => void | Promise<void>;
|
||||||
}
|
}
|
||||||
|
|
||||||
const getCommandHistoryKey = (projectName: string) => `command_history_${projectName}`;
|
const getCommandHistoryKey = (projectName: string) => `command_history_${projectName}`;
|
||||||
|
|||||||
@@ -1,7 +1,8 @@
|
|||||||
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 } from './components';
|
import { OneLineDisplay, CollapsibleDisplay, DiffViewer, MarkdownContent, FileListContent, TodoListContent, TaskListContent, TextContent, QuestionAnswerContent, SubagentContainer } 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;
|
||||||
@@ -21,6 +22,12 @@ 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 {
|
||||||
@@ -50,8 +57,24 @@ 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;
|
||||||
|
|
||||||
|
|||||||
@@ -24,7 +24,7 @@ export const CollapsibleSection: React.FC<CollapsibleSectionProps> = ({
|
|||||||
}) => {
|
}) => {
|
||||||
return (
|
return (
|
||||||
<details className={`relative group/details ${className}`} open={open}>
|
<details className={`relative group/details ${className}`} open={open}>
|
||||||
<summary className="flex items-center gap-1.5 text-xs cursor-pointer py-0.5 select-none">
|
<summary className="flex items-center gap-1.5 text-xs cursor-pointer py-0.5 select-none group-open/details:sticky group-open/details:top-0 group-open/details:z-10 group-open/details:bg-background group-open/details:-mx-1 group-open/details:px-1">
|
||||||
<svg
|
<svg
|
||||||
className="w-3 h-3 text-gray-400 dark:text-gray-500 transition-transform duration-150 group-open/details:rotate-90 flex-shrink-0"
|
className="w-3 h-3 text-gray-400 dark:text-gray-500 transition-transform duration-150 group-open/details:rotate-90 flex-shrink-0"
|
||||||
fill="none"
|
fill="none"
|
||||||
|
|||||||
180
src/components/chat/tools/components/SubagentContainer.tsx
Normal file
180
src/components/chat/tools/components/SubagentContainer.tsx
Normal file
@@ -0,0 +1,180 @@
|
|||||||
|
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,5 +2,6 @@ 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: true,
|
defaultOpen: false,
|
||||||
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,14 +424,8 @@ export const TOOL_CONFIGS: Record<string, ToolDisplayConfig> = {
|
|||||||
},
|
},
|
||||||
result: {
|
result: {
|
||||||
type: 'collapsible',
|
type: 'collapsible',
|
||||||
title: (result) => {
|
title: 'Subagent result',
|
||||||
// Check if result has content with type array (agent results often have this structure)
|
defaultOpen: false,
|
||||||
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,6 +17,14 @@ 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;
|
||||||
@@ -32,6 +40,12 @@ 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 }
|
{ content: unknown; isError: boolean; timestamp: Date; toolUseResult: unknown; subagentTools?: unknown[] }
|
||||||
>();
|
>();
|
||||||
|
|
||||||
rawMessages.forEach((message) => {
|
rawMessages.forEach((message) => {
|
||||||
@@ -368,6 +368,7 @@ 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,
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -484,6 +485,22 @@ 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: '',
|
||||||
@@ -491,6 +508,7 @@ 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:
|
||||||
@@ -503,6 +521,14 @@ 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,6 +163,7 @@ function ChatInterface({
|
|||||||
handlePermissionDecision,
|
handlePermissionDecision,
|
||||||
handleGrantToolPermission,
|
handleGrantToolPermission,
|
||||||
handleInputFocusChange,
|
handleInputFocusChange,
|
||||||
|
isInputFocused,
|
||||||
} = useChatComposerState({
|
} = useChatComposerState({
|
||||||
selectedProject,
|
selectedProject,
|
||||||
selectedSession,
|
selectedSession,
|
||||||
@@ -373,6 +374,7 @@ 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,6 +87,7 @@ 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;
|
||||||
@@ -143,6 +144,7 @@ export default function ChatComposer({
|
|||||||
onTextareaScrollSync,
|
onTextareaScrollSync,
|
||||||
onTextareaInput,
|
onTextareaInput,
|
||||||
onInputFocusChange,
|
onInputFocusChange,
|
||||||
|
isInputFocused,
|
||||||
placeholder,
|
placeholder,
|
||||||
isTextareaExpanded,
|
isTextareaExpanded,
|
||||||
sendByCtrlEnter,
|
sendByCtrlEnter,
|
||||||
@@ -162,8 +164,13 @@ 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">
|
<div className={`p-2 sm:p-4 md:p-4 flex-shrink-0 pb-2 sm:pb-4 md:pb-6 ${mobileFloatingClass}`}>
|
||||||
{!hasQuestionPanel && (
|
{!hasQuestionPanel && (
|
||||||
<div className="flex-1">
|
<div className="flex-1">
|
||||||
<ClaudeStatus
|
<ClaudeStatus
|
||||||
|
|||||||
@@ -184,6 +184,8 @@ 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,8 +62,8 @@ export default function MainContentTitle({
|
|||||||
</div>
|
</div>
|
||||||
) : showChatNewSession ? (
|
) : showChatNewSession ? (
|
||||||
<div className="min-w-0">
|
<div className="min-w-0">
|
||||||
<h2 className="text-sm font-semibold text-foreground leading-tight">{t('mainContent.newSession')}</h2>
|
<h2 className="text-base font-semibold text-foreground leading-tight">{t('mainContent.newSession')}</h2>
|
||||||
<div className="text-[11px] text-muted-foreground truncate leading-tight">{selectedProject.displayName}</div>
|
<div className="text-xs text-muted-foreground truncate leading-tight">{selectedProject.displayName}</div>
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<div className="min-w-0">
|
<div className="min-w-0">
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import { useCallback, useState } from "react";
|
|||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import { authenticatedFetch } from "../../utils/api";
|
import { authenticatedFetch } from "../../utils/api";
|
||||||
import { ReleaseInfo } from "../../types/sharedTypes";
|
import { ReleaseInfo } from "../../types/sharedTypes";
|
||||||
|
import type { InstallMode } from "../../hooks/useVersionCheck";
|
||||||
|
|
||||||
interface VersionUpgradeModalProps {
|
interface VersionUpgradeModalProps {
|
||||||
isOpen: boolean;
|
isOpen: boolean;
|
||||||
@@ -9,6 +10,7 @@ interface VersionUpgradeModalProps {
|
|||||||
releaseInfo: ReleaseInfo | null;
|
releaseInfo: ReleaseInfo | null;
|
||||||
currentVersion: string;
|
currentVersion: string;
|
||||||
latestVersion: string | null;
|
latestVersion: string | null;
|
||||||
|
installMode: InstallMode;
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function VersionUpgradeModal({
|
export default function VersionUpgradeModal({
|
||||||
@@ -16,9 +18,13 @@ export default function VersionUpgradeModal({
|
|||||||
onClose,
|
onClose,
|
||||||
releaseInfo,
|
releaseInfo,
|
||||||
currentVersion,
|
currentVersion,
|
||||||
latestVersion
|
latestVersion,
|
||||||
|
installMode
|
||||||
}: VersionUpgradeModalProps) {
|
}: VersionUpgradeModalProps) {
|
||||||
const { t } = useTranslation('common');
|
const { t } = useTranslation('common');
|
||||||
|
const upgradeCommand = installMode === 'npm'
|
||||||
|
? t('versionUpdate.npmUpgradeCommand')
|
||||||
|
: 'git checkout main && git pull && npm install';
|
||||||
const [isUpdating, setIsUpdating] = useState(false);
|
const [isUpdating, setIsUpdating] = useState(false);
|
||||||
const [updateOutput, setUpdateOutput] = useState('');
|
const [updateOutput, setUpdateOutput] = useState('');
|
||||||
const [updateError, setUpdateError] = useState('');
|
const [updateError, setUpdateError] = useState('');
|
||||||
@@ -150,7 +156,7 @@ export default function VersionUpgradeModal({
|
|||||||
<h3 className="text-sm font-medium text-gray-900 dark:text-white">{t('versionUpdate.manualUpgrade')}</h3>
|
<h3 className="text-sm font-medium text-gray-900 dark:text-white">{t('versionUpdate.manualUpgrade')}</h3>
|
||||||
<div className="bg-gray-100 dark:bg-gray-800 rounded-lg p-3 border">
|
<div className="bg-gray-100 dark:bg-gray-800 rounded-lg p-3 border">
|
||||||
<code className="text-sm text-gray-800 dark:text-gray-200 font-mono">
|
<code className="text-sm text-gray-800 dark:text-gray-200 font-mono">
|
||||||
git checkout main && git pull && npm install
|
{upgradeCommand}
|
||||||
</code>
|
</code>
|
||||||
</div>
|
</div>
|
||||||
<p className="text-xs text-gray-600 dark:text-gray-400">
|
<p className="text-xs text-gray-600 dark:text-gray-400">
|
||||||
@@ -171,7 +177,7 @@ export default function VersionUpgradeModal({
|
|||||||
<>
|
<>
|
||||||
<button
|
<button
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
navigator.clipboard.writeText('git checkout main && git pull && npm install');
|
navigator.clipboard.writeText(upgradeCommand);
|
||||||
}}
|
}}
|
||||||
className="flex-1 px-4 py-2 text-sm font-medium text-gray-700 dark:text-gray-300 bg-gray-100 dark:bg-gray-700 hover:bg-gray-200 dark:hover:bg-gray-600 rounded-md transition-colors"
|
className="flex-1 px-4 py-2 text-sm font-medium text-gray-700 dark:text-gray-300 bg-gray-100 dark:bg-gray-700 hover:bg-gray-200 dark:hover:bg-gray-600 rounded-md transition-colors"
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -38,7 +38,7 @@ function Sidebar({
|
|||||||
}: SidebarProps) {
|
}: SidebarProps) {
|
||||||
const { t } = useTranslation(['sidebar', 'common']);
|
const { t } = useTranslation(['sidebar', 'common']);
|
||||||
const { isPWA } = useDeviceSettings({ trackMobile: false });
|
const { isPWA } = useDeviceSettings({ trackMobile: false });
|
||||||
const { updateAvailable, latestVersion, currentVersion, releaseInfo } = useVersionCheck(
|
const { updateAvailable, latestVersion, currentVersion, releaseInfo, installMode } = useVersionCheck(
|
||||||
'siteboon',
|
'siteboon',
|
||||||
'claudecodeui',
|
'claudecodeui',
|
||||||
);
|
);
|
||||||
@@ -200,6 +200,7 @@ function Sidebar({
|
|||||||
releaseInfo={releaseInfo}
|
releaseInfo={releaseInfo}
|
||||||
currentVersion={currentVersion}
|
currentVersion={currentVersion}
|
||||||
latestVersion={latestVersion}
|
latestVersion={latestVersion}
|
||||||
|
installMode={installMode}
|
||||||
t={t}
|
t={t}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import Settings from '../../../Settings';
|
|||||||
import VersionUpgradeModal from '../../../modals/VersionUpgradeModal';
|
import VersionUpgradeModal from '../../../modals/VersionUpgradeModal';
|
||||||
import type { Project } from '../../../../types/app';
|
import type { Project } from '../../../../types/app';
|
||||||
import type { ReleaseInfo } from '../../../../types/sharedTypes';
|
import type { ReleaseInfo } from '../../../../types/sharedTypes';
|
||||||
|
import type { InstallMode } from '../../../../hooks/useVersionCheck';
|
||||||
import { normalizeProjectForSettings } from '../../utils/utils';
|
import { normalizeProjectForSettings } from '../../utils/utils';
|
||||||
import type { DeleteProjectConfirmation, SessionDeleteConfirmation, SettingsProject } from '../../types/types';
|
import type { DeleteProjectConfirmation, SessionDeleteConfirmation, SettingsProject } from '../../types/types';
|
||||||
|
|
||||||
@@ -30,6 +31,7 @@ type SidebarModalsProps = {
|
|||||||
releaseInfo: ReleaseInfo | null;
|
releaseInfo: ReleaseInfo | null;
|
||||||
currentVersion: string;
|
currentVersion: string;
|
||||||
latestVersion: string | null;
|
latestVersion: string | null;
|
||||||
|
installMode: InstallMode;
|
||||||
t: TFunction;
|
t: TFunction;
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -65,6 +67,7 @@ export default function SidebarModals({
|
|||||||
releaseInfo,
|
releaseInfo,
|
||||||
currentVersion,
|
currentVersion,
|
||||||
latestVersion,
|
latestVersion,
|
||||||
|
installMode,
|
||||||
t,
|
t,
|
||||||
}: SidebarModalsProps) {
|
}: SidebarModalsProps) {
|
||||||
// Settings expects project identity/path fields to be present for dropdown labels and local-scope MCP config.
|
// Settings expects project identity/path fields to be present for dropdown labels and local-scope MCP config.
|
||||||
@@ -199,6 +202,7 @@ export default function SidebarModals({
|
|||||||
releaseInfo={releaseInfo}
|
releaseInfo={releaseInfo}
|
||||||
currentVersion={currentVersion}
|
currentVersion={currentVersion}
|
||||||
latestVersion={latestVersion}
|
latestVersion={latestVersion}
|
||||||
|
installMode={installMode}
|
||||||
/>
|
/>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -21,10 +21,28 @@ const compareVersions = (v1: string, v2: string) => {
|
|||||||
return 0;
|
return 0;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export type InstallMode = 'git' | 'npm';
|
||||||
|
|
||||||
export const useVersionCheck = (owner: string, repo: string) => {
|
export const useVersionCheck = (owner: string, repo: string) => {
|
||||||
const [updateAvailable, setUpdateAvailable] = useState(false);
|
const [updateAvailable, setUpdateAvailable] = useState(false);
|
||||||
const [latestVersion, setLatestVersion] = useState<string | null>(null);
|
const [latestVersion, setLatestVersion] = useState<string | null>(null);
|
||||||
const [releaseInfo, setReleaseInfo] = useState<ReleaseInfo | null>(null);
|
const [releaseInfo, setReleaseInfo] = useState<ReleaseInfo | null>(null);
|
||||||
|
const [installMode, setInstallMode] = useState<InstallMode>('git');
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const fetchInstallMode = async () => {
|
||||||
|
try {
|
||||||
|
const response = await fetch('/health');
|
||||||
|
const data = await response.json();
|
||||||
|
if (data.installMode === 'npm' || data.installMode === 'git') {
|
||||||
|
setInstallMode(data.installMode);
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
// Default to git on error
|
||||||
|
}
|
||||||
|
};
|
||||||
|
fetchInstallMode();
|
||||||
|
}, []);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const checkVersion = async () => {
|
const checkVersion = async () => {
|
||||||
@@ -66,5 +84,5 @@ export const useVersionCheck = (owner: string, repo: string) => {
|
|||||||
return () => clearInterval(interval);
|
return () => clearInterval(interval);
|
||||||
}, [owner, repo]);
|
}, [owner, repo]);
|
||||||
|
|
||||||
return { updateAvailable, latestVersion, currentVersion: version, releaseInfo };
|
return { updateAvailable, latestVersion, currentVersion: version, releaseInfo, installMode };
|
||||||
};
|
};
|
||||||
@@ -200,6 +200,7 @@
|
|||||||
"viewFullRelease": "View full release",
|
"viewFullRelease": "View full release",
|
||||||
"updateProgress": "Update Progress:",
|
"updateProgress": "Update Progress:",
|
||||||
"manualUpgrade": "Manual upgrade:",
|
"manualUpgrade": "Manual upgrade:",
|
||||||
|
"npmUpgradeCommand": "npm install -g @siteboon/claude-code-ui@latest",
|
||||||
"manualUpgradeHint": "Or click \"Update Now\" to run the update automatically.",
|
"manualUpgradeHint": "Or click \"Update Now\" to run the update automatically.",
|
||||||
"updateCompleted": "Update completed successfully!",
|
"updateCompleted": "Update completed successfully!",
|
||||||
"restartServer": "Please restart the server to apply changes.",
|
"restartServer": "Please restart the server to apply changes.",
|
||||||
|
|||||||
@@ -200,6 +200,7 @@
|
|||||||
"viewFullRelease": "リリース全文を見る",
|
"viewFullRelease": "リリース全文を見る",
|
||||||
"updateProgress": "アップデートの進捗:",
|
"updateProgress": "アップデートの進捗:",
|
||||||
"manualUpgrade": "手動アップグレード:",
|
"manualUpgrade": "手動アップグレード:",
|
||||||
|
"npmUpgradeCommand": "npm install -g @siteboon/claude-code-ui@latest",
|
||||||
"manualUpgradeHint": "または「今すぐ更新」をクリックして自動的にアップデートを実行できます。",
|
"manualUpgradeHint": "または「今すぐ更新」をクリックして自動的にアップデートを実行できます。",
|
||||||
"updateCompleted": "アップデートが完了しました!",
|
"updateCompleted": "アップデートが完了しました!",
|
||||||
"restartServer": "変更を適用するにはサーバーを再起動してください。",
|
"restartServer": "変更を適用するにはサーバーを再起動してください。",
|
||||||
|
|||||||
@@ -200,6 +200,7 @@
|
|||||||
"viewFullRelease": "전체 릴리스 보기",
|
"viewFullRelease": "전체 릴리스 보기",
|
||||||
"updateProgress": "업데이트 진행 상황:",
|
"updateProgress": "업데이트 진행 상황:",
|
||||||
"manualUpgrade": "수동 업그레이드:",
|
"manualUpgrade": "수동 업그레이드:",
|
||||||
|
"npmUpgradeCommand": "npm install -g @siteboon/claude-code-ui@latest",
|
||||||
"manualUpgradeHint": "또는 \"지금 업데이트\"를 클릭하여 자동으로 업데이트합니다.",
|
"manualUpgradeHint": "또는 \"지금 업데이트\"를 클릭하여 자동으로 업데이트합니다.",
|
||||||
"updateCompleted": "업데이트가 완료되었습니다!",
|
"updateCompleted": "업데이트가 완료되었습니다!",
|
||||||
"restartServer": "변경사항을 적용하려면 서버를 재시작하세요.",
|
"restartServer": "변경사항을 적용하려면 서버를 재시작하세요.",
|
||||||
|
|||||||
@@ -200,6 +200,7 @@
|
|||||||
"viewFullRelease": "查看完整发布",
|
"viewFullRelease": "查看完整发布",
|
||||||
"updateProgress": "更新进度:",
|
"updateProgress": "更新进度:",
|
||||||
"manualUpgrade": "手动升级:",
|
"manualUpgrade": "手动升级:",
|
||||||
|
"npmUpgradeCommand": "npm install -g @siteboon/claude-code-ui@latest",
|
||||||
"manualUpgradeHint": "或点击'立即更新'以自动运行更新。",
|
"manualUpgradeHint": "或点击'立即更新'以自动运行更新。",
|
||||||
"updateCompleted": "更新成功完成!",
|
"updateCompleted": "更新成功完成!",
|
||||||
"restartServer": "请重启服务器以应用更改。",
|
"restartServer": "请重启服务器以应用更改。",
|
||||||
|
|||||||
@@ -5,19 +5,25 @@ 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://localhost:${env.PORT || 3001}`,
|
'/api': `http://${proxyHost}:${port}`,
|
||||||
'/ws': {
|
'/ws': {
|
||||||
target: `ws://localhost:${env.PORT || 3001}`,
|
target: `ws://${proxyHost}:${port}`,
|
||||||
ws: true
|
ws: true
|
||||||
},
|
},
|
||||||
'/shell': {
|
'/shell': {
|
||||||
target: `ws://localhost:${env.PORT || 3001}`,
|
target: `ws://${proxyHost}:${port}`,
|
||||||
ws: true
|
ws: true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user