sync: update from private repo (63a6e76)
CI / build-and-test (push) Has been cancelled

This commit is contained in:
oss-sync
2026-06-09 03:17:43 +00:00
parent d6d8e83867
commit 3848b5efd7
20 changed files with 386 additions and 29 deletions
+3 -1
View File
@@ -16,6 +16,7 @@ import { useTaskNotifications } from './hooks/useTaskNotifications';
import { DEFAULT_NOTIFY_EVENTS, type NotifyEventSettings } from './lib/notifications';
import { COLUMN_LIST, MOBILE_TAB_LIST, type MobileTabId, type PageId } from './lib/urlState';
import { confirmDiscardUnsaved } from './lib/unsavedGuard';
import { useBackdropClose } from './lib/useBackdropClose';
import { TopBar } from './components/layout/TopBar';
import { NavDrawer } from './components/layout/NavDrawer';
import { useEdgeSwipe } from './hooks/useEdgeSwipe';
@@ -157,6 +158,7 @@ function AppInner({ isAdmin, authEnabled, user }: { isAdmin: boolean; authEnable
setOverride((o) => (o && o.key === overrideKey ? o : null));
}, [overrideKey]);
const [tabletDetailOpen, setTabletDetailOpen] = useState(false);
const tabletDetailBackdrop = useBackdropClose(() => setTabletDetailOpen(false));
const [navDrawerOpen, setNavDrawerOpen] = useState(false);
const hamburgerRef = useRef<HTMLButtonElement>(null);
const compactMode = useCompactNav(isAdmin, authEnabled);
@@ -632,7 +634,7 @@ function AppInner({ isAdmin, authEnabled, user }: { isAdmin: boolean; authEnable
{/* Tablet: detail overlay */}
{tabletDetailOpen && panelOpen && (
<div className="hidden sm:block lg:hidden fixed inset-0 bg-black/40 z-40" onClick={() => setTabletDetailOpen(false)}>
<div className="hidden sm:block lg:hidden fixed inset-0 bg-black/40 z-40" {...tabletDetailBackdrop}>
<div className="absolute right-0 top-0 bottom-0 bg-canvas shadow-2xl flex flex-col overflow-hidden" style={{ width: 'min(480px, 90vw)' }} onClick={e => e.stopPropagation()}>
{localTaskId && (
<LocalDetailPanel
@@ -1,5 +1,6 @@
import { useState } from 'react';
import type { DashboardWidgetKind } from '../../api';
import { useBackdropClose } from '../../lib/useBackdropClose';
interface Props {
open: boolean;
@@ -35,6 +36,7 @@ export function AddWidgetDialog({ open, existingSlugs, onClose, onCreate }: Prop
const [title, setTitle] = useState('');
const [kind, setKind] = useState<DashboardWidgetKind>('markdown');
const [saving, setSaving] = useState(false);
const backdrop = useBackdropClose(() => { if (!saving) onClose(); });
if (!open) return null;
@@ -46,7 +48,7 @@ export function AddWidgetDialog({ open, existingSlugs, onClose, onCreate }: Prop
return (
<div
className="fixed inset-0 z-50 flex items-center justify-center bg-black/30"
onClick={() => !saving && onClose()}
{...backdrop}
>
<div
className="bg-surface rounded-md shadow-lg w-[320px] p-4 flex flex-col gap-3"
@@ -4,6 +4,7 @@ import { continueTaskWithPiece, fetchLocalTaskComments } from '../../api';
import { usePieceList } from '../../hooks/usePieces';
import { resolvePieceOptions } from '../../lib/splitPieces';
import { MarkdownText } from '../../lib/markdown-text';
import { useBackdropClose } from '../../lib/useBackdropClose';
interface PrevJobInfo {
id: string;
@@ -26,6 +27,7 @@ export function ContinueWithPieceDialog({
const [instruction, setInstruction] = useState<string>('');
const [resultExpanded, setResultExpanded] = useState<boolean>(false);
const qc = useQueryClient();
const backdrop = useBackdropClose(onClose);
const piecesQuery = usePieceList();
@@ -65,7 +67,7 @@ export function ContinueWithPieceDialog({
return (
<div
className="fixed inset-0 z-50 flex items-center justify-center bg-black/40"
onClick={e => { if (e.target === e.currentTarget) onClose(); }}
{...backdrop}
>
<div className="bg-surface rounded-xl shadow-xl w-full max-w-lg mx-4 overflow-hidden flex flex-col max-h-[90vh]">
{/* Header */}
+5 -3
View File
@@ -1,5 +1,6 @@
import { useEffect, useCallback, type ReactNode } from 'react';
import { createPortal } from 'react-dom';
import { useBackdropClose } from '../../lib/useBackdropClose';
interface EmbedModalProps {
open: boolean;
@@ -8,6 +9,7 @@ interface EmbedModalProps {
}
export function EmbedModal({ open, onClose, children }: EmbedModalProps) {
const backdrop = useBackdropClose(onClose);
const handleKeyDown = useCallback((e: KeyboardEvent) => {
if (e.key === 'Escape') onClose();
}, [onClose]);
@@ -28,10 +30,10 @@ export function EmbedModal({ open, onClose, children }: EmbedModalProps) {
return createPortal(
<div
className="fixed inset-0 z-50 flex items-center justify-center"
onClick={onClose}
>
{/* Backdrop */}
<div className="absolute inset-0 bg-black/50" />
{/* Backdrop — the close handler lives here (the actual clicked element),
not the transparent outer div, so target===currentTarget holds. */}
<div className="absolute inset-0 bg-black/50" {...backdrop} />
{/* Modal content */}
<div
+3 -1
View File
@@ -7,6 +7,7 @@ import hljs from 'highlight.js';
import { updateLocalFileContent } from '../../api';
import { EmbedBlock } from '../embed/EmbedBlock';
import { OUTPUT_PATH_REGEX, linkifyOutputPathsInEscapedHtml } from '../../lib/output-path-detect';
import { useBackdropClose } from '../../lib/useBackdropClose';
mermaid.initialize({ startOnLoad: false, theme: 'default' });
@@ -625,6 +626,7 @@ export function FilePreview({ name, content, imageSrc, markdownImageBaseUrl, onC
const [error, setError] = useState('');
const [currentContent, setCurrentContent] = useState(content);
const [printing, setPrinting] = useState(false);
const backdrop = useBackdropClose(onClose);
const canEdit = editable && taskId != null && section && filePath;
const isMarkdownFile = /\.(md|markdown)$/i.test(name);
@@ -734,7 +736,7 @@ export function FilePreview({ name, content, imageSrc, markdownImageBaseUrl, onC
const modalWidth = 'min(1400px, 96vw)';
return (
<div className="fixed inset-0 bg-black/40 flex items-center justify-center z-50 p-[env(safe-area-inset-top)_env(safe-area-inset-right)_env(safe-area-inset-bottom)_env(safe-area-inset-left)]" onClick={onClose}>
<div className="fixed inset-0 bg-black/40 flex items-center justify-center z-50 p-[env(safe-area-inset-top)_env(safe-area-inset-right)_env(safe-area-inset-bottom)_env(safe-area-inset-left)]" {...backdrop}>
<div
className="bg-surface rounded-md border border-hairline shadow-md flex flex-col overflow-hidden"
style={{ width: modalWidth, maxHeight: 'min(90vh, calc(100dvh - env(safe-area-inset-top, 0px) - env(safe-area-inset-bottom, 0px) - 24px))' }}
+3 -1
View File
@@ -1,5 +1,6 @@
import { useEffect, useRef, type ReactNode } from 'react';
import type { PageId } from '../../lib/urlState';
import { useBackdropClose } from '../../lib/useBackdropClose';
export interface NavItem {
id: PageId;
@@ -97,6 +98,7 @@ export function NavDrawer({
}: NavDrawerProps) {
const panelRef = useRef<HTMLDivElement>(null);
const firstItemRef = useRef<HTMLButtonElement>(null);
const backdrop = useBackdropClose(onClose);
useEffect(() => {
if (!open) return;
@@ -151,7 +153,7 @@ export function NavDrawer({
<>
<div
aria-hidden
onClick={onClose}
{...backdrop}
className={`fixed inset-0 z-40 bg-black/40 backdrop-blur-sm transition-opacity duration-200 ${
open ? 'opacity-100 pointer-events-auto' : 'opacity-0 pointer-events-none'
}`}
+116
View File
@@ -0,0 +1,116 @@
import { HelpText } from './HelpText';
import { FieldLabel, FieldInput } from './formUtils';
import { StringArrayEditor } from './StringArrayEditor';
import type { SectionFormProps } from './types';
/**
* Authentication config (`auth.*`): session policy, admin allowlist, and the
* Google / Gitea OAuth providers. Secrets (session secret, client secrets) are
* server-masked (config-manager SENSITIVE_PATHS) — they render as ******** and
* saving without re-entering keeps the stored value.
*
* Leaving the whole `auth` block empty (no providers) keeps the app in no-auth
* mode — every visitor is treated as a local admin.
*/
export function AuthForm({ config, onChange }: SectionFormProps) {
const auth = config.auth ?? {};
const providers = auth.providers ?? {};
const google = providers.google ?? {};
const gitea = providers.gitea ?? {};
return (
<div className="space-y-5">
<h2 className="text-base font-semibold text-slate-800">Authentication</h2>
<p className="text-[13px] text-slate-500">
providers <strong> admin</strong>
</p>
<div>
<FieldLabel>Primary Provider</FieldLabel>
<select
value={auth.primaryProvider ?? ''}
onChange={e => onChange('auth.primaryProvider', e.target.value)}
className="w-full h-8 px-2 text-[13px] border border-hairline rounded-md bg-canvas focus:ring-2 focus:ring-accent-ring focus:border-accent outline-none transition-shadow"
>
<option value=""></option>
<option value="google">google </option>
<option value="gitea">gitea </option>
</select>
<HelpText></HelpText>
</div>
<div>
<FieldLabel>Admin Emails</FieldLabel>
<StringArrayEditor
value={auth.adminEmails ?? []}
onChange={v => onChange('auth.adminEmails', v)}
placeholder="[email protected]"
/>
<HelpText>admin </HelpText>
</div>
<div>
<label className="flex items-center gap-2 text-sm text-slate-700">
<input type="checkbox" checked={auth.secureCookie === true}
onChange={e => onChange('auth.secureCookie', e.target.checked)} className="rounded" />
Secure CookieHTTPS Cookie
</label>
<HelpText>HTTPSHTTP </HelpText>
</div>
<div>
<FieldLabel>Session Max Age (ms)</FieldLabel>
<FieldInput type="number" value={auth.sessionMaxAge ?? ''}
onChange={v => onChange('auth.sessionMaxAge', v ? Number(v) : undefined)} />
<HelpText></HelpText>
</div>
<div>
<FieldLabel>Session Secret</FieldLabel>
<FieldInput type="password" value={auth.sessionSecret ?? ''}
onChange={v => onChange('auth.sessionSecret', v)} />
<HelpText></HelpText>
</div>
<h3 className="text-sm font-medium text-slate-600 mt-4 pt-3 border-t border-slate-200">Google OAuth</h3>
<div>
<FieldLabel>Client ID</FieldLabel>
<FieldInput value={google.clientId ?? ''}
onChange={v => onChange('auth.providers.google.clientId', v)} />
</div>
<div>
<FieldLabel>Client Secret</FieldLabel>
<FieldInput type="password" value={google.clientSecret ?? ''}
onChange={v => onChange('auth.providers.google.clientSecret', v)} />
</div>
<div>
<FieldLabel>Callback URL</FieldLabel>
<FieldInput value={google.callbackUrl ?? ''} placeholder="https://example.com/auth/google/callback"
onChange={v => onChange('auth.providers.google.callbackUrl', v)} />
</div>
<h3 className="text-sm font-medium text-slate-600 mt-4 pt-3 border-t border-slate-200">Gitea OAuth</h3>
<div>
<FieldLabel>Base URL</FieldLabel>
<FieldInput value={gitea.baseUrl ?? ''} placeholder="https://gitea.example.com"
onChange={v => onChange('auth.providers.gitea.baseUrl', v)} />
</div>
<div>
<FieldLabel>Client ID</FieldLabel>
<FieldInput value={gitea.clientId ?? ''}
onChange={v => onChange('auth.providers.gitea.clientId', v)} />
</div>
<div>
<FieldLabel>Client Secret</FieldLabel>
<FieldInput type="password" value={gitea.clientSecret ?? ''}
onChange={v => onChange('auth.providers.gitea.clientSecret', v)} />
</div>
<div>
<FieldLabel>Callback URL</FieldLabel>
<FieldInput value={gitea.callbackUrl ?? ''} placeholder="https://example.com/auth/gitea/callback"
onChange={v => onChange('auth.providers.gitea.callbackUrl', v)} />
</div>
</div>
);
}
@@ -91,6 +91,13 @@ export function BrowserSettingsForm({ config, onChange }: SectionFormProps) {
onChange={v => onChange('browser.maxSessions', Number(v))} />
<HelpText>デフォルト: 3</HelpText>
</div>
<div>
<FieldLabel>Task Session Idle TTL ()</FieldLabel>
<FieldInput type="number" value={browser.taskSessionIdleTtl ?? ''}
onChange={v => onChange('browser.taskSessionIdleTtl', v ? Number(v) : undefined)} />
<HelpText> CDP GCデフォルト: 300</HelpText>
</div>
</div>
);
}
@@ -28,6 +28,7 @@ import { SshForm } from './SshForm';
import { GatewayServerForm } from './GatewayServerForm';
import { NotesForm } from './NotesForm';
import { PushNotificationsForm } from './PushNotificationsForm';
import { AuthForm } from './AuthForm';
import { useAuthState } from '../../App';
@@ -189,6 +190,7 @@ function ConfigFormInner({ section }: ConfigFormProps) {
case 'reflection': return <ReflectionForm {...formProps} />;
case 'notes': return <NotesForm {...formProps} />;
case 'push-notifications': return <PushNotificationsForm {...formProps} />;
case 'auth': return <AuthForm {...formProps} />;
// ── Tools sub-sections — Step 9 split the legacy grab-bag
// ToolsForm into focused per-category forms. Each binds to the
@@ -34,6 +34,7 @@ const CONFIG_GROUPS = [
{ id: 'branding', label: 'Branding' },
{ id: 'paths-storage', label: 'Paths & Storage' },
{ id: 'execution', label: 'Execution' },
{ id: 'auth', label: 'Authentication' },
{ id: 'push-notifications', label: 'Web Push (Server)' },
],
},
@@ -68,6 +69,7 @@ const CONFIG_GROUPS = [
{ id: 'tools-browser', label: 'Browser Runtime' },
{ id: 'tools-media', label: 'Media & Documents' },
{ id: 'tools-external', label: 'External Services' },
{ id: 'search-filter', label: 'Search Filter' },
{ id: 'tools-legacy-knowledge', label: 'Legacy Knowledge' },
],
},
@@ -102,7 +104,6 @@ export const LEGACY_SECTION_REDIRECT: Record<string, string> = {
workspace: 'paths-storage',
tools: 'tools-web',
'browser-settings': 'tools-browser',
'search-filter': 'tools-web',
// browser-sessions never had a Settings page in the new layout —
// it lives in User Folder. Keep mapping so an old URL still goes
// somewhere sensible.
@@ -63,6 +63,39 @@ export function ToolsExternalForm({ config, onChange }: SectionFormProps) {
placeholder="/path/to/chrome/profile" />
<HelpText>Cookie Chrome </HelpText>
</div>
<div>
<FieldLabel>X Media Download</FieldLabel>
<select value={tools.xDownloadMedia ?? 'auto'}
onChange={e => onChange('tools.xDownloadMedia', e.target.value)}
className="w-full h-8 px-2 text-[13px] border border-hairline rounded-md bg-canvas focus:ring-2 focus:ring-accent-ring focus:border-accent outline-none transition-shadow">
<option value="auto">auto/</option>
<option value="never">never</option>
</select>
<HelpText>X 稿デフォルト: auto</HelpText>
</div>
<div>
<FieldLabel>X Video Download</FieldLabel>
<select value={tools.xDownloadVideo ?? 'thumbnail'}
onChange={e => onChange('tools.xDownloadVideo', e.target.value)}
className="w-full h-8 px-2 text-[13px] border border-hairline rounded-md bg-canvas focus:ring-2 focus:ring-accent-ring focus:border-accent outline-none transition-shadow">
<option value="thumbnail">thumbnail</option>
<option value="full">full</option>
<option value="never">never</option>
</select>
<HelpText>X 稿デフォルト: thumbnail</HelpText>
</div>
<div>
<FieldLabel>X Media Max (MB)</FieldLabel>
<FieldInput type="number" value={tools.xMediaMaxMb ?? ''}
onChange={v => onChange('tools.xMediaMaxMb', v ? Number(v) : undefined)} />
<HelpText>1 MB</HelpText>
</div>
<div>
<FieldLabel>X Media Fetch Timeout ()</FieldLabel>
<FieldInput type="number" value={tools.xMediaFetchTimeoutSeconds ?? ''}
onChange={v => onChange('tools.xMediaFetchTimeoutSeconds', v ? Number(v) : undefined)} />
<HelpText></HelpText>
</div>
</section>
<section className="space-y-5 pt-2 border-t border-hairline">
@@ -74,6 +107,12 @@ export function ToolsExternalForm({ config, onChange }: SectionFormProps) {
<FieldInput type="password" value={tools.googleMapsApiKey ?? ''} onChange={v => onChange('tools.googleMapsApiKey', v)} />
<HelpText>Google Maps Places / Directions API Nominatim / OSRM使</HelpText>
</div>
<div>
<FieldLabel>Maps Timeout ()</FieldLabel>
<FieldInput type="number" value={tools.mapsTimeout ?? ''}
onChange={v => onChange('tools.mapsTimeout', v ? Number(v) : undefined)} />
<HelpText>Maps / Nominatim / OSRM デフォルト: 30</HelpText>
</div>
</section>
<section className="space-y-5 pt-2 border-t border-hairline">
@@ -1,6 +1,7 @@
import { useState } from 'react';
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query';
import { listBrowserSessionProfiles } from '../../api';
import { useBackdropClose } from '../../lib/useBackdropClose';
export interface ParamHint {
name: string;
@@ -56,6 +57,7 @@ export function SaveAsScriptDialog({ recordingName, onClose, onSuccess }: SaveAs
const [overwrite, setOverwrite] = useState(false);
const [validationError, setValidationError] = useState<string | null>(null);
const [conflictError, setConflictError] = useState<string | null>(null);
const backdrop = useBackdropClose(onClose);
const { data: sessionProfiles = [], isLoading: profilesLoading } = useQuery({
queryKey: ['browser-session-profiles'],
@@ -141,7 +143,7 @@ export function SaveAsScriptDialog({ recordingName, onClose, onSuccess }: SaveAs
/* Backdrop */
<div
className="fixed inset-0 z-50 flex items-center justify-center bg-black/40"
onClick={e => { if (e.target === e.currentTarget) onClose(); }}
{...backdrop}
>
<div className="bg-surface rounded-xl shadow-xl w-full max-w-lg mx-4 overflow-hidden flex flex-col max-h-[90vh]">
{/* Header */}
@@ -1,6 +1,7 @@
import { useState } from 'react';
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
import { MarkdownText } from '../../lib/markdown-text';
import { useBackdropClose } from '../../lib/useBackdropClose';
interface Subscription {
consumer_user_id: string;
@@ -110,11 +111,12 @@ function NoteContentModal({
});
const title = (note.data?.fm.title as string | undefined) || fileName;
const backdrop = useBackdropClose(onClose);
return (
<div
className="fixed inset-0 z-50 flex items-center justify-center bg-black/30 p-4"
onClick={onClose}
{...backdrop}
>
<div
className="bg-surface rounded-md shadow-lg w-full max-w-3xl max-h-[85vh] flex flex-col overflow-hidden"
+1
View File
@@ -12,6 +12,7 @@ const SETTINGS_SECTIONS = [
'branding',
'paths-storage',
'execution',
'auth',
'push-notifications',
// LLM group
'llm-workers',
+33
View File
@@ -0,0 +1,33 @@
import { useRef } from 'react';
/**
* Backdrop "click outside to close" that does NOT fire when a drag started
* inside the modal (e.g. selecting text in an input) and was released on the
* backdrop. A plain `onClick={onClose}` closes in that case because the browser
* dispatches `click` on the common ancestor of mousedown+mouseup — which is the
* backdrop — losing the user's edits.
*
* Spread the returned props on the backdrop element and REMOVE the old
* `onClick={onClose}`:
*
* const backdrop = useBackdropClose(onClose);
* <div className="fixed inset-0 ..." {...backdrop}> ... </div>
*
* Close only happens when BOTH the mousedown and the click target are the
* backdrop itself, so a genuine backdrop click still closes, but a
* press-inside / release-on-backdrop drag does not.
*/
export function useBackdropClose(onClose: () => void) {
const downOnBackdrop = useRef(false);
return {
onMouseDown: (e: React.MouseEvent) => {
downOnBackdrop.current = e.target === e.currentTarget;
},
onClick: (e: React.MouseEvent) => {
if (downOnBackdrop.current && e.target === e.currentTarget) {
onClose();
}
downOnBackdrop.current = false;
},
};
}
+3 -1
View File
@@ -5,6 +5,7 @@ import { usePieceList } from '../hooks/usePieces';
import { createPiece, fetchPiece, PieceDef, DriftStatus, PieceSummary } from '../api';
import { PieceEditor } from '../components/settings/PieceEditor';
import { splitPieces } from '../lib/splitPieces';
import { useBackdropClose } from '../lib/useBackdropClose';
type PieceSource = 'builtin' | 'user-custom' | 'global-custom';
@@ -136,6 +137,7 @@ function PiecesSidebar({
const [duplicateName, setDuplicateName] = useState('');
const [duplicateError, setDuplicateError] = useState<string | null>(null);
const [duplicating, setDuplicating] = useState(false);
const duplicateBackdrop = useBackdropClose(() => cancelDuplicate());
const notifyError = (label: string, err: unknown) => {
const msg = `${label}: ${err instanceof Error ? err.message : String(err)}`;
@@ -286,7 +288,7 @@ function PiecesSidebar({
aria-modal="true"
aria-labelledby="dup-piece-label"
className="fixed inset-0 z-50 flex items-center justify-center bg-slate-900/30 px-4"
onClick={cancelDuplicate}
{...duplicateBackdrop}
>
<div
className="w-full max-w-sm rounded-lg border border-hairline bg-canvas p-4 shadow-xl"