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:
@@ -19,7 +19,7 @@
|
||||
|
||||
import { useCallback, useEffect, useRef, useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { FolderOpen } from "lucide-react";
|
||||
import { ChevronDown, FolderOpen } from "lucide-react";
|
||||
import { ConfirmModal } from "../ConfirmModal";
|
||||
import { CwdAutocomplete } from "../run/RunSetup";
|
||||
import { FolderBrowseModal } from "./FolderBrowseModal";
|
||||
@@ -27,6 +27,7 @@ import { api } from "../../lib/api";
|
||||
import type { AiRouter, CwdSuggestion } from "../../lib/api";
|
||||
import type { Lane, LaneWorktree } from "../../lib/types";
|
||||
import { useAiTools } from "../../hooks/useAiTools";
|
||||
import { AiToolIcon } from "../AiToolIcon";
|
||||
|
||||
/** Polls the lane until the background `git worktree add` finishes (status
|
||||
* leaves "provisioning"), or gives up after `timeoutMs`. Returns the final
|
||||
@@ -115,7 +116,17 @@ export function AddLaneModal({
|
||||
const aiTools = useAiTools();
|
||||
const defaultTool = aiTools.defaultId ?? "claude";
|
||||
const tool = pickedTool ?? defaultTool;
|
||||
const otherTools = aiTools.tools.filter((x) => x.installed && x.enabled && x.id !== defaultTool);
|
||||
// Installed + enabled, the default first.
|
||||
const toolChoices = aiTools.tools
|
||||
.filter((x) => x.installed && x.enabled)
|
||||
.sort((a, b) => Number(b.id === defaultTool) - Number(a.id === defaultTool));
|
||||
const [toolListOpen, setToolListOpen] = useState(false);
|
||||
const pickTool = (id: string) => {
|
||||
setPickedTool(id === defaultTool ? null : id);
|
||||
// A tool with no endpoint to rewrite runs native only (see router-apply.js).
|
||||
if (!aiTools.byId(id)?.routerCapable) setRouter("native");
|
||||
setToolListOpen(false);
|
||||
};
|
||||
const noTools = aiTools.loaded && aiTools.defaultId === null;
|
||||
const toolRouterCapable =
|
||||
aiTools.byId(tool)?.routerCapable ?? (tool === "claude" || tool === "codex");
|
||||
@@ -431,38 +442,51 @@ export function AddLaneModal({
|
||||
</a>
|
||||
</p>
|
||||
) : (
|
||||
<div className="flex items-center gap-1.5">
|
||||
<div className="flex items-center rounded-md border border-border bg-surface-2 p-0.5">
|
||||
<Seg
|
||||
active={tool === defaultTool}
|
||||
label={t("addLaneToolDefault", { tool: aiTools.label(defaultTool) })}
|
||||
onClick={() => setPickedTool(null)}
|
||||
/>
|
||||
</div>
|
||||
{otherTools.length > 0 && (
|
||||
<select
|
||||
aria-label={t("addLaneToolOther")}
|
||||
value={tool === defaultTool ? "" : tool}
|
||||
onChange={(e) => {
|
||||
const next = e.target.value || null;
|
||||
setPickedTool(next);
|
||||
// A tool with no endpoint to rewrite runs native only
|
||||
// (see router-apply.js).
|
||||
if (next && !aiTools.byId(next)?.routerCapable) setRouter("native");
|
||||
}}
|
||||
className={`rounded-md border bg-surface-2 px-2 py-1 text-xs ${
|
||||
tool !== defaultTool
|
||||
? "border-accent text-accent"
|
||||
: "border-border text-fg-secondary"
|
||||
}`}
|
||||
<div>
|
||||
<button
|
||||
type="button"
|
||||
aria-expanded={toolListOpen}
|
||||
disabled={toolChoices.length < 2}
|
||||
onClick={() => setToolListOpen((o) => !o)}
|
||||
className="flex w-full items-center gap-2 rounded-md border border-border bg-surface-2 px-2.5 py-1.5 text-left text-sm text-fg-primary hover:bg-surface-3 disabled:cursor-default disabled:hover:bg-surface-2"
|
||||
>
|
||||
<AiToolIcon tool={{ id: tool, label: aiTools.label(tool) }} className="h-5 w-5" />
|
||||
<span className="min-w-0 flex-1 truncate">{aiTools.label(tool)}</span>
|
||||
{tool === defaultTool && (
|
||||
<span className="rounded-full bg-accent/15 px-2 py-0.5 text-[10px] font-medium text-accent">
|
||||
{t("addLaneToolDefaultBadge")}
|
||||
</span>
|
||||
)}
|
||||
{toolChoices.length > 1 && (
|
||||
<ChevronDown
|
||||
aria-hidden
|
||||
className={`h-4 w-4 text-fg-muted transition-transform ${toolListOpen ? "rotate-180" : ""}`}
|
||||
/>
|
||||
)}
|
||||
</button>
|
||||
{toolListOpen && (
|
||||
<div
|
||||
role="group"
|
||||
aria-label={t("addLaneToolHint")}
|
||||
className="mt-1.5 grid grid-cols-2 gap-1.5 sm:grid-cols-3"
|
||||
>
|
||||
<option value="">{t("addLaneToolOther")}</option>
|
||||
{otherTools.map((x) => (
|
||||
<option key={x.id} value={x.id}>
|
||||
{x.label}
|
||||
</option>
|
||||
{toolChoices.map((x) => (
|
||||
<button
|
||||
key={x.id}
|
||||
type="button"
|
||||
aria-pressed={x.id === tool}
|
||||
onClick={() => pickTool(x.id)}
|
||||
className={`flex items-center gap-2 rounded-md border px-2 py-1.5 text-left text-xs transition-colors ${
|
||||
x.id === tool
|
||||
? "border-accent bg-accent/10 text-accent"
|
||||
: "border-border bg-surface-2 text-fg-secondary hover:bg-surface-3 hover:text-fg-primary"
|
||||
}`}
|
||||
>
|
||||
<AiToolIcon tool={x} className="h-5 w-5" />
|
||||
<span className="min-w-0 flex-1 truncate">{x.label}</span>
|
||||
</button>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
Reference in New Issue
Block a user