Skip to content
Merged
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
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,10 @@

### 修复

- 对话模型选择器底部增加「模型管理」,跳到侧栏同一条「模型」路由(`/models`)。
- 组织页默认只显示本机 openXYOS 嵌入和顶部蓝色状态栏;「启用组织模块」「组织OS管理」在状态栏右下。互生长导入/导出、源码下载、循环时间线等收到「组织OS管理」。iframe 高度等于可见预览区,openXYOS 弹窗按 iframe 视口水平垂直居中。
- 嵌入本机 openXYOS 登录不再因 CORS 漏掉 `http://127.0.0.1:3780` 而返回笼统的「服务器内部错误」。启动环境合并 sidecar 自源;无演示种子时写入本地 demo 账号,保证隔空第一台电脑能登录。

- Windows 安装期 `start-sidecar.ps1` 不再把数据目录写到只读自动变量 `$home` / `$HOME`(赋值抛 `SessionStateUnauthorizedAccessException`,`$ErrorActionPreference = Continue` 时脚本继续跑,SQLite 会落到 `%USERPROFILE%\\org-os\\xiongyuan.db`)。改为 `$freeosHome`(默认 `%USERPROFILE%\\.freeos`)。
- 预构建 `openxyos-runtime.zip` 现在带上 `backend-dist/migrations/*.sql`。编译后的 `server.js` 用 `__dirname/migrations` 做 `initDatabase()`,缺文件会 ENOENT(`013_audit_bundle.sql`)并让 livez 永远起不来。安装期预配也会把已有 live 目录里的 `backend/migrations` 补到 `backend-dist/migrations`。
- Windows 安装期 `start-sidecar.ps1` 不再用 PowerShell 5.1 默认的 `UseShellExecute=true` 拉起 Node(那样不会继承 `$env:CORS_ORIGIN`,生产环境 `parseOrigins` 立刻抛错,进程退出,3780 无监听,预配空等 90s 后才以退出码 12 失败)。改为 `UseShellExecute=false` 并显式写入 `ProcessStartInfo.EnvironmentVariables`,把 Node stdout/stderr 记入 `%LOCALAPPDATA%\\FreeOS\\openxyos\\start.log`;进程在 livez 前退出则预配以退出码 10 失败并附上日志摘录。
Expand Down
35 changes: 35 additions & 0 deletions dashboard/src/layouts/PageShell.module.less
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,41 @@
border-radius: 8px;
}

.pathTabsRow {
display: flex;
flex-wrap: wrap;
align-items: center;
gap: 4px;
}

.pathTabsFlush {
flex-direction: column;
flex-wrap: nowrap;
align-items: stretch;
width: 100%;
padding: 0;
background: var(--fn-bg-layout);
border-radius: 0;

.pathTabsRow {
flex-wrap: nowrap;
width: 100%;
overflow-x: auto;
scrollbar-width: none;

&::-webkit-scrollbar {
display: none;
}
}
}

.pathTabsBelowTitle {
flex-shrink: 0;
width: 100%;
margin: 0 0 12px;
background: var(--fn-bg-layout);
}

.pathTabsCompact {
width: 100%;
}
Expand Down
51 changes: 51 additions & 0 deletions dashboard/src/layouts/PageShell.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -37,4 +37,55 @@ describe("PageShell path tabs", () => {
"true",
);
});

it("places a two-row flush strip below the title without a subtitle", () => {
const labels = [
"技能",
"通道",
"连接器/MCP",
"技能包",
"工具",
"插件",
"子智能体",
"MBTI",
"记忆",
"工作台",
"远程桌面",
"ACP",
"用户",
"存储",
"主机插件",
"安全",
"高级",
"智能体配置",
];
const { container } = render(
<PageShell
title="个性化 / 技能"
pathTabsPlacement="below-title"
pathTabs={{
value: "skills",
onChange: vi.fn(),
options: labels.map((label) => ({
value: label,
label,
icon: null,
})),
}}
>
<div>body</div>
</PageShell>,
);

expect(screen.queryByText("技能、通道、连接器、技能包")).toBeNull();
expect(container.querySelector("div[class*='titleActions']")).toBeNull();
const rows = container.querySelectorAll("[data-testid='path-tabs-row']");
expect(rows).toHaveLength(2);
expect(rows[0].querySelectorAll("[role='tab']")).toHaveLength(9);
expect(rows[1].querySelectorAll("[role='tab']")).toHaveLength(9);
expect(container.querySelector("div[class*='pathTabsFlush']")).toBeTruthy();
expect(
container.querySelector("div[class*='pathTabsBelowTitle']"),
).toBeTruthy();
});
});
104 changes: 81 additions & 23 deletions dashboard/src/layouts/PageShell.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -32,45 +32,90 @@ interface PageShellProps {
/**
* Path tabs shared by Workbench / Personalization:
* desktop → title-row actions (wraps); mobile → full-width bar above content.
* Use `pathTabsPlacement="below-title"` for a two-row strip under the title.
*/
pathTabs?: PathTabsConfig;
/**
* `title-row` (default): desktop chips sit in the title actions slot and wrap.
* `below-title`: two equal rows directly under the title, on the page background.
*/
pathTabsPlacement?: "title-row" | "below-title";
/** Render agent picker below the title row, outside the scrollable content card. */
agentScoped?: boolean;
/** When true, the content area does not scroll; children fill remaining height. */
fill?: boolean;
children: React.ReactNode;
}

function splitPathTabRows(options: PathTabOption[]): PathTabOption[][] {
if (options.length <= 1) return [options];
const mid = Math.ceil(options.length / 2);
return [options.slice(0, mid), options.slice(mid)];
}

function PathTabButton({
opt,
active,
compact,
onChange,
}: {
opt: PathTabOption;
active: boolean;
compact: boolean;
onChange: (value: string | number) => void;
}) {
return (
<button
type="button"
role="tab"
aria-selected={active}
className={`${styles.pathTab} ${active ? styles.pathTabActive : ""}`}
onClick={() => onChange(opt.value)}
>
{compact ? null : opt.icon}
<span className={styles.pathTabText}>{opt.label}</span>
</button>
);
}

function PathTabsBar({
pathTabs,
compact,
splitRows,
flush,
}: {
pathTabs: PathTabsConfig;
compact: boolean;
splitRows?: boolean;
flush?: boolean;
}) {
const rows = splitRows
? splitPathTabRows(pathTabs.options)
: [pathTabs.options];
return (
<div
className={`${styles.pathTabs} ${compact ? styles.pathTabsCompact : ""}`}
className={`${styles.pathTabs} ${compact ? styles.pathTabsCompact : ""} ${
flush ? styles.pathTabsFlush : ""
}`}
role="tablist"
>
{pathTabs.options.map((opt) => {
const active = opt.value === pathTabs.value;
return (
<button
key={opt.value}
type="button"
role="tab"
aria-selected={active}
className={`${styles.pathTab} ${
active ? styles.pathTabActive : ""
}`}
onClick={() => pathTabs.onChange(opt.value)}
>
{compact ? null : opt.icon}
<span className={styles.pathTabText}>{opt.label}</span>
</button>
);
})}
{rows.map((row, index) => (
<div
key={index}
className={styles.pathTabsRow}
data-testid="path-tabs-row"
>
{row.map((opt) => (
<PathTabButton
key={opt.value}
opt={opt}
active={opt.value === pathTabs.value}
compact={compact}
onChange={pathTabs.onChange}
/>
))}
</div>
))}
</div>
);
}
Expand All @@ -87,6 +132,7 @@ function PathTabsBar({
* - Only the content area scrolls internally
* - `actions` slot: right-aligned in the title row
* - `pathTabs`: desktop in title row (flex-wrap); mobile full-width wrapping bar
* - `pathTabsPlacement="below-title"`: two rows under the title, page-bg strip
*
* Tabbed helpers: `PageShell.FillTabs` (Ant Tabs) and `PageShell.Tabbed`
* (custom tab bar) pin the tab chrome and scroll only the body on desktop.
Expand All @@ -96,6 +142,7 @@ function PageShell({
subtitle,
actions,
pathTabs,
pathTabsPlacement = "title-row",
agentScoped,
fill,
children,
Expand All @@ -104,11 +151,16 @@ function PageShell({
const outerPad = isMobile ? 12 : 32;
const outerPadTop = isMobile ? 12 : 24;
const contentPad = isMobile ? 12 : 24;
const tabsBelowTitle = Boolean(
pathTabs && pathTabsPlacement === "below-title",
);
const tabsInTitleRow = Boolean(pathTabs && !tabsBelowTitle && !isMobile);
const tabsInContent = Boolean(pathTabs && !tabsBelowTitle && isMobile);
/** Fill layout, or mobile path-tabs that must stay pinned above the body. */
const pinBody = Boolean(fill || (isMobile && pathTabs));
const pinBody = Boolean(fill || tabsInContent);

const titleActions =
!isMobile && pathTabs ? (
tabsInTitleRow && pathTabs ? (
<>
<PathTabsBar pathTabs={pathTabs} compact={false} />
{actions}
Expand All @@ -133,7 +185,7 @@ function PageShell({
<div
className={styles.titleRow}
style={{
marginBottom: agentScoped ? 12 : 24,
marginBottom: tabsBelowTitle ? 8 : agentScoped ? 12 : 24,
paddingRight: titleRowEndPadding(outerPad),
}}
>
Expand All @@ -152,6 +204,12 @@ function PageShell({
) : null}
</div>

{tabsBelowTitle && pathTabs ? (
<div className={styles.pathTabsBelowTitle}>
<PathTabsBar pathTabs={pathTabs} compact={isMobile} splitRows flush />
</div>
) : null}

{agentScoped && (
<div className={styles.agentBar}>
<AgentSelector />
Expand All @@ -178,7 +236,7 @@ function PageShell({
flexDirection: pinBody ? "column" : undefined,
}}
>
{isMobile && pathTabs && (
{tabsInContent && pathTabs && (
<div className={styles.pathTabsMobile}>
<PathTabsBar pathTabs={pathTabs} compact />
</div>
Expand Down
6 changes: 6 additions & 0 deletions dashboard/src/layouts/sidebarNav.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,12 @@ describe("sidebarNav", () => {
it("labels organization without the OS suffix", () => {
expect(zh.nav.organization).toBe("组织");
expect(en.nav.organization).toBe("Organization");
expect(zh.organization.enableModule).toBe("启用组织模块");
expect(zh.organization.manageOs).toBe("组织OS管理");
expect(en.organization.enableModule).toBe("Enable organization module");
expect(en.organization.manageOs).toBe("Organization OS");
expect(zh.chat.modelPickerManage).toBe("模型管理");
expect(en.chat.modelPickerManage).toBe("Model management");
});

it("locks the employee / colleague / expert glossary", () => {
Expand Down
7 changes: 6 additions & 1 deletion dashboard/src/locales/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,11 @@
"heroTitle": "Organization",
"heroStory": "FreeOS does the work. openXYOS owns organization and governance. They grow each other.",
"toggleLabel": "Enable organization module",
"enableModule": "Enable organization module",
"manageOs": "Organization OS",
"moduleDrawerTitle": "Enable organization module",
"manageDrawerTitle": "Organization OS",
"statusBarTitle": "Organization",
"on": "Enabled",
"off": "Disabled",
"sidecarUp": "Local openXYOS connected",
Expand Down Expand Up @@ -1401,7 +1406,7 @@
"subagentPickerSearch": "Search subagents",
"subagentPickerEmpty": "No installed subagents",
"subagentPickerManage": "Manage subagents",
"modelPickerManage": "Manage models",
"modelPickerManage": "Model management",
"sessionWorking": "Working",
"composerContextLabel": "Message context",
"contextWindow": {
Expand Down
7 changes: 6 additions & 1 deletion dashboard/src/locales/zh.json
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,11 @@
"heroTitle": "组织",
"heroStory": "FreeOS 负责干活,openXYOS 负责组织与治理,两者互相生长。",
"toggleLabel": "启用组织模块",
"enableModule": "启用组织模块",
"manageOs": "组织OS管理",
"moduleDrawerTitle": "启用组织模块",
"manageDrawerTitle": "组织OS管理",
"statusBarTitle": "组织",
"on": "已启用",
"off": "已关闭",
"sidecarUp": "本机 openXYOS 已连接",
Expand Down Expand Up @@ -1401,7 +1406,7 @@
"subagentPickerSearch": "搜索子智能体",
"subagentPickerEmpty": "暂无已安装的子智能体",
"subagentPickerManage": "管理子智能体",
"modelPickerManage": "管理模型",
"modelPickerManage": "模型管理",
"sessionWorking": "正在回复",
"composerContextLabel": "消息附加上下文",
"contextWindow": {
Expand Down
2 changes: 1 addition & 1 deletion dashboard/src/pages/Agent/Personalization/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -153,10 +153,10 @@ export default function PersonalizationPage() {
return (
<PageShell
title={pageTitle}
subtitle={t("personalization.description")}
agentScoped
fill={!isMobile}
pathTabs={pathTabs}
pathTabsPlacement="below-title"
>
<div className={styles.panels}>
{isMounted("skills") && (
Expand Down
Loading
Loading