feat(ui): AI Tools modal, one-button lane tool picker, AgentHub settings in Overview, markdown preview

- AI Tools moves from a Settings section into a modal behind a button at the
  Settings tab bar's right edge (AiToolsModal, `actions` on PageTabs,
  /settings#ai-tools); rows get an enable switch, Default badge, and an
  Update button for every tool whose catalog names an updater (no version
  lookup on open); not-installed tools fold behind a disclosure, a finished
  job's log folds to one line; shared AiToolIcon
- codex updates through its own `codex update`, openclaw/hermes gain
  `update --yes`; update-all runs every installed updater and skips an npm
  one only when npm does not manage the binary
- Add-lane tool picker is one button showing the chosen tool that opens the
  enabled tools as buttons
- Claude Code / Codex AgentHub sub-tab folded into Overview (home editor,
  hooks, pricing); the path list drops CLAUDE_HOME/CODEX_HOME;
  HomeDirSetting shows the saved value only while the field differs
- Preview/Raw toggle for markdown files in the config FileViewer and the
  Changes panel's whole-file view (MarkdownPreview: react-markdown +
  remark-gfm, raw HTML never run, frontmatter as a code block)
- docs and the CLAUDE.md marker updated
This commit is contained in:
2026-10-01 10:11:43 +07:00
parent 058c574138
commit c5aba094bd
55 changed files with 2985 additions and 853 deletions
+2 -2
View File
@@ -542,7 +542,7 @@ Request body shape:
| `POST` | `/api/ai-tools/:id/update` | Same-origin guarded: start the update job; npm only for an npm-managed tool (`server/lib/ai-tool-versions.js`), else `409 ENOTNPM`. `400 ENOUPDATE`, `409 ENOTINSTALLED`/`EBUSY`. |
| `POST` | `/api/ai-tools/update-all` | Same-origin guarded: one job over every npm-managed tool with a newer version plus every self updater. `400 ENOTHING`. |
| `GET` | `/api/ai-tools/job` | The current or last job (in memory), or `null`. |
| `GET` | `/api/ai-tools/versions` | npm version badges, cached 24 h. |
| `GET` | `/api/ai-tools/versions` | npm-managed check and newest npm version per npm-installed tool, cached 24 h. The AI Tools modal no longer calls it: Update shows for every updater. |
| `GET` | `/api/ai-tools/:id/icon` | The server-cached favicon (`server/lib/ai-tool-icons.js`), raster only; `204` when there is none (a normal state — no console error per letter avatar), `404` for an unknown id. |
| `POST` | `/api/lanes/:id/git/:action` | Same-origin guarded git write, serialized under the lane lock: `fetch`, `pull` (always `--ff-only`), `push` (`-u origin HEAD`), `stage`/`unstage`/`discard` (per file; a path must be one git reports changed, and `discard` needs `confirm:true`), `stage-hunk`/`discard-hunk` (one hunk by its `@@` line, re-read fresh — `409 EHUNKGONE` when it moved; `discard-hunk` needs `confirm:true`), `commit` (`add -A` + `-m`, or `only:"staged"`), `switch` (refuses a dirty tree), `branch-create` (never switches) `branch-delete` (`-d`, refuses protected/current/busy branches) and `publish` (`{url}`: `remote add origin` + `push -u origin HEAD`, only when the repo has no remote; URL allowlisted to https-without-userinfo/ssh/scp-like; remote rolled back on a failed push). `merge` (clean tree, repo-root lane, a ref that exists; conflicts come back as `conflicts[]`), `resolve` (`side: ours|theirs|done`, conflicted paths only, `done` refuses leftover markers), `merge-commit`, `merge-abort` (`confirm:true`); `EMERGING` blocks pull/switch/merge/hunk actions mid-merge. No rebase/reset/stash by design. A remote wanting a login is `409 EGITAUTH` with `auth: {protocol, host, username, rejected}`; resend with `credentials: {protocol, host, username, password, remember}` (a repo remote's host only, else `400 EBADHOST`; saved only after git succeeds and `remember` is set). |
@@ -595,7 +595,7 @@ renders a node as `done` (see `docs/LANES.md#stage-detection`).
A `codex` lane carries no pipeline, so nothing detects or advances its stage.
Codex's hooks are opt-in (`server/lib/codex-hooks.js`, installed from Settings →
Codex → AgentHub and trusted by the user inside Codex); a hook posted with
Codex → Overview and trusted by the user inside Codex); a hook posted with
`tool: "codex"` reaches only the codex lane at its cwd and creates a
`tool = 'codex'` session (see ARCHITECTURE.md, **Codex hooks**). Otherwise
`server/lib/codex-ingest.js` only mirrors its `status` — skipping a lane whose
+7 -1
View File
@@ -371,7 +371,12 @@ describe("jobs", () => {
}
});
it("update-all runs the badged npm tools and every self updater", async () => {
it("update-all runs every npm-managed tool and every self updater, asking npm no version", async () => {
let viewed = 0;
versions.__setExec(async (_f, args) => {
if (args[0] === "view") viewed++;
return { stdout: args[0] === "prefix" ? `${prefix}\n` : "1.0.0\n" };
});
await jobs.startUpdateAll();
const done = await settle();
assert.equal(done.status, "ok");
@@ -379,5 +384,6 @@ describe("jobs", () => {
spawned.map((s) => s.argv.slice(1).join(" ")),
["install -g npm-tool@latest", "update --yes"]
);
assert.equal(viewed, 0);
});
});
+1 -1
View File
@@ -164,7 +164,7 @@ describe("install / update / icons", () => {
const { body } = await request("GET", "/api/ai-tools");
const codex = body.tools.find((t) => t.id === "codex");
assert.deepEqual(codex.install, { kind: "npm", package: "@openai/codex" });
assert.deepEqual(codex.update, { kind: "npm" });
assert.deepEqual(codex.update, { kind: "self", args: ["update"] }); // standalone or npm
assert.equal(body.catalog.source, "builtin");
});
+4 -1
View File
@@ -14,6 +14,9 @@
"package": "@openai/codex"
},
"update": {
"kind": "npm"
"kind": "self",
"args": [
"update"
]
}
}
+8 -1
View File
@@ -15,5 +15,12 @@
"--yolo"
],
"homepageUrl": "https://hermes-agent.nousresearch.com/docs/",
"faviconDomain": "nousresearch.com"
"faviconDomain": "nousresearch.com",
"update": {
"kind": "self",
"args": [
"update",
"--yes"
]
}
}
+8 -1
View File
@@ -9,5 +9,12 @@
"delayMs": 3000
},
"homepageUrl": "https://github.com/openclaw/openclaw",
"faviconDomain": "openclaw.ai"
"faviconDomain": "openclaw.ai",
"update": {
"kind": "self",
"args": [
"update",
"--yes"
]
}
}
+7 -5
View File
@@ -247,17 +247,19 @@ async function start(id, action) {
return view(j);
}
/** One job that updates every npm-managed tool with a newer version and every self-updater. */
/**
* One job that runs every installed tool's updater — its own, or npm's when
* npm manages it (one installed another way is skipped). No version lookup:
* each updater knows whether it has anything to do.
*/
async function startUpdateAll() {
claim();
let steps = [];
try {
const v = await versions.versions();
for (const t of aiTools.list()) {
if (!aiTools.detect(t.id).installed || !t.update) continue;
if (t.update.kind === "self" || (t.update.kind === "npm" && v[t.id]?.updateAvailable)) {
steps.push(...(await stepsFor(t.id, "update")));
}
if (t.update.kind === "npm" && !(await versions.npmManaged(t.id))) continue;
steps.push(...(await stepsFor(t.id, "update")));
}
if (!steps.length) throw makeErr("ENOTHING", "no AI tool has an update to run");
} finally {