feat(config): create a new project's memory file from Settings
The Memory tab could only show this machine's user memory and the
dashboard's own project file. Setting up a project you had not run
through the dashboard meant opening a terminal — `PUT /file`'s
"project" scope always resolves against the server's cwd, on purpose.
Add one narrow route per tool that writes into the directory you name:
CLAUDE.md for Claude Code, AGENTS.md for Codex, both through the same
`project-memory.js`. Being the only write path that honours a
caller-supplied directory, it pays for that:
- the directory must be absolute and already exist — nothing is
created for you;
- the filename comes from the route, never from the body;
- an existing file, symlink included, is a 409 — this can only
create, never overwrite;
- it sits behind the loopback-Origin guard /api/run uses, so a page
in the user's browser cannot plant agent instructions in a project
directory. That guard moves to lib/same-origin.js to be shared.
The modal takes the dashboard's known project dirs as suggestions on a
free-text input rather than a closed list: a project it has never seen
is exactly the case this exists for.
Mutation-checked: dropping the origin guard, and swapping lstat for
stat, each fail a test.
This commit is contained in:
@@ -49,6 +49,7 @@ import {
|
||||
} from "../components/config/ConfirmDeleteModal";
|
||||
import { ToastNotice, type Toast } from "../components/config/ToastNotice";
|
||||
import { ScopeBadge } from "../components/config/ScopeBadge";
|
||||
import { ProjectMemoryModal } from "../components/config/ProjectMemoryModal";
|
||||
import {
|
||||
Empty,
|
||||
SkeletonRows,
|
||||
@@ -152,6 +153,7 @@ export function CodexConfig() {
|
||||
// (mirrors CcConfig.tsx's PluginsPanel: "a failure here is a normal state").
|
||||
const [cliError, setCliError] = useState<string | null>(null);
|
||||
const [backupsOpen, setBackupsOpen] = useState(false);
|
||||
const [projectMemoryOpen, setProjectMemoryOpen] = useState(false);
|
||||
const [lastUpdated, setLastUpdated] = useState<Date | null>(null);
|
||||
const [search, setSearch] = useState("");
|
||||
const [viewer, setViewer] = useState<{
|
||||
@@ -322,6 +324,20 @@ export function CodexConfig() {
|
||||
[fetchAll, t]
|
||||
);
|
||||
|
||||
// Create AGENTS.md in ANOTHER project's directory — the Codex twin of
|
||||
// CcConfig's handler; `handleSave` above only ever writes this machine's
|
||||
// user/project layers.
|
||||
const handleCreateProjectMemory = useCallback(
|
||||
async (args: { dir: string; content: string }) => {
|
||||
const result = await api.codexConfig.createProjectMemory(args);
|
||||
setProjectMemoryOpen(false);
|
||||
setToast({ kind: "success", message: t("projectMemory.created", { path: result.file }) });
|
||||
void fetchAll();
|
||||
return result.file;
|
||||
},
|
||||
[fetchAll, t]
|
||||
);
|
||||
|
||||
const handleDelete = useCallback(async () => {
|
||||
if (!confirmDelete) return;
|
||||
try {
|
||||
@@ -393,6 +409,20 @@ export function CodexConfig() {
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
{/* The memory tab hides the search bar, but still needs its own
|
||||
"new": a CLAUDE.md-style AGENTS.md in ANOTHER project's dir, not
|
||||
the user/project files listed below. */}
|
||||
{tab === "memory" && (
|
||||
<div className="flex items-center justify-end border-b border-border px-4 py-2.5">
|
||||
<button
|
||||
onClick={() => setProjectMemoryOpen(true)}
|
||||
className="h-7 text-[11px] font-medium px-2.5 rounded-md border border-accent/30 bg-accent/10 hover:bg-accent/20 text-accent inline-flex items-center gap-1.5"
|
||||
>
|
||||
<Plus className="w-3 h-3" />
|
||||
{t("projectMemory.newButton")}
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
<div className="p-4">
|
||||
<TabPanel
|
||||
tab={tab}
|
||||
@@ -429,6 +459,15 @@ export function CodexConfig() {
|
||||
onConfirm={handleDelete}
|
||||
/>
|
||||
)}
|
||||
{projectMemoryOpen && (
|
||||
<ProjectMemoryModal
|
||||
ns="codexConfig"
|
||||
filename="AGENTS.md"
|
||||
template={t("projectMemory.template")}
|
||||
onClose={() => setProjectMemoryOpen(false)}
|
||||
onCreate={handleCreateProjectMemory}
|
||||
/>
|
||||
)}
|
||||
{toast && <ToastNotice toast={toast} onDismiss={() => setToast(null)} />}
|
||||
</div>
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user