diff --git a/.gitignore b/.gitignore index d85ab2ae7..3ba197c80 100644 --- a/.gitignore +++ b/.gitignore @@ -3,6 +3,15 @@ node_modules/ .planning/ supabase/ +# Maintainer-local overlays (not part of upstream contribution contract) +.local/ +.agent/ +.claude.json + +# Python caches +__pycache__/ +*.pyc + # Credentials & runtime state .env .env.local @@ -14,3 +23,7 @@ sync-log.json # Build artifacts deno.lock +docs/walkthroughs/*/public/ +docs/assets/agent-memory/promotional/generated-backgrounds/**/result.json +docs/assets/agent-memory/screenshots/**/.chrome-*/ +integrations/openclaw-agent-memory/dist/ diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 000000000..e30862632 --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,20 @@ +# OB1 Agent Instructions + +## Required Step: Update Linear + +- For feature work tied to a Linear issue, update Linear at the start of the work, at meaningful checkpoints, and before handing back to the user. +- Use the parent issue as the living implementation log and keep child issues aligned with the files and behavior being changed. +- For the OB1 Agent Memory / OpenClaw launch work, the parent issue is `NAT-833`. Record architecture notes, implementation milestones, blockers, and verification results there. +- Do not wait until the end to document decisions. If a decision changes schema, API contract, trust policy, user-facing workflow, or publishing path, capture it in Linear while it is still fresh. + +## Agent Memory Product Guardrails + +- Keep `OB1 Agent Memory` runtime-neutral. OpenClaw is the flagship launch runtime, not the product boundary. +- Treat inferred or generated memory as evidence by default. Instruction-grade memory requires human confirmation or trusted import. +- Avoid raw transcript, model reasoning trace, secret, and large-code-block storage by default. +- Avoid profanity in all content. Keep docs, examples, seed data, UI copy, prompts, walkthroughs, and generated assets clean and professional. +- Prefer diagram-first documentation for this work: diagram, short explanation, copy-paste setup, then deeper reference. +- Carry Nate B. Jones / OB1 provenance through product surfaces, docs, diagrams, screenshots, and starter seed data. Keep it subtle and useful: micro-branding, source labels, logo marks, and provenance language instead of loud marketing copy. +- Treat public OB1 assets as helpful-first audience growth for Nate Jones. Every public guide, recipe, tutorial, package page, release note, and walkthrough should point back to Nate's Substack and site in a natural way: https://substack.com/@natesnewsletter and https://natebjones.com. +- Make the case by being genuinely useful. The CTA should feel earned: "Nate gives away practical systems like this" rather than generic marketing copy. +- For ClawHub/OpenClaw publishing, do not fall back to Jonathan's personal handle or any non-Nate namespace. If `@natebjones` / Nate OB1 ownership is not available, stop and record the blocker. diff --git a/CLAUDE.md b/CLAUDE.md index 53941a657..08513493c 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -30,6 +30,7 @@ Every contribution lives in its own subfolder under the right category and must - **No credentials, API keys, or secrets in any file.** Use environment variables. - **No binary blobs** over 1MB. No `.exe`, `.dmg`, `.zip`, `.tar.gz`. - **No `DROP TABLE`, `DROP DATABASE`, `TRUNCATE`, or unqualified `DELETE FROM`** in SQL files. +- **Avoid profanity in all content.** Keep docs, examples, seed data, UI copy, prompts, walkthroughs, and generated assets clean and professional. - **MCP servers must be remote (Supabase Edge Functions), not local.** Never use `claude_desktop_config.json`, `StdioServerTransport`, or local Node.js servers. All extensions deploy as Edge Functions and connect via Claude Desktop's custom connectors UI (Settings → Connectors → Add custom connector → paste URL). See `docs/01-getting-started.md` Step 7 for the pattern. ## PR Standards @@ -47,3 +48,12 @@ Every contribution lives in its own subfolder under the right category and must - `.github/metadata.schema.json` — JSON schema for metadata.json validation - `.github/PULL_REQUEST_TEMPLATE.md` — PR description template - `LICENSE.md` — FSL-1.1-MIT terms + +## Local GSD Execution Layer + +This repo also has a maintainer-local GSD layer in `.planning/`. + +- If `.planning/` exists, use it for local brownfield planning and phased execution. +- Start with `.planning/STATE.md`, then read `.planning/PROJECT.md`, `.planning/ROADMAP.md`, and the relevant `.planning/codebase/*.md` documents. +- Keep `.planning/` local. It is gitignored intentionally and is not part of the public contribution contract or upstream PR scope. +- Public contributor rules still come from `CLAUDE.md`, `AGENTS.md`, `CONTRIBUTING.md`, and the committed repo files. diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 780f837f9..c73e8d1d5 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -128,6 +128,7 @@ grant select, insert, update, delete on table public.your_table to service_role; - **"Cross-Extension Integration"** prominently documenting connections to other extensions - **"Next Steps"** linking to the next extension - **Tool audit link** — Any extension or integration that exposes MCP tools must link to the [MCP Tool Audit & Optimization Guide](docs/05-tool-audit.md) in its "Next Steps" or closing section. This helps users manage their tool surface area as they add extensions. The link is checked by the automated review. +- **MCP tool annotations** — Any extension or integration that exposes MCP tools must mark read-only tools with `annotations: { readOnlyHint: true }` and write tools with `annotations: { readOnlyHint: false, openWorldHint: false, destructiveHint: false }` unless the tool really can touch arbitrary external resources or destroy data. ChatGPT uses this metadata to distinguish read tools from write actions. - **Remote MCP setup** — MCP servers must be deployed as Supabase Edge Functions and connected via custom connectors (URL-based). Do NOT use local Node.js servers or `claude_desktop_config.json`. See the [extension template](extensions/_template/) for the correct pattern. **Primitives** additionally require: @@ -298,3 +299,4 @@ Every PR is checked against these rules. All must pass before human review. 13. **Internal links** — All relative links in READMEs resolve to existing files 14. **Remote MCP pattern** — Extensions and integrations must use remote MCP via Supabase Edge Functions. No `claude_desktop_config.json`, no local Node.js stdio servers. See the [Getting Started guide](docs/01-getting-started.md) for the correct pattern 15. **Tool audit link** — Extensions and integrations must link to the [MCP Tool Audit & Optimization Guide](docs/05-tool-audit.md) in their README. This ensures users are aware of tool surface area management as they add capabilities +16. **MCP tool annotations** — Read-only tools include `readOnlyHint: true`; write tools include `readOnlyHint: false`, `openWorldHint`, and `destructiveHint` diff --git a/README.md b/README.md index f2daea9e2..ebca0d5c6 100644 --- a/README.md +++ b/README.md @@ -94,6 +94,9 @@ Standalone capabilities that make your Open Brain smarter. | [Work Operating Model Activation](recipes/work-operating-model-activation/) | Conversation-first workflow that turns tacit work patterns into structured Open Brain records and agent-ready operating files | [@jonathanedwards](https://github.com/jonathanedwards) | | [World Model Diagnostic Activation](recipes/world-model-diagnostic-activation/) | Ship-now activation path for a 20-minute world-model readiness diagnostic that compounds through core Open Brain capture | [@jonathanedwards](https://github.com/jonathanedwards) | | [Research-to-Decision Workflow](recipes/research-to-decision-workflow/) | Composition recipe that chains canonical skills into operator and investor research, synthesis, meeting, and memo workflows | [@NateBJones](https://github.com/NateBJones) | +| [OpenClaw Agent Memory for OB1](recipes/openclaw-agent-memory/) | Canonical recipe for using OB1 Agent Memory as the governed continuity layer for OpenClaw workflows | OB1 Team | +| [OpenClaw Code Review Memory](recipes/openclaw-code-review-memory/) | Flagship workflow for compounding repo-specific review lessons, maintainer corrections, and false positives | OB1 Team | +| [OpenClaw TaskFlow Work Log](recipes/openclaw-taskflow-work-log/) | Durable handoff recipe for long-running OpenClaw TaskFlows across agents, models, and channels | OB1 Team | ### [`/skills`](skills/) — Agent Skills @@ -111,6 +114,7 @@ Plain-text skill packs you can drop into Claude Code, Codex, or other AI clients | [Aiception Skill Pack (formerly Claudeception)](skills/claudeception/) | Extracts reusable lessons from work sessions into new skills | [@jaredirish](https://github.com/jaredirish) | | [Work Operating Model Skill Pack](skills/work-operating-model/) | Runs a five-layer elicitation interview and saves the approved operating model into Open Brain | [@jonathanedwards](https://github.com/jonathanedwards) | | [World Model Readiness Diagnostic](skills/world-model-diagnostic/) | Runs a 20-minute world-model diagnostic that maps paradigm fit, audits the boundary layer, and labels findings by confidence | [@jonathanedwards](https://github.com/jonathanedwards) | +| [OpenClaw Agent Memory Skill Pack](skills/openclaw-agent-memory/) | Teaches OpenClaw agents to recall, write back, report usage, and respect OB1 provenance/use-policy rules | OB1 Team | ### [`/dashboards`](dashboards/) — Frontend Templates @@ -128,9 +132,19 @@ MCP server extensions, alternative deployment targets, and capture sources beyon | Integration | What It Does | Contributor | | ----------- | ------------ | ----------- | | [Kubernetes Deployment](integrations/kubernetes-deployment/) | Fully self-hosted K8s deployment with PostgreSQL + pgvector — no Supabase required | [@velo](https://github.com/velo) | +| [Agent Memory API](integrations/agent-memory-api/) | Runtime-neutral recall, write-back, review, inspector, and recall-trace API for OB1 Agent Memory | OB1 Team | +| [OpenClaw Agent Memory](integrations/openclaw-agent-memory/) | OpenClaw plugin and publishing package for using OB1 Agent Memory from OpenClaw workflows | OB1 Team | | [Slack Capture](integrations/slack-capture/) | Quick-capture thoughts via Slack messages with auto-embedding and classification | Core | | [Discord Capture](integrations/discord-capture/) | Discord bot that captures messages into Open Brain, mirroring the Slack pattern | Core | +### [`/schemas`](schemas/) — Database Extensions + +Tables and sidecars that extend the base `thoughts` model without replacing it. + +| Schema | What It Does | Contributor | +| ------ | ------------ | ----------- | +| [Agent Memory](schemas/agent-memory/) | Provenance, review, use-policy, source-reference, relation, recall-trace, and audit sidecars for agent workflow memory | OB1 Team | + ### [`/primitives`](primitives/) — Reusable Patterns | Primitive | What It Does | Contributor | diff --git a/dashboards/open-brain-dashboard-next/.env.example b/dashboards/open-brain-dashboard-next/.env.example index 573d27c0d..ff5222665 100644 --- a/dashboards/open-brain-dashboard-next/.env.example +++ b/dashboards/open-brain-dashboard-next/.env.example @@ -1,10 +1,28 @@ # Required: URL of your Open Brain REST API NEXT_PUBLIC_API_URL=https://YOUR-PROJECT-REF.supabase.co/functions/v1/open-brain-rest +# Optional: URL of your OB1 Agent Memory API. +# If omitted, the dashboard derives it from NEXT_PUBLIC_API_URL by replacing open-brain-rest with agent-memory-api. +# AGENT_MEMORY_API_URL=https://YOUR-PROJECT-REF.supabase.co/functions/v1/agent-memory-api + +# Optional: default Agent Memory scope for the dashboard governance views. +# AGENT_MEMORY_WORKSPACE_ID=ob1-staging +# AGENT_MEMORY_PROJECT_ID= + # Required: 32+ character secret for iron-session cookie encryption # Generate with: openssl rand -hex 32 SESSION_SECRET= +# Optional: force HTTPS-only auth cookies. Leave unset for localhost previews. +# Set to true on HTTPS hosts if NEXT_PUBLIC_APP_URL/APP_URL is not configured. +# AUTH_COOKIE_SECURE=true + +# Optional: local walkthrough/screenshot mode only. +# Bypasses login and uses this demo key against a local demo REST shim. +# Never enable this in a shared preview or production deployment. +# OB1_DEMO_AUTH_BYPASS=false +# OB1_DASHBOARD_DEMO_KEY=local-screenshot-key + # Optional: SHA-256 hash of passphrase to unlock restricted/sensitive content # Requires the sensitivity-tiers primitive (sensitivity_tier column on thoughts) # Generate with: echo -n "your-passphrase" | shasum -a 256 diff --git a/dashboards/open-brain-dashboard-next/README.md b/dashboards/open-brain-dashboard-next/README.md index 87e19fb53..492cbf8bd 100644 --- a/dashboards/open-brain-dashboard-next/README.md +++ b/dashboards/open-brain-dashboard-next/README.md @@ -26,11 +26,12 @@ Provides 9 pages for managing your thoughts: | **Add to Brain** | Smart ingest with auto-routing — short text goes to single capture, long text to extraction with dry-run preview | | **Audit** | Quality review for low-score thoughts with bulk delete | | **Duplicates** | Semantic similarity detection with keep/delete/keep-both resolution | +| **Agent Memory** | Review queue, memory inspector, and recall trace debugging for OB1 Agent Memory | | **Login** | API key authentication via encrypted session cookie | ## Prerequisites -- A working Open Brain setup with the **REST API gateway** (`open-brain-rest`) deployed +- A working Open Brain setup with the **REST API gateway** (`open-brain-rest`) deployed from [integrations/open-brain-rest](../../integrations/open-brain-rest/) - **Node.js 18+** installed - A **Vercel account** (free tier works) or any Node.js hosting @@ -39,7 +40,13 @@ Provides 9 pages for managing your thoughts: | Credential | Where to get it | Where it goes | |------------|----------------|---------------| | `NEXT_PUBLIC_API_URL` | Your Supabase project URL + `/functions/v1/open-brain-rest` | `.env` or hosting env vars | +| `AGENT_MEMORY_API_URL` | Optional. Your Supabase project URL + `/functions/v1/agent-memory-api` | `.env` or hosting env vars | +| `AGENT_MEMORY_WORKSPACE_ID` | Optional. Default workspace for Agent Memory governance views | `.env` or hosting env vars | +| `AGENT_MEMORY_PROJECT_ID` | Optional. Default project filter for Agent Memory governance views | `.env` or hosting env vars | | `SESSION_SECRET` | Generate: `openssl rand -hex 32` | `.env` or hosting env vars | +| `AUTH_COOKIE_SECURE` | Optional. Force HTTPS-only auth cookies when set to `true`; leave unset for localhost previews | `.env` or hosting env vars | +| `OB1_DEMO_AUTH_BYPASS` | Optional. Local walkthrough capture only; bypasses login when set to `true` | local shell only | +| `OB1_DASHBOARD_DEMO_KEY` | Optional. Local walkthrough capture key used by the demo REST shim | local shell only | | `RESTRICTED_PASSPHRASE_HASH` | Optional. Generate: `echo -n "passphrase" \| shasum -a 256` | `.env` or hosting env vars | ## Steps @@ -69,7 +76,12 @@ Edit `.env` and set your values: ``` NEXT_PUBLIC_API_URL=https://YOUR-PROJECT-REF.supabase.co/functions/v1/open-brain-rest +# Optional if your Agent Memory function follows the standard slug: +# AGENT_MEMORY_API_URL=https://YOUR-PROJECT-REF.supabase.co/functions/v1/agent-memory-api +# AGENT_MEMORY_WORKSPACE_ID=ob1-staging SESSION_SECRET=your-32-char-secret-here +# Optional on HTTPS hosts: +# AUTH_COOKIE_SECURE=true ``` ### Step 4: Run locally @@ -103,6 +115,7 @@ When working correctly: - **Search** returns results with similarity scores (semantic mode) or rank scores (full-text mode) - **Add to Brain** auto-routes short text (< 500 chars, single paragraph) to single capture, and long/structured text to extraction with dry-run preview - **Detail page** shows full thought content with metadata, inline edit for content/type/importance, and linked reflections +- **Agent Memory** shows pending agent-written memories, lets you confirm/evidence-only/reject them, inspects provenance/source data, and loads recall traces by request id ## Workflow Board @@ -162,9 +175,22 @@ The dashboard calls these endpoints on your Open Brain REST API: | `/thoughts?type=task` | GET | Workflow board (filtered by type) | | `/thought/:id` | PUT | Workflow board (status/priority updates) | +Agent Memory pages also call these endpoints on `agent-memory-api`: + +| Endpoint | Method | Used By | +|----------|--------|---------| +| `/memories` | GET | Agent Memory list by status/scope | +| `/memories/review` | GET | Pending review queue | +| `/memories/:id` | GET | Memory inspector | +| `/memories/:id/review` | PATCH | Confirm, evidence-only, reject, stale, or restrict memory | +| `/recall-traces/:request_id` | GET | Recall trace debugger | + > [!NOTE] > If your Open Brain instance doesn't have all these endpoints (e.g., no smart-ingest or duplicates), those pages will show errors but the core pages (dashboard, browse, search, detail) will still work. +> [!IMPORTANT] +> OB1's real `thoughts.id` values are UUID strings. The dashboard treats thought IDs as strings end to end so detail links, workflow updates, audit deletes, and duplicate resolution work against production Supabase rows. + ## Optional: Restricted Content If you've applied the [sensitivity-tiers](https://github.com/NateBJones-Projects/OB1/pull/110) primitive and want to control access to sensitive thoughts: @@ -188,6 +214,21 @@ The dashboard uses **iron-session** for encrypted HTTP-only session cookies: No API key is stored in environment variables or exposed to the browser. +## Local Walkthrough Capture + +The walkthrough asset pipeline lives in [docs/walkthroughs/ob1-agent-dashboard](../../docs/walkthroughs/ob1-agent-dashboard). It seeds the Dashboard, Thoughts, Workflow, Duplicates, Audit, Agent Memory, and Recall Trace surfaces with Nate B. Jones / OB1 demo data for screenshots, PDF guides, and video walkthroughs. + +The dashboard has a gated local-only bypass for that capture flow: + +```bash +OB1_DEMO_AUTH_BYPASS=true +OB1_DASHBOARD_DEMO_KEY=local-screenshot-key +NEXT_PUBLIC_API_URL=http://127.0.0.1:3024 +AGENT_MEMORY_API_URL=http://127.0.0.1:3022 +``` + +Do not enable `OB1_DEMO_AUTH_BYPASS` in shared previews or production. It exists so repeatable screenshot and video generation can run without putting real API keys in browser automation. + ## Tech Stack - **Next.js 16** (App Router) diff --git a/dashboards/open-brain-dashboard-next/app/agent-memory/[id]/page.tsx b/dashboards/open-brain-dashboard-next/app/agent-memory/[id]/page.tsx new file mode 100644 index 000000000..c99c7c748 --- /dev/null +++ b/dashboards/open-brain-dashboard-next/app/agent-memory/[id]/page.tsx @@ -0,0 +1,182 @@ +import Link from "next/link"; +import type { ReactNode } from "react"; +import { notFound } from "next/navigation"; +import { revalidatePath } from "next/cache"; +import { + fetchAgentMemory, + reviewAgentMemory, +} from "@/lib/agent-memory"; +import { requireSessionOrRedirect } from "@/lib/auth"; +import { + MemoryRecordPolicy, + ProvenanceBadge, + StatusBadge, +} from "@/components/AgentMemoryBadges"; +import { FormattedDate } from "@/components/FormattedDate"; +import type { AgentMemoryReviewAction } from "@/lib/types"; + +export const dynamic = "force-dynamic"; + +export default async function AgentMemoryDetailPage({ + params, +}: { + params: Promise<{ id: string }>; +}) { + const { apiKey } = await requireSessionOrRedirect(); + const { id } = await params; + + let memory; + try { + memory = await fetchAgentMemory(apiKey, id); + } catch { + notFound(); + } + + async function reviewAction(formData: FormData) { + "use server"; + const { apiKey } = await requireSessionOrRedirect(); + const action = String(formData.get("action") || "") as AgentMemoryReviewAction; + await reviewAgentMemory(apiKey, id, action, { + actor_label: "Open Brain dashboard", + notes: `Dashboard detail ${action}`, + }); + revalidatePath(`/agent-memory/${id}`); + revalidatePath("/agent-memory"); + } + + return ( +
+
+
+ + Back to Agent Memory + +

Memory Inspector

+

+ {memory.summary} +

+
+
+
+ + +
+
+ + +
+
+ + +
+
+
+ +
+
+
+
+ + + +
+

+ {memory.content} +

+
+ +
+

Source References

+ {memory.agent_memory_source_refs?.length ? ( +
+ {memory.agent_memory_source_refs.map((source) => ( +
+

+ {source.title || source.source_kind} +

+

{source.source_kind}

+ {source.uri && ( +

+ {source.uri} +

+ )} +
+ ))} +
+ ) : ( +

No source references saved.

+ )} +
+ +
+

Metadata

+
+              {JSON.stringify(memory.metadata, null, 2)}
+            
+
+
+ + +
+
+ ); +} + +function Row({ + label, + value, +}: { + label: string; + value: ReactNode; +}) { + return ( +
+
{label}
+
{value}
+
+ ); +} diff --git a/dashboards/open-brain-dashboard-next/app/agent-memory/page.tsx b/dashboards/open-brain-dashboard-next/app/agent-memory/page.tsx new file mode 100644 index 000000000..c18d05590 --- /dev/null +++ b/dashboards/open-brain-dashboard-next/app/agent-memory/page.tsx @@ -0,0 +1,221 @@ +import Link from "next/link"; +import { revalidatePath } from "next/cache"; +import { + agentMemoryDefaults, + fetchAgentMemories, + reviewAgentMemory, +} from "@/lib/agent-memory"; +import { requireSessionOrRedirect } from "@/lib/auth"; +import { + PolicyBadges, + ProvenanceBadge, + StatusBadge, +} from "@/components/AgentMemoryBadges"; +import { FormattedDate } from "@/components/FormattedDate"; +import type { AgentMemoryReviewAction } from "@/lib/types"; + +export const dynamic = "force-dynamic"; + +const STATUSES = ["pending", "evidence_only", "confirmed", "rejected", "stale"] as const; +const STATUS_LABELS: Record = { + pending: "Pending", + evidence_only: "Evidence", + confirmed: "Confirmed", + rejected: "Rejected", + stale: "Stale", + all: "All", +}; + +export default async function AgentMemoryPage({ + searchParams, +}: { + searchParams: Promise>; +}) { + const { apiKey } = await requireSessionOrRedirect(); + const defaults = agentMemoryDefaults(); + const params = await searchParams; + const workspaceId = params.workspace_id || defaults.workspaceId; + const projectId = params.project_id ?? defaults.projectId; + const status = params.review_status ?? "pending"; + const limit = parseInt(params.limit || "50", 10); + + let data; + let error: string | null = null; + try { + data = await fetchAgentMemories(apiKey, { + workspace_id: workspaceId, + project_id: projectId || undefined, + review_status: status === "all" ? undefined : status, + limit, + }); + } catch (err) { + data = { memories: [], count: 0 }; + error = err instanceof Error ? err.message : "Failed to load agent memory"; + } + + async function reviewAction(formData: FormData) { + "use server"; + const { apiKey } = await requireSessionOrRedirect(); + const memoryId = String(formData.get("memory_id") || ""); + const action = String(formData.get("action") || "") as AgentMemoryReviewAction; + await reviewAgentMemory(apiKey, memoryId, action, { + actor_label: "Open Brain dashboard", + notes: `Dashboard ${action}`, + }); + revalidatePath("/agent-memory"); + } + + function statusUrl(nextStatus: string) { + const sp = new URLSearchParams(); + sp.set("workspace_id", workspaceId); + if (projectId) sp.set("project_id", projectId); + sp.set("review_status", nextStatus); + return `/agent-memory?${sp.toString()}`; + } + + return ( +
+
+
+ {STATUSES.map((item) => ( + + {STATUS_LABELS[item]} + + ))} + + All + +
+ +
+ Nate Jones Personal OB1 +

+ {workspaceId} + / + {projectId || "all-projects"} +

+ + Recall traces + +
+
+ + {error &&

{error}

} + +
+ + + + + + + + + + + + {data.memories.length === 0 ? ( + + + + ) : ( + data.memories.map((memory) => ( + + + + + + + + )) + )} + +
+ Memories:{" "} + + {data.count}/{limit} + + TrustPolicyCreatedAction
+ No agent memories match this view. +
+ + {memory.summary} + +

+ {memory.content} +

+
+ {memory.scope.visibility} + {memory.scope.project_id && {memory.scope.project_id}} + {memory.provenance.runtime && {memory.provenance.runtime}} +
+
+
+ + +
+

+ Confidence {(memory.provenance.confidence * 100).toFixed(0)}% +

+
+ + + + +
+
+ + + +
+
+ + + +
+
+ + + +
+
+
+
+
+ ); +} diff --git a/dashboards/open-brain-dashboard-next/app/agent-memory/traces/page.tsx b/dashboards/open-brain-dashboard-next/app/agent-memory/traces/page.tsx new file mode 100644 index 000000000..898665dca --- /dev/null +++ b/dashboards/open-brain-dashboard-next/app/agent-memory/traces/page.tsx @@ -0,0 +1,164 @@ +import Link from "next/link"; +import { fetchRecallTrace } from "@/lib/agent-memory"; +import { requireSessionOrRedirect } from "@/lib/auth"; +import { PolicyBadges } from "@/components/AgentMemoryBadges"; +import { FormattedDate } from "@/components/FormattedDate"; + +export const dynamic = "force-dynamic"; + +export default async function RecallTracePage({ + searchParams, +}: { + searchParams: Promise>; +}) { + const { apiKey } = await requireSessionOrRedirect(); + const params = await searchParams; + const requestId = params.request_id || ""; + + let data = null; + let error: string | null = null; + if (requestId) { + try { + data = await fetchRecallTrace(apiKey, requestId); + } catch (err) { + error = err instanceof Error ? err.message : "Failed to load recall trace"; + } + } + + return ( +
+
+
+ + Back to Agent Memory + +

Retrieval Debugging

+

Recall Trace

+
+

+ Debug what an agent asked for, what OB1 returned, and what the agent used. +

+
+ +
+ + +
+ + {error &&

{error}

} + + {!requestId && ( +
+ Waiting for a recall request id from an OpenClaw run, API smoke harness, or Agent Memory API response. +
+ )} + + {data && ( +
+
+
+ {data.trace.request_id} + {data.trace.runtime_name && {data.trace.runtime_name}} + {data.trace.project_id && {data.trace.project_id}} + + + +
+

{data.trace.query}

+
+ +
+ + + + + + + + + + + + {data.items.map((item) => ( + + + + + + + + ))} + +
RankMemoryScorePolicyUse
+ {item.rank} + + {item.agent_memories ? ( + + {item.agent_memories.summary} + + ) : ( + {item.memory_id} + )} + {item.agent_memories?.content && ( +

+ {item.agent_memories.content} +

+ )} +
+

Similarity {formatScore(item.similarity)}

+

Rank {formatScore(item.ranking_score)}

+
+ + + {item.used === true ? ( + used + ) : item.used === false ? ( + + ignored{item.ignored_reason ? `: ${item.ignored_reason}` : ""} + + ) : ( + unreported + )} +
+
+ +
+

Request Payload

+
+              {JSON.stringify(data.trace.request_payload, null, 2)}
+            
+
+
+ )} +
+ ); +} + +function formatScore(value: number | null) { + return value === null || value === undefined ? "n/a" : Number(value).toFixed(3); +} diff --git a/dashboards/open-brain-dashboard-next/app/api/audit/delete/route.ts b/dashboards/open-brain-dashboard-next/app/api/audit/delete/route.ts index c87410ddd..7c4f2344f 100644 --- a/dashboards/open-brain-dashboard-next/app/api/audit/delete/route.ts +++ b/dashboards/open-brain-dashboard-next/app/api/audit/delete/route.ts @@ -14,7 +14,7 @@ export async function POST(request: NextRequest) { } try { - const { ids } = (await request.json()) as { ids: number[] }; + const { ids } = (await request.json()) as { ids: string[] }; if (!Array.isArray(ids) || ids.length === 0) { return NextResponse.json({ error: "No IDs provided" }, { status: 400 }); } diff --git a/dashboards/open-brain-dashboard-next/app/api/duplicates/resolve/route.ts b/dashboards/open-brain-dashboard-next/app/api/duplicates/resolve/route.ts index 4b352b4ab..e9d5556e6 100644 --- a/dashboards/open-brain-dashboard-next/app/api/duplicates/resolve/route.ts +++ b/dashboards/open-brain-dashboard-next/app/api/duplicates/resolve/route.ts @@ -15,8 +15,8 @@ export async function POST(request: NextRequest) { try { const { action, thought_id_a, thought_id_b } = (await request.json()) as { action: "keep_a" | "keep_b" | "keep_both"; - thought_id_a: number; - thought_id_b: number; + thought_id_a: string; + thought_id_b: string; }; if (!thought_id_a || !thought_id_b) { diff --git a/dashboards/open-brain-dashboard-next/app/api/kanban/delete/route.ts b/dashboards/open-brain-dashboard-next/app/api/kanban/delete/route.ts index cee605279..67bfe5626 100644 --- a/dashboards/open-brain-dashboard-next/app/api/kanban/delete/route.ts +++ b/dashboards/open-brain-dashboard-next/app/api/kanban/delete/route.ts @@ -16,9 +16,9 @@ export async function POST(request: NextRequest) { const body = await request.json(); const { thoughtId } = body; - if (!thoughtId || typeof thoughtId !== "number") { + if (!thoughtId || typeof thoughtId !== "string") { return NextResponse.json( - { error: "thoughtId (number) is required" }, + { error: "thoughtId (string) is required" }, { status: 400 } ); } diff --git a/dashboards/open-brain-dashboard-next/app/api/kanban/update/route.ts b/dashboards/open-brain-dashboard-next/app/api/kanban/update/route.ts index 532f69afe..658ca4b1f 100644 --- a/dashboards/open-brain-dashboard-next/app/api/kanban/update/route.ts +++ b/dashboards/open-brain-dashboard-next/app/api/kanban/update/route.ts @@ -25,9 +25,9 @@ export async function POST(request: NextRequest) { const body = await request.json(); const { thoughtId, status, importance, content, type } = body; - if (!thoughtId || typeof thoughtId !== "number") { + if (!thoughtId || typeof thoughtId !== "string") { return NextResponse.json( - { error: "thoughtId (number) is required" }, + { error: "thoughtId (string) is required" }, { status: 400 } ); } diff --git a/dashboards/open-brain-dashboard-next/app/audit/page.tsx b/dashboards/open-brain-dashboard-next/app/audit/page.tsx index 463eb4d10..9e227bb83 100644 --- a/dashboards/open-brain-dashboard-next/app/audit/page.tsx +++ b/dashboards/open-brain-dashboard-next/app/audit/page.tsx @@ -8,7 +8,7 @@ import type { Thought, BrowseResponse } from "@/lib/types"; export default function AuditPage() { const [data, setData] = useState(null); - const [selected, setSelected] = useState>(new Set()); + const [selected, setSelected] = useState>(new Set()); const [page, setPage] = useState(1); const [loading, setLoading] = useState(true); const [showDelete, setShowDelete] = useState(false); @@ -33,7 +33,7 @@ export default function AuditPage() { load(); }, [load]); - const toggleSelect = (id: number) => { + const toggleSelect = (id: string) => { setSelected((prev) => { const next = new Set(prev); if (next.has(id)) next.delete(id); diff --git a/dashboards/open-brain-dashboard-next/app/globals.css b/dashboards/open-brain-dashboard-next/app/globals.css index ab4324b21..ccc49fbf7 100644 --- a/dashboards/open-brain-dashboard-next/app/globals.css +++ b/dashboards/open-brain-dashboard-next/app/globals.css @@ -1,23 +1,23 @@ @import "tailwindcss"; @theme inline { - --color-bg-primary: #0a0a0f; - --color-bg-surface: #111118; - --color-bg-elevated: #1a1a24; - --color-bg-hover: #222230; - --color-border: #2a2a3a; - --color-border-subtle: #1e1e2e; - --color-text-primary: #e8e8ef; - --color-text-secondary: #9090a8; - --color-text-muted: #606078; - --color-violet: #8b5cf6; - --color-violet-dim: #7c3aed; - --color-violet-glow: rgba(139, 92, 246, 0.15); - --color-violet-surface: rgba(139, 92, 246, 0.08); - --color-success: #34d399; - --color-warning: #fbbf24; - --color-danger: #f87171; - --color-info: #60a5fa; + --color-bg-primary: #1d201e; + --color-bg-surface: rgba(255, 255, 255, 0.045); + --color-bg-elevated: rgba(255, 255, 255, 0.07); + --color-bg-hover: rgba(255, 255, 255, 0.085); + --color-border: rgba(236, 229, 209, 0.16); + --color-border-subtle: rgba(236, 229, 209, 0.085); + --color-text-primary: #f2efe5; + --color-text-secondary: rgba(242, 239, 229, 0.68); + --color-text-muted: rgba(242, 239, 229, 0.42); + --color-violet: #b7d985; + --color-violet-dim: #9ac466; + --color-violet-glow: rgba(183, 217, 133, 0.18); + --color-violet-surface: rgba(183, 217, 133, 0.1); + --color-success: #87d18a; + --color-warning: #e7c565; + --color-danger: #ed8a7a; + --color-info: #8ec9d9; --font-sans: var(--font-geist-sans), system-ui, sans-serif; --font-mono: var(--font-geist-mono), monospace; } @@ -28,6 +28,180 @@ body { font-family: var(--font-sans); } +.ob1-graphite-bg { + --ob1-fiber-light-opacity: 0.018; + --ob1-fiber-dark-opacity: 0.05; + --ob1-bg-tilt: 4deg; + --ob1-bg-rake: -18deg; + position: relative; + isolation: isolate; + background: + linear-gradient(180deg, rgba(255, 255, 255, 0.018), transparent 34rem), + var(--color-bg-primary); +} + +.ob1-graphite-bg::before { + content: ""; + position: fixed; + top: 50%; + left: 50%; + width: 200vmax; + height: 200vmax; + transform: translate(-50%, -50%) rotate(var(--ob1-bg-tilt)) + perspective(1600px) rotateX(var(--ob1-bg-rake)); + transform-origin: center center; + pointer-events: none; + z-index: -2; + background-image: + url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='120' height='20' viewBox='0 0 120 20'%3E%3Ctext x='0' y='8' fill='%23ece5d1' fill-opacity='.092' font-family='monospace' font-size='5' letter-spacing='1.55'%3ENBJ OB1%3C/text%3E%3Ctext x='60' y='18' fill='%23ece5d1' fill-opacity='.064' font-family='monospace' font-size='5' letter-spacing='1.55'%3ENBJ OB1%3C/text%3E%3C/svg%3E"), + repeating-linear-gradient( + 2deg, + rgb(255 255 255 / var(--ob1-fiber-light-opacity)) 0, + rgb(255 255 255 / var(--ob1-fiber-light-opacity)) 1px, + transparent 1px, + transparent 23px + ), + repeating-linear-gradient( + 88deg, + rgb(0 0 0 / var(--ob1-fiber-dark-opacity)) 0, + rgb(0 0 0 / var(--ob1-fiber-dark-opacity)) 1px, + transparent 1px, + transparent 29px + ), + radial-gradient(circle at 16% 14%, rgb(255 255 255 / 0.055), transparent 32%), + linear-gradient(180deg, transparent 42%, rgba(0, 0, 0, 0.24)); + background-size: + 120px 20px, + 160px 160px, + 220px 220px, + 100% 100%, + 100% 100%; +} + +.ob1-graphite-bg::after { + content: ""; + position: fixed; + inset: 0; + pointer-events: none; + z-index: -1; + background: + radial-gradient(ellipse at center, transparent 38%, rgb(0 0 0 / 0.34) 100%), + linear-gradient(90deg, rgb(0 0 0 / 0.16), transparent 22%, transparent 78%, rgb(0 0 0 / 0.16)); +} + +.ob1-brand-signature { + position: fixed; + top: 1rem; + right: 1.25rem; + z-index: 1; + pointer-events: none; + color: rgba(242, 239, 229, 0.28); + font-family: var(--font-mono); + font-size: 0.625rem; + font-weight: 600; + letter-spacing: 0.18em; + text-transform: uppercase; +} + +.ob1-brand-kicker { + display: block; + color: rgba(183, 217, 133, 0.66); + font-family: var(--font-mono); + font-size: 0.625rem; + font-weight: 600; + letter-spacing: 0.12em; + line-height: 1.1; + text-transform: uppercase; +} + +.ob1-brand-stamp { + color: rgba(242, 239, 229, 0.44); + font-family: var(--font-mono); + font-size: 0.6875rem; + font-weight: 700; + letter-spacing: 0.16em; + text-transform: uppercase; +} + +.ob1-login-panel { + border: 1px solid var(--color-border); + background: + linear-gradient(180deg, rgba(255, 255, 255, 0.055), rgba(255, 255, 255, 0.03)), + rgba(29, 32, 30, 0.74); + -webkit-backdrop-filter: blur(18px) saturate(1.1); + backdrop-filter: blur(18px) saturate(1.1); + box-shadow: 0 24px 80px rgba(0, 0, 0, 0.34); +} + +.ob1-glass-panel { + border: 1px solid var(--color-border); + background: var(--color-bg-surface); + -webkit-backdrop-filter: blur(2px) saturate(1.08); + backdrop-filter: blur(2px) saturate(1.08); +} + +.ob1-frost-panel { + border: 1px solid rgba(255, 255, 255, 0.16); + background: rgba(255, 255, 255, 0.05); + -webkit-backdrop-filter: blur(24px) saturate(1.18); + backdrop-filter: blur(24px) saturate(1.18); +} + +.ob1-section-label { + color: var(--color-text-muted); + font-size: 0.6875rem; + font-weight: 600; + letter-spacing: 0.08em; + text-transform: uppercase; +} + +.ob1-command-button { + display: inline-flex; + align-items: center; + justify-content: center; + gap: 0.5rem; + border: 1px solid var(--color-border); + background: rgba(255, 255, 255, 0.045); + color: var(--color-text-secondary); + transition: + border-color 180ms ease, + background-color 180ms ease, + color 180ms ease, + transform 180ms ease; +} + +.ob1-command-button:hover { + border-color: rgba(236, 229, 209, 0.28); + background: rgba(255, 255, 255, 0.075); + color: var(--color-text-primary); +} + +.ob1-memory-row { + position: relative; + overflow: hidden; + transition: + background-color 180ms ease, + transform 180ms ease; +} + +.ob1-memory-row::after { + content: ""; + position: absolute; + inset: 0; + pointer-events: none; + transform: translateX(-130%) skewX(-12deg); + background: linear-gradient(90deg, transparent, rgba(255, 255, 255, 0.035), transparent); +} + +.ob1-memory-row:hover { + background: var(--color-bg-hover); +} + +.ob1-memory-row:hover::after { + transform: translateX(150%) skewX(-12deg); + transition: transform 900ms ease-out; +} + /* Scrollbar */ ::-webkit-scrollbar { width: 6px; diff --git a/dashboards/open-brain-dashboard-next/app/layout.tsx b/dashboards/open-brain-dashboard-next/app/layout.tsx index 43619b11c..8282ff139 100644 --- a/dashboards/open-brain-dashboard-next/app/layout.tsx +++ b/dashboards/open-brain-dashboard-next/app/layout.tsx @@ -14,8 +14,8 @@ const geistMono = Geist_Mono({ }); export const metadata: Metadata = { - title: "Open Brain", - description: "Second brain dashboard", + title: "Open Brain | Nate B. Jones", + description: "Nate B. Jones Open Brain dashboard", }; export default function RootLayout({ @@ -29,10 +29,13 @@ export default function RootLayout({ className={`${geistSans.variable} ${geistMono.variable} h-full antialiased`} suppressHydrationWarning > - + +
-
+
{children}
diff --git a/dashboards/open-brain-dashboard-next/app/login/LoginForm.tsx b/dashboards/open-brain-dashboard-next/app/login/LoginForm.tsx index 73ef31019..3d6176e3c 100644 --- a/dashboards/open-brain-dashboard-next/app/login/LoginForm.tsx +++ b/dashboards/open-brain-dashboard-next/app/login/LoginForm.tsx @@ -21,7 +21,7 @@ export function LoginForm({ htmlFor="apiKey" className="block text-sm font-medium text-text-secondary mb-1.5" > - MCP Access Key + OB1 Access Key @@ -41,7 +41,7 @@ export function LoginForm({ diff --git a/dashboards/open-brain-dashboard-next/app/login/page.tsx b/dashboards/open-brain-dashboard-next/app/login/page.tsx index 0e44281ad..56789593a 100644 --- a/dashboards/open-brain-dashboard-next/app/login/page.tsx +++ b/dashboards/open-brain-dashboard-next/app/login/page.tsx @@ -1,3 +1,4 @@ +import Image from "next/image"; import { redirect } from "next/navigation"; import { getSession } from "@/lib/auth"; import { LoginForm } from "./LoginForm"; @@ -38,12 +39,20 @@ export default async function LoginPage() { } return ( -
-
+
+
-
- OB +
+
+

Nate B. Jones / OB1

Open Brain

diff --git a/dashboards/open-brain-dashboard-next/app/thoughts/[id]/page.tsx b/dashboards/open-brain-dashboard-next/app/thoughts/[id]/page.tsx index c23f1148d..c4d85544d 100644 --- a/dashboards/open-brain-dashboard-next/app/thoughts/[id]/page.tsx +++ b/dashboards/open-brain-dashboard-next/app/thoughts/[id]/page.tsx @@ -26,8 +26,8 @@ export default async function ThoughtDetailPage({ const session = await getSession(); const excludeRestricted = !session.restrictedUnlocked; const { id } = await params; - const thoughtId = parseInt(id, 10); - if (isNaN(thoughtId)) notFound(); + const thoughtId = id.trim(); + if (!thoughtId) notFound(); let thought; try { diff --git a/dashboards/open-brain-dashboard-next/components/AgentMemoryBadges.tsx b/dashboards/open-brain-dashboard-next/components/AgentMemoryBadges.tsx new file mode 100644 index 000000000..944e04db1 --- /dev/null +++ b/dashboards/open-brain-dashboard-next/components/AgentMemoryBadges.tsx @@ -0,0 +1,73 @@ +import type { AgentMemory, AgentMemoryRecord } from "@/lib/types"; + +type PolicyLike = + | AgentMemory["use_policy"] + | { + can_use_as_instruction: boolean; + can_use_as_evidence: boolean; + requires_user_confirmation: boolean; + }; + +export function StatusBadge({ value }: { value: string }) { + const tone = + value === "confirmed" + ? "border-success/30 bg-success/10 text-success" + : value === "pending" + ? "border-warning/30 bg-warning/10 text-warning" + : value === "rejected" || value === "disputed" + ? "border-danger/30 bg-danger/10 text-danger" + : "border-border bg-bg-elevated text-text-secondary"; + + return ( + + {value} + + ); +} + +export function ProvenanceBadge({ value }: { value: string }) { + return ( + + {value} + + ); +} + +export function PolicyBadges({ policy }: { policy: PolicyLike }) { + return ( +
+ {policy.can_use_as_instruction && ( + + instruction + + )} + {policy.can_use_as_evidence && ( + + evidence + + )} + {policy.requires_user_confirmation && ( + + needs review + + )} + {!policy.can_use_as_instruction && !policy.can_use_as_evidence && ( + + blocked + + )} +
+ ); +} + +export function MemoryRecordPolicy({ memory }: { memory: AgentMemoryRecord }) { + return ( + + ); +} diff --git a/dashboards/open-brain-dashboard-next/components/ConnectionsPanel.tsx b/dashboards/open-brain-dashboard-next/components/ConnectionsPanel.tsx index 8eaf4ad9f..3c18835e4 100644 --- a/dashboards/open-brain-dashboard-next/components/ConnectionsPanel.tsx +++ b/dashboards/open-brain-dashboard-next/components/ConnectionsPanel.tsx @@ -6,7 +6,7 @@ import { TypeBadge } from "./ThoughtCard"; import { FormattedDate } from "./FormattedDate"; interface Connection { - id: number; + id: string; type: string; importance: number; preview: string; @@ -23,26 +23,35 @@ export function ConnectionsPanel({ thoughtId, hasMetadata, }: { - thoughtId: number; + thoughtId: string; hasMetadata: boolean; }) { - const [connections, setConnections] = useState([]); - const [loading, setLoading] = useState(true); + const [connections, setConnections] = useState( + hasMetadata ? null : [] + ); useEffect(() => { - if (!hasMetadata) { - setLoading(false); - return; - } + if (!hasMetadata) return; + let cancelled = false; fetch(`/api/thoughts/${thoughtId}/connections`) .then((res) => res.json()) - .then((data) => setConnections(data.connections ?? [])) - .catch(() => setConnections([])) - .finally(() => setLoading(false)); + .then((data) => { + if (!cancelled) setConnections(data.connections ?? []); + }) + .catch(() => { + if (!cancelled) setConnections([]); + }); + + return () => { + cancelled = true; + }; }, [thoughtId, hasMetadata]); - if (!hasMetadata || (!loading && connections.length === 0)) return null; + const loading = hasMetadata && connections === null; + const items = connections ?? []; + + if (!hasMetadata || (!loading && items.length === 0)) return null; return (
@@ -53,7 +62,7 @@ export function ConnectionsPanel({

Loading connections...

) : (
- {connections.map((c) => ( + {items.map((c) => ( ): Promise { const res = await fetch("/api/kanban/update", { @@ -112,7 +112,7 @@ export function KanbanBoard() { const { active, over } = event; if (!over) return; - const thoughtId = active.id as number; + const thoughtId = String(active.id); const newStatus = over.id as string; // Find which column the thought is currently in @@ -138,7 +138,7 @@ export function KanbanBoard() { }); } - async function handlePriorityChange(thoughtId: number, newImportance: number) { + async function handlePriorityChange(thoughtId: string, newImportance: number) { previousThoughts.current = [...thoughts]; setThoughts((prev) => prev.map((t) => @@ -155,7 +155,7 @@ export function KanbanBoard() { } } - async function handleArchive(thoughtId: number) { + async function handleArchive(thoughtId: string) { previousThoughts.current = [...thoughts]; setThoughts((prev) => prev.map((t) => @@ -174,7 +174,7 @@ export function KanbanBoard() { } } - async function handleDelete(thoughtId: number) { + async function handleDelete(thoughtId: string) { previousThoughts.current = [...thoughts]; setThoughts((prev) => prev.filter((t) => t.id !== thoughtId)); @@ -193,7 +193,7 @@ export function KanbanBoard() { } async function handleModalSave( - thoughtId: number, + thoughtId: string, updates: Record ) { previousThoughts.current = [...thoughts]; diff --git a/dashboards/open-brain-dashboard-next/components/KanbanCard.tsx b/dashboards/open-brain-dashboard-next/components/KanbanCard.tsx index 20edf1f12..d76ebfa56 100644 --- a/dashboards/open-brain-dashboard-next/components/KanbanCard.tsx +++ b/dashboards/open-brain-dashboard-next/components/KanbanCard.tsx @@ -20,9 +20,9 @@ function formatAge(dateString: string): string { interface KanbanCardProps { thought: Thought; onCardClick: (thought: Thought) => void; - onPriorityChange: (thoughtId: number, importance: number) => void; + onPriorityChange: (thoughtId: string, importance: number) => void; showArchiveButton?: boolean; - onArchive?: (thoughtId: number) => void; + onArchive?: (thoughtId: string) => void; } export function KanbanCard({ diff --git a/dashboards/open-brain-dashboard-next/components/KanbanCardModal.tsx b/dashboards/open-brain-dashboard-next/components/KanbanCardModal.tsx index 5042919da..5b9aa0c9d 100644 --- a/dashboards/open-brain-dashboard-next/components/KanbanCardModal.tsx +++ b/dashboards/open-brain-dashboard-next/components/KanbanCardModal.tsx @@ -1,6 +1,6 @@ "use client"; -import { useState, useEffect, useRef } from "react"; +import { useState, useEffect, useRef, useCallback } from "react"; import { createPortal } from "react-dom"; import type { Thought, KanbanStatus } from "@/lib/types"; import { KANBAN_STATUSES, KANBAN_LABELS, PRIORITY_LEVELS, getPriorityLevel, THOUGHT_TYPES, KANBAN_TYPES } from "@/lib/types"; @@ -8,11 +8,11 @@ import { KANBAN_STATUSES, KANBAN_LABELS, PRIORITY_LEVELS, getPriorityLevel, THOU interface KanbanCardModalProps { thought: Thought; onSave: ( - thoughtId: number, + thoughtId: string, updates: { content?: string; status?: string; importance?: number; type?: string } ) => void; - onArchive: (thoughtId: number) => void; - onDelete: (thoughtId: number) => void; + onArchive: (thoughtId: string) => void; + onDelete: (thoughtId: string) => void; onClose: () => void; } @@ -27,20 +27,23 @@ export function KanbanCardModal({ const [status, setStatus] = useState(thought.status ?? "new"); const [importance, setImportance] = useState(thought.importance); const [type, setType] = useState(thought.type); - const [hasChanges, setHasChanges] = useState(false); const [showDiscardConfirm, setShowDiscardConfirm] = useState(false); const [showDeleteConfirm, setShowDeleteConfirm] = useState(false); const backdropRef = useRef(null); const textareaRef = useRef(null); + const hasChanges = + content !== thought.content || + status !== (thought.status ?? "new") || + importance !== thought.importance || + type !== thought.type; - useEffect(() => { - const isChanged = - content !== thought.content || - status !== (thought.status ?? "new") || - importance !== thought.importance || - type !== thought.type; - setHasChanges(isChanged); - }, [content, status, importance, type, thought]); + const tryClose = useCallback(() => { + if (hasChanges) { + setShowDiscardConfirm(true); + } else { + onClose(); + } + }, [hasChanges, onClose]); useEffect(() => { function handleEscape(e: KeyboardEvent) { @@ -50,7 +53,7 @@ export function KanbanCardModal({ } document.addEventListener("keydown", handleEscape); return () => document.removeEventListener("keydown", handleEscape); - }, [hasChanges]); + }, [tryClose]); // Lock body scroll useEffect(() => { @@ -70,14 +73,6 @@ export function KanbanCardModal({ } }, [content]); - function tryClose() { - if (hasChanges) { - setShowDiscardConfirm(true); - } else { - onClose(); - } - } - function handleBackdropClick(e: React.MouseEvent) { if (e.target === backdropRef.current) { tryClose(); diff --git a/dashboards/open-brain-dashboard-next/components/KanbanColumn.tsx b/dashboards/open-brain-dashboard-next/components/KanbanColumn.tsx index 3195418a7..ab9e762b1 100644 --- a/dashboards/open-brain-dashboard-next/components/KanbanColumn.tsx +++ b/dashboards/open-brain-dashboard-next/components/KanbanColumn.tsx @@ -1,6 +1,6 @@ "use client"; -import { useState, useEffect } from "react"; +import { useState } from "react"; import { useDroppable } from "@dnd-kit/core"; import { SortableContext, verticalListSortingStrategy } from "@dnd-kit/sortable"; import type { Thought, KanbanStatus } from "@/lib/types"; @@ -24,8 +24,8 @@ interface KanbanColumnProps { status: string; thoughts: Thought[]; onCardClick: (thought: Thought) => void; - onPriorityChange: (thoughtId: number, importance: number) => void; - onArchive: (thoughtId: number) => void; + onPriorityChange: (thoughtId: string, importance: number) => void; + onArchive: (thoughtId: string) => void; } export function KanbanColumn({ @@ -35,13 +35,10 @@ export function KanbanColumn({ onPriorityChange, onArchive, }: KanbanColumnProps) { - const [isCollapsed, setIsCollapsed] = useState(false); - - // Load collapse state from localStorage - useEffect(() => { - const stored = localStorage.getItem(collapseKey(status)); - if (stored === "true") setIsCollapsed(true); - }, [status]); + const [isCollapsed, setIsCollapsed] = useState(() => { + if (typeof window === "undefined") return false; + return localStorage.getItem(collapseKey(status)) === "true"; + }); function toggleCollapse() { const nextState = !isCollapsed; diff --git a/dashboards/open-brain-dashboard-next/components/ReflectionComposer.tsx b/dashboards/open-brain-dashboard-next/components/ReflectionComposer.tsx index 4fb3e9e39..77cb1f4f7 100644 --- a/dashboards/open-brain-dashboard-next/components/ReflectionComposer.tsx +++ b/dashboards/open-brain-dashboard-next/components/ReflectionComposer.tsx @@ -4,7 +4,6 @@ import { useState } from "react"; import { useRouter } from "next/navigation"; import type { ReflectionInput, - ReflectionOption, ReflectionFactor, } from "@/lib/types"; @@ -23,7 +22,7 @@ const emptyForm: ReflectionInput = { reflection_type: "decision_trace", }; -export function ReflectionComposer({ thoughtId }: { thoughtId: number }) { +export function ReflectionComposer({ thoughtId }: { thoughtId: string }) { const [open, setOpen] = useState(false); const [form, setForm] = useState({ ...emptyForm }); const [submitting, setSubmitting] = useState(false); diff --git a/dashboards/open-brain-dashboard-next/components/RestrictedToggle.tsx b/dashboards/open-brain-dashboard-next/components/RestrictedToggle.tsx index fb8105be1..185919d24 100644 --- a/dashboards/open-brain-dashboard-next/components/RestrictedToggle.tsx +++ b/dashboards/open-brain-dashboard-next/components/RestrictedToggle.tsx @@ -21,9 +21,6 @@ export function RestrictedToggle() { .catch(() => {}); }, []); - // Don't render if restricted content feature is not configured - if (!configured) return null; - const handleUnlock = useCallback(async () => { if (!passphrase.trim()) return; setLoading(true); @@ -62,6 +59,9 @@ export function RestrictedToggle() { } }, []); + // Don't render if restricted content feature is not configured + if (!configured) return null; + return ( <> {/* Lock/unlock button */} diff --git a/dashboards/open-brain-dashboard-next/components/Sidebar.tsx b/dashboards/open-brain-dashboard-next/components/Sidebar.tsx index 7f95093f9..a8aeafd62 100644 --- a/dashboards/open-brain-dashboard-next/components/Sidebar.tsx +++ b/dashboards/open-brain-dashboard-next/components/Sidebar.tsx @@ -1,5 +1,6 @@ "use client"; +import Image from "next/image"; import Link from "next/link"; import { usePathname } from "next/navigation"; import { RestrictedToggle } from "@/components/RestrictedToggle"; @@ -8,6 +9,7 @@ const nav = [ { href: "/", label: "Dashboard", icon: DashboardIcon }, { href: "/thoughts", label: "Thoughts", icon: ThoughtsIcon }, { href: "/kanban", label: "Workflow", icon: KanbanIcon }, + { href: "/agent-memory", label: "Agent Memory", icon: MemoryIcon }, { href: "/search", label: "Search", icon: SearchIcon }, { href: "/audit", label: "Audit", icon: AuditIcon }, { href: "/duplicates", label: "Duplicates", icon: DuplicatesIcon }, @@ -26,19 +28,29 @@ export function Sidebar({ isOpen = false, onClose }: SidebarProps) { return (