Add read-only runtime inventory settings (#130)

* add gated codex goals support

* sync README SDK versions

* bump claude agent sdk

* add codex goals settings toggle

* reuse status dashboard message and simplify settings actions

* refine settings page UX

* fix settings nav hash routing

* add dashboard UX verification

* refine dashboard settings and inbox UX

* remove inbox top-level navigation

* remove dashboard health top-level navigation

* fix: allow runtime image attachment paths

* feat: configure attachment allowlist dirs

* fix: clean duplicate dashboard status messages

* fix: poll dashboard duplicate cleanup between status updates

* fix: delete dashboard duplicates on create

* Refine settings IA with tabbed sections

* Add read-only runtime inventory settings
This commit is contained in:
Eyejoker
2026-05-04 02:13:42 +09:00
committed by GitHub
parent c7d4bf82d7
commit 2da6052eff
11 changed files with 890 additions and 1 deletions

View File

@@ -0,0 +1,130 @@
.runtime-inventory {
display: grid;
gap: 18px;
}
.runtime-summary-card,
.runtime-agent-card,
.runtime-skill-card {
border: 1px solid rgba(148, 163, 184, 0.18);
border-radius: 20px;
background: rgba(15, 23, 42, 0.48);
box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.04);
}
.runtime-summary-card {
display: grid;
grid-template-columns: repeat(2, minmax(0, 1fr));
gap: 14px;
padding: 18px;
}
.runtime-summary-card > div {
display: grid;
gap: 6px;
min-width: 0;
}
.runtime-summary-card strong {
color: var(--text);
font-size: 1rem;
}
.runtime-summary-card code,
.runtime-path-row code,
.runtime-skill-card code {
color: var(--muted);
font-size: 0.78rem;
overflow-wrap: anywhere;
}
.runtime-agent-card {
padding: 18px;
}
.runtime-card-head,
.runtime-skill-card header {
display: flex;
align-items: flex-start;
justify-content: space-between;
gap: 12px;
}
.runtime-card-head h4 {
margin: 0;
}
.runtime-path-list,
.runtime-skill-list {
display: grid;
gap: 10px;
margin: 14px 0 0;
padding: 0;
list-style: none;
}
.runtime-path-row {
display: flex;
align-items: flex-start;
justify-content: space-between;
gap: 12px;
border-radius: 14px;
background: rgba(15, 23, 42, 0.38);
padding: 12px;
}
.runtime-path-row > span {
display: grid;
gap: 4px;
min-width: 0;
}
.runtime-skill-grid {
display: grid;
grid-template-columns: repeat(2, minmax(0, 1fr));
gap: 12px;
margin-top: 14px;
}
.runtime-skill-card {
padding: 14px;
}
.runtime-skill-card header > div {
display: grid;
gap: 4px;
min-width: 0;
}
.runtime-skill-list li {
display: grid;
gap: 3px;
border-top: 1px solid rgba(148, 163, 184, 0.12);
padding-top: 9px;
}
.runtime-skill-list li:first-child {
border-top: 0;
padding-top: 0;
}
.runtime-skill-list span {
color: var(--muted);
font-size: 0.82rem;
line-height: 1.4;
}
@media (max-width: 980px) {
.runtime-summary-card,
.runtime-skill-grid {
grid-template-columns: 1fr;
}
}
@media (max-width: 720px) {
.runtime-card-head,
.runtime-skill-card header,
.runtime-path-row {
flex-direction: column;
}
}

View File

@@ -0,0 +1,174 @@
import { useEffect, useState } from 'react';
import {
fetchRuntimeInventory,
type RuntimeAgentInventory,
type RuntimeInventorySnapshot,
type RuntimePathSnapshot,
type RuntimeSkillDirSnapshot,
} from './api';
import { SettingsSectionHeading } from './SettingsPanelChrome';
import './RuntimeInventorySettings.css';
function ExistsBadge({ exists }: { exists: boolean }) {
return (
<span className={`settings-account-badge ${exists ? 'is-active' : ''}`}>
{exists ? '감지됨' : '없음'}
</span>
);
}
function PathRow({ item }: { item: RuntimePathSnapshot }) {
return (
<li className="runtime-path-row">
<span>
<strong>{item.label}</strong>
<code>{item.path}</code>
</span>
<ExistsBadge exists={item.exists} />
</li>
);
}
function SkillDirCard({ dir }: { dir: RuntimeSkillDirSnapshot }) {
const preview = dir.skills.slice(0, 6);
return (
<article className="runtime-skill-card">
<header>
<div>
<strong>{dir.label}</strong>
<code>{dir.path}</code>
</div>
<span className="settings-account-badge is-active">
{dir.count} skills
</span>
</header>
{preview.length === 0 ? (
<p className="settings-hint"> SKILL.md </p>
) : (
<ul className="runtime-skill-list">
{preview.map((skill) => (
<li key={skill.path}>
<strong>{skill.name}</strong>
{skill.description ? <span>{skill.description}</span> : null}
</li>
))}
</ul>
)}
{dir.count > preview.length ? (
<small className="settings-hint">
{dir.count - preview.length}
</small>
) : null}
</article>
);
}
function AgentInventoryCard({
title,
inventory,
}: {
title: string;
inventory: RuntimeAgentInventory;
}) {
return (
<article className="runtime-agent-card">
<header className="runtime-card-head">
<h4>{title}</h4>
<span className="settings-account-badge is-active">
MCP {inventory.mcp.ejclawConfigured ? '연결' : '미감지'}
</span>
</header>
<ul className="runtime-path-list">
{inventory.configFiles.map((item) => (
<PathRow item={item} key={item.path} />
))}
<PathRow item={inventory.mcp.configPath} />
</ul>
<p className="settings-hint">
MCP servers {inventory.mcp.serverCount} · EJClaw section{' '}
{inventory.mcp.ejclawConfigured ? '있음' : '없음'}
</p>
<div className="runtime-skill-grid">
{inventory.skillDirs.map((dir) => (
<SkillDirCard dir={dir} key={dir.path} />
))}
</div>
</article>
);
}
export function RuntimeInventorySettings() {
const [snapshot, setSnapshot] = useState<RuntimeInventorySnapshot | null>(
null,
);
const [error, setError] = useState<string | null>(null);
useEffect(() => {
let cancelled = false;
fetchRuntimeInventory()
.then((value) => {
if (cancelled) return;
setSnapshot(value);
setError(null);
})
.catch((err) => {
if (cancelled) return;
setError(err instanceof Error ? err.message : String(err));
});
return () => {
cancelled = true;
};
}, []);
return (
<section
aria-labelledby="settings-runtime-tab"
className="settings-section"
id="settings-runtime"
role="tabpanel"
>
<SettingsSectionHeading
detail="Runtime inventory"
title="런타임"
description="Codex/Claude Code 설정, 스킬, MCP 연결 상태를 읽기 전용으로 확인합니다."
/>
{error ? <p className="settings-error">{error}</p> : null}
{!snapshot ? (
<p className="settings-hint"> </p>
) : (
<div className="runtime-inventory">
<section className="runtime-summary-card">
<div>
<span className="settings-kicker">Current service</span>
<strong>{snapshot.service.id}</strong>
<small>
{snapshot.service.agentType} · session{' '}
{snapshot.service.sessionScope}
</small>
</div>
<div>
<span className="settings-kicker">Project</span>
<code>{snapshot.projectRoot}</code>
<small>data {snapshot.dataDir}</small>
</div>
</section>
<AgentInventoryCard title="Codex" inventory={snapshot.codex} />
<AgentInventoryCard title="Claude Code" inventory={snapshot.claude} />
<article className="runtime-agent-card">
<header className="runtime-card-head">
<h4>EJClaw bridge</h4>
<ExistsBadge exists={snapshot.ejclaw.mcpServer.exists} />
</header>
<ul className="runtime-path-list">
<PathRow item={snapshot.ejclaw.mcpServer} />
<PathRow item={snapshot.ejclaw.runnerSkillDir} />
</ul>
</article>
</div>
)}
</section>
);
}

View File

@@ -31,21 +31,24 @@ describe('SettingsPanel', () => {
expect(html).toContain('English');
});
it('renders model, MoA, fast mode, and account controls', () => {
it('renders model, runtime, MoA, fast mode, and account controls', () => {
const html = renderToStaticMarkup(createElement(SettingsPanel, baseProps));
expect(html).toContain('role="tablist"');
expect(html).toContain('role="tabpanel"');
expect(html).toContain('aria-selected="true"');
expect(html).toContain('data-settings-target="settings-models"');
expect(html).toContain('data-settings-target="settings-runtime"');
expect(html).toContain('data-settings-target="settings-moa"');
expect(html).toContain('data-settings-target="settings-codex"');
expect(html).toContain('data-settings-target="settings-accounts"');
expect(html).toContain('aria-controls="settings-runtime"');
expect(html).toContain('aria-controls="settings-codex"');
expect(html).not.toContain('href="#settings-codex"');
expect(html).toContain('settings-apply-card');
expect(html).not.toContain('settings-apply-bar');
expect(html).toContain('저장 후 재시작');
expect(html).toContain('런타임');
expect(html).toContain('Claude');
expect(html).toContain('계정');
expect(html).toContain('스택 재시작');

View File

@@ -22,6 +22,7 @@ import {
} from './api';
import { type Locale, type Messages } from './i18n';
import { MoaSettingsPanel } from './MoaSettingsPanel';
import { RuntimeInventorySettings } from './RuntimeInventorySettings';
import {
GeneralSettings,
SettingsApplyCard,
@@ -83,6 +84,10 @@ export function SettingsPanel({
<ModelSettings />
</div>
<div hidden={activeSection !== 'settings-runtime'}>
<RuntimeInventorySettings />
</div>
<div hidden={activeSection !== 'settings-moa'}>
<MoaSettingsPanel />
</div>

View File

@@ -7,6 +7,11 @@ export const SETTINGS_NAV_ITEMS = [
title: '모델',
detail: 'owner · reviewer · arbiter',
},
{
targetId: 'settings-runtime',
title: '런타임',
detail: 'skills · MCP · config',
},
{ targetId: 'settings-moa', title: 'MoA', detail: '참조 모델 · 연결 테스트' },
{ targetId: 'settings-codex', title: 'Codex', detail: 'fast mode · /goal' },
{ targetId: 'settings-accounts', title: '계정', detail: 'Claude · Codex' },

View File

@@ -392,6 +392,52 @@ export interface CodexFeatureSnapshot {
goals: boolean;
}
export interface RuntimePathSnapshot {
label: string;
path: string;
exists: boolean;
}
export interface RuntimeSkillSummary {
name: string;
description: string | null;
path: string;
}
export interface RuntimeSkillDirSnapshot extends RuntimePathSnapshot {
count: number;
skills: RuntimeSkillSummary[];
}
export interface RuntimeMcpSnapshot {
configPath: RuntimePathSnapshot;
ejclawConfigured: boolean;
serverCount: number;
}
export interface RuntimeAgentInventory {
configFiles: RuntimePathSnapshot[];
skillDirs: RuntimeSkillDirSnapshot[];
mcp: RuntimeMcpSnapshot;
}
export interface RuntimeInventorySnapshot {
generatedAt: string;
projectRoot: string;
dataDir: string;
service: {
id: string;
sessionScope: string;
agentType: string;
};
codex: RuntimeAgentInventory;
claude: RuntimeAgentInventory;
ejclaw: {
runnerSkillDir: RuntimeSkillDirSnapshot;
mcpServer: RuntimePathSnapshot;
};
}
export interface MoaReferenceStatus {
model: string;
checkedAt: string;
@@ -546,6 +592,10 @@ export async function fetchCodexFeatures(): Promise<CodexFeatureSnapshot> {
return fetchJson('/api/settings/codex-features');
}
export async function fetchRuntimeInventory(): Promise<RuntimeInventorySnapshot> {
return fetchJson('/api/settings/runtime-inventory');
}
export async function updateCodexFeatures(
input: Partial<CodexFeatureSnapshot>,
): Promise<CodexFeatureSnapshot> {