Initial commit: Nano Agent:Windows 优先、本地优先的 Codex 风格桌面智能体 MVP

This commit is contained in:
WpyQwq
2026-09-19 11:58:56 +08:00
commit aafc3f5c6b
23 changed files with 2359 additions and 0 deletions
+47
View File
@@ -0,0 +1,47 @@
version: 1
scope:
include: []
exclude:
- "node_modules/**"
- "assets/**"
repowiki:
template: architecture
notes:
- text: "本项目是 Windows 优先、本地优先的 Codex 风格桌面智能体 MVP,基于 Electron 41,无前端框架,全部使用原生 JS。"
- text: "支持 OpenAI Chat Completions 与 Anthropic Messages 双供应商兼容接口,通过统一 callProvider 抽象切换。"
- text: "主进程 main.js 承载 Agent 工具循环(最多 8 步)和 Windows 原生电脑操控(截图、鼠标、键盘、窗口列表),通过 PowerShell + Win32 P/Invoke 实现。"
documents:
- title: "主进程与 Agent 循环"
goal: "分析 main.js 中的应用入口、IPC 注册、Agent 工具循环流程、双供应商调用抽象和权限控制机制。"
parent: ""
hints:
- "重点描述 runAgentLoop 的步进逻辑与 approval 中断机制"
- "说明 callProvider 对 OpenAI / Anthropic 的请求构造差异"
- title: "Windows 电脑操控层"
goal: "梳理 main.js 中截图、鼠标、键盘、滚轮、窗口列表等系统级能力的实现方式与安全边界。"
parent: ""
hints:
- "PowerShell 脚本 + Win32 P/Invoke 的调用链路"
- "protectedTool 权限门控逻辑"
- title: "Preload 桥接层"
goal: "说明 preload.js 如何通过 contextBridge 向渲染进程安全暴露 IPC 接口。"
parent: ""
- title: "渲染进程与 UI 逻辑"
goal: "分析 src/renderer.js 中的任务管理、对话流、Composer 交互、设置面板和电脑操控面板的状态管理与事件驱动。"
parent: ""
hints:
- "无框架,纯 DOM 操作 + 事件监听"
- "agent:event 流式消息的接收与渲染"
- title: "视图结构与样式体系"
goal: "描述 src/index.html 的页面骨架和 src/styles.css 的深色主题设计系统。"
parent: ""
hints:
- "自定义标题栏、侧边栏、对话区、Composer 布局"
- "CSS 变量与深色主题约定"
knowledgecard:
notes:
- text: "模块划分建议:主进程(Agent 循环 + 系统交互)、Preload 桥接、渲染进程(UI + 状态)、样式层。"
- text: "本项目无任何第三方运行时依赖(仅 devDependency electron),所有功能均为手写实现,知识卡应突出各模块的手写约定与接口契约。"
+50
View File
@@ -0,0 +1,50 @@
# Nano Agent
一个 Windows 优先、本地优先的 Codex 风格智能体工作台 MVP。
## 当前已实现
- Codex 风格深色桌面 UI。
- 任务列表、欢迎页、任务对话和底部 Composer。
- 项目目录选择。
- OpenAI Chat Completions 兼容请求。
- Anthropic Messages 兼容请求。
- 演示模式,无 API Key 时也可以打开和体验界面。
- Windows 原生截图。
- Windows 原生鼠标移动、点击、滚轮和文本输入基础接口。
- 当前窗口列表。
- 电脑操控面板和危险点击确认。
- 本地设置保存。
## 启动
```powershell
cd D:\Nano
npm install
npm start
```
如果 Electron 二进制下载因网络原因超时,可先确认本机已存在:
```powershell
Test-Path .\node_modules\electron\dist\electron.exe
```
## 使用真实模型
打开“设置”,配置:
- 供应商:OpenAI 兼容或 Anthropic 兼容。
- 模型名称。
- API Base URL。
- API Key。
当前 MVP 在本地设置中保存配置。正式版本需要替换为 Windows Credential Manager 加密存储。
## 下一步开发重点
1. 独立感知层:Windows UI Automation、浏览器 DOM、OCR、可选视觉模型。
2. 统一 Computer Controller:observe、locate、checkPolicy、execute、verify。
3. 文件读取、搜索、Diff 和命令执行工具接入真实智能体循环。
4. Git 分支、提交、回滚和测试修复闭环。
5. MCP / 插件系统和后台任务。
Binary file not shown.

After

Width:  |  Height:  |  Size: 224 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 232 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 240 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 258 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 280 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 744 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 410 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1021 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 388 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 745 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 334 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 303 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 303 KiB

+327
View File
@@ -0,0 +1,327 @@
const { app, BrowserWindow, ipcMain, dialog, shell, screen } = require('electron');
const path = require('node:path');
const fs = require('node:fs/promises');
const os = require('node:os');
const { execFile } = require('node:child_process');
let mainWindow;
function projectPathOrThrow(projectPath) {
if (!projectPath || typeof projectPath !== 'string') throw new Error('未选择项目目录');
return path.resolve(projectPath);
}
function isIgnored(name) {
return new Set(['.git', 'node_modules', 'dist', 'build', '.next', 'target']).has(name);
}
async function walkFiles(root, current = root, result = []) {
if (result.length >= 500) return result;
const entries = await fs.readdir(current, { withFileTypes: true });
for (const entry of entries) {
if (isIgnored(entry.name)) continue;
const absolute = path.join(current, entry.name);
if (entry.isDirectory()) await walkFiles(root, absolute, result);
else result.push(path.relative(root, absolute));
if (result.length >= 500) break;
}
return result;
}
async function projectFiles(projectPath) {
const root = projectPathOrThrow(projectPath);
const files = await walkFiles(root);
return { root, files };
}
function safeProjectFile(projectPath, relativePath) {
const root = projectPathOrThrow(projectPath);
const target = path.resolve(root, relativePath || '');
if (target !== root && !target.startsWith(`${root}${path.sep}`)) throw new Error('路径超出项目目录范围');
return { root, target };
}
async function searchProject(projectPath, query) {
const root = projectPathOrThrow(projectPath);
const files = await walkFiles(root);
const results = [];
for (const relative of files) {
if (results.length >= 100) break;
const absolute = path.join(root, relative);
try {
const content = await fs.readFile(absolute, 'utf8');
const lines = content.split(/\r?\n/);
lines.forEach((line, index) => {
if (results.length < 100 && line.toLowerCase().includes(String(query || '').toLowerCase())) results.push({ file: relative, line: index + 1, text: line.trim().slice(0, 240) });
});
} catch { /* binary or unreadable file */ }
}
return results;
}
function runCommand(command, cwd) {
return new Promise((resolve, reject) => {
execFile('powershell.exe', ['-NoProfile', '-NonInteractive', '-ExecutionPolicy', 'Bypass', '-Command', command], { cwd: projectPathOrThrow(cwd), windowsHide: true, maxBuffer: 8 * 1024 * 1024, timeout: 120000 }, (error, stdout, stderr) => {
if (error) return reject(new Error(stderr.trim() || stdout.trim() || error.message));
resolve({ stdout, stderr, code: 0 });
});
});
}
function runPowerShell(script) {
return new Promise((resolve, reject) => {
execFile('powershell.exe', ['-NoProfile', '-NonInteractive', '-ExecutionPolicy', 'Bypass', '-Command', script],
{ windowsHide: true, maxBuffer: 20 * 1024 * 1024 },
(error, stdout, stderr) => {
if (error) return reject(new Error(stderr.trim() || error.message));
resolve(stdout.trim());
});
});
}
async function takeScreenshot() {
const output = path.join(os.tmpdir(), 'nano-agent-screen.png');
const escaped = output.replace(/'/g, "''");
const script = `
Add-Type -AssemblyName System.Drawing
Add-Type -AssemblyName System.Windows.Forms
$bounds = [System.Windows.Forms.SystemInformation]::VirtualScreen
$bitmap = New-Object System.Drawing.Bitmap $bounds.Width, $bounds.Height
$graphics = [System.Drawing.Graphics]::FromImage($bitmap)
$graphics.CopyFromScreen($bounds.Left, $bounds.Top, 0, 0, $bitmap.Size)
$bitmap.Save('${escaped}', [System.Drawing.Imaging.ImageFormat]::Png)
$graphics.Dispose(); $bitmap.Dispose()
Write-Output '${escaped}'
`;
await runPowerShell(script);
const data = await fs.readFile(output);
return { path: output, dataUrl: `data:image/png;base64,${data.toString('base64')}` };
}
async function computerAction(action) {
const type = action?.type;
if (type === 'screenshot') return takeScreenshot();
const x = Number.isFinite(action?.x) ? Math.round(action.x) : 0;
const y = Number.isFinite(action?.y) ? Math.round(action.y) : 0;
const buttonMap = { left: 0x0002 | 0x0004, right: 0x0008 | 0x0010, middle: 0x0020 | 0x0040 };
const button = buttonMap[action?.button || 'left'] || buttonMap.left;
const keyMap = { ENTER: 0x0D, ESC: 0x1B, TAB: 0x09, CTRL: 0x11, SHIFT: 0x10, ALT: 0x12, BACKSPACE: 0x08, DELETE: 0x2E, SPACE: 0x20 };
const keys = (action?.keys || []).map((key) => keyMap[String(key).toUpperCase()] || String(key).charCodeAt(0)).filter(Boolean);
const text = String(action?.text || '').replace(/'/g, "''");
let body = '';
if (type === 'mouse_move') {
body = `[Win32]::SetCursorPos(${x}, ${y}) | Out-Null`;
} else if (type === 'mouse_click') {
body = `[Win32]::SetCursorPos(${x}, ${y}) | Out-Null; [Win32]::mouse_event(${button}, 0, 0, 0, 0)`;
} else if (type === 'scroll') {
const amount = Math.round(Number(action.amount || 1) * 120);
body = `[Win32]::mouse_event(0x0800, 0, 0, ${amount}, 0)`;
} else if (type === 'key_press') {
body = keys.map((code) => `[Win32]::keybd_event(${code}, 0, 0, 0); [Win32]::keybd_event(${code}, 0, 2, 0)`).join(';');
} else if (type === 'type') {
body = `Set-Clipboard -Value '${text}'; [Win32]::keybd_event(0x11,0,0,0); [Win32]::keybd_event(0x56,0,0,0); [Win32]::keybd_event(0x56,0,2,0); [Win32]::keybd_event(0x11,0,2,0)`;
} else {
throw new Error(`Unsupported computer action: ${type}`);
}
const script = `
Add-Type @'
using System;
using System.Runtime.InteropServices;
public static class Win32 {
[DllImport("user32.dll")] public static extern bool SetCursorPos(int X, int Y);
[DllImport("user32.dll")] public static extern void mouse_event(uint flags, uint dx, uint dy, uint data, UIntPtr extraInfo);
[DllImport("user32.dll")] public static extern void keybd_event(byte key, byte scan, uint flags, UIntPtr extraInfo);
}
'@
${body}
Write-Output 'ok'
`;
await runPowerShell(script);
return { ok: true, type };
}
async function listWindows() {
const script = `Get-Process | Where-Object { $_.MainWindowTitle -and $_.MainWindowTitle.Trim() } | Select-Object Id, ProcessName, MainWindowTitle | ConvertTo-Json -Compress`;
const output = await runPowerShell(script);
if (!output) return [];
const parsed = JSON.parse(output);
return Array.isArray(parsed) ? parsed : [parsed];
}
function createWindow() {
mainWindow = new BrowserWindow({
width: 1300,
height: 840,
minWidth: 1120,
minHeight: 720,
show: false,
backgroundColor: '#00000000',
transparent: true,
hasShadow: true,
titleBarStyle: 'hidden',
webPreferences: {
preload: path.join(__dirname, 'preload.js'),
contextIsolation: true,
nodeIntegration: false
}
});
mainWindow.loadFile(path.join(__dirname, 'src', 'index.html'));
mainWindow.once('ready-to-show', () => mainWindow.show());
}
const AGENT_TOOLS = [
{ type: 'function', function: { name: 'list_files', description: '列出项目目录中的文件。', parameters: { type: 'object', properties: { depth: { type: 'number', description: '最大递归深度,默认 3。' } } } } },
{ type: 'function', function: { name: 'read_file', description: '读取项目中的一个文本文件。', parameters: { type: 'object', properties: { path: { type: 'string' } }, required: ['path'] } } },
{ type: 'function', function: { name: 'search_text', description: '在项目文件中搜索文本。', parameters: { type: 'object', properties: { query: { type: 'string' } }, required: ['query'] } } },
{ type: 'function', function: { name: 'write_file', description: '创建或修改项目文件。执行前必须确认用户授权。', parameters: { type: 'object', properties: { path: { type: 'string' }, content: { type: 'string' } }, required: ['path', 'content'] } } },
{ type: 'function', function: { name: 'run_command', description: '在项目目录执行 PowerShell 命令。执行前必须确认用户授权。', parameters: { type: 'object', properties: { command: { type: 'string' } }, required: ['command'] } } },
{ type: 'function', function: { name: 'computer_screenshot', description: '获取当前 Windows 屏幕截图,用于观察界面。', parameters: { type: 'object', properties: {} } } },
{ type: 'function', function: { name: 'computer_windows', description: '获取当前桌面窗口列表。', parameters: { type: 'object', properties: {} } } },
{ type: 'function', function: { name: 'computer_action', description: '执行鼠标、键盘或滚轮动作。执行前必须确认用户授权。', parameters: { type: 'object', properties: { type: { type: 'string' }, x: { type: 'number' }, y: { type: 'number' }, button: { type: 'string' }, text: { type: 'string' }, keys: { type: 'array', items: { type: 'string' } }, amount: { type: 'number' } }, required: ['type'] } } }
];
async function callProvider(config, messages, tools = AGENT_TOOLS) {
if (!config?.apiKey || !config?.model) {
return { text: '演示模式已启用。请在设置中配置 API Base URL、API Key 和模型,即可连接真实模型。当前界面、任务流和电脑操控基础能力已经可用。', toolCalls: [] };
}
const provider = config.provider || 'openai';
const baseUrl = (config.baseUrl || (provider === 'anthropic' ? 'https://api.anthropic.com' : 'https://api.openai.com/v1')).replace(/\/$/, '');
let url;
let headers;
let body;
if (provider === 'anthropic') {
url = `${baseUrl}/v1/messages`;
headers = { 'content-type': 'application/json', 'x-api-key': config.apiKey, 'anthropic-version': '2023-06-01' };
const system = messages.find((message) => message.role === 'system')?.content;
body = { model: config.model, max_tokens: config.maxOutputTokens || 4096, system, messages: messages.filter((message) => message.role !== 'system'), tools: tools.map((tool) => ({ name: tool.function.name, description: tool.function.description, input_schema: tool.function.parameters })) };
} else {
url = `${baseUrl}/chat/completions`;
headers = { 'content-type': 'application/json', authorization: `Bearer ${config.apiKey}` };
body = { model: config.model, messages, tools, tool_choice: 'auto', stream: false, temperature: config.temperature ?? 0.2 };
}
const response = await fetch(url, { method: 'POST', headers, body: JSON.stringify(body) });
const raw = await response.text();
if (!response.ok) throw new Error(`${response.status}: ${raw.slice(0, 500)}`);
const data = JSON.parse(raw);
if (provider === 'anthropic') {
return { text: data.content?.filter((item) => item.type === 'text').map((item) => item.text || '').join('') || '', toolCalls: data.content?.filter((item) => item.type === 'tool_use').map((item) => ({ id: item.id, name: item.name, input: item.input, raw: item })) || [], assistantContent: data.content || [] };
}
const message = data.choices?.[0]?.message || {};
return { text: message.content || '', toolCalls: (message.tool_calls || []).map((call) => ({ id: call.id, name: call.function.name, input: JSON.parse(call.function.arguments || '{}'), raw: call })), assistantMessage: message };
}
async function executeAgentTool(name, input, payload) {
const projectPath = payload.projectPath;
if (name === 'list_files') return projectFiles(projectPath);
if (name === 'read_file') return projectReadFile(projectPath, input.path);
if (name === 'search_text') return searchProject(projectPath, input.query);
if (name === 'write_file') return projectWriteFile(projectPath, input.path, input.content);
if (name === 'run_command') return runCommand(input.command, projectPath);
if (name === 'computer_screenshot') { const result = await takeScreenshot(); return { path: result.path, note: '截图已生成;支持视觉输入的模型可以继续分析截图。' }; }
if (name === 'computer_windows') return listWindows();
if (name === 'computer_action') return computerAction(input);
throw new Error(`未知工具:${name}`);
}
async function projectReadFile(projectPath, relativePath) {
const { target } = safeProjectFile(projectPath, relativePath);
return { path: relativePath, content: await fs.readFile(target, 'utf8') };
}
async function projectWriteFile(projectPath, relativePath, content) {
const { target } = safeProjectFile(projectPath, relativePath);
await fs.mkdir(path.dirname(target), { recursive: true });
await fs.writeFile(target, String(content || ''), 'utf8');
return { ok: true, path: relativePath };
}
async function runAgentLoop(event, payload) {
const messages = [...payload.messages];
for (let step = 0; step < 8; step += 1) {
const response = await callProvider(payload.config, messages);
if (response.text) {
for (const chunk of response.text.match(/.{1,18}/gs) || [response.text]) {
event.sender.send('agent:event', { type: 'message.delta', content: chunk });
await new Promise((resolve) => setTimeout(resolve, 14));
}
}
if (!response.toolCalls.length) return;
if (payload.config?.provider === 'anthropic') {
messages.push({ role: 'assistant', content: response.assistantContent });
} else {
messages.push({ role: 'assistant', content: response.assistantMessage.content || null, tool_calls: response.assistantMessage.tool_calls });
}
for (const toolCall of response.toolCalls) {
event.sender.send('agent:event', { type: 'tool.started', callId: toolCall.id, name: toolCall.name, input: toolCall.input });
const protectedTool = new Set(['write_file', 'run_command', 'computer_action']).has(toolCall.name);
if (protectedTool && payload.permissionMode !== 'full') {
event.sender.send('agent:event', { type: 'approval.required', approvalId: toolCall.id, reason: `工具 ${toolCall.name} 需要完全访问权限` });
return;
}
let result;
try {
result = await executeAgentTool(toolCall.name, toolCall.input || {}, payload);
} catch (error) {
result = { error: error.message };
}
event.sender.send('agent:event', { type: 'tool.completed', callId: toolCall.id, name: toolCall.name, output: result });
if (toolCall.name === 'write_file' && result.path) event.sender.send('agent:event', { type: 'file.changed', path: result.path });
if (payload.config?.provider === 'anthropic') {
messages.push({ role: 'user', content: [{ type: 'tool_result', tool_use_id: toolCall.id, content: JSON.stringify(result) }] });
} else {
messages.push({ role: 'tool', tool_call_id: toolCall.id, content: JSON.stringify(result) });
}
}
}
throw new Error('智能体工具循环超过最大步骤数');
}
ipcMain.handle('app:info', () => ({ version: app.getVersion(), platform: process.platform, displays: screen.getAllDisplays().length }));
ipcMain.handle('window:control', (_event, action) => {
if (!mainWindow) return;
if (action === 'minimize') mainWindow.minimize();
if (action === 'maximize') mainWindow.isMaximized() ? mainWindow.unmaximize() : mainWindow.maximize();
if (action === 'close') mainWindow.close();
});
ipcMain.handle('project:choose', async () => {
const result = await dialog.showOpenDialog(mainWindow, { properties: ['openDirectory'] });
return result.canceled ? null : result.filePaths[0];
});
ipcMain.handle('project:files', (_event, projectPath) => projectFiles(projectPath));
ipcMain.handle('project:read', async (_event, payload) => {
const { target } = safeProjectFile(payload.projectPath, payload.relativePath);
return { path: payload.relativePath, content: await fs.readFile(target, 'utf8') };
});
ipcMain.handle('project:search', (_event, payload) => searchProject(payload.projectPath, payload.query));
ipcMain.handle('project:write', async (_event, payload) => {
const { target } = safeProjectFile(payload.projectPath, payload.relativePath);
await fs.mkdir(path.dirname(target), { recursive: true });
await fs.writeFile(target, String(payload.content || ''), 'utf8');
return { ok: true, path: payload.relativePath };
});
ipcMain.handle('shell:run', (_event, payload) => runCommand(payload.command, payload.projectPath));
ipcMain.handle('shell:open', (_event, target) => shell.openPath(target));
ipcMain.handle('computer:action', (_event, action) => computerAction(action));
ipcMain.handle('computer:windows', () => listWindows());
ipcMain.handle('agent:run', async (event, payload) => {
try {
event.sender.send('agent:event', { type: 'run.started' });
await runAgentLoop(event, payload);
event.sender.send('agent:event', { type: 'run.completed', status: 'success' });
return { ok: true };
} catch (error) {
event.sender.send('agent:event', { type: 'run.error', message: error.message });
return { ok: false, error: error.message };
}
});
app.whenReady().then(() => {
createWindow();
app.on('activate', () => { if (BrowserWindow.getAllWindows().length === 0) createWindow(); });
});
app.on('window-all-closed', () => { if (process.platform !== 'darwin') app.quit(); });
+871
View File
@@ -0,0 +1,871 @@
{
"name": "nano-agent",
"version": "0.1.0",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "nano-agent",
"version": "0.1.0",
"devDependencies": {
"electron": "^36.0.0"
}
},
"node_modules/@electron/get": {
"version": "2.0.3",
"resolved": "https://registry.npmjs.org/@electron/get/-/get-2.0.3.tgz",
"integrity": "sha512-Qkzpg2s9GnVV2I2BjRksUi43U5e6+zaQMcjoJy0C+C5oxaKl+fmckGDQFtRpZpZV0NQekuZZ+tGz7EA9TVnQtQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"debug": "^4.1.1",
"env-paths": "^2.2.0",
"fs-extra": "^8.1.0",
"got": "^11.8.5",
"progress": "^2.0.3",
"semver": "^6.2.0",
"sumchecker": "^3.0.1"
},
"engines": {
"node": ">=12"
},
"optionalDependencies": {
"global-agent": "^3.0.0"
}
},
"node_modules/@sindresorhus/is": {
"version": "4.6.0",
"resolved": "https://registry.npmjs.org/@sindresorhus/is/-/is-4.6.0.tgz",
"integrity": "sha512-t09vSN3MdfsyCHoFcTRCH/iUtG7OJ0CsjzB8cjAmKc/va/kIgeDI/TxsigdncE/4be734m0cvIYwNaV4i2XqAw==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=10"
},
"funding": {
"url": "https://github.com/sindresorhus/is?sponsor=1"
}
},
"node_modules/@szmarczak/http-timer": {
"version": "4.0.6",
"resolved": "https://registry.npmjs.org/@szmarczak/http-timer/-/http-timer-4.0.6.tgz",
"integrity": "sha512-4BAffykYOgO+5nzBWYwE3W90sBgLJoUPRWWcL8wlyiM8IB8ipJz3UMJ9KXQd1RKQXpKp8Tutn80HZtWsu2u76w==",
"dev": true,
"license": "MIT",
"dependencies": {
"defer-to-connect": "^2.0.0"
},
"engines": {
"node": ">=10"
}
},
"node_modules/@types/cacheable-request": {
"version": "6.0.3",
"resolved": "https://registry.npmjs.org/@types/cacheable-request/-/cacheable-request-6.0.3.tgz",
"integrity": "sha512-IQ3EbTzGxIigb1I3qPZc1rWJnH0BmSKv5QYTalEwweFvyBDLSAe24zP0le/hyi7ecGfZVlIVAg4BZqb8WBwKqw==",
"dev": true,
"license": "MIT",
"dependencies": {
"@types/http-cache-semantics": "*",
"@types/keyv": "^3.1.4",
"@types/node": "*",
"@types/responselike": "^1.0.0"
}
},
"node_modules/@types/http-cache-semantics": {
"version": "4.2.0",
"resolved": "https://registry.npmjs.org/@types/http-cache-semantics/-/http-cache-semantics-4.2.0.tgz",
"integrity": "sha512-L3LgimLHXtGkWikKnsPg0/VFx9OGZaC+eN1u4r+OB1XRqH3meBIAVC2zr1WdMH+RHmnRkqliQAOHNJ/E0j/e0Q==",
"dev": true,
"license": "MIT"
},
"node_modules/@types/keyv": {
"version": "3.1.4",
"resolved": "https://registry.npmjs.org/@types/keyv/-/keyv-3.1.4.tgz",
"integrity": "sha512-BQ5aZNSCpj7D6K2ksrRCTmKRLEpnPvWDiLPfoGyhZ++8YtiK9d/3DBKPJgry359X/P1PfruyYwvnvwFjuEiEIg==",
"dev": true,
"license": "MIT",
"dependencies": {
"@types/node": "*"
}
},
"node_modules/@types/node": {
"version": "22.20.1",
"resolved": "https://registry.npmjs.org/@types/node/-/node-22.20.1.tgz",
"integrity": "sha512-EANqOCF9QFyra+4pfxUcX9STKJpCLjMbObVzljIJomAWSnuSIEAvyzEU53GaajbXJEgdh0iEcPL+DGvpUd4k1Q==",
"dev": true,
"license": "MIT",
"dependencies": {
"undici-types": "~6.21.0"
}
},
"node_modules/@types/responselike": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/@types/responselike/-/responselike-1.0.3.tgz",
"integrity": "sha512-H/+L+UkTV33uf49PH5pCAUBVPNj2nDBXTN+qS1dOwyyg24l3CcicicCA7ca+HMvJBZcFgl5r8e+RR6elsb4Lyw==",
"dev": true,
"license": "MIT",
"dependencies": {
"@types/node": "*"
}
},
"node_modules/@types/yauzl": {
"version": "2.10.3",
"resolved": "https://registry.npmjs.org/@types/yauzl/-/yauzl-2.10.3.tgz",
"integrity": "sha512-oJoftv0LSuaDZE3Le4DbKX+KS9G36NzOeSap90UIK0yMA/NhKJhqlSGtNDORNRaIbQfzjXDrQa0ytJ6mNRGz/Q==",
"dev": true,
"license": "MIT",
"optional": true,
"dependencies": {
"@types/node": "*"
}
},
"node_modules/boolean": {
"version": "3.2.0",
"resolved": "https://registry.npmjs.org/boolean/-/boolean-3.2.0.tgz",
"integrity": "sha512-d0II/GO9uf9lfUHH2BQsjxzRJZBdsjgsBiW4BvhWk/3qoKwQFjIDVN19PfX8F2D/r9PCMTtLWjYVCFrpeYUzsw==",
"deprecated": "Package no longer supported. Contact Support at https://www.npmjs.com/support for more info.",
"dev": true,
"license": "MIT",
"optional": true
},
"node_modules/buffer-crc32": {
"version": "0.2.13",
"resolved": "https://registry.npmjs.org/buffer-crc32/-/buffer-crc32-0.2.13.tgz",
"integrity": "sha512-VO9Ht/+p3SN7SKWqcrgEzjGbRSJYTx+Q1pTQC0wrWqHx0vpJraQ6GtHx8tvcg1rlK1byhU5gccxgOgj7B0TDkQ==",
"dev": true,
"license": "MIT",
"engines": {
"node": "*"
}
},
"node_modules/cacheable-lookup": {
"version": "5.0.4",
"resolved": "https://registry.npmjs.org/cacheable-lookup/-/cacheable-lookup-5.0.4.tgz",
"integrity": "sha512-2/kNscPhpcxrOigMZzbiWF7dz8ilhb/nIHU3EyZiXWXpeq/au8qJ8VhdftMkty3n7Gj6HIGalQG8oiBNB3AJgA==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=10.6.0"
}
},
"node_modules/cacheable-request": {
"version": "7.0.4",
"resolved": "https://registry.npmjs.org/cacheable-request/-/cacheable-request-7.0.4.tgz",
"integrity": "sha512-v+p6ongsrp0yTGbJXjgxPow2+DL93DASP4kXCDKb8/bwRtt9OEF3whggkkDkGNzgcWy2XaF4a8nZglC7uElscg==",
"dev": true,
"license": "MIT",
"dependencies": {
"clone-response": "^1.0.2",
"get-stream": "^5.1.0",
"http-cache-semantics": "^4.0.0",
"keyv": "^4.0.0",
"lowercase-keys": "^2.0.0",
"normalize-url": "^6.0.1",
"responselike": "^2.0.0"
},
"engines": {
"node": ">=8"
}
},
"node_modules/clone-response": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/clone-response/-/clone-response-1.0.3.tgz",
"integrity": "sha512-ROoL94jJH2dUVML2Y/5PEDNaSHgeOdSDicUyS7izcF63G6sTc/FTjLub4b8Il9S8S0beOfYt0TaA5qvFK+w0wA==",
"dev": true,
"license": "MIT",
"dependencies": {
"mimic-response": "^1.0.0"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/debug": {
"version": "4.4.3",
"resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz",
"integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==",
"dev": true,
"license": "MIT",
"dependencies": {
"ms": "^2.1.3"
},
"engines": {
"node": ">=6.0"
},
"peerDependenciesMeta": {
"supports-color": {
"optional": true
}
}
},
"node_modules/decompress-response": {
"version": "6.0.0",
"resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-6.0.0.tgz",
"integrity": "sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"mimic-response": "^3.1.0"
},
"engines": {
"node": ">=10"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/decompress-response/node_modules/mimic-response": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-3.1.0.tgz",
"integrity": "sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=10"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/defer-to-connect": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/defer-to-connect/-/defer-to-connect-2.0.1.tgz",
"integrity": "sha512-4tvttepXG1VaYGrRibk5EwJd1t4udunSOVMdLSAL6mId1ix438oPwPZMALY41FCijukO1L0twNcGsdzS7dHgDg==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=10"
}
},
"node_modules/define-data-property": {
"version": "1.1.4",
"resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.4.tgz",
"integrity": "sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==",
"dev": true,
"license": "MIT",
"optional": true,
"dependencies": {
"es-define-property": "^1.0.0",
"es-errors": "^1.3.0",
"gopd": "^1.0.1"
},
"engines": {
"node": ">= 0.4"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/define-properties": {
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.2.1.tgz",
"integrity": "sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg==",
"dev": true,
"license": "MIT",
"optional": true,
"dependencies": {
"define-data-property": "^1.0.1",
"has-property-descriptors": "^1.0.0",
"object-keys": "^1.1.1"
},
"engines": {
"node": ">= 0.4"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/detect-node": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/detect-node/-/detect-node-2.1.0.tgz",
"integrity": "sha512-T0NIuQpnTvFDATNuHN5roPwSBG83rFsuO+MXXH9/3N1eFbn4wcPjttvjMLEPWJ0RGUYgQE7cGgS3tNxbqCGM7g==",
"dev": true,
"license": "MIT",
"optional": true
},
"node_modules/electron": {
"version": "36.9.5",
"resolved": "https://registry.npmjs.org/electron/-/electron-36.9.5.tgz",
"integrity": "sha512-1UCss2IqxqujSzg/2jkRjuiT3G+EEXgd6UKB5kUekwQW1LJ6d4QCr8YItfC3Rr9VIGRDJ29eOERmnRNO1Eh+NA==",
"dev": true,
"hasInstallScript": true,
"license": "MIT",
"dependencies": {
"@electron/get": "^2.0.0",
"@types/node": "^22.7.7",
"extract-zip": "^2.0.1"
},
"bin": {
"electron": "cli.js"
},
"engines": {
"node": ">= 12.20.55"
}
},
"node_modules/end-of-stream": {
"version": "1.4.5",
"resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.5.tgz",
"integrity": "sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg==",
"dev": true,
"license": "MIT",
"dependencies": {
"once": "^1.4.0"
}
},
"node_modules/env-paths": {
"version": "2.2.1",
"resolved": "https://registry.npmjs.org/env-paths/-/env-paths-2.2.1.tgz",
"integrity": "sha512-+h1lkLKhZMTYjog1VEpJNG7NZJWcuc2DDk/qsqSTRRCOXiLjeQ1d1/udrUGhqMxUgAlwKNZ0cf2uqan5GLuS2A==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=6"
}
},
"node_modules/es-define-property": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz",
"integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==",
"dev": true,
"license": "MIT",
"optional": true,
"engines": {
"node": ">= 0.4"
}
},
"node_modules/es-errors": {
"version": "1.3.0",
"resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz",
"integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==",
"dev": true,
"license": "MIT",
"optional": true,
"engines": {
"node": ">= 0.4"
}
},
"node_modules/es6-error": {
"version": "4.1.1",
"resolved": "https://registry.npmjs.org/es6-error/-/es6-error-4.1.1.tgz",
"integrity": "sha512-Um/+FxMr9CISWh0bi5Zv0iOD+4cFh5qLeks1qhAopKVAJw3drgKbKySikp7wGhDL0HPeaja0P5ULZrxLkniUVg==",
"dev": true,
"license": "MIT",
"optional": true
},
"node_modules/escape-string-regexp": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz",
"integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==",
"dev": true,
"license": "MIT",
"optional": true,
"engines": {
"node": ">=10"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/extract-zip": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/extract-zip/-/extract-zip-2.0.1.tgz",
"integrity": "sha512-GDhU9ntwuKyGXdZBUgTIe+vXnWj0fppUEtMDL0+idd5Sta8TGpHssn/eusA9mrPr9qNDym6SxAYZjNvCn/9RBg==",
"dev": true,
"license": "BSD-2-Clause",
"dependencies": {
"debug": "^4.1.1",
"get-stream": "^5.1.0",
"yauzl": "^2.10.0"
},
"bin": {
"extract-zip": "cli.js"
},
"engines": {
"node": ">= 10.17.0"
},
"optionalDependencies": {
"@types/yauzl": "^2.9.1"
}
},
"node_modules/fd-slicer": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/fd-slicer/-/fd-slicer-1.1.0.tgz",
"integrity": "sha512-cE1qsB/VwyQozZ+q1dGxR8LBYNZeofhEdUNGSMbQD3Gw2lAzX9Zb3uIU6Ebc/Fmyjo9AWWfnn0AUCHqtevs/8g==",
"dev": true,
"license": "MIT",
"dependencies": {
"pend": "~1.2.0"
}
},
"node_modules/fs-extra": {
"version": "8.1.0",
"resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-8.1.0.tgz",
"integrity": "sha512-yhlQgA6mnOJUKOsRUFsgJdQCvkKhcz8tlZG5HBQfReYZy46OwLcY+Zia0mtdHsOo9y/hP+CxMN0TU9QxoOtG4g==",
"dev": true,
"license": "MIT",
"dependencies": {
"graceful-fs": "^4.2.0",
"jsonfile": "^4.0.0",
"universalify": "^0.1.0"
},
"engines": {
"node": ">=6 <7 || >=8"
}
},
"node_modules/get-stream": {
"version": "5.2.0",
"resolved": "https://registry.npmjs.org/get-stream/-/get-stream-5.2.0.tgz",
"integrity": "sha512-nBF+F1rAZVCu/p7rjzgA+Yb4lfYXrpl7a6VmJrU8wF9I1CKvP/QwPNZHnOlwbTkY6dvtFIzFMSyQXbLoTQPRpA==",
"dev": true,
"license": "MIT",
"dependencies": {
"pump": "^3.0.0"
},
"engines": {
"node": ">=8"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/global-agent": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/global-agent/-/global-agent-3.0.0.tgz",
"integrity": "sha512-PT6XReJ+D07JvGoxQMkT6qji/jVNfX/h364XHZOWeRzy64sSFr+xJ5OX7LI3b4MPQzdL4H8Y8M0xzPpsVMwA8Q==",
"dev": true,
"license": "BSD-3-Clause",
"optional": true,
"dependencies": {
"boolean": "^3.0.1",
"es6-error": "^4.1.1",
"matcher": "^3.0.0",
"roarr": "^2.15.3",
"semver": "^7.3.2",
"serialize-error": "^7.0.1"
},
"engines": {
"node": ">=10.0"
}
},
"node_modules/global-agent/node_modules/semver": {
"version": "7.8.5",
"resolved": "https://registry.npmjs.org/semver/-/semver-7.8.5.tgz",
"integrity": "sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA==",
"dev": true,
"license": "ISC",
"optional": true,
"bin": {
"semver": "bin/semver.js"
},
"engines": {
"node": ">=10"
}
},
"node_modules/globalthis": {
"version": "1.0.4",
"resolved": "https://registry.npmjs.org/globalthis/-/globalthis-1.0.4.tgz",
"integrity": "sha512-DpLKbNU4WylpxJykQujfCcwYWiV/Jhm50Goo0wrVILAv5jOr9d+H+UR3PhSCD2rCCEIg0uc+G+muBTwD54JhDQ==",
"dev": true,
"license": "MIT",
"optional": true,
"dependencies": {
"define-properties": "^1.2.1",
"gopd": "^1.0.1"
},
"engines": {
"node": ">= 0.4"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/gopd": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz",
"integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==",
"dev": true,
"license": "MIT",
"optional": true,
"engines": {
"node": ">= 0.4"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/got": {
"version": "11.8.6",
"resolved": "https://registry.npmjs.org/got/-/got-11.8.6.tgz",
"integrity": "sha512-6tfZ91bOr7bOXnK7PRDCGBLa1H4U080YHNaAQ2KsMGlLEzRbk44nsZF2E1IeRc3vtJHPVbKCYgdFbaGO2ljd8g==",
"dev": true,
"license": "MIT",
"dependencies": {
"@sindresorhus/is": "^4.0.0",
"@szmarczak/http-timer": "^4.0.5",
"@types/cacheable-request": "^6.0.1",
"@types/responselike": "^1.0.0",
"cacheable-lookup": "^5.0.3",
"cacheable-request": "^7.0.2",
"decompress-response": "^6.0.0",
"http2-wrapper": "^1.0.0-beta.5.2",
"lowercase-keys": "^2.0.0",
"p-cancelable": "^2.0.0",
"responselike": "^2.0.0"
},
"engines": {
"node": ">=10.19.0"
},
"funding": {
"url": "https://github.com/sindresorhus/got?sponsor=1"
}
},
"node_modules/graceful-fs": {
"version": "4.2.11",
"resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz",
"integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==",
"dev": true,
"license": "ISC"
},
"node_modules/has-property-descriptors": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.2.tgz",
"integrity": "sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==",
"dev": true,
"license": "MIT",
"optional": true,
"dependencies": {
"es-define-property": "^1.0.0"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/http-cache-semantics": {
"version": "4.2.0",
"resolved": "https://registry.npmjs.org/http-cache-semantics/-/http-cache-semantics-4.2.0.tgz",
"integrity": "sha512-dTxcvPXqPvXBQpq5dUr6mEMJX4oIEFv6bwom3FDwKRDsuIjjJGANqhBuoAn9c1RQJIdAKav33ED65E2ys+87QQ==",
"dev": true,
"license": "BSD-2-Clause"
},
"node_modules/http2-wrapper": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/http2-wrapper/-/http2-wrapper-1.0.3.tgz",
"integrity": "sha512-V+23sDMr12Wnz7iTcDeJr3O6AIxlnvT/bmaAAAP/Xda35C90p9599p0F1eHR/N1KILWSoWVAiOMFjBBXaXSMxg==",
"dev": true,
"license": "MIT",
"dependencies": {
"quick-lru": "^5.1.1",
"resolve-alpn": "^1.0.0"
},
"engines": {
"node": ">=10.19.0"
}
},
"node_modules/json-buffer": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz",
"integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==",
"dev": true,
"license": "MIT"
},
"node_modules/json-stringify-safe": {
"version": "5.0.1",
"resolved": "https://registry.npmjs.org/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz",
"integrity": "sha512-ZClg6AaYvamvYEE82d3Iyd3vSSIjQ+odgjaTzRuO3s7toCdFKczob2i0zCh7JE8kWn17yvAWhUVxvqGwUalsRA==",
"dev": true,
"license": "ISC",
"optional": true
},
"node_modules/jsonfile": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-4.0.0.tgz",
"integrity": "sha512-m6F1R3z8jjlf2imQHS2Qez5sjKWQzbuuhuJ/FKYFRZvPE3PuHcSMVZzfsLhGVOkfd20obL5SWEBew5ShlquNxg==",
"dev": true,
"license": "MIT",
"optionalDependencies": {
"graceful-fs": "^4.1.6"
}
},
"node_modules/keyv": {
"version": "4.5.4",
"resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz",
"integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==",
"dev": true,
"license": "MIT",
"dependencies": {
"json-buffer": "3.0.1"
}
},
"node_modules/lowercase-keys": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/lowercase-keys/-/lowercase-keys-2.0.0.tgz",
"integrity": "sha512-tqNXrS78oMOE73NMxK4EMLQsQowWf8jKooH9g7xPavRT706R6bkQJ6DY2Te7QukaZsulxa30wQ7bk0pm4XiHmA==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=8"
}
},
"node_modules/matcher": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/matcher/-/matcher-3.0.0.tgz",
"integrity": "sha512-OkeDaAZ/bQCxeFAozM55PKcKU0yJMPGifLwV4Qgjitu+5MoAfSQN4lsLJeXZ1b8w0x+/Emda6MZgXS1jvsapng==",
"dev": true,
"license": "MIT",
"optional": true,
"dependencies": {
"escape-string-regexp": "^4.0.0"
},
"engines": {
"node": ">=10"
}
},
"node_modules/mimic-response": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-1.0.1.tgz",
"integrity": "sha512-j5EctnkH7amfV/q5Hgmoal1g2QHFJRraOtmx0JpIqkxhBhI/lJSl1nMpQ45hVarwNETOoWEimndZ4QK0RHxuxQ==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=4"
}
},
"node_modules/ms": {
"version": "2.1.3",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
"integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
"dev": true,
"license": "MIT"
},
"node_modules/normalize-url": {
"version": "6.1.0",
"resolved": "https://registry.npmjs.org/normalize-url/-/normalize-url-6.1.0.tgz",
"integrity": "sha512-DlL+XwOy3NxAQ8xuC0okPgK46iuVNAK01YN7RueYBqqFeGsBjV9XmCAzAdgt+667bCl5kPh9EqKKDwnaPG1I7A==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=10"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/object-keys": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/object-keys/-/object-keys-1.1.1.tgz",
"integrity": "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==",
"dev": true,
"license": "MIT",
"optional": true,
"engines": {
"node": ">= 0.4"
}
},
"node_modules/once": {
"version": "1.4.0",
"resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz",
"integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==",
"dev": true,
"license": "ISC",
"dependencies": {
"wrappy": "1"
}
},
"node_modules/p-cancelable": {
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/p-cancelable/-/p-cancelable-2.1.1.tgz",
"integrity": "sha512-BZOr3nRQHOntUjTrH8+Lh54smKHoHyur8We1V8DSMVrl5A2malOOwuJRnKRDjSnkoeBh4at6BwEnb5I7Jl31wg==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=8"
}
},
"node_modules/pend": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/pend/-/pend-1.2.0.tgz",
"integrity": "sha512-F3asv42UuXchdzt+xXqfW1OGlVBe+mxa2mqI0pg5yAHZPvFmY3Y6drSf/GQ1A86WgWEN9Kzh/WrgKa6iGcHXLg==",
"dev": true,
"license": "MIT"
},
"node_modules/progress": {
"version": "2.0.3",
"resolved": "https://registry.npmjs.org/progress/-/progress-2.0.3.tgz",
"integrity": "sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=0.4.0"
}
},
"node_modules/pump": {
"version": "3.0.4",
"resolved": "https://registry.npmjs.org/pump/-/pump-3.0.4.tgz",
"integrity": "sha512-VS7sjc6KR7e1ukRFhQSY5LM2uBWAUPiOPa/A3mkKmiMwSmRFUITt0xuj+/lesgnCv+dPIEYlkzrcyXgquIHMcA==",
"dev": true,
"license": "MIT",
"dependencies": {
"end-of-stream": "^1.1.0",
"once": "^1.3.1"
}
},
"node_modules/quick-lru": {
"version": "5.1.1",
"resolved": "https://registry.npmjs.org/quick-lru/-/quick-lru-5.1.1.tgz",
"integrity": "sha512-WuyALRjWPDGtt/wzJiadO5AXY+8hZ80hVpe6MyivgraREW751X3SbhRvG3eLKOYN+8VEvqLcf3wdnt44Z4S4SA==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=10"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/resolve-alpn": {
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/resolve-alpn/-/resolve-alpn-1.2.1.tgz",
"integrity": "sha512-0a1F4l73/ZFZOakJnQ3FvkJ2+gSTQWz/r2KE5OdDY0TxPm5h4GkqkWWfM47T7HsbnOtcJVEF4epCVy6u7Q3K+g==",
"dev": true,
"license": "MIT"
},
"node_modules/responselike": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/responselike/-/responselike-2.0.1.tgz",
"integrity": "sha512-4gl03wn3hj1HP3yzgdI7d3lCkF95F21Pz4BPGvKHinyQzALR5CapwC8yIi0Rh58DEMQ/SguC03wFj2k0M/mHhw==",
"dev": true,
"license": "MIT",
"dependencies": {
"lowercase-keys": "^2.0.0"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/roarr": {
"version": "2.15.4",
"resolved": "https://registry.npmjs.org/roarr/-/roarr-2.15.4.tgz",
"integrity": "sha512-CHhPh+UNHD2GTXNYhPWLnU8ONHdI+5DI+4EYIAOaiD63rHeYlZvyh8P+in5999TTSFgUYuKUAjzRI4mdh/p+2A==",
"dev": true,
"license": "BSD-3-Clause",
"optional": true,
"dependencies": {
"boolean": "^3.0.1",
"detect-node": "^2.0.4",
"globalthis": "^1.0.1",
"json-stringify-safe": "^5.0.1",
"semver-compare": "^1.0.0",
"sprintf-js": "^1.1.2"
},
"engines": {
"node": ">=8.0"
}
},
"node_modules/semver": {
"version": "6.3.1",
"resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz",
"integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==",
"dev": true,
"license": "ISC",
"bin": {
"semver": "bin/semver.js"
}
},
"node_modules/semver-compare": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/semver-compare/-/semver-compare-1.0.0.tgz",
"integrity": "sha512-YM3/ITh2MJ5MtzaM429anh+x2jiLVjqILF4m4oyQB18W7Ggea7BfqdH/wGMK7dDiMghv/6WG7znWMwUDzJiXow==",
"dev": true,
"license": "MIT",
"optional": true
},
"node_modules/serialize-error": {
"version": "7.0.1",
"resolved": "https://registry.npmjs.org/serialize-error/-/serialize-error-7.0.1.tgz",
"integrity": "sha512-8I8TjW5KMOKsZQTvoxjuSIa7foAwPWGOts+6o7sgjz41/qMD9VQHEDxi6PBvK2l0MXUmqZyNpUK+T2tQaaElvw==",
"dev": true,
"license": "MIT",
"optional": true,
"dependencies": {
"type-fest": "^0.13.1"
},
"engines": {
"node": ">=10"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/sprintf-js": {
"version": "1.1.3",
"resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.1.3.tgz",
"integrity": "sha512-Oo+0REFV59/rz3gfJNKQiBlwfHaSESl1pcGyABQsnnIfWOFt6JNj5gCog2U6MLZ//IGYD+nA8nI+mTShREReaA==",
"dev": true,
"license": "BSD-3-Clause",
"optional": true
},
"node_modules/sumchecker": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/sumchecker/-/sumchecker-3.0.1.tgz",
"integrity": "sha512-MvjXzkz/BOfyVDkG0oFOtBxHX2u3gKbMHIF/dXblZsgD3BWOFLmHovIpZY7BykJdAjcqRCBi1WYBNdEC9yI7vg==",
"dev": true,
"license": "Apache-2.0",
"dependencies": {
"debug": "^4.1.0"
},
"engines": {
"node": ">= 8.0"
}
},
"node_modules/type-fest": {
"version": "0.13.1",
"resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.13.1.tgz",
"integrity": "sha512-34R7HTnG0XIJcBSn5XhDd7nNFPRcXYRZrBB2O2jdKqYODldSzBAqzsWoZYYvduky73toYS/ESqxPvkDf/F0XMg==",
"dev": true,
"license": "(MIT OR CC0-1.0)",
"optional": true,
"engines": {
"node": ">=10"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/undici-types": {
"version": "6.21.0",
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz",
"integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==",
"dev": true,
"license": "MIT"
},
"node_modules/universalify": {
"version": "0.1.2",
"resolved": "https://registry.npmjs.org/universalify/-/universalify-0.1.2.tgz",
"integrity": "sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">= 4.0.0"
}
},
"node_modules/wrappy": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz",
"integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==",
"dev": true,
"license": "ISC"
},
"node_modules/yauzl": {
"version": "2.10.0",
"resolved": "https://registry.npmjs.org/yauzl/-/yauzl-2.10.0.tgz",
"integrity": "sha512-p4a9I6X6nu6IhoGmBqAcbJy1mlC4j27vEPZX9F4L4/vZT3Lyq1VkFHw/V/PUcB9Buo+DG3iHkT0x3Qya58zc3g==",
"dev": true,
"license": "MIT",
"dependencies": {
"buffer-crc32": "~0.2.3",
"fd-slicer": "~1.1.0"
}
}
}
}
+14
View File
@@ -0,0 +1,14 @@
{
"name": "nano-agent",
"version": "0.1.0",
"description": "Codex-style local-first desktop agent for Windows",
"main": "main.js",
"private": true,
"scripts": {
"start": "electron .",
"check": "node --check main.js && node --check preload.js && node --check src/renderer.js"
},
"devDependencies": {
"electron": "^41.0.2"
}
}
+21
View File
@@ -0,0 +1,21 @@
const { contextBridge, ipcRenderer } = require('electron');
contextBridge.exposeInMainWorld('nano', {
appInfo: () => ipcRenderer.invoke('app:info'),
windowControl: (action) => ipcRenderer.invoke('window:control', action),
chooseProject: () => ipcRenderer.invoke('project:choose'),
listProjectFiles: (projectPath) => ipcRenderer.invoke('project:files', projectPath),
readProjectFile: (payload) => ipcRenderer.invoke('project:read', payload),
searchProject: (payload) => ipcRenderer.invoke('project:search', payload),
writeProjectFile: (payload) => ipcRenderer.invoke('project:write', payload),
runCommand: (payload) => ipcRenderer.invoke('shell:run', payload),
openPath: (target) => ipcRenderer.invoke('shell:open', target),
computerAction: (action) => ipcRenderer.invoke('computer:action', action),
listWindows: () => ipcRenderer.invoke('computer:windows'),
runAgent: (payload) => ipcRenderer.invoke('agent:run', payload),
onAgentEvent: (callback) => {
const listener = (_event, data) => callback(data);
ipcRenderer.on('agent:event', listener);
return () => ipcRenderer.removeListener('agent:event', listener);
}
});
+10
View File
@@ -0,0 +1,10 @@
Add-Type -AssemblyName System.Drawing
Add-Type -AssemblyName System.Windows.Forms
$bounds = [System.Windows.Forms.SystemInformation]::VirtualScreen
$bitmap = New-Object System.Drawing.Bitmap $bounds.Width, $bounds.Height
$graphics = [System.Drawing.Graphics]::FromImage($bitmap)
$graphics.CopyFromScreen($bounds.Left, $bounds.Top, 0, 0, $bitmap.Size)
$bitmap.Save('d:\Nano\assets\ui-redesign-v2.png', [System.Drawing.Imaging.ImageFormat]::Png)
$graphics.Dispose()
$bitmap.Dispose()
Write-Output 'done'
+107
View File
@@ -0,0 +1,107 @@
<!doctype html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Nano Agent</title>
<link rel="preconnect" href="https://fonts.googleapis.com" />
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;450;500;550;600;700&family=JetBrains+Mono:wght@400;500&display=swap" rel="stylesheet" />
<link rel="stylesheet" href="./styles.css" />
</head>
<body>
<div class="app-shell">
<header class="titlebar">
<div class="titlebar-left">
<button class="window-toggle chrome-icon" title="Toggle sidebar"><svg viewBox="0 0 24 24"><rect x="4" y="5" width="16" height="14" rx="2"/><path d="M9 5v14"/></svg></button>
<span class="titlebar-divider"></span>
<button class="chrome-icon" title="Back">‹</button>
<button class="chrome-icon muted" title="Forward">›</button>
<button class="menu-button">File</button>
<button class="menu-button">Edit</button>
<button class="menu-button">View</button>
<button class="menu-button">Help</button>
</div>
<div class="titlebar-right">
<span class="live-dot"></span><span class="workspace-label">Nano Agent</span>
<button class="window-control" data-window-control="minimize">─</button>
<button class="window-control" data-window-control="maximize">□</button>
<button class="window-control close" data-window-control="close">✕</button>
</div>
</header>
<div class="app-grid">
<aside class="sidebar" id="sidebar">
<nav class="primary-nav">
<button class="nav-item active" data-view="task"><span class="nav-glyph"><svg viewBox="0 0 24 24"><path d="M12 5v14M5 12h14"/></svg></span><span>New task</span></button>
<button class="nav-item" data-view="search"><span class="nav-glyph"><svg viewBox="0 0 24 24"><circle cx="10.8" cy="10.8" r="5.8"/><path d="m15.2 15.2 4.1 4.1"/></svg></span><span>Search</span></button>
<button class="nav-item" data-view="computer"><span class="nav-glyph"><svg viewBox="0 0 24 24"><circle cx="12" cy="12" r="7.5"/><path d="m9.5 12 1.7 1.7 3.5-3.7"/></svg></span><span>Plugins</span></button>
<button class="nav-item" data-view="project"><span class="nav-glyph"><svg viewBox="0 0 24 24"><path d="M4 7.5h6l1.8 2H20v8.2a2 2 0 0 1-2 2H6a2 2 0 0 1-2-2Z"/><path d="M4 7.5v-1a2 2 0 0 1 2-2h3l1.8 2"/></svg></span><span>Project</span></button>
</nav>
<div class="section-heading">Tasks</div>
<div class="task-list" id="taskList"></div>
<div class="sidebar-grow"></div>
<button class="nav-item settings-button" data-view="settings"><span class="nav-glyph"><svg viewBox="0 0 24 24"><circle cx="12" cy="12" r="3"/><path d="M19 13.2a7.7 7.7 0 0 0 0-2.4l1.5-1.2-1.5-2.6-1.8.7a7.3 7.3 0 0 0-2-1.2L15 4.5h-3l-.3 2a7.3 7.3 0 0 0-2 1.2l-1.8-.7-1.5 2.6L8 10.8a7.7 7.7 0 0 0 0 2.4l-1.5 1.2L8 17l1.8-.7a7.3 7.3 0 0 0 2 1.2l.3 2h3l.3-2a7.3 7.3 0 0 0 2-1.2l1.8.7 1.5-2.6Z"/></svg></span><span>Settings</span><span class="key-hint">⌘,</span></button>
<div class="sidebar-bottom"><span><span class="live-dot"></span><span id="providerLabel">演示模式</span></span><button class="help-button">?</button></div>
</aside>
<main class="main-panel">
<section class="view task-view active" id="taskView">
<header class="task-header">
<div class="task-name"><span class="task-ring"></span><strong id="taskTitle">实现 Codex 风格智能体</strong><span class="slash">/</span><button id="projectButton" class="project-button">⌂ 选择项目</button></div>
<div class="task-header-actions"><button class="quiet-button" id="modelButton">Luna High ⌄</button><button class="more-button">···</button></div>
</header>
<div class="conversation" id="conversation"></div>
<aside class="turn-rail" aria-label="Conversation turns">
<div class="turn-markers" id="turnMarkers"></div>
<div class="turn-preview" id="turnPreview">
<strong>对味了!别忘了改这个,还有右侧侧边快速选择...</strong>
<p>我看到你标出的具体问题了:设置图标和消息操作图标要统一成更厚的线性图标;项目选择器不能再用 Windows 原生下拉框,否则会出现廉价蓝色。</p>
<div class="preview-tags"><span>index.html</span><span>styles.css</span><span>+1</span></div>
</div>
</aside>
<div class="composer-dock">
<div class="change-pill">1 file changed <span>+8</span> <b>-1</b></div>
<div class="pending-queue" id="pendingQueue"></div>
<div class="composer-context"><button id="composerProject" class="project-button">⌂ 选择项目</button><span>支持文件、命令和桌面操作</span></div>
<div class="composer">
<textarea id="composerInput" rows="1" placeholder="Ask for follow-up changes"></textarea>
<div class="composer-row">
<div class="composer-left"><button class="add-button">+</button><button class="access-mode" id="accessMode"><span>◉</span> Full access</button></div>
<div class="composer-right"><span id="modelMeta">Luna High</span><button class="send-button" id="sendButton">↑</button></div>
</div>
</div>
</div>
</section>
<section class="view secondary-view" id="searchView">
<div class="secondary-header"><div><span class="eyebrow">WORKSPACE</span><h2>Search</h2></div><span class="shortcut-chip">⌘ K</span></div>
<div class="search-box"><span>⌕</span><input id="searchInput" placeholder="Search tasks, messages and files…" /></div>
<div class="empty-secondary" id="searchResults">输入关键词开始搜索。</div>
</section>
<section class="view secondary-view" id="projectView">
<div class="secondary-header"><div><span class="eyebrow">PROJECTS</span><h2>Project</h2></div><button class="primary-button" id="chooseProject">选择目录</button></div>
<div class="project-card"><div class="project-icon">▱</div><div><strong id="projectPath">尚未选择项目</strong><p>选择一个本地目录,Nano 才能读取文件、执行命令和维护上下文。</p></div></div>
</section>
<section class="view secondary-view" id="computerView">
<div class="secondary-header"><div><span class="eyebrow">COMPUTER CONTROL</span><h2>Plugins</h2></div><span class="status-chip"><span></span> Windows 原生</span></div>
<div class="computer-grid"><div class="screen-card"><div class="card-heading"><strong>当前屏幕</strong><button class="quiet-button" id="refreshScreen">截图</button></div><div class="screen-preview" id="screenPreview"><div class="screen-placeholder"><span>◉</span><p>截图后显示当前桌面</p></div></div></div><div class="control-card"><div class="card-heading"><strong>基础动作</strong><span class="muted-label">受控执行</span></div><div class="action-grid"><button data-computer="mouse_move">移动鼠标</button><button data-computer="mouse_click">点击当前位置</button><button data-computer="scroll">向上滚动</button><button data-computer="type">输入测试文本</button></div><p class="control-note">动作完成后会自动截图验证,高风险操作需要确认。</p></div></div>
<div class="windows-card"><div class="card-heading"><strong>窗口列表</strong><button class="quiet-button" id="refreshWindows">刷新</button></div><div class="window-list" id="windowList"><div class="empty-secondary">点击刷新获取当前窗口。</div></div></div>
</section>
<section class="view secondary-view" id="settingsView">
<div class="secondary-header"><div><span class="eyebrow">SETTINGS</span><h2>Settings</h2></div><button class="primary-button" id="saveSettings">保存</button></div>
<div class="settings-grid"><label>供应商<div class="select-control" id="providerSelect" data-value="demo"><button class="select-trigger" type="button"><span class="select-value">演示模式</span><span class="select-chevron">⌄</span></button><div class="select-menu"><button type="button" data-value="demo">演示模式</button><button type="button" data-value="openai">OpenAI 兼容</button><button type="button" data-value="anthropic">Anthropic 兼容</button></div></div></label><label>模型<input id="modelInput" placeholder="例如 gpt-4.1 / claude-sonnet" /></label><label>API Base URL<input id="baseUrlInput" placeholder="https://api.openai.com/v1" /></label><label>API Key<input id="apiKeyInput" type="password" placeholder="只保存在本机设置" /></label></div>
<div class="settings-note">ⓘ 首期配置用于本地 MVP。正式版本会接入 Windows Credential Manager 加密保存密钥。</div>
</section>
</main>
</div>
</div>
<div class="toast" id="toast"></div>
<script src="./renderer.js"></script>
</body>
</html>
+250
View File
@@ -0,0 +1,250 @@
const state = {
view: 'task',
projectPath: '',
tasks: [
{ title: '实现 Codex 风格智能体', age: '现在' },
{ title: '分析开发习惯', age: '38m' },
{ title: '回应问候', age: '42m' },
{ title: '删除快捷方式', age: '16h' },
{ title: '用人声示例生成歌曲', age: '3d' },
{ title: '转码为MP3', age: '3d' },
{ title: '查找语言模型数据集', age: '3d' },
{ title: '了解 NIGHT DANCER', age: '4d' },
{ title: '评估本机训练NLP模型', age: '4d' },
{ title: '查看Agent2项目', age: '4d' },
{ title: '检查 Ollama 是否运行', age: '4d' }
],
messages: [],
agentWorking: false,
pendingPrompts: [],
turnCount: 4,
activeTurn: 4,
config: JSON.parse(localStorage.getItem('nano-config') || '{"provider":"demo","model":"Luna High","baseUrl":"","apiKey":""}')
};
const $ = (id) => document.getElementById(id);
const conversation = $('conversation');
function toast(message) {
const element = $('toast');
element.textContent = message;
element.classList.add('show');
clearTimeout(toast.timer);
toast.timer = setTimeout(() => element.classList.remove('show'), 2600);
}
function renderTasks() {
$('taskList').innerHTML = state.tasks.map((task, index) => `<button class="task-item ${index === 0 ? 'selected' : ''}"><span>${escapeHtml(task.title)}</span><span class="task-age">${task.age}</span></button>`).join('');
}
function renderTurnRail() {
const markers = $('turnMarkers');
if (!markers) return;
markers.innerHTML = Array.from({ length: state.turnCount }, (_, index) => `<button class="turn-chip ${index + 1 === state.activeTurn ? 'active' : ''}" data-turn="${index + 1}" aria-label="第 ${index + 1} 轮对话"></button>`).join('');
markers.querySelectorAll('.turn-chip').forEach((button) => button.addEventListener('click', () => {
state.activeTurn = Number(button.dataset.turn);
renderTurnRail();
toast(`已定位到第 ${button.dataset.turn} 轮对话`);
}));
}
function escapeHtml(value) { return String(value).replace(/[&<>"']/g, (char) => ({ '&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;', "'": '&#039;' }[char])); }
function showView(view) {
state.view = view;
document.querySelectorAll('.view').forEach((element) => element.classList.toggle('active', element.id === `${view}View`));
document.querySelectorAll('.nav-item[data-view]').forEach((element) => element.classList.toggle('active', element.dataset.view === view));
}
function appendMessage(role, content) {
state.messages.push({ role, content });
const wrapper = document.createElement('div');
wrapper.className = `message ${role}`;
wrapper.innerHTML = `<div><div class="message-label">${role === 'user' ? '你' : 'Nano'}</div><div class="message-bubble">${escapeHtml(content)}</div></div>`;
conversation.appendChild(wrapper);
conversation.scrollTop = conversation.scrollHeight;
$('welcomeState')?.remove();
return wrapper.querySelector('.message-bubble');
}
function appendRunCard(text, icon = '◌') {
const card = document.createElement('div');
card.className = 'run-card';
card.innerHTML = `<span class="run-icon">${icon}</span>${escapeHtml(text)}`;
conversation.appendChild(card);
conversation.scrollTop = conversation.scrollHeight;
return card;
}
function renderPendingQueue() {
const queue = $('pendingQueue');
if (!queue) return;
queue.innerHTML = state.pendingPrompts.map((prompt, index) => `<div class="pending-item"><span class="pending-icon">↳</span><span class="pending-text">${escapeHtml(prompt)}</span><span class="pending-meta">待发送</span><button class="pending-remove" data-pending-index="${index}" title="移除">×</button></div>`).join('');
queue.querySelectorAll('[data-pending-index]').forEach((button) => button.addEventListener('click', () => { state.pendingPrompts.splice(Number(button.dataset.pendingIndex), 1); renderPendingQueue(); }));
}
function queuePrompt(prompt) {
const text = String(prompt || '').trim();
if (!text) return;
state.pendingPrompts.push(text);
renderPendingQueue();
$('composerInput').value = '';
$('composerInput').style.height = 'auto';
toast('已暂存,当前任务完成后自动发送');
}
function setWorkingVisual(working) {
document.querySelector('.composer')?.classList.toggle('working', working);
}
function flushPendingPrompt() {
if (state.agentWorking || state.pendingPrompts.length === 0) return;
const next = state.pendingPrompts.shift();
renderPendingQueue();
setTimeout(() => sendPrompt(next), 180);
}
function renderWelcomeConversation() {
conversation.innerHTML = `<div class="welcome-state" id="welcomeState"><svg viewBox="0 0 64 64" class="welcome-icon"><path d="M31.5 9c5.2 0 9.5 3.7 10.3 8.7a14.8 14.8 0 0 1 14.6 14.8c0 1-.1 2-.3 2.9a12.9 12.9 0 0 1-4.2 25.1H17.8A15.8 15.8 0 0 1 15 29.2 15.4 15.4 0 0 1 31.5 9Z"/><path d="M25.2 38.5h.1m13.5 0h.1m-11.2 6.3c2.8 2 5.6 2 8.4 0"/><path d="M31.8 27.8v5.7l-3.5 2.2"/></svg><h1>你想完成什么工作?</h1><p>告诉 Nano 目标,它可以理解项目、修改文件、执行命令,也能直接操作你的 Windows 桌面。</p><div class="quick-actions"><button data-prompt="分析当前项目结构,并告诉我从哪里开始。">✨ 分析项目</button><button data-prompt="检查当前项目是否能正常运行,并修复发现的问题。">⚡ 运行检查</button><button data-prompt="打开浏览器,搜索今天的开发者新闻,并整理成 Markdown。">🖥️ 操作电脑</button></div></div>`;
bindQuickActions();
}
function renderDemoConversation() {
$('taskTitle').textContent = '实现 Codex 风格智能体';
conversation.innerHTML = `<div class="message user"><div><div class="message-bubble">能不能读一下 Codex 的代码?我毕竟安装了你</div></div></div>
<div class="message assistant"><div><div class="message-bubble"><p>不能直接读取 Codex 的内部源代码。</p><p>你安装的是 Codex 应用,不代表安装了它的源码、设计稿或内部组件库。当前我能读取的是:</p><ul><li><code>D:\Nano</code> 里的项目源码</li><li>你提供的截图和图片</li><li>你明确提供路径且有权限访问的本地文件</li></ul><p>所以之前我只能根据截图和公开可见的交互去复刻,无法直接拿到 Codex 的真实 CSS、组件、字体、设计 Token 或状态逻辑。这也是视觉上一直有差距的原因。</p><p>如果你能提供 Codex 的源码目录、设计文件或可访问的前端资源路径,我可以直接分析并迁移;否则我会继续按你给的截图做像素级复刻。</p><p>当前 <code>D:\Nano</code> 的源代码我已经可以正常读取和修改。</p></div><div class="message-tools"><span class="message-tool-icon">▢</span><span class="message-tool-icon">↗</span><span class="message-tool-icon">↻</span><span>15:30</span></div></div></div>`;
conversation.scrollTop = conversation.scrollHeight;
}
function buildMessages() {
const system = `你是 Nano Agent,一个本地优先的 Codex 风格电脑智能体。你可以读取项目、修改文件、执行命令,也可以通过 computer 工具操作 Windows。所有付款、发送、删除、发布、管理员操作都必须请求用户确认。当前项目:${state.projectPath || '未选择'}`;
return [{ role: 'system', content: system }, ...state.messages.map(({ role, content }) => ({ role, content }))];
}
async function sendPrompt(prompt) {
const text = String(prompt || '').trim();
if (!text) return;
if (state.agentWorking) {
queuePrompt(text);
return;
}
state.agentWorking = true;
state.turnCount += 1;
state.activeTurn = state.turnCount;
renderTurnRail();
setWorkingVisual(true);
appendMessage('user', text);
$('composerInput').value = '';
const runCard = appendRunCard('正在理解任务并准备执行…');
const bubble = appendMessage('assistant', '');
const unsubscribe = window.nano.onAgentEvent((event) => {
if (event.type === 'run.started') runCard.innerHTML = '<span class="run-icon">◌</span>正在运行智能体…';
if (event.type === 'message.delta') bubble.textContent += event.content;
if (event.type === 'tool.started') runCard.innerHTML = `<span class="run-icon">⌁</span>正在调用 ${escapeHtml(event.name)}…`;
if (event.type === 'tool.completed') runCard.innerHTML = `<span class="run-icon">✓</span>已完成 ${escapeHtml(event.name)}`;
if (event.type === 'approval.required') { runCard.innerHTML = `<span class="run-icon">!</span>${escapeHtml(event.reason)}`; toast(event.reason); }
if (event.type === 'file.changed') toast(`已修改文件:${event.path}`);
if (event.type === 'run.completed') { runCard.innerHTML = '<span class="run-icon">✓</span>任务完成'; state.agentWorking = false; setWorkingVisual(false); unsubscribe(); flushPendingPrompt(); }
if (event.type === 'run.error') { runCard.innerHTML = `<span class="run-icon">!</span>${escapeHtml(event.message)}`; state.agentWorking = false; setWorkingVisual(false); toast(event.message); unsubscribe(); flushPendingPrompt(); }
conversation.scrollTop = conversation.scrollHeight;
});
const result = await window.nano.runAgent({ config: state.config.provider === 'demo' ? null : state.config, messages: buildMessages(), projectPath: state.projectPath, permissionMode: 'full' });
if (!result.ok) { state.agentWorking = false; setWorkingVisual(false); toast(result.error || '任务执行失败'); flushPendingPrompt(); }
}
async function chooseProject() {
const project = await window.nano.chooseProject();
if (!project) return;
state.projectPath = project;
$('projectPath').textContent = project;
$('projectButton').textContent = `⌂ ${project.split('\\').pop()}`;
$('composerProject').textContent = `⌂ ${project.split('\\').pop()}`;
toast(`已选择项目:${project}`);
}
async function screenshot() {
try {
const result = await window.nano.computerAction({ type: 'screenshot' });
$('screenPreview').innerHTML = `<img src="${result.dataUrl}" alt="当前屏幕截图" />`;
toast('截图已更新');
} catch (error) { toast(`截图失败:${error.message}`); }
}
async function runComputer(type) {
let action = { type };
if (type === 'mouse_move') { action = { type, x: 500, y: 400 }; }
if (type === 'mouse_click') { action = { type, x: 500, y: 400, button: 'left' }; }
if (type === 'scroll') { action = { type, amount: 3 }; }
if (type === 'type') { action = { type, text: 'Nano Agent test input' }; }
if (type === 'mouse_click' && !window.confirm('即将点击屏幕坐标 (500, 400),确定执行吗?')) return;
try { await window.nano.computerAction(action); await screenshot(); } catch (error) { toast(`动作失败:${error.message}`); }
}
async function refreshWindows() {
try {
const windows = await window.nano.listWindows();
$('windowList').innerHTML = windows.length ? windows.map((item) => `<div class="window-row"><span>${escapeHtml(item.MainWindowTitle)}</span><small>${escapeHtml(item.ProcessName)} · ${item.Id}</small></div>`).join('') : '<div class="empty-secondary">没有找到活动窗口。</div>';
} catch (error) { toast(`窗口读取失败:${error.message}`); }
}
function bindQuickActions() {
document.querySelectorAll('[data-prompt]').forEach((button) => button.addEventListener('click', () => sendPrompt(button.dataset.prompt)));
}
function loadSettings() {
setProviderSelect(state.config.provider || 'demo');
$('modelInput').value = state.config.model || '';
$('baseUrlInput').value = state.config.baseUrl || '';
$('apiKeyInput').value = state.config.apiKey || '';
$('providerLabel').textContent = state.config.provider === 'demo' ? '演示模式' : state.config.provider;
$('modelMeta').textContent = state.config.model || '未配置模型';
$('modelButton').textContent = `${state.config.model || '未配置模型'} ▾`;
}
function setProviderSelect(value) {
const control = $('providerSelect');
if (!control) return;
const option = control.querySelector(`[data-value="${value}"]`) || control.querySelector('[data-value="demo"]');
control.dataset.value = option.dataset.value;
control.querySelector('.select-value').textContent = option.textContent;
control.querySelectorAll('.select-menu button').forEach((button) => button.classList.toggle('active', button === option));
}
function saveSettings() {
state.config = { provider: $('providerSelect').dataset.value || 'demo', model: $('modelInput').value.trim(), baseUrl: $('baseUrlInput').value.trim(), apiKey: $('apiKeyInput').value };
localStorage.setItem('nano-config', JSON.stringify(state.config));
loadSettings();
toast('设置已保存');
}
document.querySelectorAll('.nav-item[data-view]').forEach((element) => element.addEventListener('click', () => {
showView(element.dataset.view);
if (element.dataset.view === 'task') renderWelcomeConversation();
}));
document.querySelector('.window-toggle').addEventListener('click', () => $('sidebar').classList.toggle('collapsed'));
document.querySelectorAll('[data-window-control]').forEach((button) => button.addEventListener('click', () => window.nano.windowControl(button.dataset.windowControl)));
$('sendButton').addEventListener('click', () => sendPrompt($('composerInput').value));
$('composerInput').addEventListener('keydown', (event) => {
if (event.key === 'Enter' && !event.shiftKey) {
if (state.agentWorking) { event.preventDefault(); queuePrompt(event.target.value); }
else if (event.ctrlKey || event.metaKey) { event.preventDefault(); sendPrompt(event.target.value); }
}
});
$('composerInput').addEventListener('input', (event) => { event.target.style.height = 'auto'; event.target.style.height = `${Math.min(event.target.scrollHeight, 140)}px`; });
$('projectButton').addEventListener('click', chooseProject);
$('composerProject').addEventListener('click', chooseProject);
$('chooseProject').addEventListener('click', chooseProject);
$('refreshScreen').addEventListener('click', screenshot);
$('refreshWindows').addEventListener('click', refreshWindows);
$('saveSettings').addEventListener('click', saveSettings);
document.querySelectorAll('[data-computer]').forEach((button) => button.addEventListener('click', () => runComputer(button.dataset.computer)));
document.querySelectorAll('.select-control').forEach((control) => {
control.querySelector('.select-trigger').addEventListener('click', () => control.classList.toggle('open'));
control.querySelectorAll('.select-menu button').forEach((option) => option.addEventListener('click', () => { setProviderSelect(option.dataset.value); control.classList.remove('open'); }));
});
$('searchInput').addEventListener('input', (event) => { const term = event.target.value.toLowerCase(); $('searchResults').textContent = term ? state.tasks.filter((task) => task.title.toLowerCase().includes(term)).map((task) => task.title).join(' · ') || '没有找到匹配任务。' : '输入关键词开始搜索。'; });
renderTasks();
loadSettings();
renderTurnRail();
renderDemoConversation();
+662
View File
@@ -0,0 +1,662 @@
/* ═══════════════════════════════════════════
Nano Agent — Design System v2
Inspired by Linear / Raycast aesthetics
═══════════════════════════════════════════ */
:root {
color-scheme: dark;
/* ── Core palette ── */
--canvas: #09090b;
--surface-0: #0f0f12;
--surface-1: #16161a;
--surface-2: #1c1c21;
--surface-3: #242429;
--surface-4: #2c2c33;
--sidebar-bg: rgba(13, 13, 16, .92);
/* ── Borders ── */
--border: rgba(255, 255, 255, .06);
--border-hover: rgba(255, 255, 255, .12);
--border-strong: rgba(255, 255, 255, .18);
--border-focus: rgba(139, 148, 255, .5);
/* ── Text ── */
--text-primary: #f4f4f5;
--text-secondary: #a1a1aa;
--text-tertiary: #71717a;
--text-ghost: #52525b;
/* ── Accents ── */
--accent: #8b94ff;
--accent-soft: rgba(139, 148, 255, .12);
--accent-glow: rgba(139, 148, 255, .25);
--green: #4ade80;
--green-soft: rgba(74, 222, 128, .1);
--orange: #fb923c;
--orange-soft: rgba(251, 146, 60, .1);
--red: #f87171;
--red-soft: rgba(248, 113, 113, .1);
--blue: #60a5fa;
/* ── Radii ── */
--r-xs: 6px;
--r-sm: 8px;
--r-md: 12px;
--r-lg: 16px;
--r-xl: 20px;
--r-full: 999px;
/* ── Shadows ── */
--shadow-sm: 0 1px 2px rgba(0,0,0,.3);
--shadow-md: 0 4px 12px rgba(0,0,0,.4);
--shadow-lg: 0 12px 32px rgba(0,0,0,.5);
--shadow-glow: 0 0 20px var(--accent-glow);
/* ── Typography ── */
--font-sans: "Inter", -apple-system, BlinkMacSystemFont, "Segoe UI", system-ui, sans-serif;
--font-mono: "JetBrains Mono", "Cascadia Code", "Fira Code", ui-monospace, monospace;
/* ── Transitions ── */
--ease: cubic-bezier(.4, 0, .2, 1);
--ease-spring: cubic-bezier(.34, 1.56, .64, 1);
--duration: 180ms;
font-family: var(--font-sans);
font-feature-settings: "cv02", "cv03", "cv04", "cv11";
-webkit-font-smoothing: antialiased;
}
/* ── Reset ── */
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
html, body { width: 100%; height: 100%; overflow: hidden; background: transparent; }
body { color: var(--text-primary); font-size: 13px; line-height: 1.5; }
button, input, textarea, select { font: inherit; color: inherit; }
button { border: 0; cursor: pointer; background: none; }
::selection { background: var(--accent-soft); color: var(--text-primary); }
/* ── Scrollbar ── */
::-webkit-scrollbar { width: 6px; }
::-webkit-scrollbar-track { background: transparent; }
::-webkit-scrollbar-thumb { background: rgba(255,255,255,.1); border-radius: 3px; }
::-webkit-scrollbar-thumb:hover { background: rgba(255,255,255,.18); }
/* ═══ App Shell ═══ */
.app-shell {
width: 100%; height: 100%;
display: flex; flex-direction: column;
overflow: hidden;
border: 1px solid var(--border);
border-radius: var(--r-xl);
background: var(--canvas);
box-shadow: var(--shadow-lg), inset 0 1px 0 rgba(255,255,255,.03);
}
/* ═══ Titlebar ═══ */
.titlebar {
height: 48px; min-height: 48px;
display: flex; align-items: center; justify-content: space-between;
padding: 0 12px 0 14px;
border-bottom: 1px solid var(--border);
background: var(--surface-0);
user-select: none;
-webkit-app-region: drag;
}
.titlebar button, .titlebar .workspace-label, .titlebar .live-dot { -webkit-app-region: no-drag; }
.titlebar-left, .titlebar-right { height: 100%; display: flex; align-items: center; gap: 2px; }
.titlebar-divider { width: 1px; height: 18px; margin: 0 8px; background: var(--border-hover); }
.chrome-icon, .window-control, .menu-button {
color: var(--text-tertiary);
border-radius: var(--r-sm);
transition: all var(--duration) var(--ease);
}
.chrome-icon {
width: 28px; height: 28px;
display: grid; place-items: center;
font-size: 18px; line-height: 1;
}
.chrome-icon svg { width: 15px; height: 15px; fill: none; stroke: currentColor; stroke-width: 1.8; stroke-linecap: round; stroke-linejoin: round; }
.chrome-icon:hover, .window-control:hover, .menu-button:hover {
color: var(--text-primary);
background: rgba(255,255,255,.07);
}
.chrome-icon.muted { opacity: .35; pointer-events: none; }
.menu-button { padding: 5px 10px; margin-left: 1px; font-size: 12px; font-weight: 450; letter-spacing: .01em; }
.workspace-label { color: var(--text-tertiary); font-size: 11px; font-weight: 500; margin: 0 14px 0 4px; letter-spacing: .02em; }
.live-dot {
display: inline-block; width: 7px; height: 7px;
border-radius: 50%;
background: var(--green);
box-shadow: 0 0 6px rgba(74, 222, 128, .4);
animation: pulse-dot 2.5s ease-in-out infinite;
}
@keyframes pulse-dot {
0%, 100% { box-shadow: 0 0 4px rgba(74, 222, 128, .3); }
50% { box-shadow: 0 0 10px rgba(74, 222, 128, .6); }
}
.window-control { width: 32px; height: 28px; font-size: 14px; border-radius: var(--r-sm); }
.window-control.close:hover { background: var(--red-soft); color: var(--red); }
/* ═══ Layout Grid ═══ */
.app-grid { min-height: 0; flex: 1; display: flex; }
/* ═══ Sidebar ═══ */
.sidebar {
width: 248px; min-width: 248px;
display: flex; flex-direction: column;
padding: 16px 10px 12px;
border-right: 1px solid var(--border);
background: var(--sidebar-bg);
backdrop-filter: blur(20px) saturate(120%);
transition: width var(--duration) var(--ease), min-width var(--duration) var(--ease), padding var(--duration) var(--ease);
}
.primary-nav { display: grid; gap: 2px; }
.nav-item {
width: 100%; height: 34px;
display: flex; align-items: center; gap: 10px;
padding: 0 10px;
border-radius: var(--r-sm);
color: var(--text-secondary);
font-size: 13px; font-weight: 450;
text-align: left;
transition: all var(--duration) var(--ease);
}
.nav-item:hover { color: var(--text-primary); background: rgba(255,255,255,.05); }
.nav-item.active {
color: var(--text-primary);
background: var(--accent-soft);
box-shadow: inset 0 0 0 1px rgba(139, 148, 255, .15);
}
.nav-item.active .nav-glyph { color: var(--accent); }
.nav-glyph {
width: 18px; height: 18px;
display: grid; place-items: center;
color: var(--text-tertiary);
transition: color var(--duration) var(--ease);
}
.nav-glyph svg { width: 16px; height: 16px; fill: none; stroke: currentColor; stroke-width: 1.8; stroke-linecap: round; stroke-linejoin: round; }
.section-heading {
padding: 24px 10px 8px;
color: var(--text-ghost);
font-family: var(--font-mono);
font-size: 10px; font-weight: 500;
letter-spacing: 1.5px;
text-transform: uppercase;
}
.task-list { display: grid; gap: 1px; }
.task-item {
width: 100%; height: 32px;
display: flex; align-items: center; justify-content: space-between;
padding: 0 10px;
border-radius: var(--r-sm);
color: var(--text-secondary);
font-size: 12.5px;
text-align: left;
white-space: nowrap; overflow: hidden;
transition: all var(--duration) var(--ease);
}
.task-item:hover { color: var(--text-primary); background: rgba(255,255,255,.05); }
.task-item.selected { color: var(--text-primary); background: rgba(255,255,255,.08); }
.task-item span:first-child { overflow: hidden; text-overflow: ellipsis; }
.task-age { margin-left: 8px; color: var(--text-ghost); font-family: var(--font-mono); font-size: 10px; }
.sidebar-grow { flex: 1; }
.settings-button { margin-bottom: 8px; }
.key-hint { margin-left: auto; color: var(--text-ghost); font-size: 10px; font-family: var(--font-mono); }
.sidebar-bottom {
display: flex; align-items: center; justify-content: space-between;
padding: 12px 10px 0;
border-top: 1px solid var(--border);
color: var(--text-tertiary); font-size: 11px;
}
.sidebar-bottom > span { display: flex; align-items: center; gap: 8px; }
.help-button {
width: 20px; height: 20px;
border: 1px solid var(--border-hover);
border-radius: 50%;
color: var(--text-tertiary); font-size: 11px;
transition: all var(--duration) var(--ease);
}
.help-button:hover { border-color: var(--border-strong); color: var(--text-primary); }
/* Sidebar collapsed */
.sidebar.collapsed { width: 56px; min-width: 56px; padding: 16px 8px 12px; }
.sidebar.collapsed .nav-item span:not(.nav-glyph),
.sidebar.collapsed .section-heading,
.sidebar.collapsed .task-list,
.sidebar.collapsed .sidebar-bottom span,
.sidebar.collapsed .key-hint { display: none; }
/* ═══ Main Panel ═══ */
.main-panel {
min-width: 0; flex: 1;
position: relative; overflow: hidden;
background: var(--canvas);
}
.view { display: none; width: 100%; height: 100%; }
.view.active { display: flex; }
/* ═══ Task View ═══ */
.task-view { position: relative; flex-direction: column; }
.task-header {
height: 48px; min-height: 48px;
display: flex; align-items: center; justify-content: space-between;
padding: 0 20px;
border-bottom: 1px solid var(--border);
background: rgba(15, 15, 18, .8);
backdrop-filter: blur(12px);
}
.task-name, .task-header-actions { display: flex; align-items: center; gap: 10px; }
.task-name strong { font-size: 13px; font-weight: 550; letter-spacing: -.01em; }
.task-ring {
width: 8px; height: 8px;
border: 1.5px solid var(--accent);
border-radius: 50%;
box-shadow: 0 0 6px var(--accent-glow);
}
.slash { color: var(--text-ghost); }
.project-button, .quiet-button, .more-button {
border-radius: var(--r-sm);
color: var(--text-tertiary);
transition: all var(--duration) var(--ease);
}
.project-button { padding: 5px 8px; font-size: 12px; }
.quiet-button { padding: 6px 10px; font-size: 12px; font-weight: 450; }
.more-button { padding: 4px 8px; color: var(--text-secondary); font-size: 16px; letter-spacing: 2px; }
.project-button:hover, .quiet-button:hover, .more-button:hover {
color: var(--text-primary);
background: rgba(255,255,255,.06);
}
/* ═══ Conversation ═══ */
.conversation {
min-height: 0; flex: 1;
overflow-y: auto; overflow-x: hidden;
padding: 24px max(40px, 10vw) 160px;
}
.welcome-state {
min-height: 100%;
display: flex; flex-direction: column;
align-items: center; justify-content: center;
padding-bottom: 48px;
text-align: center;
animation: fade-up .5s var(--ease) both;
}
.welcome-icon { width: 56px; height: 56px; fill: none; stroke: var(--accent); stroke-width: 1.5; stroke-linecap: round; stroke-linejoin: round; filter: drop-shadow(0 0 12px var(--accent-glow)); margin-bottom: 20px; }
.welcome-state h1 { margin: 0 0 12px; font-size: 26px; font-weight: 600; letter-spacing: -.04em; background: linear-gradient(135deg, var(--text-primary), var(--text-secondary)); -webkit-background-clip: text; -webkit-text-fill-color: transparent; }
.welcome-state p { max-width: 480px; margin: 0; color: var(--text-tertiary); font-size: 14px; line-height: 1.7; }
.quick-actions { display: flex; gap: 8px; margin-top: 28px; }
.quick-actions button {
padding: 9px 14px;
border: 1px solid var(--border-hover);
border-radius: var(--r-full);
background: var(--surface-1);
color: var(--text-secondary);
font-size: 12.5px; font-weight: 450;
transition: all var(--duration) var(--ease);
}
.quick-actions button:hover {
border-color: var(--accent);
color: var(--text-primary);
background: var(--accent-soft);
box-shadow: var(--shadow-glow);
transform: translateY(-1px);
}
/* Messages */
.message { display: flex; max-width: 720px; margin: 20px auto; animation: fade-up .25s var(--ease) both; }
.message.user { justify-content: flex-end; }
.message-bubble { max-width: 100%; color: var(--text-secondary); line-height: 1.7; font-size: 13.5px; }
.message.user .message-bubble {
padding: 11px 15px;
border-radius: var(--r-lg) var(--r-lg) var(--r-xs) var(--r-lg);
background: var(--surface-3);
color: var(--text-primary);
border: 1px solid var(--border);
}
.message.assistant .message-bubble p { margin: 0 0 14px; }
.message.assistant .message-bubble p:last-child { margin-bottom: 0; }
.message.assistant .message-bubble ul { margin: 0 0 14px; padding-left: 20px; }
.message.assistant .message-bubble li { margin: 6px 0; }
.message.assistant .message-bubble code {
padding: 2px 6px;
border-radius: var(--r-xs);
background: rgba(139, 148, 255, .08);
border: 1px solid rgba(139, 148, 255, .12);
color: var(--accent);
font-family: var(--font-mono);
font-size: .88em;
}
.message-tools { display: flex; gap: 12px; padding: 10px 2px 0; color: var(--text-ghost); font-size: 11px; }
.message-tool-icon { color: var(--text-tertiary); font-size: 13px; }
.message-label { display: none; }
/* Activity & Run cards */
.activity-stream { position: relative; max-width: 720px; margin: 8px auto 16px; padding-left: 28px; display: grid; gap: 8px; }
.activity-item { position: relative; display: flex; align-items: flex-start; gap: 9px; color: var(--text-tertiary); font-size: 12px; }
.activity-icon { width: 16px; color: var(--text-tertiary); text-align: center; }
.activity-item strong { color: var(--text-secondary); font-weight: 500; }
.activity-meta { color: var(--text-ghost); margin-left: 5px; }
.thinking-label { max-width: 720px; margin: 26px auto 11px; color: var(--text-tertiary); font-size: 12px; }
.step-pill {
width: fit-content; margin: 0 auto 18px;
padding: 7px 14px;
border: 1px solid var(--border);
border-radius: var(--r-full);
background: var(--surface-1);
color: var(--text-secondary); font-size: 12px;
}
.step-pill span {
display: inline-block; width: 8px; height: 8px;
margin-right: 8px;
border: 2px solid var(--accent);
border-right-color: transparent;
border-radius: 50%;
vertical-align: -1px;
animation: spin .8s linear infinite;
}
@keyframes spin { to { transform: rotate(360deg); } }
.run-card {
max-width: 720px; margin: 10px auto;
padding: 10px 14px;
border: 1px solid var(--border);
border-radius: var(--r-md);
background: var(--surface-1);
color: var(--text-tertiary); font-size: 12px;
transition: all var(--duration) var(--ease);
}
.run-icon { color: var(--green); margin-right: 8px; }
/* Turn Rail */
.turn-rail {
position: absolute; top: 50%; left: 14px; z-index: 3;
width: 28px;
display: flex; flex-direction: column; align-items: flex-start;
transform: translateY(-50%);
}
.turn-rail::before { display: none; }
.turn-markers { display: grid; gap: 12px; padding: 8px 0; }
.turn-chip {
position: relative; z-index: 1;
width: 12px; height: 3px;
border: 0; border-radius: 2px;
background: var(--text-ghost);
color: transparent; font-size: 0;
transition: all var(--duration) var(--ease);
}
.turn-chip:hover { width: 20px; background: var(--text-secondary); }
.turn-chip.active { width: 24px; background: var(--accent); box-shadow: 0 0 8px var(--accent-glow); }
.turn-preview {
position: absolute; left: 38px; top: 72px;
width: 320px; padding: 14px;
border: 1px solid var(--border-hover);
border-radius: var(--r-lg);
background: var(--surface-2);
color: var(--text-secondary);
box-shadow: var(--shadow-lg);
}
.turn-preview strong { display: block; overflow: hidden; color: var(--text-primary); font-size: 12px; font-weight: 550; text-overflow: ellipsis; white-space: nowrap; }
.turn-preview p { display: -webkit-box; overflow: hidden; margin: 8px 0 10px; color: var(--text-tertiary); font-size: 12px; line-height: 1.6; -webkit-box-orient: vertical; -webkit-line-clamp: 3; }
.preview-tags { display: flex; gap: 5px; }
.preview-tags span {
padding: 3px 7px;
border: 1px solid var(--border);
border-radius: var(--r-xs);
background: var(--surface-0);
color: var(--text-secondary);
font-family: var(--font-mono); font-size: 10px;
}
/* ═══ Composer ═══ */
.composer-dock {
position: absolute; left: 50%; bottom: 20px; z-index: 4;
width: min(720px, calc(100% - 72px));
transform: translateX(-50%);
}
.change-pill {
width: fit-content; margin: 0 auto 10px;
padding: 7px 14px;
border: 1px solid var(--border);
border-radius: var(--r-full);
background: var(--surface-1);
color: var(--text-secondary); font-size: 12px;
backdrop-filter: blur(8px);
}
.change-pill span { color: var(--green); margin-left: 4px; }
.change-pill b { color: var(--red); font-weight: 400; }
.pending-queue { display: grid; gap: 4px; margin-bottom: 4px; }
.pending-item {
display: flex; align-items: center; gap: 10px;
min-height: 40px; padding: 0 14px;
border: 1px solid var(--border);
border-radius: var(--r-md);
background: var(--surface-1);
color: var(--text-secondary); font-size: 12.5px;
}
.pending-item .pending-icon { color: var(--text-tertiary); font-size: 13px; }
.pending-item .pending-text { flex: 1; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.pending-item .pending-meta { color: var(--text-ghost); font-size: 11px; }
.pending-item .pending-remove { padding: 3px 6px; border-radius: var(--r-xs); color: var(--text-ghost); transition: all var(--duration) var(--ease); }
.pending-item .pending-remove:hover { color: var(--red); background: var(--red-soft); }
.composer-context { display: none; }
.composer-context span { margin-left: auto; color: var(--text-ghost); font-size: 10px; }
.composer {
min-height: 104px;
padding: 16px 14px 12px;
border: 1px solid var(--border-hover);
border-radius: var(--r-xl);
background: var(--surface-2);
box-shadow: var(--shadow-md), 0 0 0 0 var(--accent-glow);
transition: border-color var(--duration) var(--ease), box-shadow var(--duration) var(--ease);
}
.composer:focus-within {
border-color: var(--border-focus);
box-shadow: var(--shadow-md), 0 0 0 3px var(--accent-soft);
}
.composer textarea {
display: block; width: 100%;
min-height: 40px; max-height: 140px;
resize: none; border: 0; outline: 0;
background: transparent;
color: var(--text-primary);
font-size: 13.5px; line-height: 1.6;
}
.composer textarea::placeholder { color: var(--text-ghost); }
.composer-row { display: flex; align-items: center; justify-content: space-between; margin-top: 10px; }
.composer-left, .composer-right { display: flex; align-items: center; gap: 10px; }
.add-button {
width: 26px; height: 26px;
display: grid; place-items: center;
border-radius: 50%;
color: var(--text-tertiary); font-size: 20px; line-height: 1;
transition: all var(--duration) var(--ease);
}
.add-button:hover { color: var(--text-primary); background: rgba(255,255,255,.06); }
.access-mode {
padding: 4px 10px;
border-radius: var(--r-full);
color: var(--orange); font-size: 11.5px; font-weight: 500;
transition: all var(--duration) var(--ease);
}
.access-mode:hover { background: var(--orange-soft); }
.composer-right { color: var(--text-tertiary); font-size: 11.5px; }
.send-button {
width: 30px; height: 30px;
display: grid; place-items: center;
border-radius: 50%;
background: var(--accent);
color: #0f0f12;
font-size: 0;
transition: all var(--duration) var(--ease);
box-shadow: 0 2px 8px var(--accent-glow);
}
.send-button::after { content: '↑'; font-size: 16px; font-weight: 600; line-height: 1; }
.composer.working .send-button::after { content: '■'; font-size: 9px; }
.send-button:hover { transform: scale(1.08); box-shadow: 0 4px 16px var(--accent-glow); }
/* ═══ Secondary Views ═══ */
.secondary-view { flex-direction: column; overflow-y: auto; padding: 44px 7vw; }
.secondary-header { display: flex; align-items: flex-start; justify-content: space-between; padding-bottom: 20px; border-bottom: 1px solid var(--border); }
.eyebrow { color: var(--accent); font-family: var(--font-mono); font-size: 10px; font-weight: 500; letter-spacing: 1.5px; text-transform: uppercase; }
.secondary-header h2 { margin: 8px 0 0; font-size: 24px; font-weight: 600; letter-spacing: -.03em; }
.shortcut-chip, .status-chip {
padding: 5px 12px;
border: 1px solid var(--border-hover);
border-radius: var(--r-full);
color: var(--text-tertiary); font-size: 11px; font-family: var(--font-mono);
}
.status-chip { display: flex; align-items: center; gap: 7px; color: var(--green); border-color: rgba(74, 222, 128, .2); }
.status-chip span { width: 6px; height: 6px; border-radius: 50%; background: var(--green); box-shadow: 0 0 6px rgba(74, 222, 128, .4); }
.primary-button {
padding: 8px 16px;
border-radius: var(--r-sm);
background: var(--text-primary);
color: var(--canvas);
font-size: 12.5px; font-weight: 600;
transition: all var(--duration) var(--ease);
}
.primary-button:hover { opacity: .9; transform: translateY(-1px); box-shadow: var(--shadow-md); }
/* Search */
.search-box {
display: flex; align-items: center; gap: 12px;
max-width: 640px; margin: 28px auto 0;
padding: 12px 16px;
border: 1px solid var(--border-hover);
border-radius: var(--r-lg);
background: var(--surface-1);
transition: all var(--duration) var(--ease);
}
.search-box:focus-within { border-color: var(--border-focus); box-shadow: 0 0 0 3px var(--accent-soft); }
.search-box span { color: var(--text-tertiary); font-size: 18px; }
.search-box input { flex: 1; border: 0; outline: 0; background: transparent; font-size: 13.5px; }
.search-box input::placeholder { color: var(--text-ghost); }
.empty-secondary { padding: 52px 20px; color: var(--text-ghost); text-align: center; font-size: 13px; }
/* Project */
.project-card, .windows-card, .screen-card, .control-card {
border: 1px solid var(--border);
border-radius: var(--r-lg);
background: var(--surface-1);
transition: border-color var(--duration) var(--ease);
}
.project-card:hover, .windows-card:hover, .screen-card:hover, .control-card:hover { border-color: var(--border-hover); }
.project-card { display: flex; gap: 16px; max-width: 640px; margin: 28px auto 0; padding: 20px; }
.project-icon {
width: 40px; height: 40px;
display: grid; place-items: center;
border-radius: var(--r-md);
background: var(--accent-soft);
color: var(--accent); font-size: 18px;
}
.project-card strong { font-weight: 550; font-size: 13.5px; }
.project-card p { margin: 8px 0 0; color: var(--text-tertiary); font-size: 12.5px; line-height: 1.6; }
/* Computer Control */
.computer-grid { display: grid; grid-template-columns: 1.6fr 1fr; gap: 14px; margin-top: 24px; }
.screen-card, .control-card, .windows-card { overflow: hidden; }
.card-heading { display: flex; align-items: center; justify-content: space-between; padding: 14px 16px; border-bottom: 1px solid var(--border); }
.card-heading strong { font-size: 13px; font-weight: 550; }
.muted-label { color: var(--text-ghost); font-size: 11px; }
.screen-preview { min-height: 280px; display: grid; place-items: center; background: var(--canvas); }
.screen-preview img { display: block; max-width: 100%; max-height: 440px; object-fit: contain; border-radius: var(--r-sm); }
.screen-placeholder { color: var(--text-ghost); text-align: center; }
.screen-placeholder span { display: block; margin-bottom: 10px; color: var(--text-tertiary); font-size: 32px; }
.screen-placeholder p { margin: 0; font-size: 12px; }
.action-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 8px; padding: 16px; }
.action-grid button {
padding: 11px 10px;
border: 1px solid var(--border);
border-radius: var(--r-md);
background: var(--surface-2);
color: var(--text-secondary);
font-size: 12px; font-weight: 450;
transition: all var(--duration) var(--ease);
}
.action-grid button:hover { border-color: var(--border-hover); background: var(--surface-3); color: var(--text-primary); transform: translateY(-1px); }
.control-note { margin: 0; padding: 0 16px 16px; color: var(--text-ghost); font-size: 12px; line-height: 1.6; }
.windows-card { margin-top: 14px; }
.window-row { display: flex; justify-content: space-between; gap: 15px; padding: 11px 16px; border-bottom: 1px solid var(--border); color: var(--text-secondary); font-size: 12.5px; transition: background var(--duration) var(--ease); }
.window-row:hover { background: rgba(255,255,255,.02); }
.window-row:last-child { border-bottom: 0; }
.window-row small { color: var(--text-ghost); font-family: var(--font-mono); font-size: 10.5px; }
/* Settings */
.settings-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 16px; max-width: 720px; margin-top: 28px; }
.settings-grid label { display: grid; gap: 8px; color: var(--text-tertiary); font-size: 12px; font-weight: 500; }
.settings-grid input {
width: 100%; padding: 10px 12px;
border: 1px solid var(--border-hover);
border-radius: var(--r-sm);
outline: 0;
background: var(--surface-1);
font-size: 13px;
transition: all var(--duration) var(--ease);
}
.settings-grid input:focus { border-color: var(--border-focus); box-shadow: 0 0 0 3px var(--accent-soft); }
.settings-grid input::placeholder { color: var(--text-ghost); }
.select-control { position: relative; width: 100%; }
.select-trigger {
width: 100%; height: 40px;
display: flex; align-items: center; justify-content: space-between;
padding: 0 12px;
border: 1px solid var(--border-hover);
border-radius: var(--r-sm);
background: var(--surface-1);
color: var(--text-secondary);
text-align: left;
transition: all var(--duration) var(--ease);
}
.select-trigger:hover, .select-control.open .select-trigger { border-color: var(--border-focus); }
.select-chevron { color: var(--text-tertiary); font-size: 14px; transform: translateY(-1px); }
.select-menu {
position: absolute; left: 0; right: 0; top: calc(100% + 6px); z-index: 20;
display: none; padding: 4px;
border: 1px solid var(--border-hover);
border-radius: var(--r-md);
background: var(--surface-2);
box-shadow: var(--shadow-lg);
}
.select-control.open .select-menu { display: grid; }
.select-menu button { padding: 9px 10px; border-radius: var(--r-xs); color: var(--text-secondary); text-align: left; font-size: 12.5px; transition: all 120ms var(--ease); }
.select-menu button:hover, .select-menu button.active { background: var(--accent-soft); color: var(--text-primary); }
.settings-note {
max-width: 720px; margin-top: 20px;
padding: 12px 14px;
border: 1px solid var(--border);
border-radius: var(--r-md);
background: var(--surface-1);
color: var(--text-tertiary); font-size: 12px; line-height: 1.6;
}
/* ═══ Toast ═══ */
.toast {
position: fixed; right: 20px; bottom: 20px; z-index: 99;
max-width: 380px; padding: 12px 16px;
border: 1px solid var(--border-hover);
border-radius: var(--r-md);
background: var(--surface-3);
color: var(--text-primary);
font-size: 12.5px; font-weight: 450;
box-shadow: var(--shadow-lg);
opacity: 0; pointer-events: none;
transform: translateY(8px) scale(.97);
transition: all .25s var(--ease-spring);
}
.toast.show { opacity: 1; transform: translateY(0) scale(1); }
/* ═══ Animations ═══ */
@keyframes fade-up {
from { opacity: 0; transform: translateY(6px); }
to { opacity: 1; transform: translateY(0); }
}
/* ═══ Responsive ═══ */
@media (max-width: 900px) {
.sidebar { width: 210px; min-width: 210px; }
.conversation { padding-left: 28px; padding-right: 28px; }
.computer-grid { grid-template-columns: 1fr; }
}