Flat layout, agent-executed setup, write approvals, and the display arc

One name per role: src/gcontext moves to gcontext/ (flat layout), the
instructions files split into framework-instructions.md (G0) and the
project's agent.md (G1). Adds the built-in describe-first setup prompt,
state files as MCP resources (gcontext://<path>), and --version.

Exec: run_script splits into run_script and run_adhoc_script; results are
plain readable text with a status line, output_schema=None on all six
tools kills the {"result": ...} wrapper. 100k-char stream caps.

Writes: connection.yaml is agent-writable (only secrets.env stays
blocked), write_file returns a unified diff and warns on index.md drift,
and the instructions require user approval before every write. New
learn-from-errors rule records fail-then-fix lessons where they belong.

Fixes: secrets.load() strips surrounding quotes (quoted tokens caused
misleading 401s). Tests: 61 passing.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
bernatsampera
2026-08-01 16:47:28 +02:00
co-authored by Claude Fable 5
parent 0f3062ff39
commit e15e45dd61
45 changed files with 1061 additions and 323 deletions
+3 -3
View File
@@ -1,5 +1,5 @@
import React, { useEffect, useMemo, useRef, useState } from "react";
import { getJSON, filePrompt } from "./lib.js";
import { getJSON, fileRef } from "./lib.js";
import { C, mono, Chip, GhostBtn, sectionLabel, useHover, useIsMobile, pageTitle, EmptyState } from "./ui.jsx";
import CopyPrompt from "./Copy.jsx";
@@ -107,7 +107,7 @@ function Row({ e, first, onOpen }) {
<TierChip tier={e.tier} />
<span style={{ fontFamily: mono, fontSize: 12, fontWeight: 600, color: e.error ? C.danger : C.ink, flexShrink: 0 }}>{e.name}</span>
<span style={{ fontSize: 12, color: C.tMuted, flex: 1, minWidth: 0, overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap" }}>{e.detail}{e.error ? " · failed" : ""}</span>
{ref && h && <CopyPrompt icon text={filePrompt(ref)} title={`Copy a prompt to read ${ref}`} style={{ width: 22, height: 22 }} />}
{ref && h && <CopyPrompt icon text={fileRef(ref)} title={`Copy a reference to ${ref}`} style={{ width: 22, height: 22 }} />}
<span title={`${w.label}: ${nfmt(e.tokens_out)} tokens added to context`} style={{ display: "inline-flex", alignItems: "center", gap: 8, flexShrink: 0 }}>
<Bar frac={w.frac} color={w.color} w={44} />
<span style={{ ...monoNum, fontSize: 11, color: heavy ? C.accent : C.t3, width: 56, textAlign: "right", fontWeight: heavy ? 600 : 400 }}>{nfmt(e.tokens_out)} tk</span>
@@ -175,7 +175,7 @@ function EventModal({ e, onClose }) {
<div style={{ ...label, marginBottom: 6 }}>What it was about</div>
<div style={{ display: "flex", alignItems: "center", gap: 10, padding: "11px 14px", background: C.subtle, border: `1px solid ${C.borderInner}`, borderRadius: 9 }}>
<span style={{ fontFamily: mono, fontSize: 13, color: C.ink, lineHeight: 1.6, wordBreak: "break-word", flex: 1 }}>{e.detail}</span>
{ref && <CopyPrompt icon text={filePrompt(ref)} title={`Copy a prompt to read ${ref}`} />}
{ref && <CopyPrompt icon text={fileRef(ref)} title={`Copy a reference to ${ref}`} />}
</div>
</div>
)}
+2 -2
View File
@@ -1,5 +1,5 @@
import React, { useEffect, useState } from "react";
import { getJSON, relSeen } from "./lib.js";
import { getJSON, relSeen, setServerName } from "./lib.js";
import { C, mono, UiProvider, useHover, useIsMobile } from "./ui.jsx";
import Overview from "./Overview.jsx";
import Connections from "./Connections.jsx";
@@ -81,7 +81,7 @@ export default function App() {
useEffect(() => { localStorage.setItem("gc.section", section); }, [section]);
const refresh = () => {
getJSON("/api/project").then((p) => { setProject(p); setErr(null); }).catch((e) => setErr(e.message));
getJSON("/api/project").then((p) => { setProject(p); setServerName(p.name); setErr(null); }).catch((e) => setErr(e.message));
getJSON("/api/sessions").then((d) => setSessions(d.sessions)).catch(() => {});
};
useEffect(() => {
+2 -2
View File
@@ -1,5 +1,5 @@
import React, { useEffect, useState } from "react";
import { getJSON, filePrompt } from "./lib.js";
import { getJSON, fileRef } from "./lib.js";
import { C, mono, Chip, cardBase, cardHover, pageTitle, sectionLabel, EmptyState, useHover } from "./ui.jsx";
import CopyPrompt from "./Copy.jsx";
@@ -33,7 +33,7 @@ function CommandCard({ cmd }) {
)}
<div style={{ display: "flex", alignItems: "center", gap: 8 }}>
<span style={{ fontFamily: mono, fontSize: 10.5, color: C.t3, flex: 1, minWidth: 0, overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap" }} title={cmd.path}>{cmd.path}</span>
<CopyPrompt icon text={filePrompt(cmd.path)} title={`Copy a prompt to read ${cmd.path}`} />
<CopyPrompt icon text={fileRef(cmd.path)} title={`Copy a reference to ${cmd.path}`} />
</div>
{!cmd.error && (
<div style={{ marginTop: "auto", display: "flex", alignItems: "center", gap: 8 }}>
+3 -3
View File
@@ -1,5 +1,5 @@
import React, { useEffect, useState } from "react";
import { getJSON, filePrompt, folderPrompt } from "./lib.js";
import { getJSON, fileRef, folderRef } from "./lib.js";
import { C, mono, Chip, cardBase, cardHover, cardGrid, pageTitle, sectionLabel, EmptyState, useHover } from "./ui.jsx";
import CopyPrompt from "./Copy.jsx";
@@ -13,7 +13,7 @@ function FileRow({ path }) {
return (
<div {...hp} style={{ display: "flex", alignItems: "center", gap: 8, padding: "3px 0" }}>
<span style={{ fontFamily: mono, fontSize: 11.5, color: h ? C.ink : C.t2, flex: 1, minWidth: 0, overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap", transition: "color .12s" }} title={path}>{path}</span>
<CopyPrompt icon text={filePrompt(path)} title={`Copy a prompt to read ${path}`} />
<CopyPrompt icon text={fileRef(path)} title={`Copy a reference to ${path}`} />
</div>
);
}
@@ -48,7 +48,7 @@ function ConnectionCard({ conn }) {
</div>
)}
<div style={{ marginTop: "auto", paddingTop: 4 }}>
<CopyPrompt text={folderPrompt(folder)} title={`Copy a prompt to explore ${folder}`} style={{ width: "100%", justifyContent: "center" }} />
<CopyPrompt text={folderRef(folder)} title={`Copy a reference to ${folder}`} style={{ width: "100%", justifyContent: "center" }} />
</div>
</div>
);
+5 -5
View File
@@ -2,12 +2,12 @@ import React from "react";
import { C, mono, useHover, useUi } from "./ui.jsx";
import { copyText } from "./lib.js";
// The one action surface in the app: copy a prompt for the agent, fire a
// toast. The dashboard SEES the project; the agent (via MCP) USES it, so
// every action hands an agent-ready prompt to the clipboard.
// full pill -> <CopyPrompt text=… /> (⧉ Copy prompt, terracotta)
// The one action surface in the app: copy a resource reference for the agent,
// fire a toast. The dashboard SEES the project; the agent (via MCP) USES it,
// so every action hands an @server:gcontext://path reference to the clipboard.
// full pill -> <CopyPrompt text=… /> (⧉ Copy reference, terracotta)
// icon only -> <CopyPrompt text=… icon /> (26x26 ⧉, list rows)
export default function CopyPrompt({ text, label = "Copy prompt", toast = "Copied, paste it into your agent", title, icon, style }) {
export default function CopyPrompt({ text, label = "Copy reference", toast = "Copied, paste it into your agent", title, icon, style }) {
const ui = useUi();
const [h, hp] = useHover();
const copy = (e) => {
+2 -2
View File
@@ -46,7 +46,7 @@ function TreeRow({ node, depth, selected, open, onToggle, onSelect }) {
: <span style={{ width: 10, flexShrink: 0 }} />}
<span style={{ fontFamily: mono, fontSize: 12, fontWeight: node.dir ? 600 : 400, color: node.dir ? C.tFolder : C.t2, overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap", flex: 1 }}>{node.name}{node.dir ? "/" : ""}</span>
</button>
{h && <CopyPrompt icon text={refPrompt(ref, node.dir)} title={`Copy a prompt for ${ref}`} style={{ width: 22, height: 22 }} />}
{h && <CopyPrompt icon text={refPrompt(ref, node.dir)} title={`Copy a reference to ${ref}`} style={{ width: 22, height: 22 }} />}
</div>
{node.dir && open.has(node.path) && node.children.map((c) => (
<TreeRow key={c.path} node={c} depth={depth + 1} selected={selected} open={open} onToggle={onToggle} onSelect={onSelect} />
@@ -82,7 +82,7 @@ function Viewer({ path }) {
<FileGlyph w={16} />
<span style={{ fontFamily: mono, fontSize: 12.5, fontWeight: 600, color: C.ink, flex: 1, minWidth: 0, overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap" }}>{file.path}</span>
<span style={{ fontFamily: mono, fontSize: 10.5, color: C.t3, flexShrink: 0 }}>{fileLabel(path)} · {file.size} B</span>
<CopyPrompt text={refPrompt(file.path, false)} title={`Copy a prompt to read ${file.path}`} style={{ padding: "6px 12px" }} />
<CopyPrompt text={refPrompt(file.path, false)} title={`Copy a reference to ${file.path}`} style={{ padding: "6px 12px" }} />
</div>
<div style={{ padding: "18px 20px" }}>
{isMd ? (
+3 -3
View File
@@ -1,5 +1,5 @@
import React, { useEffect, useState } from "react";
import { getJSON, filePrompt, folderPrompt } from "./lib.js";
import { getJSON, fileRef, folderRef } from "./lib.js";
import { C, mono, Chip, cardBase, cardHover, cardGrid, pageTitle, sectionLabel, EmptyState, useHover } from "./ui.jsx";
import CopyPrompt from "./Copy.jsx";
@@ -12,7 +12,7 @@ function FileRow({ path }) {
return (
<div {...hp} style={{ display: "flex", alignItems: "center", gap: 8, padding: "3px 0" }}>
<span style={{ fontFamily: mono, fontSize: 11.5, color: h ? C.ink : C.t2, flex: 1, minWidth: 0, overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap", transition: "color .12s" }} title={path}>{path}</span>
<CopyPrompt icon text={filePrompt(path)} title={`Copy a prompt to read ${path}`} />
<CopyPrompt icon text={fileRef(path)} title={`Copy a reference to ${path}`} />
</div>
);
}
@@ -35,7 +35,7 @@ function ModuleCard({ mod }) {
</div>
)}
<div style={{ marginTop: "auto", paddingTop: 4 }}>
<CopyPrompt text={folderPrompt(folder)} title={`Copy a prompt to explore ${folder}`} style={{ width: "100%", justifyContent: "center" }} />
<CopyPrompt text={folderRef(folder)} title={`Copy a reference to ${folder}`} style={{ width: "100%", justifyContent: "center" }} />
</div>
</div>
);
+1 -1
View File
@@ -92,7 +92,7 @@ export default function Overview({ project, sessions }) {
{(project.has_instructions || archivedParts.length > 0) && (
<div style={{ marginTop: 22, display: "flex", flexDirection: "column", gap: 6, fontSize: 12.5, color: C.tMuted }}>
{project.has_instructions && (
<span>System prompt: <span style={{ fontFamily: mono }}>instructions.md</span> ({project.instructions_lines} lines)</span>
<span>System prompt: <span style={{ fontFamily: mono }}>agent.md</span> ({project.instructions_lines} lines)</span>
)}
{archivedParts.length > 0 && (
<span><span style={{ fontFamily: mono }}>archive/</span>: {archivedParts.join(", ")} (not scanned, readable by path)</span>
+9 -8
View File
@@ -21,14 +21,15 @@ export function copyText(text) {
ta.remove();
}
// Agent-ready prompts for a file or folder reference. The copied text names
// the gcontext MCP server, so any agent can locate the path without guessing
// which server or tool it belongs to.
export const filePrompt = (path) =>
`From the gcontext MCP server, read "${path}" and use it as context for this task.`;
export const folderPrompt = (path) =>
`From the gcontext MCP server, explore the folder "${path.replace(/\/$/, "")}/": check its files and read the relevant ones.`;
export const refPrompt = (path, isDir) => (isDir ? folderPrompt(path) : filePrompt(path));
// Direct MCP resource references: @<server>:gcontext://<path>. Runtimes that
// support resource mentions (Claude Code et al.) resolve these mechanically;
// the server exposes every state file at gcontext://<path>. The server name
// is set once by App.jsx from /api/project.
let serverName = "gcontext";
export const setServerName = (name) => { if (name) serverName = name; };
export const fileRef = (path) => `@${serverName}:gcontext://${path}`;
export const folderRef = (path) => `@${serverName}:gcontext://${path.replace(/\/$/, "")}/`;
export const refPrompt = (path, isDir) => (isDir ? folderRef(path) : fileRef(path));
// File-card label: "notes.md" -> "md", extensionless -> "file". Dotfiles (".env")
// stay "file" (lastIndexOf > 0), so the label never repeats the whole name.