Compare commits

..

5 Commits

Author SHA1 Message Date
Simos Mikelatos
9d7d12742f Merge branch 'main' into feature/restart-server-on-update 2026-04-14 17:18:33 +02:00
Haileyesus
1a0f10217d feat: simplify update commands for platform 2026-04-14 16:34:05 +03:00
Haileyesus
f8d1a0dd2c feat: optimize platform update command by omitting dev dependencies 2026-04-14 15:59:06 +03:00
Haileyesus
78c1d35a5d feat: add update platform script to package.json 2026-04-14 15:32:04 +03:00
Haileyesus
906997391d feat: support restart server on update for platform 2026-04-14 15:30:23 +03:00
14 changed files with 272 additions and 120 deletions

View File

@@ -3,32 +3,6 @@
All notable changes to CloudCLI UI will be documented in this file. All notable changes to CloudCLI UI will be documented in this file.
## [1.29.2](https://github.com/siteboon/claudecodeui/compare/v1.29.1...v1.29.2) (2026-04-14)
### Bug Fixes
* **sandbox:** use backgrounded sbx run to keep sandbox alive ([9b11c03](https://github.com/siteboon/claudecodeui/commit/9b11c034d9a19710a23b56c62dcf07c21a17bd97))
## [1.29.1](https://github.com/siteboon/claudecodeui/compare/v1.29.0...v1.29.1) (2026-04-14)
### Bug Fixes
* add latest tag to docker npx command and change the detach mode to work without spawn ([4a56972](https://github.com/siteboon/claudecodeui/commit/4a569725dae320a505753359d8edfd8ca79f0fd7))
## [1.29.0](https://github.com/siteboon/claudecodeui/compare/v1.28.1...v1.29.0) (2026-04-14)
### New Features
* adding docker sandbox environments ([13e97e2](https://github.com/siteboon/claudecodeui/commit/13e97e2c71254de7a60afb5495b21064c4bc4241))
### Bug Fixes
* **thinking-mode:** fix dropdown positioning ([#646](https://github.com/siteboon/claudecodeui/issues/646)) ([c7a5baf](https://github.com/siteboon/claudecodeui/commit/c7a5baf1479404bd40e23aa58bd9f677df9a04c6))
### Maintenance
* update release flow node version ([e2459cb](https://github.com/siteboon/claudecodeui/commit/e2459cb0f8b35f54827778a7b444e6c3ca326506))
## [1.28.1](https://github.com/siteboon/claudecodeui/compare/v1.28.0...v1.28.1) (2026-04-10) ## [1.28.1](https://github.com/siteboon/claudecodeui/compare/v1.28.0...v1.28.1) (2026-04-10)
### New Features ### New Features

View File

@@ -100,7 +100,7 @@ Die **[Dokumentation →](https://cloudcli.ai/docs)** enthält weitere Konfigura
Agents in isolierten Sandboxes mit Hypervisor-Isolation ausführen. Standardmäßig wird Claude Code gestartet. Erfordert die [`sbx` CLI](https://docs.docker.com/ai/sandboxes/get-started/). Agents in isolierten Sandboxes mit Hypervisor-Isolation ausführen. Standardmäßig wird Claude Code gestartet. Erfordert die [`sbx` CLI](https://docs.docker.com/ai/sandboxes/get-started/).
``` ```
npx @cloudcli-ai/cloudcli@latest sandbox ~/my-project npx @cloudcli-ai/cloudcli sandbox ~/my-project
``` ```
Unterstützt Claude Code, Codex und Gemini CLI. Weitere Details in der [Sandbox-Dokumentation](docker/). Unterstützt Claude Code, Codex und Gemini CLI. Weitere Details in der [Sandbox-Dokumentation](docker/).

View File

@@ -96,7 +96,7 @@ cloudcli
ハイパーバイザーレベルの分離でエージェントをサンドボックスで実行します。デフォルトでは Claude Code が起動します。[`sbx` CLI](https://docs.docker.com/ai/sandboxes/get-started/) が必要です。 ハイパーバイザーレベルの分離でエージェントをサンドボックスで実行します。デフォルトでは Claude Code が起動します。[`sbx` CLI](https://docs.docker.com/ai/sandboxes/get-started/) が必要です。
``` ```
npx @cloudcli-ai/cloudcli@latest sandbox ~/my-project npx @cloudcli-ai/cloudcli sandbox ~/my-project
``` ```
Claude Code、Codex、Gemini CLI に対応。詳細は[サンドボックスのドキュメント](docker/)をご覧ください。 Claude Code、Codex、Gemini CLI に対応。詳細は[サンドボックスのドキュメント](docker/)をご覧ください。

View File

@@ -96,7 +96,7 @@ cloudcli
하이퍼바이저 수준 격리로 에이전트를 샌드박스에서 실행합니다. 기본 에이전트는 Claude Code입니다. [`sbx` CLI](https://docs.docker.com/ai/sandboxes/get-started/)가 필요합니다. 하이퍼바이저 수준 격리로 에이전트를 샌드박스에서 실행합니다. 기본 에이전트는 Claude Code입니다. [`sbx` CLI](https://docs.docker.com/ai/sandboxes/get-started/)가 필요합니다.
``` ```
npx @cloudcli-ai/cloudcli@latest sandbox ~/my-project npx @cloudcli-ai/cloudcli sandbox ~/my-project
``` ```
Claude Code, Codex, Gemini CLI를 지원합니다. 자세한 내용은 [샌드박스 문서](docker/)를 참고하세요. Claude Code, Codex, Gemini CLI를 지원합니다. 자세한 내용은 [샌드박스 문서](docker/)를 참고하세요.

View File

@@ -100,7 +100,7 @@ Visit the **[documentation →](https://cloudcli.ai/docs)** for full configurati
Run agents in isolated sandboxes with hypervisor-level isolation. Starts Claude Code by default. Requires the [`sbx` CLI](https://docs.docker.com/ai/sandboxes/get-started/). Run agents in isolated sandboxes with hypervisor-level isolation. Starts Claude Code by default. Requires the [`sbx` CLI](https://docs.docker.com/ai/sandboxes/get-started/).
``` ```
npx @cloudcli-ai/cloudcli@latest sandbox ~/my-project npx @cloudcli-ai/cloudcli sandbox ~/my-project
``` ```
Supports Claude Code, Codex, and Gemini CLI. See the [sandbox docs](docker/) for setup and advanced options. Supports Claude Code, Codex, and Gemini CLI. See the [sandbox docs](docker/) for setup and advanced options.
@@ -116,7 +116,7 @@ CloudCLI UI is the open source UI layer that powers CloudCLI Cloud. You can self
|---|---|---|---| |---|---|---|---|
| **Best for** | Local agent sessions on your own machine | Isolated agents with web/mobile IDE | Teams who want agents in the cloud | | **Best for** | Local agent sessions on your own machine | Isolated agents with web/mobile IDE | Teams who want agents in the cloud |
| **How you access it** | Browser via `[yourip]:port` | Browser via `localhost:port` | Browser, any IDE, REST API, n8n | | **How you access it** | Browser via `[yourip]:port` | Browser via `localhost:port` | Browser, any IDE, REST API, n8n |
| **Setup** | `npx @cloudcli-ai/cloudcli` | `npx @cloudcli-ai/cloudcli@latest sandbox ~/project` | No setup required | | **Setup** | `npx @cloudcli-ai/cloudcli` | `npx @cloudcli-ai/cloudcli sandbox ~/project` | No setup required |
| **Isolation** | Runs on your host | Hypervisor-level sandbox (microVM) | Full cloud isolation | | **Isolation** | Runs on your host | Hypervisor-level sandbox (microVM) | Full cloud isolation |
| **Machine needs to stay on** | Yes | Yes | No | | **Machine needs to stay on** | Yes | Yes | No |
| **Mobile access** | Any browser on your network | Any browser on your network | Any device, native app coming | | **Mobile access** | Any browser on your network | Any browser on your network | Any device, native app coming |

View File

@@ -100,7 +100,7 @@ cloudcli
Запускайте агентов в изолированных песочницах с гипервизорной изоляцией. По умолчанию запускается Claude Code. Требуется [`sbx` CLI](https://docs.docker.com/ai/sandboxes/get-started/). Запускайте агентов в изолированных песочницах с гипервизорной изоляцией. По умолчанию запускается Claude Code. Требуется [`sbx` CLI](https://docs.docker.com/ai/sandboxes/get-started/).
``` ```
npx @cloudcli-ai/cloudcli@latest sandbox ~/my-project npx @cloudcli-ai/cloudcli sandbox ~/my-project
``` ```
Поддерживаются Claude Code, Codex и Gemini CLI. Подробнее в [документации sandbox](docker/). Поддерживаются Claude Code, Codex и Gemini CLI. Подробнее в [документации sandbox](docker/).

View File

@@ -96,7 +96,7 @@ cloudcli
在隔离的沙箱中运行代理,具有虚拟机管理程序级别的隔离。默认启动 Claude Code。需要 [`sbx` CLI](https://docs.docker.com/ai/sandboxes/get-started/)。 在隔离的沙箱中运行代理,具有虚拟机管理程序级别的隔离。默认启动 Claude Code。需要 [`sbx` CLI](https://docs.docker.com/ai/sandboxes/get-started/)。
``` ```
npx @cloudcli-ai/cloudcli@latest sandbox ~/my-project npx @cloudcli-ai/cloudcli sandbox ~/my-project
``` ```
支持 Claude Code、Codex 和 Gemini CLI。详情请参阅 [沙箱文档](docker/)。 支持 Claude Code、Codex 和 Gemini CLI。详情请参阅 [沙箱文档](docker/)。

View File

@@ -29,7 +29,7 @@ sbx secret set -g anthropic
### 3. Launch Claude Code ### 3. Launch Claude Code
```bash ```bash
npx @cloudcli-ai/cloudcli@latest sandbox ~/my-project npx @cloudcli-ai/cloudcli sandbox ~/my-project
``` ```
Open **http://localhost:3001**. Set a password on first visit. Start building. Open **http://localhost:3001**. Set a password on first visit. Start building.
@@ -41,11 +41,11 @@ Store the matching API key and pass `--agent`:
```bash ```bash
# OpenAI Codex # OpenAI Codex
sbx secret set -g openai sbx secret set -g openai
npx @cloudcli-ai/cloudcli@latest sandbox ~/my-project --agent codex npx @cloudcli-ai/cloudcli sandbox ~/my-project --agent codex
# Gemini CLI # Gemini CLI
sbx secret set -g google sbx secret set -g google
npx @cloudcli-ai/cloudcli@latest sandbox ~/my-project --agent gemini npx @cloudcli-ai/cloudcli sandbox ~/my-project --agent gemini
``` ```
### Available templates ### Available templates
@@ -61,19 +61,11 @@ These are used with `--template` when running `sbx` directly (see [Advanced usag
## Managing sandboxes ## Managing sandboxes
```bash ```bash
sbx ls # List all sandboxes cloudcli sandbox ls # List all sandboxes
sbx stop my-project # Stop (preserves state) cloudcli sandbox stop my-project # Stop (preserves state)
sbx start my-project # Restart a stopped sandbox
sbx rm my-project # Remove everything
sbx exec my-project bash # Open a shell inside the sandbox
```
If you install CloudCLI globally (`npm install -g @cloudcli-ai/cloudcli`), you can also use:
```bash
cloudcli sandbox ls
cloudcli sandbox start my-project # Restart and re-launch web UI cloudcli sandbox start my-project # Restart and re-launch web UI
cloudcli sandbox logs my-project # View server logs cloudcli sandbox logs my-project # View server logs
cloudcli sandbox rm my-project # Remove everything
``` ```
## What you get ## What you get
@@ -92,20 +84,14 @@ Your project directory is mounted bidirectionally — edits propagate in real ti
Set variables at creation time with `--env`: Set variables at creation time with `--env`:
```bash ```bash
npx @cloudcli-ai/cloudcli@latest sandbox ~/my-project --env SERVER_PORT=8080 npx @cloudcli-ai/cloudcli sandbox ~/my-project --env SERVER_PORT=8080
``` ```
Or inside a running sandbox: Or inside a running sandbox:
```bash ```bash
sbx exec my-project bash -c 'echo "export SERVER_PORT=8080" >> /etc/sandbox-persistent.sh' sbx exec my-project bash -c 'echo "export SERVER_PORT=8080" >> /etc/sandbox-persistent.sh'
``` sbx exec my-project bash -c 'pkill -f "server/index.js"; . ~/.cloudcli-start.sh'
Restart CloudCLI for changes to take effect:
```bash
sbx exec my-project bash -c 'pkill -f "server/index.js"'
sbx exec -d my-project cloudcli start --port 3001
``` ```
| Variable | Default | Description | | Variable | Default | Description |

4
package-lock.json generated
View File

@@ -1,12 +1,12 @@
{ {
"name": "@cloudcli-ai/cloudcli", "name": "@cloudcli-ai/cloudcli",
"version": "1.29.2", "version": "1.28.1",
"lockfileVersion": 3, "lockfileVersion": 3,
"requires": true, "requires": true,
"packages": { "packages": {
"": { "": {
"name": "@cloudcli-ai/cloudcli", "name": "@cloudcli-ai/cloudcli",
"version": "1.29.2", "version": "1.28.1",
"hasInstallScript": true, "hasInstallScript": true,
"license": "AGPL-3.0-or-later", "license": "AGPL-3.0-or-later",
"dependencies": { "dependencies": {

View File

@@ -1,6 +1,6 @@
{ {
"name": "@cloudcli-ai/cloudcli", "name": "@cloudcli-ai/cloudcli",
"version": "1.29.2", "version": "1.28.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",

View File

@@ -367,7 +367,7 @@ Advanced usage:
} }
async function sandboxCommand(args) { async function sandboxCommand(args) {
const { execFileSync, spawn: spawnProcess } = await import('child_process'); const { execFileSync } = await import('child_process');
// Safe execution — uses execFileSync (no shell) to prevent injection // Safe execution — uses execFileSync (no shell) to prevent injection
const sbx = (subcmd, opts = {}) => { const sbx = (subcmd, opts = {}) => {
@@ -443,15 +443,12 @@ async function sandboxCommand(args) {
process.exit(1); process.exit(1);
} }
console.log(`\n${c.info('▶')} Starting sandbox ${c.bright(opts.name)}...`); console.log(`\n${c.info('▶')} Starting sandbox ${c.bright(opts.name)}...`);
const restartRun = spawnProcess('sbx', ['run', opts.name], { try {
detached: true, sbx(['start', opts.name], { inherit: true });
stdio: ['ignore', 'ignore', 'ignore'], } catch { /* might already be running */ }
});
restartRun.unref();
await new Promise(resolve => setTimeout(resolve, 5000));
console.log(`${c.info('▶')} Launching CloudCLI web server...`); console.log(`${c.info('▶')} Launching CloudCLI web server...`);
sbx(['exec', opts.name, 'bash', '-c', 'cloudcli start --port 3001 &']); sbx(['exec', '-d', opts.name, 'bash', '-c', '. ~/.cloudcli-start.sh']);
console.log(`${c.info('▶')} Forwarding port ${opts.port} → 3001...`); console.log(`${c.info('▶')} Forwarding port ${opts.port} → 3001...`);
try { try {
@@ -518,19 +515,22 @@ async function sandboxCommand(args) {
} }
console.log(c.dim('─'.repeat(50))); console.log(c.dim('─'.repeat(50)));
// Step 1: Launch sandbox with sbx run in background. // Step 1: Create sandbox
// sbx run creates the sandbox (or reconnects) AND holds an active session,
// which prevents the sandbox from auto-stopping.
console.log(`\n${c.info('▶')} Creating sandbox ${c.bright(opts.name)}...`); console.log(`\n${c.info('▶')} Creating sandbox ${c.bright(opts.name)}...`);
const bgRun = spawnProcess('sbx', [ try {
'run', '--template', opts.template, '--name', opts.name, opts.agent, workspace, sbx(
], { ['create', '--template', opts.template, '--name', opts.name, opts.agent, workspace],
detached: true, { inherit: true }
stdio: ['ignore', 'ignore', 'ignore'], );
}); } catch (e) {
bgRun.unref(); const msg = e.stdout || e.stderr || e.message || '';
// Wait for sandbox to be ready if (msg.includes('already exists')) {
await new Promise(resolve => setTimeout(resolve, 5000)); console.log(`${c.warn('⚠')} Sandbox ${c.bright(opts.name)} already exists. Starting it instead...\n`);
try { sbx(['start', opts.name]); } catch { /* may already be running */ }
} else {
throw e;
}
}
// Step 2: Inject environment variables // Step 2: Inject environment variables
if (opts.env.length > 0) { if (opts.env.length > 0) {
@@ -548,9 +548,14 @@ async function sandboxCommand(args) {
} }
} }
// Step 3: Start CloudCLI inside the sandbox // Step 3: Start CloudCLI
console.log(`${c.info('▶')} Launching CloudCLI web server...`); console.log(`${c.info('▶')} Launching CloudCLI web server...`);
sbx(['exec', opts.name, 'bash', '-c', 'cloudcli start --port 3001 &']); try {
sbx(['exec', '-d', opts.name, 'bash', '-c', '. ~/.cloudcli-start.sh']);
} catch (e) {
console.error(`${c.error('❌')} Failed to start CloudCLI: ${e.message}`);
process.exit(1);
}
// Step 4: Forward port // Step 4: Forward port
console.log(`${c.info('▶')} Forwarding port ${opts.port} → 3001...`); console.log(`${c.info('▶')} Forwarding port ${opts.port} → 3001...`);
@@ -577,11 +582,10 @@ async function sandboxCommand(args) {
console.log(`\n${c.ok('✔')} ${c.bright('CloudCLI is ready!')}`); console.log(`\n${c.ok('✔')} ${c.bright('CloudCLI is ready!')}`);
console.log(` ${c.info('→')} Open ${c.bright(`http://localhost:${opts.port}`)}`); console.log(` ${c.info('→')} Open ${c.bright(`http://localhost:${opts.port}`)}`);
console.log(`\n${c.dim(' Manage with:')}`); console.log(`\n${c.dim(' Manage with:')}`);
console.log(` ${c.dim('$')} sbx ls`); console.log(` ${c.dim('$')} cloudcli sandbox ls`);
console.log(` ${c.dim('$')} sbx stop ${opts.name}`); console.log(` ${c.dim('$')} cloudcli sandbox stop ${opts.name}`);
console.log(` ${c.dim('$')} sbx start ${opts.name}`); console.log(` ${c.dim('$')} cloudcli sandbox start ${opts.name}`);
console.log(` ${c.dim('$')} sbx rm ${opts.name}`); console.log(` ${c.dim('$')} cloudcli sandbox rm ${opts.name}\n`);
console.log(`\n${c.dim(' Or install globally:')} npm install -g @cloudcli-ai/cloudcli\n`);
break; break;
} }

View File

@@ -2,6 +2,7 @@ import express from 'express';
import { promises as fs } from 'fs'; import { promises as fs } from 'fs';
import path from 'path'; import path from 'path';
import os from 'os'; import os from 'os';
import { spawn } from 'child_process';
import sqlite3 from 'sqlite3'; import sqlite3 from 'sqlite3';
import { open } from 'sqlite'; import { open } from 'sqlite';
import crypto from 'crypto'; import crypto from 'crypto';
@@ -577,4 +578,221 @@ router.get('/sessions', async (req, res) => {
}); });
} }
}); });
// GET /api/cursor/sessions/:sessionId - Get specific Cursor session from SQLite
router.get('/sessions/:sessionId', async (req, res) => {
try {
const { sessionId } = req.params;
const { projectPath } = req.query;
// Calculate cwdID hash for the project path
const cwdId = crypto.createHash('md5').update(projectPath || process.cwd()).digest('hex');
const storeDbPath = path.join(os.homedir(), '.cursor', 'chats', cwdId, sessionId, 'store.db');
// Open SQLite database
const db = await open({
filename: storeDbPath,
driver: sqlite3.Database,
mode: sqlite3.OPEN_READONLY
});
// Get all blobs to build the DAG structure
const allBlobs = await db.all(`
SELECT rowid, id, data FROM blobs
`);
// Build the DAG structure from parent-child relationships
const blobMap = new Map(); // id -> blob data
const parentRefs = new Map(); // blob id -> [parent blob ids]
const childRefs = new Map(); // blob id -> [child blob ids]
const jsonBlobs = []; // Clean JSON messages
for (const blob of allBlobs) {
blobMap.set(blob.id, blob);
// Check if this is a JSON blob (actual message) or protobuf (DAG structure)
if (blob.data && blob.data[0] === 0x7B) { // Starts with '{' - JSON blob
try {
const parsed = JSON.parse(blob.data.toString('utf8'));
jsonBlobs.push({ ...blob, parsed });
} catch (e) {
console.log('Failed to parse JSON blob:', blob.rowid);
}
} else if (blob.data) { // Protobuf blob - extract parent references
const parents = [];
let i = 0;
// Scan for parent references (0x0A 0x20 followed by 32-byte hash)
while (i < blob.data.length - 33) {
if (blob.data[i] === 0x0A && blob.data[i+1] === 0x20) {
const parentHash = blob.data.slice(i+2, i+34).toString('hex');
if (blobMap.has(parentHash)) {
parents.push(parentHash);
}
i += 34;
} else {
i++;
}
}
if (parents.length > 0) {
parentRefs.set(blob.id, parents);
// Update child references
for (const parentId of parents) {
if (!childRefs.has(parentId)) {
childRefs.set(parentId, []);
}
childRefs.get(parentId).push(blob.id);
}
}
}
}
// Perform topological sort to get chronological order
const visited = new Set();
const sorted = [];
// DFS-based topological sort
function visit(nodeId) {
if (visited.has(nodeId)) return;
visited.add(nodeId);
// Visit all parents first (dependencies)
const parents = parentRefs.get(nodeId) || [];
for (const parentId of parents) {
visit(parentId);
}
// Add this node after all its parents
const blob = blobMap.get(nodeId);
if (blob) {
sorted.push(blob);
}
}
// Start with nodes that have no parents (roots)
for (const blob of allBlobs) {
if (!parentRefs.has(blob.id)) {
visit(blob.id);
}
}
// Visit any remaining nodes (disconnected components)
for (const blob of allBlobs) {
visit(blob.id);
}
// Now extract JSON messages in the order they appear in the sorted DAG
const messageOrder = new Map(); // JSON blob id -> order index
let orderIndex = 0;
for (const blob of sorted) {
// Check if this blob references any JSON messages
if (blob.data && blob.data[0] !== 0x7B) { // Protobuf blob
// Look for JSON blob references
for (const jsonBlob of jsonBlobs) {
try {
const jsonIdBytes = Buffer.from(jsonBlob.id, 'hex');
if (blob.data.includes(jsonIdBytes)) {
if (!messageOrder.has(jsonBlob.id)) {
messageOrder.set(jsonBlob.id, orderIndex++);
}
}
} catch (e) {
// Skip if can't convert ID
}
}
}
}
// Sort JSON blobs by their appearance order in the DAG
const sortedJsonBlobs = jsonBlobs.sort((a, b) => {
const orderA = messageOrder.get(a.id) ?? Number.MAX_SAFE_INTEGER;
const orderB = messageOrder.get(b.id) ?? Number.MAX_SAFE_INTEGER;
if (orderA !== orderB) return orderA - orderB;
// Fallback to rowid if not in order map
return a.rowid - b.rowid;
});
// Use sorted JSON blobs
const blobs = sortedJsonBlobs.map((blob, idx) => ({
...blob,
sequence_num: idx + 1,
original_rowid: blob.rowid
}));
// Get metadata from meta table
const metaRows = await db.all(`
SELECT key, value FROM meta
`);
// Parse metadata
let metadata = {};
for (const row of metaRows) {
if (row.value) {
try {
// Try to decode as hex-encoded JSON
const hexMatch = row.value.toString().match(/^[0-9a-fA-F]+$/);
if (hexMatch) {
const jsonStr = Buffer.from(row.value, 'hex').toString('utf8');
metadata[row.key] = JSON.parse(jsonStr);
} else {
metadata[row.key] = row.value.toString();
}
} catch (e) {
metadata[row.key] = row.value.toString();
}
}
}
// Extract messages from sorted JSON blobs
const messages = [];
for (const blob of blobs) {
try {
// We already parsed JSON blobs earlier
const parsed = blob.parsed;
if (parsed) {
// Filter out ONLY system messages at the server level
// Check both direct role and nested message.role
const role = parsed?.role || parsed?.message?.role;
if (role === 'system') {
continue; // Skip only system messages
}
messages.push({
id: blob.id,
sequence: blob.sequence_num,
rowid: blob.original_rowid,
content: parsed
});
}
} catch (e) {
// Skip blobs that cause errors
console.log(`Skipping blob ${blob.id}: ${e.message}`);
}
}
await db.close();
res.json({
success: true,
session: {
id: sessionId,
projectPath: projectPath,
messages: messages,
metadata: metadata,
cwdId: cwdId
}
});
} catch (error) {
console.error('Error reading Cursor session:', error);
res.status(500).json({
error: 'Failed to read Cursor session',
details: error.message
});
}
});
export default router; export default router;

View File

@@ -18,10 +18,9 @@ export const CLAUDE_MODELS = {
{ value: "haiku", label: "Haiku" }, { value: "haiku", label: "Haiku" },
{ value: "opusplan", label: "Opus Plan" }, { value: "opusplan", label: "Opus Plan" },
{ value: "sonnet[1m]", label: "Sonnet [1M]" }, { value: "sonnet[1m]", label: "Sonnet [1M]" },
{ value: "opus[1m]", label: "Opus [1M]" },
], ],
DEFAULT: "opus", DEFAULT: "sonnet",
}; };
/** /**
@@ -59,7 +58,6 @@ export const CURSOR_MODELS = {
export const CODEX_MODELS = { export const CODEX_MODELS = {
OPTIONS: [ OPTIONS: [
{ value: "gpt-5.4", label: "GPT-5.4" }, { value: "gpt-5.4", label: "GPT-5.4" },
{ value: "gpt-5.4-mini", label: "GPT-5.4 mini" },
{ value: "gpt-5.3-codex", label: "GPT-5.3 Codex" }, { value: "gpt-5.3-codex", label: "GPT-5.3 Codex" },
{ value: "gpt-5.2-codex", label: "GPT-5.2 Codex" }, { value: "gpt-5.2-codex", label: "GPT-5.2 Codex" },
{ value: "gpt-5.2", label: "GPT-5.2" }, { value: "gpt-5.2", label: "GPT-5.2" },
@@ -90,5 +88,5 @@ export const GEMINI_MODELS = {
}, },
], ],
DEFAULT: "gemini-3.1-pro-preview", DEFAULT: "gemini-2.5-flash",
}; };

View File

@@ -1,4 +1,4 @@
import { useCallback, useEffect, useState } from "react"; 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";
@@ -15,8 +15,6 @@ interface VersionUpgradeModalProps {
installMode: InstallMode; installMode: InstallMode;
} }
const RELOAD_COUNTDOWN_START = 30;
export function VersionUpgradeModal({ export function VersionUpgradeModal({
isOpen, isOpen,
onClose, onClose,
@@ -34,30 +32,10 @@ export function VersionUpgradeModal({
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('');
const [reloadCountdown, setReloadCountdown] = useState<number | null>(null);
useEffect(() => {
if (!IS_PLATFORM || reloadCountdown === null || reloadCountdown <= 0) {
return;
}
const timeoutId = window.setTimeout(() => {
setReloadCountdown((previousCountdown) => {
if (previousCountdown === null) {
return null;
}
return Math.max(previousCountdown - 1, 0);
});
}, 1000);
return () => window.clearTimeout(timeoutId);
}, [reloadCountdown]);
const handleUpdateNow = useCallback(async () => { const handleUpdateNow = useCallback(async () => {
setIsUpdating(true); setIsUpdating(true);
setUpdateOutput('Starting update...\n'); setUpdateOutput('Starting update...\n');
setReloadCountdown(IS_PLATFORM ? RELOAD_COUNTDOWN_START : null);
setUpdateError(''); setUpdateError('');
try { try {
@@ -71,7 +49,8 @@ export function VersionUpgradeModal({
if (response.ok) { if (response.ok) {
setUpdateOutput(prev => prev + data.output + '\n'); setUpdateOutput(prev => prev + data.output + '\n');
setUpdateOutput(prev => prev + '\n✅ Update completed successfully!\n'); setUpdateOutput(prev => prev + '\n✅ Update completed successfully!\n');
setUpdateOutput(prev => prev + 'Please restart the server to apply changes.' + '\n'); const text = IS_PLATFORM ? 'Please refresh the page after 5 seconds to load the new version. If that doesn\'t work, RESTART the environment.' : 'Please restart the server to apply changes.';
setUpdateOutput(prev => prev + text + '\n');
} else { } else {
setUpdateError(data.error || 'Update failed'); setUpdateError(data.error || 'Update failed');
setUpdateOutput(prev => prev + '\n❌ Update failed: ' + (data.error || 'Unknown error') + '\n'); setUpdateOutput(prev => prev + '\n❌ Update failed: ' + (data.error || 'Unknown error') + '\n');
@@ -168,13 +147,6 @@ export function VersionUpgradeModal({
<div className="max-h-48 overflow-y-auto rounded-lg border border-gray-700 bg-gray-900 p-4 dark:bg-gray-950"> <div className="max-h-48 overflow-y-auto rounded-lg border border-gray-700 bg-gray-900 p-4 dark:bg-gray-950">
<pre className="whitespace-pre-wrap font-mono text-xs text-green-400">{updateOutput}</pre> <pre className="whitespace-pre-wrap font-mono text-xs text-green-400">{updateOutput}</pre>
</div> </div>
{IS_PLATFORM && reloadCountdown !== null && (
<div className="rounded-md border border-blue-200 bg-blue-50 px-3 py-2 text-xs text-blue-700 dark:border-blue-900/40 dark:bg-blue-900/20 dark:text-blue-200">
{reloadCountdown === 0
? 'Refresh the page now. If that doesn\'t work, RESTART the environment.'
: `Refresh the page in ${reloadCountdown} ${reloadCountdown === 1 ? 'second' : 'seconds'}. If that doesn\'t work, RESTART the environment.`}
</div>
)}
{updateError && ( {updateError && (
<div className="rounded-md border border-red-200 bg-red-50 px-3 py-2 text-xs text-red-700 dark:border-red-900/40 dark:bg-red-900/20 dark:text-red-200"> <div className="rounded-md border border-red-200 bg-red-50 px-3 py-2 text-xs text-red-700 dark:border-red-900/40 dark:bg-red-900/20 dark:text-red-200">
{updateError} {updateError}