diff --git a/emrg/gui/renderer/css/layout.css b/emrg/gui/renderer/css/layout.css index 6346ad37..6db46a77 100644 --- a/emrg/gui/renderer/css/layout.css +++ b/emrg/gui/renderer/css/layout.css @@ -195,6 +195,29 @@ animation: banner-in var(--dur-med) var(--ease); } +/* ── GitHub 连接横幅(Windows GCM rant Stage 2) ────── */ +#github-banner { + position: absolute; + top: var(--sp-3); + left: 50%; + transform: translateX(-50%); + display: flex; + align-items: center; + gap: var(--sp-2); + background: var(--amber-soft); + color: var(--amber); + border: 1px solid var(--border); + border-radius: 999px; + padding: 6px var(--sp-3) 6px var(--sp-4); + font-size: var(--fs-secondary); + box-shadow: var(--shadow-md); + z-index: 20; + animation: banner-in var(--dur-med) var(--ease); + white-space: nowrap; + max-width: 90%; +} +#github-banner .btn { padding: 4px 12px; min-height: 0; font-size: var(--fs-secondary); } + /* ── 侧边栏折叠(⌘B / 窄屏) ────────────── */ body.sidebar-collapsed #sidebar { display: none; diff --git a/emrg/gui/renderer/index.html b/emrg/gui/renderer/index.html index a19c6b83..e69ff749 100644 --- a/emrg/gui/renderer/index.html +++ b/emrg/gui/renderer/index.html @@ -37,6 +37,13 @@ + + +
diff --git a/emrg/gui/renderer/js/app.js b/emrg/gui/renderer/js/app.js index 4adc7b3f..bfbf3b6a 100644 --- a/emrg/gui/renderer/js/app.js +++ b/emrg/gui/renderer/js/app.js @@ -707,6 +707,8 @@ const App = (() => { const prev = state.lastKnownEvolutionCount; state.lastKnownEvolutionCount = state.evolutionCount; if (prev == null || state.evolutionCount <= prev) return; // 首次连接/无增长不提示 + // 演化刚完成 = 演化真正需要 GitHub 的时刻(Windows GCM rant Stage 2) + maybeShowGithubBanner(); // 频率控制:一天最多 1 次(localStorage 可能不可用 → 静默跳过) const today = new Date().toISOString().slice(0, 10); try { @@ -733,6 +735,50 @@ const App = (() => { if (star) star.classList.remove("pulse"); } + // ── GitHub 连接横幅(Windows GCM rant Stage 2) ──── + // 触发时机:演化计数增长(演化刚完成、需要 GitHub 推 PR)且未认证时。 + // 本地聊天不依赖 GitHub → 启动时不打扰。横幅可关闭;关闭后本会话不再弹。 + let _githubBannerDismissed = false; + + function showGithubBanner() { + const b = $("github-banner"); + if (!b) return; + const msg = $("github-banner-msg"); + if (msg) msg.textContent = _t("settings.githubBannerMsg"); + b.classList.remove("hidden"); + } + + function hideGithubBanner() { + const b = $("github-banner"); + if (b) b.classList.add("hidden"); + } + + async function maybeShowGithubBanner() { + if (_githubBannerDismissed) return; + try { + const s = await window.emrg.githubStatus(); + if (s && s.authenticated) return; // 已连接 → 无需提示 + showGithubBanner(); + } catch { /* githubStatus 不可用(daemon 忙/未合入)→ 静默跳过 */ } + } + + function initGithubBanner() { + const connect = $("github-banner-connect"); + if (connect) { + connect.addEventListener("click", () => { + hideGithubBanner(); + Dialogs.showSettings(); // 设置页 GitHub 连接区(Stage 2a) + }); + } + const dismiss = $("github-banner-dismiss"); + if (dismiss) { + dismiss.addEventListener("click", () => { + _githubBannerDismissed = true; + hideGithubBanner(); + }); + } + } + function initEvolutionToast() { const see = $("evolution-toast-see"); if (see) { @@ -1186,6 +1232,7 @@ const App = (() => { Dialogs.initRenameDialog(); Dialogs.initGithubSection(); // Windows GCM rant Stage 2:设置页 GitHub 连接 Dialogs.initDeviceDialog(); // Windows GCM rant Stage 2b:device flow 对话框 + initGithubBanner(); // Windows GCM rant Stage 2:演化需 GitHub 但未认证时的连接横幅 initModelSwitcher(); initModeSwitcher(); // WorkBuddy P2:Ask/Auto 工作模式 ResultPanel.init(); // WorkBuddy P1:结果面板(⌘\ 折叠 + 窄屏自动隐藏) diff --git a/emrg/gui/renderer/js/i18n.js b/emrg/gui/renderer/js/i18n.js index 68803af5..a6a8f50e 100644 --- a/emrg/gui/renderer/js/i18n.js +++ b/emrg/gui/renderer/js/i18n.js @@ -102,6 +102,10 @@ const I18N = (() => { "settings.githubDeviceCancel": "取消", "settings.githubDeviceWait": "等待浏览器中确认…", "settings.githubDeviceFailed": "GitHub 连接失败:{msg}", + // Windows GCM rant Stage 2:连接横幅 + "settings.githubBannerMsg": "启用自进化需连接 GitHub →", + "settings.githubBannerConnect": "去连接", + "settings.githubBannerDismiss": "关闭", // 首启引导 "welcome.title": "欢迎使用 EMRG", @@ -388,6 +392,10 @@ const I18N = (() => { "settings.githubDeviceCancel": "Cancel", "settings.githubDeviceWait": "Waiting for confirmation in browser…", "settings.githubDeviceFailed": "GitHub connect failed: {msg}", + // Windows GCM rant Stage 2: connect banner + "settings.githubBannerMsg": "Connect GitHub to enable self-evolution →", + "settings.githubBannerConnect": "Connect", + "settings.githubBannerDismiss": "Dismiss", // Welcome / onboarding "welcome.title": "Welcome to EMRG", diff --git a/emrg/gui/test/renderer.smoke.test.js b/emrg/gui/test/renderer.smoke.test.js index a1c2404d..9f37229d 100644 --- a/emrg/gui/test/renderer.smoke.test.js +++ b/emrg/gui/test/renderer.smoke.test.js @@ -79,6 +79,7 @@ const ELEMENT_IDS = [ "tasks-dialog", "tasks-list", "tasks-close", "result-panel", "result-list", "result-toggle", "growth-card", "growth-count", "about-recent", + "github-banner", "github-banner-msg", "github-banner-connect", "github-banner-dismiss", ]; /** 构造浏览器沙箱(win 即全局对象) */ @@ -297,9 +298,55 @@ test("多模型管理:modelDetails 加载渲染 + saveSettings 传 models 数 assert.strictEqual(saved.theme, "system"); assert.ok(Array.isArray(saved.models)); assert.strictEqual(saved.models.length, 2, "models 数组含默认 + gpt-4o"); + const gpt = saved.models.find((m) => m.name === "gpt-4o"); assert.strictEqual(gpt.vision, true); }); +test("GCM rant Stage 2:演化增长 + 未认证 → GitHub 连接横幅出现(正反两态)", async () => { + // 正态:未认证 + 演化计数增长 → 横幅出现(先加 hidden 模拟 index.html 初始态) + const { ctx } = makeSandbox({ + githubStatus: async () => ({ authenticated: false, user: null }), + }); + await tick(); + await vm.runInContext(`(async function() { + document.getElementById("github-banner").classList.add("hidden"); + App.state.evolutionCount = 42; + App.state.lastKnownEvolutionCount = 40; + App.maybeShowEvolutionToast(); + })()`, ctx); + await tick(); // 等待 githubStatus promise 解析 + const visible = vm.runInContext( + '!document.getElementById("github-banner").classList.contains("hidden")', + ctx + ); + assert.strictEqual(visible, true, "未认证 + 演化增长 → 横幅应出现"); + + // 负态:已认证 → 横幅保持隐藏 + const { ctx: ctx2 } = makeSandbox({ + githubStatus: async () => ({ authenticated: true, user: "octocat" }), + }); + await tick(); + await vm.runInContext(`(async function() { + document.getElementById("github-banner").classList.add("hidden"); + App.state.evolutionCount = 42; + App.state.lastKnownEvolutionCount = 40; + App.maybeShowEvolutionToast(); + })()`, ctx2); + await tick(); + const hidden = vm.runInContext( + 'document.getElementById("github-banner").classList.contains("hidden")', + ctx2 + ); + assert.strictEqual(hidden, true, "已认证 → 横幅应保持隐藏"); + + // 源码断言:连接/关闭按钮绑定 + 演化增长钩子 + const src = fs.readFileSync(path.join(RENDERER_JS, "app.js"), "utf8"); + assert.ok(src.includes("Dialogs.showSettings()"), "横幅[去连接]应打开设置页 GitHub 区"); + assert.ok(src.includes("_githubBannerDismissed = true"), "关闭横幅应本会话不再弹"); + const toastIdx = src.indexOf("function maybeShowEvolutionToast()"); + const toastBlock = src.slice(toastIdx, toastIdx + 800); + assert.ok(toastBlock.includes("maybeShowGithubBanner()"), "演化增长应触发 GitHub 横幅检查"); +}); test("右键菜单:重命名对话框 → renameSession 调用(设计 §3.2)", async () => { let renamed = null;