sync: update from private repo (3a0870b)
Some checks failed
CI / build-and-test (push) Has been cancelled
Some checks failed
CI / build-and-test (push) Has been cancelled
This commit is contained in:
parent
9f8958c4a2
commit
dfc5950117
@ -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 & 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)の実行履歴を確認できます。
|
自動学習(reflection)の実行履歴・差分・revert を確認できます。
|
||||||
|
メモリエントリの閲覧・編集は ユーザーフォルダ → memory/ へ移動しました。
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<MemoryEntriesPanel />
|
|
||||||
<ReflectionTimelinePanel />
|
<ReflectionTimelinePanel />
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@ -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 履歴' },
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|||||||
@ -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,
|
||||||
|
|||||||
387
ui/src/components/userfolder/MemoryPanel.tsx
Normal file
387
ui/src/components/userfolder/MemoryPanel.tsx
Normal 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.md(index)がタスク起動時に system prompt へ自動注入されます。
|
||||||
|
自動学習(reflection)の実行履歴と revert は Settings → Reflection 履歴 で確認できます。
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<MemoryEntriesPanel />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@ -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} />
|
||||||
|
|||||||
@ -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)」)。
|
||||||
|
|
||||||
## ファイルの作成・編集
|
## ファイルの作成・編集
|
||||||
|
|
||||||
|
|||||||
@ -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 コントロールを確認できます。
|
||||||
|
|
||||||
|
|||||||
@ -28,7 +28,7 @@ YAML キーは **スネークケース** (`max_concurrency`)、コード内は *
|
|||||||
|-----------|------|
|
|-----------|------|
|
||||||
| Preferences | 自分の新規タスクのデフォルト公開範囲などの個人設定 |
|
| Preferences | 自分の新規タスクのデフォルト公開範囲などの個人設定 |
|
||||||
| 🔔 Notifications | ブラウザ通知 / Web Push の購読設定 |
|
| 🔔 Notifications | ブラウザ通知 / Web Push の購読設定 |
|
||||||
| 🧠 Memory & Learning | memory エントリの編集と Reflection 履歴の閲覧・revert |
|
| 🧠 Reflection 履歴 | Reflection の実行履歴・差分の閲覧と revert(memory エントリの編集は ユーザーフォルダ → 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 で保存するため、この共通保存バーは出ない (各フォーム内で完結)
|
||||||
|
|
||||||
## 反映タイミング
|
## 反映タイミング
|
||||||
|
|
||||||
|
|||||||
@ -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) ベースで安全に実装されています。
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user