fix installed skills discovery and improve editor UX (#1296)
* fix skills discovery from opencode * Fix stale skill description after frontmatter removal * fix: align vscode skill discovery parity
This commit is contained in:
@@ -53,12 +53,17 @@ import {
|
||||
const SETTINGS_NAV_MIN_WIDTH = 176;
|
||||
const SETTINGS_NAV_MAX_WIDTH = 280;
|
||||
const SETTINGS_NAV_RESIZE_STEP = 8;
|
||||
const SETTINGS_DETAIL_HISTORY_KEY = '__openchamberSettingsDetail';
|
||||
|
||||
function clampSettingsNavWidth(width: number): number {
|
||||
return Math.min(SETTINGS_NAV_MAX_WIDTH, Math.max(SETTINGS_NAV_MIN_WIDTH, width));
|
||||
}
|
||||
|
||||
type MobileStage = 'nav' | 'page-sidebar' | 'page-content';
|
||||
type SettingsDetailHistoryEntry = {
|
||||
page: SettingsPageSlug;
|
||||
stage: 'page-content';
|
||||
};
|
||||
|
||||
interface SettingsViewProps {
|
||||
onClose?: () => void;
|
||||
@@ -106,6 +111,37 @@ function isPageAvailable(page: SettingsPageMeta, ctx: SettingsRuntimeContext): b
|
||||
return page.isAvailable(ctx);
|
||||
}
|
||||
|
||||
function isObjectRecord(value: unknown): value is Record<string, unknown> {
|
||||
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
||||
}
|
||||
|
||||
function getSettingsDetailHistoryEntry(state: unknown): SettingsDetailHistoryEntry | null {
|
||||
if (!isObjectRecord(state)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const detail = state[SETTINGS_DETAIL_HISTORY_KEY];
|
||||
if (!isObjectRecord(detail)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const page = detail.page;
|
||||
const stage = detail.stage;
|
||||
if (typeof page !== 'string' || stage !== 'page-content') {
|
||||
return null;
|
||||
}
|
||||
|
||||
const resolvedPage = resolveSettingsSlug(page);
|
||||
return { page: resolvedPage, stage };
|
||||
}
|
||||
|
||||
function getCurrentHistoryState(): Record<string, unknown> {
|
||||
if (typeof window === 'undefined' || !isObjectRecord(window.history.state)) {
|
||||
return {};
|
||||
}
|
||||
return window.history.state;
|
||||
}
|
||||
|
||||
// eslint-disable-next-line react-refresh/only-export-components
|
||||
export function getSettingsNavIcon(slug: SettingsPageSlug): IconName | null {
|
||||
switch (slug) {
|
||||
@@ -559,11 +595,84 @@ export const SettingsView: React.FC<SettingsViewProps> = ({ onClose, forceMobile
|
||||
}, [isMobile, mobileStage, settingsSlug]);
|
||||
|
||||
const showBackButton = isMobile && mobileStage !== 'nav';
|
||||
const backButtonTargetsPageSidebar = isMobile && mobileStage === 'page-content' && settingsSlug === 'skills.installed';
|
||||
const showOpenPageSidebarButton = mobileStage === 'page-content'
|
||||
&& activePageMeta?.kind === 'split'
|
||||
&& !backButtonTargetsPageSidebar;
|
||||
const mobileBackButtonLabel = backButtonTargetsPageSidebar
|
||||
? t('settings.view.actions.back')
|
||||
: showBackButton
|
||||
? t('settings.view.actions.backToSettings')
|
||||
: t('settings.view.actions.closeSettings');
|
||||
const shortcutKey = getModifierLabel();
|
||||
|
||||
const pushMobileSplitDetailHistory = React.useCallback((slug: SettingsPageSlug) => {
|
||||
if (typeof window === 'undefined' || runtimeCtx.isVSCode) {
|
||||
return;
|
||||
}
|
||||
|
||||
const currentDetail = getSettingsDetailHistoryEntry(window.history.state);
|
||||
if (currentDetail?.page === slug && currentDetail.stage === 'page-content') {
|
||||
return;
|
||||
}
|
||||
|
||||
window.history.pushState(
|
||||
{
|
||||
...getCurrentHistoryState(),
|
||||
[SETTINGS_DETAIL_HISTORY_KEY]: { page: slug, stage: 'page-content' },
|
||||
},
|
||||
'',
|
||||
window.location.href,
|
||||
);
|
||||
}, [runtimeCtx.isVSCode]);
|
||||
|
||||
const handleMobilePageSidebarItemSelect = React.useCallback(() => {
|
||||
setMobileStage('page-content');
|
||||
if (settingsSlug === 'skills.installed') {
|
||||
pushMobileSplitDetailHistory(settingsSlug);
|
||||
}
|
||||
}, [pushMobileSplitDetailHistory, settingsSlug]);
|
||||
|
||||
const handleBack = React.useCallback(() => {
|
||||
if (backButtonTargetsPageSidebar) {
|
||||
const currentDetail = typeof window !== 'undefined'
|
||||
? getSettingsDetailHistoryEntry(window.history.state)
|
||||
: null;
|
||||
if (currentDetail?.page === settingsSlug && !runtimeCtx.isVSCode) {
|
||||
window.history.back();
|
||||
return;
|
||||
}
|
||||
setMobileStage('page-sidebar');
|
||||
return;
|
||||
}
|
||||
|
||||
setMobileStage('nav');
|
||||
}, []);
|
||||
}, [backButtonTargetsPageSidebar, runtimeCtx.isVSCode, settingsSlug]);
|
||||
|
||||
React.useEffect(() => {
|
||||
if (!isMobile || runtimeCtx.isVSCode) {
|
||||
return;
|
||||
}
|
||||
|
||||
const handlePopState = (event: PopStateEvent) => {
|
||||
if (settingsSlug !== 'skills.installed') {
|
||||
return;
|
||||
}
|
||||
|
||||
const detail = getSettingsDetailHistoryEntry(event.state);
|
||||
if (detail?.page === 'skills.installed') {
|
||||
setMobileStage('page-content');
|
||||
return;
|
||||
}
|
||||
|
||||
setMobileStage((stage) => stage === 'page-content' ? 'page-sidebar' : stage);
|
||||
};
|
||||
|
||||
window.addEventListener('popstate', handlePopState);
|
||||
return () => {
|
||||
window.removeEventListener('popstate', handlePopState);
|
||||
};
|
||||
}, [isMobile, runtimeCtx.isVSCode, settingsSlug]);
|
||||
|
||||
const handleOpenPageSidebar = React.useCallback(() => {
|
||||
setMobileStage('page-sidebar');
|
||||
@@ -670,7 +779,7 @@ export const SettingsView: React.FC<SettingsViewProps> = ({ onClose, forceMobile
|
||||
return (
|
||||
<div className={cn('flex-1 min-h-0 overflow-hidden', runtimeCtx.isVSCode ? 'bg-background' : 'bg-sidebar')}>
|
||||
<ErrorBoundary>
|
||||
{renderPageSidebar(settingsSlug, { onItemSelect: () => setMobileStage('page-content') })}
|
||||
{renderPageSidebar(settingsSlug, { onItemSelect: handleMobilePageSidebarItemSelect })}
|
||||
</ErrorBoundary>
|
||||
</div>
|
||||
);
|
||||
@@ -724,7 +833,7 @@ export const SettingsView: React.FC<SettingsViewProps> = ({ onClose, forceMobile
|
||||
<button
|
||||
type="button"
|
||||
onClick={showBackButton ? handleBack : onClose}
|
||||
aria-label={showBackButton ? t('settings.view.actions.backToSettings') : t('settings.view.actions.closeSettings')}
|
||||
aria-label={mobileBackButtonLabel}
|
||||
className="inline-flex h-9 w-9 flex-shrink-0 items-center justify-center rounded-lg p-2 text-muted-foreground hover:text-foreground hover:bg-interactive-hover/50 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary"
|
||||
>
|
||||
<Icon name="arrow-left-s" className="h-5 w-5" />
|
||||
@@ -736,7 +845,7 @@ export const SettingsView: React.FC<SettingsViewProps> = ({ onClose, forceMobile
|
||||
: (activePageMeta ? getPageTitle(activePageMeta.slug) : t('settings.view.home.title'))}
|
||||
</div>
|
||||
|
||||
{mobileStage === 'page-content' && activePageMeta?.kind === 'split' && (
|
||||
{showOpenPageSidebarButton && (
|
||||
<button
|
||||
type="button"
|
||||
onClick={handleOpenPageSidebar}
|
||||
|
||||
Reference in New Issue
Block a user