Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
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
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@

### Added

- #519:桌面端首启体验。冷启动先播品牌标启动画(约 0.9s),再过 0.4s 的共享元素过渡淡入登录 / 注册卡,总时长 ≈1.3s;已有会话则 ≈1.2s 直接进工作区,不显示登录卡;会话过期时落在登录卡上并给一条非阻塞行内提示,不用错误弹窗。登录 / 注册同卡切换,客户端校验错误行内展示;字段在过渡结束前不自动聚焦。`prefers-reduced-motion` 下只留淡入淡出并把启动画压到 240ms。窗口改为 `ready-to-show` 后再显示,消除首帧白闪。认证走 `auth-adapter` 窄接口,默认实现是明确标注的 stub(本地记录、不落密码、不做比对),真实按部署登录仍由 #395 负责,两者可独立合入。零新增运行时依赖。Refs #519。

- #469:可选 Laya 接力停止建议。仅在 `ROLEWEAVE_LAYA_ENABLED` 开启、已完成步骤得到正向建议且仍有未执行步骤时暂停;桌面端由 owner 显式选择“停止剩余步骤”或“继续接力”。30 秒超时、SSE 断连、无效或失败建议均继续原 mention order;Laya 只接收 `status`、`errorCode`、`hasOutput`,不接收输入、输出或 handoff 文本。已接受的消息与 spawn 清单保持不可变。Refs #422。

- #360:工作区 `work/` 领地布局。initialize/create 脚手架 `work/README.md`(已有工作区不加强制迁移)。hire 创建 `work/<positionId>/`,仅给 hire 岗位生成 SKILL.md Territory 段,并把新角色的 `memoryScope` 写成 `./work/<positionId>/`;项目负责人保持 `memoryScope: "/"`,SKILL 不声称不存在的 `work/<owner>/`。默认 `toolAllow` 只有 Read/Grep/Glob,不含 Write/Edit/Bash。hire 任何 staging 失败都回滚声明文件。employee.json 摘要密封机制不变。Refs #360。
Expand Down
44 changes: 1 addition & 43 deletions apps/desktop/renderer/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ import type {
} from "@roleweave/shared";
import { ChevronsRight, FolderKanban, FolderOpen, Inbox, MessagesSquare, MoreHorizontal, Network, PanelLeft, PencilLine, Plus, Settings, Undo2, X } from "lucide-react";
import { useThemeMode, useThemeProfile } from "./theme-toggle";
import { WindowControls } from "./WindowControls";
import { useTheme, ThemeProvider } from "./theme-context";
import { themeToAntdSeed } from "./theme-resolution";
import { DEFAULT_PRESET_ID } from "./theme-presets";
Expand Down Expand Up @@ -2557,49 +2558,6 @@ function normalizePositionForDisplay(position: PositionCardData, locale: OwbLoca
}, locale);
}

function WindowControls() {
const t = useT();
return (
<span className="owb-wintitle__controls">
<button
type="button"
className="owb-wctl owb-wctl--close"
aria-label={t("win.close")}
title={t("win.closeTitle")}
onClick={() => void window.owb.windowClose?.()}
>
<svg viewBox="0 0 10 10" aria-hidden="true">
<path d="M2.5 2.5l5 5M7.5 2.5l-5 5" />
</svg>
</button>
<button
type="button"
className="owb-wctl owb-wctl--min"
aria-label={t("win.minimize")}
title={t("win.minimizeTitle")}
onClick={() => void window.owb.windowMinimize?.()}
>
<svg viewBox="0 0 10 10" aria-hidden="true">
<path d="M2.2 5h5.6" />
</svg>
</button>
{/* 文案保持静态:WSLg 下 isMaximized() 不可信,不向用户谎报当前状态。 */}
<button
type="button"
className="owb-wctl owb-wctl--max"
aria-label={t("win.maximize")}
title={t("win.maximizeTitle")}
onClick={() => void window.owb.windowToggleMaximize?.()}
>
{/* #248 小 UI 单②:fullscreen 为绿底斜杠 ⃠ glyph。 */}
<svg viewBox="0 0 10 10" aria-hidden="true">
<path d="M2.8 7.2L7.2 2.8" />
</svg>
</button>
</span>
);
}

function Breadcrumbs({
workspace,
}: {
Expand Down
59 changes: 59 additions & 0 deletions apps/desktop/renderer/src/WindowControls.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
/**
* The three window-control buttons of the 40px custom title bar (#73). The
* window is frameless, so these are the only way to close, minimize, or
* maximize it — #94 and #248 exist because that affordance is not decorative.
*
* Extracted out of `App.tsx` for #519: the first-run splash and the
* login/register card also run inside the frameless window, but `<App />` is
* deliberately not mounted yet at that point. Keeping the buttons inside App
* would have left the user with a window they could neither drag nor close
* during the splash and while the login card is up. Sharing the component is
* the point — a second copy would drift out of the #94 / #248 guards in
* window-controls-affordance.test.cjs, which read the built bundle by class
* name and would keep passing while the copy they never see rots.
*/
import { useT } from "@roleweave/ui";

export function WindowControls() {
const t = useT();
return (
<span className="owb-wintitle__controls">
<button
type="button"
className="owb-wctl owb-wctl--close"
aria-label={t("win.close")}
title={t("win.closeTitle")}
onClick={() => void window.owb.windowClose?.()}
>
<svg viewBox="0 0 10 10" aria-hidden="true">
<path d="M2.5 2.5l5 5M7.5 2.5l-5 5" />
</svg>
</button>
<button
type="button"
className="owb-wctl owb-wctl--min"
aria-label={t("win.minimize")}
title={t("win.minimizeTitle")}
onClick={() => void window.owb.windowMinimize?.()}
>
<svg viewBox="0 0 10 10" aria-hidden="true">
<path d="M2.2 5h5.6" />
</svg>
</button>
{/* The label stays static on purpose: `isMaximized()` is not trustworthy
under WSLg, and the button must not report a state it cannot know. */}
<button
type="button"
className="owb-wctl owb-wctl--max"
aria-label={t("win.maximize")}
title={t("win.maximizeTitle")}
onClick={() => void window.owb.windowToggleMaximize?.()}
>
{/* #248: fullscreen is a green disc with a struck-through glyph. */}
<svg viewBox="0 0 10 10" aria-hidden="true">
<path d="M2.8 7.2L7.2 2.8" />
</svg>
</button>
</span>
);
}
43 changes: 42 additions & 1 deletion apps/desktop/renderer/src/main.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -20,11 +20,18 @@ import "./roleweave-data.css";
import "./workspace-polish.css";
import "./memory/memory-workspace.css";
import "./control-legibility.css";
import "./onboarding/onboarding.css";
import { App } from "./App";
import { initThemeMode } from "./theme-mode";
import { OnboardingGate } from "./onboarding/OnboardingGate";

export const PACKAGED_SMOKE_QUERY_KEY = "orgWorkbenchPackagedSmoke";
const PACKAGED_SMOKE_NONCE = /^[a-f0-9]{64}$/;
/** #519: the #127 full-app layout harness is declared by main as a query
* parameter so the renderer can skip the first-run gate for it. Keep in sync
* with LAYOUT_SMOKE_QUERY_KEY in apps/desktop/src/packaged-smoke.cjs —
* `apps/desktop/test/onboarding-motion.test.cjs` fails if the two drift. */
export const LAYOUT_SMOKE_QUERY_KEY = "orgWorkbenchLayoutSmoke";

type RendererLocation = Pick<Location, "protocol" | "search">;

Expand All @@ -37,6 +44,18 @@ export function isPackagedSmokeEntry(location: RendererLocation): boolean {
PACKAGED_SMOKE_NONCE.test(query.get(PACKAGED_SMOKE_QUERY_KEY) ?? "");
}

/** The layout harness renders the real app, and a fresh CI install has no saved
* session — without this the gate would sit on the login card and the columns
* being measured would never mount. */
export function isLayoutSmokeEntry(location: RendererLocation): boolean {
if (location.protocol !== "file:") return false;
const query = new URLSearchParams(location.search);
const keys = [...query.keys()];
return keys.length === 1 &&
keys[0] === LAYOUT_SMOKE_QUERY_KEY &&
query.get(LAYOUT_SMOKE_QUERY_KEY) === "1";
}

export function rendererEntryElement(
location: RendererLocation,
AppComponent: React.ComponentType = App,
Expand All @@ -61,6 +80,28 @@ export function rendererEntryElement(
const root = document.getElementById("root");
if (!root) throw new Error("renderer root element missing");

/**
* What actually gets mounted: the entry, wrapped in the #519 first-run gate.
*
* The gate sits **above** `<App />` on purpose — see OnboardingGate's header.
* `<App />` stays the workspace shell, so `App.test.tsx`'s 30+ direct renders
* keep meaning what they meant.
*
* The packaged smoke entry is deliberately exempt: it is a static marker the
* external staging harness scores, it must call no bridge method, and a timed
* splash in front of it would turn that harness into a timing test. The layout
* harness is exempt for a different reason — it measures the real workspace, so
* it has to reach it without a session on a fresh CI machine.
*/
export function rendererRootElement(
location: RendererLocation,
AppComponent: React.ComponentType = App,
): React.ReactElement {
const entry = rendererEntryElement(location, AppComponent);
if (isPackagedSmokeEntry(location) || isLayoutSmokeEntry(location)) return entry;
return <OnboardingGate>{entry}</OnboardingGate>;
}

// Theme seed before the first render (#94). index.html can only ship a static
// data-theme literal — its CSP is `script-src 'self'`, so the usual pre-paint
// inline script is not an option — and this runs before createRoot(), which is
Expand All @@ -72,5 +113,5 @@ initThemeMode();
// The antd ConfigProvider (ADR-0002 theme tokens) lives inside <App /> so the
// test harness renders the exact same configuration as production.
createRoot(root).render(
rendererEntryElement(window.location),
rendererRootElement(window.location),
);
43 changes: 43 additions & 0 deletions apps/desktop/renderer/src/onboarding/BrandMark.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
/**
* RoleWeave app mark, drawn as JSX so the splash and the login card can render
* it without a network/asset request and without a new build loader.
*
* Source of truth is `branding/roleweave/roleweave-icon.svg` (see that folder's
* README: the canvas is filled edge-to-edge so no platform adds a white square
* around it). This file carries **geometry only** — every `id`/`fill` of the
* asset became a class, and the brand palette lives in `onboarding.css` as
* custom properties.
*
* Both halves of that split are pinned against the asset by
* `apps/desktop/test/onboarding-brand-mark.test.cjs`: path data, viewBox and
* transform must match the SVG, and the CSS custom properties must carry the
* asset's own hex values. A copy that silently drifts from the shipped icon
* therefore fails the suite instead of shipping.
*/

export interface BrandMarkProps {
/** Size/shape come from CSS; this only adds the consumer's own hook. */
className?: string;
}

export function BrandMark({ className }: BrandMarkProps) {
return (
<svg
className={className === undefined ? "owb-brandmark" : `owb-brandmark ${className}`}
viewBox="0 0 512 512"
aria-hidden="true"
focusable="false"
>
<g className="owb-brandmark__artwork" transform="translate(32 32) scale(0.875)">
<rect className="owb-brandmark__tile" x="0" y="0" width="512" height="512" rx="126" />
<rect className="owb-brandmark__edge" x="8" y="8" width="496" height="496" rx="118" />
<path className="owb-brandmark__role" d="M116 406V106h104c78 0 126 38 126 102 0 46-23 78-66 94l79 104h-87l-66-91h-22v91h-68Zm68-236v89h31c40 0 61-15 61-43 0-30-21-46-61-46h-31Z" />
<path className="owb-brandmark__weave" d="M150 244h43l33 79 33-53 33 53 33-79h43l-53 145h-43l-30-54-30 54h-43Z" />
<path className="owb-brandmark__cut" d="m213 362 22-34 10 10-22 34Z" />
<path className="owb-brandmark__cut" d="m337 338 10-10 22 34-10 10Z" />
<circle className="owb-brandmark__node" cx="116" cy="106" r="9" />
<circle className="owb-brandmark__node" cx="412" cy="264" r="9" />
</g>
</svg>
);
}
Loading
Loading