sync: update from private repo (10f4905)
Some checks failed
CI / build-and-test (push) Has been cancelled

This commit is contained in:
oss-sync 2026-06-05 06:52:55 +00:00
parent 337c2be3c3
commit afae52873b
12 changed files with 177 additions and 14 deletions

View File

@ -64,15 +64,20 @@ COPY --from=builder /app/vendor ./vendor
COPY pieces ./pieces
COPY docs ./docs
# Ship a runnable default while still allowing a read-only config mount.
# Ship a runnable default while still allowing a config bind-mount.
COPY config.yaml.example ./config.yaml
RUN mkdir -p /data /workspaces \
&& chown -R node:node /data /workspaces
# The app runs as the non-root `node` user and writes its state under ./data
# (db, users, skills, secrets) — relative to WORKDIR /app, i.e. /app/data — plus
# /workspaces (worktree) and config.yaml (Settings save). Create and own those
# so a fresh deploy doesn't hit EACCES. /app/data and /workspaces are the volume
# mount points in docker-compose.
RUN mkdir -p /app/data /workspaces \
&& chown -R node:node /app/data /workspaces config.yaml
ENV NODE_ENV=production \
PORT=9876 \
DB_PATH=/data/maestro.db
DB_PATH=/app/data/maestro.db
EXPOSE 9876

View File

@ -16,15 +16,17 @@ services:
environment:
- NODE_ENV=production
- PORT=9876
- DB_PATH=/data/maestro.db
- DB_PATH=/app/data/maestro.db
- WORKTREE_DIR=/workspaces
volumes:
# SQLite DB 永続化
- maestro-data:/data
# アプリの状態 (DB / users / skills / secrets) を永続化。
# アプリは WORKDIR /app からの相対 ./data に書くので /app/data にマウントする。
- maestro-data:/app/data
# エージェントワークスペース永続化
- maestro-workspaces:/workspaces
# 設定ファイル (任意でホストから read-only マウント)
# - ./config.yaml:/app/config.yaml:ro
# config.yaml をホストから永続化したい場合は bind-mount (書き込み可)。
# Settings UI / npm run setup で書き換えるなら :ro は付けないこと。
# - ./config.yaml:/app/config.yaml
healthcheck:
test: ["CMD", "node", "-e", "fetch('http://localhost:9876/health').then(r => process.exit(r.ok ? 0 : 1)).catch(() => process.exit(1))"]
interval: 30s

View File

@ -392,7 +392,7 @@ export function ChatMessage({ comment, taskId, imageBaseUrl, isStaleThinking }:
<div className="text-2xs text-green-500 mb-1.5">
{author} · {new Date(createdAt).toLocaleString()}
</div>
<div className="text-sm leading-relaxed prose prose-sm prose-slate max-w-none">
<div className="text-sm leading-relaxed prose prose-sm prose-slate dark:prose-invert max-w-none">
<MarkdownPreview content={body} imageBaseUrl={imageBaseUrl ?? `/api/local/tasks/${taskId}/files/raw?section=output&path=`} taskId={taskId} />
</div>
</div>

View File

@ -328,7 +328,7 @@ export function MarkdownPreview({ content, imageBaseUrl, taskId, showOutline = f
};
const content_el = (
<div ref={containerRef} className={`${showOutline ? 'prose prose-slate max-w-none mdxg-reader' : 'prose prose-sm max-w-none'} min-w-0 break-words [&_a]:[overflow-wrap:anywhere] [&_a]:break-all [&_code]:[overflow-wrap:anywhere] [&_pre]:max-w-full [&_pre]:overflow-x-auto`}>
<div ref={containerRef} className={`${showOutline ? 'prose prose-slate dark:prose-invert max-w-none mdxg-reader' : 'prose prose-sm dark:prose-invert max-w-none'} min-w-0 break-words [&_a]:[overflow-wrap:anywhere] [&_a]:break-all [&_code]:[overflow-wrap:anywhere] [&_pre]:max-w-full [&_pre]:overflow-x-auto`}>
{segments.map((seg, i) => {
if (seg.type === 'embed') {
return <EmbedBlock key={`embed-${seg.refId}-${i}`} refId={seg.refId} taskId={taskId!} />;

View File

@ -0,0 +1,92 @@
import { useState } from 'react';
import { type ThemePref, readStoredTheme, setThemePref } from '../../lib/theme';
const ICON_PROPS = {
width: 14,
height: 14,
viewBox: '0 0 24 24',
fill: 'none',
stroke: 'currentColor',
strokeWidth: 1.8,
strokeLinecap: 'round' as const,
strokeLinejoin: 'round' as const,
'aria-hidden': true,
};
const OPTIONS: Array<{ value: ThemePref; label: string; icon: JSX.Element }> = [
{
value: 'system',
label: 'システム設定に合わせる',
icon: (
<svg {...ICON_PROPS}>
<rect x="2" y="4" width="20" height="13" rx="2" />
<path d="M8 21h8M12 17v4" />
</svg>
),
},
{
value: 'light',
label: 'ライト',
icon: (
<svg {...ICON_PROPS}>
<circle cx="12" cy="12" r="4" />
<path d="M12 2v2M12 20v2M4.9 4.9l1.4 1.4M17.7 17.7l1.4 1.4M2 12h2M20 12h2M4.9 19.1l1.4-1.4M17.7 6.3l1.4-1.4" />
</svg>
),
},
{
value: 'dark',
label: 'ダーク',
icon: (
<svg {...ICON_PROPS}>
<path d="M21 12.8A9 9 0 1 1 11.2 3a7 7 0 0 0 9.8 9.8z" />
</svg>
),
},
];
/**
* Compact 3-way theme switch (system / light / dark) for the TopBar.
*
* All theme logic lives in lib/theme.ts: this only tracks the displayed
* preference and calls setThemePref (persist + apply) on selection. The
* pre-paint inline script in index.html + initTheme() keep the actual
* [data-theme] attribute correct on load and on OS changes.
*/
export function ThemeToggle() {
const [pref, setPref] = useState<ThemePref>(() => readStoredTheme());
const choose = (value: ThemePref) => {
setPref(value);
setThemePref(value);
};
return (
<div
role="group"
aria-label="テーマ"
className="inline-flex items-center gap-0.5 rounded-md border border-hairline bg-surface p-0.5"
>
{OPTIONS.map((opt) => {
const active = pref === opt.value;
return (
<button
key={opt.value}
type="button"
onClick={() => choose(opt.value)}
aria-label={opt.label}
aria-pressed={active}
title={opt.label}
className={`flex items-center justify-center w-6 h-6 rounded transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-accent-ring ${
active
? 'bg-canvas text-slate-900 shadow-sm'
: 'text-slate-500 hover:text-slate-800'
}`}
>
{opt.icon}
</button>
);
})}
</div>
);
}

View File

@ -1,6 +1,7 @@
import { useEffect, useState } from 'react';
import type { PageId } from '../../lib/urlState';
import type { AuthUser } from '../../App';
import { ThemeToggle } from './ThemeToggle';
interface TopBarProps {
currentPage: PageId;
@ -137,6 +138,7 @@ export function TopBar({
</div>
<div className="flex items-center gap-2">
<ThemeToggle />
{user && (
<div className="flex items-center gap-2">
<div className="flex items-center gap-1.5">

View File

@ -729,3 +729,37 @@
}
.mdxg-outline a.depth-2 { padding-left: 1.25rem; }
.mdxg-outline a.depth-3 { padding-left: 2rem; font-size: 0.7rem; }
/* Dark mode overrides for the MD reader + outline
`.mdxg-reader pre` (code blocks) are github-dark already and stay as-is.
Body text comes from `prose prose-slate` whose slate scale is var-remapped
(auto light in dark). These override only the hardcoded light-mode chrome
(#18181b headings, light table/blockquote fills, pink inline code, the
outline's light hover/active) that would otherwise be dark-on-dark or a
light island in dark mode. */
[data-theme="dark"] .mdxg-reader h1,
[data-theme="dark"] .mdxg-reader h2,
[data-theme="dark"] .mdxg-reader h3,
[data-theme="dark"] .mdxg-reader h4,
[data-theme="dark"] .mdxg-reader h5,
[data-theme="dark"] .mdxg-reader h6,
[data-theme="dark"] .mdxg-reader strong,
[data-theme="dark"] .mdxg-reader b { color: var(--ink); }
[data-theme="dark"] .mdxg-reader h1 { border-bottom-color: var(--slate-700); }
[data-theme="dark"] .mdxg-reader h2 { border-left-color: var(--slate-700); }
[data-theme="dark"] .mdxg-reader h3::before { background: var(--slate-700); }
[data-theme="dark"] .mdxg-reader h1 .mdxg-anchor,
[data-theme="dark"] .mdxg-reader h2 .mdxg-anchor,
[data-theme="dark"] .mdxg-reader h3 .mdxg-anchor { color: var(--slate-500); }
[data-theme="dark"] .mdxg-reader thead th { background: var(--surface-2); border-bottom-color: var(--hairline); }
[data-theme="dark"] .mdxg-reader tbody td { border-bottom-color: var(--hairline); }
[data-theme="dark"] .mdxg-reader tbody tr:nth-child(even) { background: var(--surface); }
[data-theme="dark"] .mdxg-reader blockquote { background: var(--surface); color: var(--muted); }
[data-theme="dark"] .mdxg-reader :not(pre) > code {
background: var(--surface-2);
color: #f3a4c0;
border-color: var(--hairline);
}
[data-theme="dark"] .mdxg-outline a { color: var(--muted); }
[data-theme="dark"] .mdxg-outline a:hover { background: var(--surface-2); color: var(--ink); }
[data-theme="dark"] .mdxg-outline a.active { background: var(--surface-2); color: var(--ink); }

View File

@ -57,7 +57,7 @@ const md = new Marked({ gfm: true, breaks: true, renderer });
// remove any doubt we ALSO use Tailwind's `!` important prefix.
// Result: paragraphs ~4px apart, lines tight, headings reasonable.
const COMPACT_PROSE = [
'prose prose-sm prose-slate max-w-none break-words min-w-0',
'prose prose-sm prose-slate dark:prose-invert max-w-none break-words min-w-0',
// overflow-wrap: anywhere ensures unbreakable strings (long URLs, hashes,
// file paths) wrap mid-token. `break-words` alone only honors word
// boundaries which leaves them poking out of the container.

View File

@ -1,5 +1,5 @@
import { describe, it, expect, vi, afterEach } from 'vitest';
import { resolveTheme, isThemePref, readStoredTheme, writeStoredTheme, THEME_STORAGE_KEY } from './theme';
import { resolveTheme, isThemePref, readStoredTheme, writeStoredTheme, systemPrefersDark, THEME_STORAGE_KEY } from './theme';
describe('resolveTheme', () => {
it('explicit dark/light win regardless of system', () => {
@ -65,3 +65,13 @@ describe('writeStoredTheme', () => {
expect(() => writeStoredTheme('dark')).not.toThrow();
});
});
describe('systemPrefersDark', () => {
afterEach(() => vi.unstubAllGlobals());
it('reflects the OS matchMedia result', () => {
vi.stubGlobal('window', { matchMedia: () => ({ matches: true }) });
expect(systemPrefersDark()).toBe(true);
vi.stubGlobal('window', { matchMedia: () => ({ matches: false }) });
expect(systemPrefersDark()).toBe(false);
});
});

View File

@ -38,6 +38,20 @@ export function applyTheme(root: HTMLElement, resolved: ResolvedTheme): void {
const DARK_MQ = '(prefers-color-scheme: dark)';
/** True if the OS currently prefers a dark color scheme. */
export function systemPrefersDark(): boolean {
return typeof window !== 'undefined' && window.matchMedia(DARK_MQ).matches;
}
/**
* Persist a new preference and apply it to the document immediately. Used by
* the theme toggle UI. The pure resolution lives in resolveTheme.
*/
export function setThemePref(pref: ThemePref): void {
writeStoredTheme(pref);
applyTheme(document.documentElement, resolveTheme(pref, systemPrefersDark()));
}
/**
* Wire runtime theme. The INITIAL attribute is set by the inline script in
* index.html (pre-paint, no FOUC); this keeps it live and re-applies when the

View File

@ -124,7 +124,7 @@ export function HelpPage({ isAdmin, onAskAi, selectedId, onSelect }: HelpPagePro
<main ref={mainRef} className="flex-1 min-w-0 overflow-y-auto bg-canvas">
<div className="flex max-w-5xl mx-auto px-8 py-8 gap-8">
<article
className="prose prose-sm flex-1 min-w-0"
className="prose prose-sm dark:prose-invert flex-1 min-w-0"
onClick={handleContentClick}
dangerouslySetInnerHTML={{ __html: rendered.html }}
/>

View File

@ -12,6 +12,10 @@ import typography from '@tailwindcss/typography';
/** @type {import('tailwindcss').Config} */
export default {
content: ['./index.html', './src/**/*.{ts,tsx}'],
// Drive the `dark:` variant off our runtime [data-theme="dark"] attribute
// (set by index.html's pre-paint script + lib/theme.ts), not the OS media
// query — so `dark:prose-invert` follows the in-app theme toggle.
darkMode: ['selector', '[data-theme="dark"]'],
theme: {
extend: {
colors: {