Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
14 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 6 additions & 3 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,12 @@

### Changed

- 聊天面板(PR Agent)内部重构:2578 行的单文件 `ChatPane.tsx` 按「容器 / 领域组件 / hooks / 工具方法」分层拆分到 `components/chat/`,状态与生命周期、业务动作、时间线归并各自成 hook,展示组件与工具方法独立成文件。对外接口与界面行为保持不变,仅改善可维护性;token 用量 ↑/↓ 的绿红色值一并从内联样式收进设计令牌与样式类。
- 首启向导内部重构:`OnboardingWizard.tsx` 的四个步骤组件(欢迎 / 平台 / LLM / 完成)拆分到 `steps/` 各自成文件,容器只留向导骨架(步骤指示 + 切换 + 导航)。对外接口与界面行为不变。
- `components/` 目录按职责重组:扁平堆叠的组件归入三类——`common/`(基础公共 UI)、`layout/`(应用骨架)、`features/`(业务领域:pr / diff / comments / drafts / settings / chat / onboarding),顶层只剩这三个桶。纯文件位置调整 + import 路径改写,无逻辑 / 界面变更。
- **前端代码结构重构(可维护性)**:纯结构调整,对外接口与界面 / 交互行为均不变。重点:
- 组件按 `common/`(基础 UI)/ `layout/`(应用骨架)/ `features/`(业务领域)三层归类;样式 `styles/` 同构归并
- 超大组件按「容器 + 领域组件 + hooks + 工具方法」分层拆分:ChatPane、SettingsModal、MainPane、StatusBar
- 业务逻辑下沉所属领域:PR 列表 / 详情 / 工作区归 `features/pr`;App 主入口退化为组合根,启动 / 布局 / 更新提示等拆成 app 级 hooks
- 抽出通用基础组件 `Modal` / `StatusChip`;状态栏 chip 按归属下沉到各 feature
- 其它整理:目录归并、工具方法去重、main 进程 splash 拆分

### Fixed

Expand Down
70 changes: 1 addition & 69 deletions apps/desktop/src/main/index.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
import { app, BrowserWindow, Menu, nativeTheme, shell } from 'electron';
import path from 'node:path';
import { readFileSync } from 'node:fs';
import { execSync } from 'node:child_process';
import { fileURLToPath } from 'node:url';
import type { Logger } from 'pino';
Expand All @@ -14,6 +13,7 @@ import { RepoMirrorManager } from '@meebox/repo-mirror';
import type { PlatformAdapter, PlatformUser, PrAgentStatus } from '@meebox/shared';
import { JsonFileStateStore } from '@meebox/state-store';
import { buildAdapters, type ConnectionRuntime } from './adapters.js';
import { createSplash } from './splash.js';
import { initMainI18n } from './i18n/index.js';
import { registerIpcHandlers } from './ipc.js';
import { buildProxyEnv } from './utils/proxy.js';
Expand Down Expand Up @@ -438,31 +438,6 @@ app.on('before-quit', (event) => {
setTimeout(() => app.quit(), 800);
});

/**
* 读取品牌 logo 并转成 base64 data URI,内联进 splash data URL(splash 是独立 data URL
* 文档,无法走 file:// 相对路径引用资源,故必须内联)。两路探测:
* - 打包态:`<resources>/icon.png`(electron-builder extraResources copy)
* - dev:仓库 `assets/icons/icon.png`
* 两路都读不到(如 LFS 未拉取)则返回 null,splash 优雅回退为纯 spinner。
*/
function resolveSplashLogo(): string | null {
const candidates = [
path.join(process.resourcesPath, 'icon.png'),
path.join(app.getAppPath(), '../../assets/icons/icon.png'),
];
for (const p of candidates) {
try {
const buf = readFileSync(p);
// LFS 指针文件不是合法 PNG(无 \x89PNG magic)→ 跳过,避免 splash 显示裂图
if (buf.length < 8 || buf[0] !== 0x89 || buf[1] !== 0x50) continue;
return `data:image/png;base64,${buf.toString('base64')}`;
} catch {
/* 试下一个候选 */
}
}
return null;
}

/**
* 版本更新检测(config.update.check_enabled 开启时)。由 poller tick 顺带发起,内部用
* lastUpdateCheckMs 时间戳门控成「至多每小时一次」——复用既有 poll 周期,不引入额外定时器。
Expand Down Expand Up @@ -497,49 +472,6 @@ async function runUpdateCheckIfDue(): Promise<void> {
}
}

/**
* 启动闪屏:独立的无边框轻量窗口,加载内联 data URL(品牌 logo + 纯 CSS spinner),
* 几十 ms 即可呈现,遮住主窗口首帧前的渲染层加载空窗。主窗口 ready-to-show 时关闭。
* logo 经 base64 内联(见 resolveSplashLogo),data URL 自包含、dev/打包行为一致。
*/
function createSplash(): BrowserWindow {
const splash = new BrowserWindow({
width: 280,
height: 240,
frame: false,
resizable: false,
movable: false,
center: true,
show: false,
alwaysOnTop: true,
skipTaskbar: true,
backgroundColor: '#1e1e1e',
webPreferences: { contextIsolation: true, nodeIntegration: false, sandbox: true },
});
const logo = resolveSplashLogo();
const logoEl = logo ? `<img class="logo" src="${logo}" alt="" />` : '';
const html = `<!doctype html><html><head><meta charset="utf-8"><style>
html,body{margin:0;height:100%;}
body{background:#1e1e1e;color:#fff;-webkit-user-select:none;user-select:none;
font-family:system-ui,'Segoe UI',Roboto,sans-serif;
display:flex;flex-direction:column;align-items:center;justify-content:center;gap:14px;}
.logo{width:72px;height:72px;border-radius:16px;}
.name{font-size:17px;font-weight:600;letter-spacing:.3px;}
.row{display:flex;align-items:center;gap:8px;color:#9d9d9d;font-size:12px;}
.ring{width:14px;height:14px;border-radius:50%;border:2px solid rgba(255,255,255,.16);
border-top-color:#0e639c;animation:spin .8s linear infinite;}
@keyframes spin{to{transform:rotate(360deg);}}
</style></head><body>
${logoEl}<div class="name">Code Meeseeks</div>
<div class="row"><div class="ring"></div><span>启动中…</span></div>
</body></html>`;
void splash.loadURL('data:text/html;charset=utf-8,' + encodeURIComponent(html));
splash.once('ready-to-show', () => {
if (!splash.isDestroyed()) splash.show();
});
return splash;
}

function createWindow(splash?: BrowserWindow): void {
// 最小尺寸保证核心三栏 (sidebar 240 + file-tree 180 + diff 内容)
// 在 chat-pane 折叠态下仍可用;高度兜住 pr-header + tabs + diff + statusbar
Expand Down
71 changes: 71 additions & 0 deletions apps/desktop/src/main/splash.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
import { app, BrowserWindow } from 'electron';
import path from 'node:path';
import { readFileSync } from 'node:fs';

/**
* 读取品牌 logo 并转成 base64 data URI,内联进 splash data URL(splash 是独立 data URL
* 文档,无法走 file:// 相对路径引用资源,故必须内联)。两路探测:
* - 打包态:`<resources>/icon.png`(electron-builder extraResources copy)
* - dev:仓库 `assets/icons/icon.png`
* 两路都读不到(如 LFS 未拉取)则返回 null,splash 优雅回退为纯 spinner。
*/
function resolveSplashLogo(): string | null {
const candidates = [
path.join(process.resourcesPath, 'icon.png'),
path.join(app.getAppPath(), '../../assets/icons/icon.png'),
];
for (const p of candidates) {
try {
const buf = readFileSync(p);
// LFS 指针文件不是合法 PNG(无 \x89PNG magic)→ 跳过,避免 splash 显示裂图
if (buf.length < 8 || buf[0] !== 0x89 || buf[1] !== 0x50) continue;
return `data:image/png;base64,${buf.toString('base64')}`;
} catch {
/* 试下一个候选 */
}
}
return null;
}

/**
* 启动闪屏:独立的无边框轻量窗口,加载内联 data URL(品牌 logo + 纯 CSS spinner),
* 几十 ms 即可呈现,遮住主窗口首帧前的渲染层加载空窗。主窗口 ready-to-show 时关闭。
* logo 经 base64 内联(见 resolveSplashLogo),data URL 自包含、dev/打包行为一致。
*/
export function createSplash(): BrowserWindow {
const splash = new BrowserWindow({
width: 280,
height: 240,
frame: false,
resizable: false,
movable: false,
center: true,
show: false,
alwaysOnTop: true,
skipTaskbar: true,
backgroundColor: '#1e1e1e',
webPreferences: { contextIsolation: true, nodeIntegration: false, sandbox: true },
});
const logo = resolveSplashLogo();
const logoEl = logo ? `<img class="logo" src="${logo}" alt="" />` : '';
const html = `<!doctype html><html><head><meta charset="utf-8"><style>
html,body{margin:0;height:100%;}
body{background:#1e1e1e;color:#fff;-webkit-user-select:none;user-select:none;
font-family:system-ui,'Segoe UI',Roboto,sans-serif;
display:flex;flex-direction:column;align-items:center;justify-content:center;gap:14px;}
.logo{width:72px;height:72px;border-radius:16px;}
.name{font-size:17px;font-weight:600;letter-spacing:.3px;}
.row{display:flex;align-items:center;gap:8px;color:#9d9d9d;font-size:12px;}
.ring{width:14px;height:14px;border-radius:50%;border:2px solid rgba(255,255,255,.16);
border-top-color:#0e639c;animation:spin .8s linear infinite;}
@keyframes spin{to{transform:rotate(360deg);}}
</style></head><body>
${logoEl}<div class="name">Code Meeseeks</div>
<div class="row"><div class="ring"></div><span>启动中…</span></div>
</body></html>`;
void splash.loadURL('data:text/html;charset=utf-8,' + encodeURIComponent(html));
splash.once('ready-to-show', () => {
if (!splash.isDestroyed()) splash.show();
});
return splash;
}
28 changes: 14 additions & 14 deletions apps/desktop/src/renderer/src/App.scss
Original file line number Diff line number Diff line change
Expand Up @@ -15,18 +15,18 @@
// drafts-panel "草稿" tab 列表页 (.drafts-panel / -filter / -item / -anchor)
// modal SettingsModal + LlmEditorModal 子模态 + LLM profile 列表
@use './styles/base';
@use './styles/titlebar';
@use './styles/statusbar';
@use './styles/sidebar';
@use './styles/main-pane';
@use './styles/file-tree';
@use './styles/diff';
@use './styles/diff-search';
@use './styles/comment-zone';
@use './styles/draft-zone';
@use './styles/layout/titlebar';
@use './styles/layout/statusbar';
@use './styles/layout/sidebar';
@use './styles/layout/main-pane';
@use './styles/features/file-tree';
@use './styles/features/diff';
@use './styles/features/diff-search';
@use './styles/features/comment-zone';
@use './styles/features/draft-zone';
@use './styles/markdown';
@use './styles/pr-info';
@use './styles/chat-pane';
@use './styles/drafts-panel';
@use './styles/modal';
@use './styles/onboarding';
@use './styles/features/pr-info';
@use './styles/features/chat-pane';
@use './styles/features/drafts-panel';
@use './styles/common/modal';
@use './styles/features/onboarding';
Loading
Loading