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

This commit is contained in:
oss-sync 2026-06-10 04:16:39 +00:00
parent 9f8958c4a2
commit dfc5950117
9 changed files with 414 additions and 378 deletions

View File

@ -1,34 +1,15 @@
/** /**
* MemoryLearningForm.tsx "Memory & Learning" settings section * MemoryLearningForm.tsx "Reflection 履歴" settings section
* *
* Two stacked panels: * ReflectionTimelinePanel paged snapshot history + revert + 30-day metrics.
* 1. MemoryEntriesPanel list / inline-edit / delete user memory entries * (Memory entry editing moved to User Folder memory/ MemoryPanel.tsx.)
* 2. ReflectionTimelinePanel paged snapshot history + revert + 30-day metrics
*/ */
import { useState, useEffect } from 'react'; import { useState } from 'react';
import { useQuery, useMutation, useQueryClient, useInfiniteQuery } from '@tanstack/react-query'; import { useQuery, useMutation, useQueryClient, useInfiniteQuery } from '@tanstack/react-query';
import { HelpText } from './HelpText';
// ── API types ───────────────────────────────────────────────────────────────── // ── API types ─────────────────────────────────────────────────────────────────
type MemoryType = 'user' | 'feedback' | 'project' | 'reference';
// Mirrors the server's flat shape from `listMemoryEntries` in
// src/user-folder/memory.ts and `GET /api/local/memory/entries` in
// src/bridge/memory-api.ts. If you change this shape, update both.
interface MemoryEntry {
name: string;
description: string;
type: MemoryType;
body: string;
}
interface MemoryListResponse {
entries: MemoryEntry[];
index: string;
}
interface SnapshotIndexEntry { interface SnapshotIndexEntry {
ts: string; ts: string;
snapshotId: string; snapshotId: string;
@ -80,35 +61,6 @@ interface ReflectionMetrics {
// ── API helpers ─────────────────────────────────────────────────────────────── // ── API helpers ───────────────────────────────────────────────────────────────
async function fetchMemoryEntries(): Promise<MemoryListResponse> {
const res = await fetch('/api/local/memory/entries');
if (!res.ok) throw new Error(`メモリエントリの読み込みに失敗しました (${res.status})`);
return res.json();
}
async function upsertMemoryEntry(
name: string,
payload: { description: string; type: MemoryType; body: string },
): Promise<void> {
const res = await fetch(`/api/local/memory/entries/${encodeURIComponent(name)}`, {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(payload),
});
const data = await res.json().catch(() => ({ error: res.statusText }));
if (!res.ok) throw new Error(data.error ?? res.statusText);
}
async function deleteMemoryEntry(name: string): Promise<void> {
const res = await fetch(`/api/local/memory/entries/${encodeURIComponent(name)}`, {
method: 'DELETE',
});
if (!res.ok) {
const data = await res.json().catch(() => ({ error: res.statusText }));
throw new Error(data.error ?? res.statusText);
}
}
async function fetchHistoryPage(cursor?: string): Promise<HistoryPage> { async function fetchHistoryPage(cursor?: string): Promise<HistoryPage> {
const params = new URLSearchParams({ limit: '20' }); const params = new URLSearchParams({ limit: '20' });
if (cursor) params.set('before', cursor); if (cursor) params.set('before', cursor);
@ -171,313 +123,6 @@ function formatTs(ts: string): string {
} }
} }
// ── Validator rejection code messages ─────────────────────────────────────────
const REJECTION_MESSAGES: Record<string, string> = {
rejected_bad_name: '名前が無効です英数字・ハイフン・アンダースコア、1〜64文字',
rejected_bad_description: '概要は必須で、1行以内で入力してください',
rejected_unknown_type: 'タイプは user / feedback / project / reference のいずれかを指定してください',
rejected_bad_body: '本文は文字列で指定してください',
rejected_body_too_large: '本文が許容サイズを超えています',
rejected_bad_request: 'リクエストの形式が正しくありません',
};
function rejectionMessage(code: string): string {
return REJECTION_MESSAGES[code] ?? code;
}
// ── MemoryEntryModal ──────────────────────────────────────────────────────────
interface EntryFormState {
name: string;
description: string;
type: MemoryType;
body: string;
}
const MEMORY_TYPES: MemoryType[] = ['user', 'feedback', 'project', 'reference'];
function MemoryEntryModal({
initial,
isNew,
onClose,
onSaved,
}: {
initial: EntryFormState;
isNew: boolean;
onClose: () => void;
onSaved: () => void;
}) {
const [form, setForm] = useState<EntryFormState>(initial);
const [error, setError] = useState<string | null>(null);
const [saving, setSaving] = useState(false);
const set = <K extends keyof EntryFormState>(k: K, v: EntryFormState[K]) =>
setForm(prev => ({ ...prev, [k]: v }));
const handleSave = async () => {
setSaving(true);
setError(null);
try {
await upsertMemoryEntry(form.name, {
description: form.description,
type: form.type,
body: form.body,
});
onSaved();
onClose();
} catch (e: any) {
setError(rejectionMessage(e.message));
} finally {
setSaving(false);
}
};
return (
<div className="fixed inset-0 z-50 flex items-center justify-center bg-black/40">
<div className="bg-surface rounded-lg shadow-xl w-full max-w-lg mx-4 flex flex-col max-h-[90vh]">
<div className="flex items-center justify-between px-4 py-3 border-b border-hairline">
<h3 className="text-sm font-semibold text-slate-800">
{isNew ? '新しいメモリエントリ' : `編集 — ${initial.name}`}
</h3>
<button
onClick={onClose}
className="text-slate-400 hover:text-slate-700 text-lg leading-none"
aria-label="閉じる"
>
×
</button>
</div>
<div className="overflow-y-auto p-4 space-y-3 flex-1">
{/* Name — only editable when creating */}
<div>
<label className="block text-2xs font-medium text-slate-600 mb-1">
{isNew && <span className="text-slate-400 ml-1"></span>}
</label>
<input
type="text"
value={form.name}
onChange={e => set('name', e.target.value)}
disabled={!isNew}
placeholder="my-fact"
className={`w-full h-8 px-2.5 text-[13px] font-mono border border-hairline rounded-md focus:ring-2 focus:ring-accent-ring focus:border-accent outline-none ${
!isNew ? 'bg-slate-50 text-slate-500 cursor-not-allowed' : 'bg-canvas'
}`}
/>
</div>
<div>
<label className="block text-2xs font-medium text-slate-600 mb-1"></label>
<input
type="text"
value={form.description}
onChange={e => set('description', e.target.value)}
placeholder="メモリ一覧に表示される1行の説明"
className="w-full h-8 px-2.5 text-[13px] border border-hairline rounded-md focus:ring-2 focus:ring-accent-ring focus:border-accent outline-none"
/>
</div>
<div>
<label className="block text-2xs font-medium text-slate-600 mb-1"></label>
<select
value={form.type}
onChange={e => set('type', e.target.value as MemoryType)}
className="w-full h-8 px-2 text-[13px] border border-hairline rounded-md focus:ring-2 focus:ring-accent-ring outline-none bg-canvas"
>
{MEMORY_TYPES.map(t => (
<option key={t} value={t}>{t}</option>
))}
</select>
<HelpText>
user: あなた固有の好み feedback: 過去のフィードバック project: プロジェクト別の文脈 reference: 参照資料
</HelpText>
</div>
<div>
<label className="block text-2xs font-medium text-slate-600 mb-1"></label>
<textarea
value={form.body}
onChange={e => set('body', e.target.value)}
rows={8}
className="w-full px-2.5 py-2 text-xs font-mono border border-hairline rounded-md focus:ring-2 focus:ring-accent-ring focus:border-accent outline-none resize-y"
placeholder="Markdown またはプレーンテキスト…"
/>
</div>
{error && (
<div className="text-xs text-red-600 dark:text-red-300 bg-red-50 dark:bg-red-500/15 border border-red-200 dark:border-red-500/30 px-3 py-2 rounded-md">
{error}
</div>
)}
</div>
<div className="flex justify-end gap-2 px-4 py-3 border-t border-hairline">
<button
onClick={onClose}
className="px-3 h-8 text-xs text-slate-700 border border-hairline bg-canvas rounded-md hover:bg-surface transition-colors"
>
</button>
<button
onClick={() => void handleSave()}
disabled={saving || !form.name.trim() || !form.description.trim()}
className="px-3 h-8 text-xs font-semibold bg-accent text-accent-fg rounded-md hover:bg-accent-deep disabled:opacity-50 transition-colors"
>
{saving ? '保存中…' : '保存'}
</button>
</div>
</div>
</div>
);
}
// ── MemoryEntriesPanel ────────────────────────────────────────────────────────
function MemoryEntriesPanel() {
const qc = useQueryClient();
const { data, isLoading, error } = useQuery<MemoryListResponse>({
queryKey: ['memory-entries'],
queryFn: fetchMemoryEntries,
});
const [modal, setModal] = useState<{ entry: EntryFormState; isNew: boolean } | null>(null);
const [deleting, setDeleting] = useState<string | null>(null);
const [deleteError, setDeleteError] = useState<string | null>(null);
const handleNew = () => {
setModal({
isNew: true,
entry: { name: '', description: '', type: 'user', body: '' },
});
};
const handleEdit = (e: MemoryEntry) => {
setModal({
isNew: false,
entry: {
name: e.name,
description: e.description,
type: e.type,
body: e.body,
},
});
};
const handleDelete = async (name: string) => {
if (!confirm(`メモリエントリ「${name}」を削除しますか?`)) return;
setDeleting(name);
setDeleteError(null);
try {
await deleteMemoryEntry(name);
await qc.invalidateQueries({ queryKey: ['memory-entries'] });
} catch (e: any) {
setDeleteError(`${name}」の削除に失敗しました: ${e.message}`);
} finally {
setDeleting(null);
}
};
const handleSaved = () => {
void qc.invalidateQueries({ queryKey: ['memory-entries'] });
};
return (
<div className="rounded-lg border border-hairline">
<div className="flex items-center justify-between px-4 py-3 border-b border-hairline bg-surface rounded-t-lg">
<div>
<h3 className="text-sm font-semibold text-slate-800"></h3>
<p className="text-2xs text-slate-500 mt-0.5">
</p>
</div>
<button
onClick={handleNew}
className="px-2.5 h-7 text-2xs font-medium bg-accent text-accent-fg rounded-md hover:bg-accent-deep transition-colors"
>
+
</button>
</div>
{isLoading && (
<div className="px-4 py-6 text-xs text-slate-400 text-center"></div>
)}
{error && (
<div className="px-4 py-3 text-xs text-red-600">
: {String(error)}
</div>
)}
{deleteError && (
<div className="px-4 py-2 text-xs text-red-600 dark:text-red-300 bg-red-50 dark:bg-red-500/15">
{deleteError}
</div>
)}
{data && data.entries.length === 0 && (
<div className="px-4 py-8 text-center">
<p className="text-xs text-slate-400"></p>
<p className="text-2xs text-slate-400 mt-1">
reflection
</p>
</div>
)}
{data && data.entries.length > 0 && (
<ul className="divide-y divide-hairline">
{data.entries.map(entry => (
<li key={entry.name} className="flex items-start gap-3 px-4 py-3 hover:bg-surface/60 transition-colors">
<div className="flex-1 min-w-0">
<div className="flex items-center gap-2 flex-wrap">
<span className="text-xs font-mono font-medium text-slate-800 truncate">
{entry.name}
</span>
<span className="text-[10px] px-1.5 py-0.5 rounded bg-slate-100 text-slate-500 flex-shrink-0">
{entry.type}
</span>
</div>
<p className="text-2xs text-slate-500 mt-0.5 truncate">{entry.description}</p>
{entry.body && (
<p className="text-2xs text-slate-400 mt-0.5 line-clamp-2 font-mono whitespace-pre-wrap break-words">
{entry.body.slice(0, 200)}{entry.body.length > 200 ? '…' : ''}
</p>
)}
</div>
<div className="flex gap-1.5 flex-shrink-0 mt-0.5">
<button
onClick={() => handleEdit(entry)}
className="px-2 h-6 text-2xs text-slate-600 border border-hairline bg-canvas hover:bg-surface rounded transition-colors"
>
</button>
<button
onClick={() => void handleDelete(entry.name)}
disabled={deleting === entry.name}
className="px-2 h-6 text-2xs text-red-700 dark:text-red-300 border border-red-200 bg-canvas hover:bg-red-50 dark:hover:bg-red-500/15 rounded transition-colors disabled:opacity-50"
>
{deleting === entry.name ? '…' : '削除'}
</button>
</div>
</li>
))}
</ul>
)}
{modal && (
<MemoryEntryModal
initial={modal.entry}
isNew={modal.isNew}
onClose={() => setModal(null)}
onSaved={handleSaved}
/>
)}
</div>
);
}
// ── SnapshotCard ────────────────────────────────────────────────────────────── // ── SnapshotCard ──────────────────────────────────────────────────────────────
function SnapshotCard({ item, onReverted }: { item: SnapshotIndexEntry; onReverted: () => void }) { function SnapshotCard({ item, onReverted }: { item: SnapshotIndexEntry; onReverted: () => void }) {
@ -971,12 +616,12 @@ function ReflectionTimelinePanel() {
export function MemoryLearningForm() { export function MemoryLearningForm() {
return ( return (
<div className="space-y-6"> <div className="space-y-6">
<h2 className="text-base font-semibold text-slate-800">Memory &amp; Learning</h2> <h2 className="text-base font-semibold text-slate-800">Reflection </h2>
<p className="text-xs text-slate-500 -mt-4"> <p className="text-xs text-slate-500 -mt-4">
reflection reflectionrevert
memory/
</p> </p>
<MemoryEntriesPanel />
<ReflectionTimelinePanel /> <ReflectionTimelinePanel />
</div> </div>
); );

View File

@ -24,7 +24,7 @@ const CONFIG_GROUPS = [
sections: [ sections: [
{ id: 'preferences', label: 'Preferences' }, { id: 'preferences', label: 'Preferences' },
{ id: 'notifications', label: '🔔 Notifications' }, { id: 'notifications', label: '🔔 Notifications' },
{ id: 'memory-learning', label: '🧠 Memory & Learning' }, { id: 'memory-learning', label: '🧠 Reflection 履歴' },
], ],
}, },
{ {

View File

@ -5,7 +5,7 @@ import { useState } from 'react';
export type SubdirId = 'agents-md' | 'browser-macros' | 'recordings' | 'trash' | 'memory' | 'browser-sessions' | 'mcp' | 'skills' | 'pets' | 'ssh-connections' | 'notes' | 'subscribed-notes'; export type SubdirId = 'agents-md' | 'browser-macros' | 'recordings' | 'trash' | 'memory' | 'browser-sessions' | 'mcp' | 'skills' | 'pets' | 'ssh-connections' | 'notes' | 'subscribed-notes';
/** True for subdirs that have actual files on disk */ /** True for subdirs that have actual files on disk */
export const FILE_SUBDIRS: SubdirId[] = ['browser-macros', 'recordings', 'trash', 'memory', 'notes']; export const FILE_SUBDIRS: SubdirId[] = ['browser-macros', 'recordings', 'trash', 'notes'];
export interface FileEntry { export interface FileEntry {
name: string; name: string;
@ -59,7 +59,7 @@ const SUBDIR_ICONS: Record<SubdirId, string> = {
}; };
/** Virtual subdirs that don't show a file list (they render custom panel content instead). */ /** Virtual subdirs that don't show a file list (they render custom panel content instead). */
const VIRTUAL_SUBDIRS = new Set<SubdirId>(['agents-md', 'browser-sessions', 'mcp', 'skills', 'pets', 'ssh-connections', 'subscribed-notes']); const VIRTUAL_SUBDIRS = new Set<SubdirId>(['agents-md', 'browser-sessions', 'mcp', 'skills', 'pets', 'ssh-connections', 'subscribed-notes', 'memory']);
export function FileTree({ export function FileTree({
subdirData, subdirData,

View File

@ -0,0 +1,387 @@
/**
* MemoryPanel.tsx User Folder memory/
*
*
* Settings Memory & Learning memory
* User Folder Settings Reflection
*/
import { useState } from 'react';
import { useQuery, useQueryClient } from '@tanstack/react-query';
import { HelpText } from '../settings/HelpText';
// ── API types ─────────────────────────────────────────────────────────────────
type MemoryType = 'user' | 'feedback' | 'project' | 'reference';
// Mirrors the server's flat shape from `listMemoryEntries` in
// src/user-folder/memory.ts and `GET /api/local/memory/entries` in
// src/bridge/memory-api.ts. If you change this shape, update both.
interface MemoryEntry {
name: string;
description: string;
type: MemoryType;
body: string;
}
interface MemoryListResponse {
entries: MemoryEntry[];
index: string;
}
// ── API helpers ───────────────────────────────────────────────────────────────
async function fetchMemoryEntries(): Promise<MemoryListResponse> {
const res = await fetch('/api/local/memory/entries');
if (!res.ok) throw new Error(`メモリエントリの読み込みに失敗しました (${res.status})`);
return res.json();
}
async function upsertMemoryEntry(
name: string,
payload: { description: string; type: MemoryType; body: string },
): Promise<void> {
const res = await fetch(`/api/local/memory/entries/${encodeURIComponent(name)}`, {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(payload),
});
const data = await res.json().catch(() => ({ error: res.statusText }));
if (!res.ok) throw new Error(data.error ?? res.statusText);
}
async function deleteMemoryEntry(name: string): Promise<void> {
const res = await fetch(`/api/local/memory/entries/${encodeURIComponent(name)}`, {
method: 'DELETE',
});
if (!res.ok) {
const data = await res.json().catch(() => ({ error: res.statusText }));
throw new Error(data.error ?? res.statusText);
}
}
// ── Validator rejection code messages ─────────────────────────────────────────
const REJECTION_MESSAGES: Record<string, string> = {
rejected_bad_name: '名前が無効です英数字・ハイフン・アンダースコア、1〜64文字',
rejected_bad_description: '概要は必須で、1行以内で入力してください',
rejected_unknown_type: 'タイプは user / feedback / project / reference のいずれかを指定してください',
rejected_bad_body: '本文は文字列で指定してください',
rejected_body_too_large: '本文が許容サイズを超えています',
rejected_bad_request: 'リクエストの形式が正しくありません',
};
function rejectionMessage(code: string): string {
return REJECTION_MESSAGES[code] ?? code;
}
// ── MemoryEntryModal ──────────────────────────────────────────────────────────
interface EntryFormState {
name: string;
description: string;
type: MemoryType;
body: string;
}
const MEMORY_TYPES: MemoryType[] = ['user', 'feedback', 'project', 'reference'];
function MemoryEntryModal({
initial,
isNew,
onClose,
onSaved,
}: {
initial: EntryFormState;
isNew: boolean;
onClose: () => void;
onSaved: () => void;
}) {
const [form, setForm] = useState<EntryFormState>(initial);
const [error, setError] = useState<string | null>(null);
const [saving, setSaving] = useState(false);
const set = <K extends keyof EntryFormState>(k: K, v: EntryFormState[K]) =>
setForm(prev => ({ ...prev, [k]: v }));
const handleSave = async () => {
setSaving(true);
setError(null);
try {
await upsertMemoryEntry(form.name, {
description: form.description,
type: form.type,
body: form.body,
});
onSaved();
onClose();
} catch (e: any) {
setError(rejectionMessage(e.message));
} finally {
setSaving(false);
}
};
return (
<div className="fixed inset-0 z-50 flex items-center justify-center bg-black/40">
<div className="bg-surface rounded-lg shadow-xl w-full max-w-lg mx-4 flex flex-col max-h-[90vh]">
<div className="flex items-center justify-between px-4 py-3 border-b border-hairline">
<h3 className="text-sm font-semibold text-slate-800">
{isNew ? '新しいメモリエントリ' : `編集 — ${initial.name}`}
</h3>
<button
onClick={onClose}
className="text-slate-400 hover:text-slate-700 text-lg leading-none"
aria-label="閉じる"
>
×
</button>
</div>
<div className="overflow-y-auto p-4 space-y-3 flex-1">
{/* Name — only editable when creating */}
<div>
<label className="block text-2xs font-medium text-slate-600 mb-1">
{isNew && <span className="text-slate-400 ml-1"></span>}
</label>
<input
type="text"
value={form.name}
onChange={e => set('name', e.target.value)}
disabled={!isNew}
placeholder="my-fact"
className={`w-full h-8 px-2.5 text-[13px] font-mono border border-hairline rounded-md focus:ring-2 focus:ring-accent-ring focus:border-accent outline-none ${
!isNew ? 'bg-slate-50 text-slate-500 cursor-not-allowed' : 'bg-canvas'
}`}
/>
</div>
<div>
<label className="block text-2xs font-medium text-slate-600 mb-1"></label>
<input
type="text"
value={form.description}
onChange={e => set('description', e.target.value)}
placeholder="メモリ一覧に表示される1行の説明"
className="w-full h-8 px-2.5 text-[13px] border border-hairline rounded-md focus:ring-2 focus:ring-accent-ring focus:border-accent outline-none"
/>
</div>
<div>
<label className="block text-2xs font-medium text-slate-600 mb-1"></label>
<select
value={form.type}
onChange={e => set('type', e.target.value as MemoryType)}
className="w-full h-8 px-2 text-[13px] border border-hairline rounded-md focus:ring-2 focus:ring-accent-ring outline-none bg-canvas"
>
{MEMORY_TYPES.map(t => (
<option key={t} value={t}>{t}</option>
))}
</select>
<HelpText>
user: あなた固有の好み feedback: 過去のフィードバック project: プロジェクト別の文脈 reference: 参照資料
</HelpText>
</div>
<div>
<label className="block text-2xs font-medium text-slate-600 mb-1"></label>
<textarea
value={form.body}
onChange={e => set('body', e.target.value)}
rows={8}
className="w-full px-2.5 py-2 text-xs font-mono border border-hairline rounded-md focus:ring-2 focus:ring-accent-ring focus:border-accent outline-none resize-y"
placeholder="Markdown またはプレーンテキスト…"
/>
</div>
{error && (
<div className="text-xs text-red-600 dark:text-red-300 bg-red-50 dark:bg-red-500/15 border border-red-200 dark:border-red-500/30 px-3 py-2 rounded-md">
{error}
</div>
)}
</div>
<div className="flex justify-end gap-2 px-4 py-3 border-t border-hairline">
<button
onClick={onClose}
className="px-3 h-8 text-xs text-slate-700 border border-hairline bg-canvas rounded-md hover:bg-surface transition-colors"
>
</button>
<button
onClick={() => void handleSave()}
disabled={saving || !form.name.trim() || !form.description.trim()}
className="px-3 h-8 text-xs font-semibold bg-accent text-accent-fg rounded-md hover:bg-accent-deep disabled:opacity-50 transition-colors"
>
{saving ? '保存中…' : '保存'}
</button>
</div>
</div>
</div>
);
}
// ── MemoryEntriesPanel ────────────────────────────────────────────────────────
function MemoryEntriesPanel() {
const qc = useQueryClient();
const { data, isLoading, error } = useQuery<MemoryListResponse>({
queryKey: ['memory-entries'],
queryFn: fetchMemoryEntries,
});
const [modal, setModal] = useState<{ entry: EntryFormState; isNew: boolean } | null>(null);
const [deleting, setDeleting] = useState<string | null>(null);
const [deleteError, setDeleteError] = useState<string | null>(null);
const handleNew = () => {
setModal({
isNew: true,
entry: { name: '', description: '', type: 'user', body: '' },
});
};
const handleEdit = (e: MemoryEntry) => {
setModal({
isNew: false,
entry: {
name: e.name,
description: e.description,
type: e.type,
body: e.body,
},
});
};
const handleDelete = async (name: string) => {
if (!confirm(`メモリエントリ「${name}」を削除しますか?`)) return;
setDeleting(name);
setDeleteError(null);
try {
await deleteMemoryEntry(name);
await qc.invalidateQueries({ queryKey: ['memory-entries'] });
} catch (e: any) {
setDeleteError(`${name}」の削除に失敗しました: ${e.message}`);
} finally {
setDeleting(null);
}
};
const handleSaved = () => {
void qc.invalidateQueries({ queryKey: ['memory-entries'] });
};
return (
<div className="rounded-lg border border-hairline">
<div className="flex items-center justify-between px-4 py-3 border-b border-hairline bg-surface rounded-t-lg">
<div>
<h3 className="text-sm font-semibold text-slate-800"></h3>
<p className="text-2xs text-slate-500 mt-0.5">
</p>
</div>
<button
onClick={handleNew}
className="px-2.5 h-7 text-2xs font-medium bg-accent text-accent-fg rounded-md hover:bg-accent-deep transition-colors"
>
+
</button>
</div>
{isLoading && (
<div className="px-4 py-6 text-xs text-slate-400 text-center"></div>
)}
{error && (
<div className="px-4 py-3 text-xs text-red-600">
: {String(error)}
</div>
)}
{deleteError && (
<div className="px-4 py-2 text-xs text-red-600 dark:text-red-300 bg-red-50 dark:bg-red-500/15">
{deleteError}
</div>
)}
{data && data.entries.length === 0 && (
<div className="px-4 py-8 text-center">
<p className="text-xs text-slate-400"></p>
<p className="text-2xs text-slate-400 mt-1">
reflection
</p>
</div>
)}
{data && data.entries.length > 0 && (
<ul className="divide-y divide-hairline">
{data.entries.map(entry => (
<li key={entry.name} className="flex items-start gap-3 px-4 py-3 hover:bg-surface/60 transition-colors">
<div className="flex-1 min-w-0">
<div className="flex items-center gap-2 flex-wrap">
<span className="text-xs font-mono font-medium text-slate-800 truncate">
{entry.name}
</span>
<span className="text-[10px] px-1.5 py-0.5 rounded bg-slate-100 text-slate-500 flex-shrink-0">
{entry.type}
</span>
</div>
<p className="text-2xs text-slate-500 mt-0.5 truncate">{entry.description}</p>
{entry.body && (
<p className="text-2xs text-slate-400 mt-0.5 line-clamp-2 font-mono whitespace-pre-wrap break-words">
{entry.body.slice(0, 200)}{entry.body.length > 200 ? '…' : ''}
</p>
)}
</div>
<div className="flex gap-1.5 flex-shrink-0 mt-0.5">
<button
onClick={() => handleEdit(entry)}
className="px-2 h-6 text-2xs text-slate-600 border border-hairline bg-canvas hover:bg-surface rounded transition-colors"
>
</button>
<button
onClick={() => void handleDelete(entry.name)}
disabled={deleting === entry.name}
className="px-2 h-6 text-2xs text-red-700 dark:text-red-300 border border-red-200 bg-canvas hover:bg-red-50 dark:hover:bg-red-500/15 rounded transition-colors disabled:opacity-50"
>
{deleting === entry.name ? '…' : '削除'}
</button>
</div>
</li>
))}
</ul>
)}
{modal && (
<MemoryEntryModal
initial={modal.entry}
isNew={modal.isNew}
onClose={() => setModal(null)}
onSaved={handleSaved}
/>
)}
</div>
);
}
// ── MemoryPanel (User Folder root) ───────────────────────────────────────────
export function MemoryPanel() {
return (
<div className="h-full overflow-y-auto">
<div className="max-w-3xl mx-auto px-6 py-8 space-y-4">
<div>
<h2 className="text-base font-semibold text-slate-900 mb-1">memory/</h2>
<p className="text-[13px] text-slate-500 leading-relaxed">
MEMORY.mdindex system prompt
reflection revert Settings Reflection
</p>
</div>
<MemoryEntriesPanel />
</div>
</div>
);
}

View File

@ -13,6 +13,7 @@ import { SshConnectionsPanel } from './SshConnectionsPanel';
import { NotesPanel } from './NotesPanel'; import { NotesPanel } from './NotesPanel';
import { SubscriptionsPanel } from './SubscriptionsPanel'; import { SubscriptionsPanel } from './SubscriptionsPanel';
import { SkillsPanel } from './SkillsPanel'; import { SkillsPanel } from './SkillsPanel';
import { MemoryPanel } from './MemoryPanel';
/** All subdirs shown in the tree — both real file-based and virtual. */ /** All subdirs shown in the tree — both real file-based and virtual. */
const ALL_SUBDIRS: SubdirId[] = ['agents-md', 'browser-macros', 'recordings', 'notes', 'subscribed-notes', 'pets', 'browser-sessions', 'mcp', 'skills', 'ssh-connections', 'trash', 'memory']; const ALL_SUBDIRS: SubdirId[] = ['agents-md', 'browser-macros', 'recordings', 'notes', 'subscribed-notes', 'pets', 'browser-sessions', 'mcp', 'skills', 'ssh-connections', 'trash', 'memory'];
@ -63,8 +64,8 @@ const SUBDIR_INFO: { id: SubdirId; icon: string; title: string; desc: string; ag
id: 'memory', id: 'memory',
icon: '🧠', icon: '🧠',
title: 'memory/', title: 'memory/',
desc: 'エージェントの永続事実置き場。`MEMORY.md` (index) がタスク起動時に system prompt へ自動注入 (32 KB cap)。`{name}.md` は frontmatter (type ∈ user/feedback/project/reference) + 本文の構造。UpdateUserMemory / ReadUserMemory ツール経由でエージェントが管理。', desc: 'エージェントの永続事実置き場。`MEMORY.md` (index) がタスク起動時に system prompt へ自動注入 (32 KB cap)。エントリの閲覧・編集・削除はこのパネルで行う。reflection の実行履歴・revert は Settings → Reflection 履歴。',
agency: 'エージェント管理 / UI からは read-only', agency: 'エージェント管理 + ユーザー編集(このパネル)',
}, },
{ {
id: 'mcp', id: 'mcp',
@ -169,7 +170,7 @@ async function apiFolderDelete(subdir: SubdirId, path: string): Promise<void> {
} }
/** Virtual subdirs don't have real files on disk */ /** Virtual subdirs don't have real files on disk */
const VIRTUAL_SUBDIRS = new Set<SubdirId>(['agents-md', 'browser-sessions', 'mcp', 'skills', 'pets', 'ssh-connections', 'subscribed-notes']); const VIRTUAL_SUBDIRS = new Set<SubdirId>(['agents-md', 'browser-sessions', 'mcp', 'skills', 'pets', 'ssh-connections', 'subscribed-notes', 'memory']);
/** Subdirs where users can create new files from the UI */ /** Subdirs where users can create new files from the UI */
const WRITABLE_USER_SUBDIRS = new Set<SubdirId>(['browser-macros']); const WRITABLE_USER_SUBDIRS = new Set<SubdirId>(['browser-macros']);
@ -353,6 +354,11 @@ export function UserFolderTab({ showToast }: UserFolderTabProps = {}) {
<SkillsPanel /> <SkillsPanel />
)} )}
{/* memory virtual pane */}
{isVirtualSelected && selectedSubdir === 'memory' && (
<MemoryPanel />
)}
{/* pets virtual pane */} {/* pets virtual pane */}
{isVirtualSelected && selectedSubdir === 'pets' && ( {isVirtualSelected && selectedSubdir === 'pets' && (
<PetsPanel showToast={showToast} /> <PetsPanel showToast={showToast} />

View File

@ -27,7 +27,7 @@ TopBar → **ユーザーフォルダ** タブで開きます。左にサブフ
| ssh-connections/ | SSH 接続定義・暗号鍵 | ユーザー | | ssh-connections/ | SSH 接続定義・暗号鍵 | ユーザー |
| Subscribed Notes | 他ユーザーが公開した notes の購読 | ユーザー | | Subscribed Notes | 他ユーザーが公開した notes の購読 | ユーザー |
| trash/ | 削除ファイルの退避先(自動 cleanup | 自動 | | trash/ | 削除ファイルの退避先(自動 cleanup | 自動 |
| memory/ | エージェントの永続事実置き場(UI からは閲覧のみ) | エージェント | | memory/ | エージェントの永続事実置き場(閲覧・編集) | エージェント / ユーザー |
## AGENTS.md ## AGENTS.md
@ -79,7 +79,7 @@ CAPTCHA / 2FA を越えて取得した cookie / storage を user-scoped に暗
## memory/ ## memory/
エージェントの永続事実置き場です。User Folder からは閲覧のみで、編集は Settings → メモリと学習 から行います(→「[メモリと学習](12-memory.md)」)。 エージェントの永続事実置き場です。エントリの閲覧・編集・削除はこのパネルで行います。自動学習reflectionの実行履歴・revert は Settings → **Reflection 履歴** にあります(→「[メモリと学習](12-memory.md)」)。
## ファイルの作成・編集 ## ファイルの作成・編集

View File

@ -37,9 +37,9 @@ keywords: [メモリ, memory, MEMORY.md, 学習, Memory & Learning]
手動で書いたメモリは Reflection が動いていなくても確実に注入されるため、「いつも忘れられる」と感じる指示はメモリに 1 件書くのが確実です。 手動で書いたメモリは Reflection が動いていなくても確実に注入されるため、「いつも忘れられる」と感じる指示はメモリに 1 件書くのが確実です。
## メモリの閲覧・編集(Settings → Memory & Learning ## メモリの閲覧・編集(ユーザーフォルダ → memory/
Settings → **メモリと学習** で管理します。2 つのパネルが縦に並びます。 メモリエントリの管理は **ユーザーフォルダ → memory/** で行います。
### メモリエントリ ### メモリエントリ
@ -51,9 +51,7 @@ Settings → **メモリと学習** で管理します。2 つのパネルが縦
- **本文**Markdown またはプレーンテキスト) - **本文**Markdown またはプレーンテキスト)
- 各行の **編集** / **削除** ボタンで更新 - 各行の **編集** / **削除** ボタンで更新
メモリは User Folder の memory/ タブからは閲覧のみで、編集はこの画面で行います(→「[User Folder](09-userfolder.md)」)。 ### Reflection タイムラインSettings → Reflection 履歴)
### Reflection タイムライン
自動学習Reflectionの実行履歴です。各行を展開すると、推論・変更前後の差分・revert コントロールを確認できます。 自動学習Reflectionの実行履歴です。各行を展開すると、推論・変更前後の差分・revert コントロールを確認できます。

View File

@ -28,7 +28,7 @@ YAML キーは **スネークケース** (`max_concurrency`)、コード内は *
|-----------|------| |-----------|------|
| Preferences | 自分の新規タスクのデフォルト公開範囲などの個人設定 | | Preferences | 自分の新規タスクのデフォルト公開範囲などの個人設定 |
| 🔔 Notifications | ブラウザ通知 / Web Push の購読設定 | | 🔔 Notifications | ブラウザ通知 / Web Push の購読設定 |
| 🧠 Memory & Learning | memory エントリの編集と Reflection 履歴の閲覧・revert | | 🧠 Reflection 履歴 | Reflection の実行履歴・差分の閲覧と revertmemory エントリの編集は ユーザーフォルダ → memory/ |
通知の詳細は [ブラウザ通知](#notifications)、memory の詳細は [メモリと学習](#memory) を参照。 通知の詳細は [ブラウザ通知](#notifications)、memory の詳細は [メモリと学習](#memory) を参照。
@ -97,7 +97,7 @@ Gateway の運用は [LLM Gateway 連携](#llm-gateway) を参照。
- 未保存のまま別タブへ移動しようとすると確認ダイアログが出る (`useUnsavedGuard`) - 未保存のまま別タブへ移動しようとすると確認ダイアログが出る (`useUnsavedGuard`)
- 保存は **ETag ベースの楽観ロック**。他の管理者が先に保存していると「設定が他で変更されました。再読み込みしますか?」と表示される - 保存は **ETag ベースの楽観ロック**。他の管理者が先に保存していると「設定が他で変更されました。再読み込みしますか?」と表示される
- Preferences / Notifications / Memory & Learning の 3 つは個人 API で保存するため、この共通保存バーは出ない (各フォーム内で完結) - Preferences / Notifications / Reflection 履歴 の 3 つは個人 API で保存するため、この共通保存バーは出ない (各フォーム内で完結)
## 反映タイミング ## 反映タイミング

View File

@ -61,7 +61,7 @@ Reflection が piece への変更を提案しても、**組み込み piece (`pie
学習結果は次の 2 か所で見えます。 学習結果は次の 2 か所で見えます。
- **設定 → 🧠 Memory & Learning** — memory の現在値、Reflection の適用履歴、各履歴の **revert ボタン**。詳細は [メモリと学習](#memory) を参照 - **設定 → 🧠 Reflection 履歴** — Reflection の適用履歴と各履歴の **revert ボタン**memory の現在値・編集は ユーザーフォルダ → memory/。詳細は [メモリと学習](#memory) を参照
- **タスク詳細の概要タブ** — そのタスクの Reflection が実際に変更を加えた場合だけ **🧠 Learned N things** バッジが出る (piece も編集した場合は「+ piece edit」付き) - **タスク詳細の概要タブ** — そのタスクの Reflection が実際に変更を加えた場合だけ **🧠 Learned N things** バッジが出る (piece も編集した場合は「+ piece edit」付き)
revert は before snapshot から memory / piece を書き戻します。ユーザー自身の編集を上書きしないよう CAS (Compare-and-Swap) ベースで安全に実装されています。 revert は before snapshot から memory / piece を書き戻します。ユーザー自身の編集を上書きしないよう CAS (Compare-and-Swap) ベースで安全に実装されています。