Z ZPI 文档 v0.3.8
介绍页

源码

packages/desktop/src/renderer/ResourceSettings.tsx

tsx328 行查看原文
import * as Dialog from "@radix-ui/react-dialog";
import {
  ChevronDown,
  FilePenLine,
  FilePlus2,
  FileSearch,
  RefreshCcw,
  SquareTerminal,
  WandSparkles,
  Wrench,
  X,
} from "lucide-react";
import { useEffect, useState } from "react";
import type { PromptPreview, SkillSettings, ToolInfo } from "../shared/bridge.ts";
import { SettingsHelp } from "./SettingsHelp.tsx";
import { refresh, useStore } from "./store.ts";
import { unwrap } from "./unwrap.ts";
import { userErrorMessage } from "./user-error.ts";
export function ResourceSettings({ page }: { page: "prompt" | "tools" | "skills" }) {
  const settings = useStore((s) => s.settings);
  const [preview, setPreview] = useState<PromptPreview>();
  const [tools, setTools] = useState<ToolInfo[]>([]),
    [toolName, setToolName] = useState("");
  const [skills, setSkills] = useState<SkillSettings>(),
    [skillPath, setSkillPath] = useState(""),
    [body, setBody] = useState("");
  const [error, setError] = useState(""),
    [notice, setNotice] = useState(""),
    [saving, setSaving] = useState(false),
    [revision, setRevision] = useState(0);
  useEffect(() => {
    let active = true;
    setError("");
    if (page === "prompt") {
      void window.zpi
        .previewPrompt()
        .then(unwrap)
        .then((value) => {
          if (active) setPreview(value);
        })
        .catch((error) => {
          if (active) {
            setPreview(undefined);
            setError(userErrorMessage(error));
          }
        });
    }
    if (page === "tools")
      void window.zpi
        .listTools()
        .then(unwrap)
        .then((v) => {
          if (active) setTools(v);
        })
        .catch((e) => {
          if (active) setError(userErrorMessage(e));
        });
    if (page === "skills")
      void window.zpi
        .getSkillSettings()
        .then(unwrap)
        .then((v) => {
          if (active) setSkills(v);
        })
        .catch((e) => {
          if (active) setError(userErrorMessage(e));
        });
    return () => {
      active = false;
    };
  }, [page, revision, settings]);
  useEffect(() => {
    let active = true;
    setBody("");
    if (skillPath)
      void window.zpi
        .readSkill(skillPath)
        .then(unwrap)
        .then((v) => {
          if (active) setBody(v);
        })
        .catch((e) => {
          if (active) setError(userErrorMessage(e));
        });
    return () => {
      active = false;
    };
  }, [skillPath, revision]);
  const action = async (work: () => Promise<void>) => {
    setError("");
    setNotice("");
    setSaving(true);
    try {
      await work();
      setRevision((r) => r + 1);
    } catch (e) {
      setError(userErrorMessage(e));
    } finally {
      setSaving(false);
    }
  };
  const adopt = (value: NonNullable<typeof settings>) => {
    useStore.setState({ settings: value });
  };
  return (
    <div className="resource-settings">
      {error && (
        <div className="run-error" role="alert">
          {error}
        </div>
      )}
      {notice && <p role="status">{notice}</p>}
      {page === "prompt" && (
        <>
          <h2 className="preview-heading" aria-label="基础系统提示词 只读">
            <span className="settings-label">
              基础系统提示词{" "}
              <SettingsHelp
                label="基础系统提示词"
                text={`默认预览工作目录:${preview?.cwd ?? "正在读取…"}。新任务使用其绑定的工作目录和项目指令;此处只读。`}
              />
            </span>{" "}
            <small>只读</small>
          </h2>
          <pre className="prompt-preview">{preview?.basePrompt ?? "正在生成预览…"}</pre>
          <h2 className="preview-heading" aria-label="追加指令 只读">
            <span className="settings-label">
              追加指令{" "}
              <SettingsHelp
                label="追加指令"
                text="按启用的插件追加规则,可在插件页调整。新对话首次发送时生效,已有对话保留原配置;此处只读。"
              />
            </span>{" "}
            <small>只读</small>
          </h2>
          <pre className="custom-instructions-preview">
            {preview ? preview.customInstructions || "未启用追加指令。" : "正在生成预览…"}
          </pre>
        </>
      )}
      {page === "tools" && (
        <>
          <div className="resource-group-heading">
            <h3>
              <span className="settings-label">
                内置工具{" "}
                <SettingsHelp
                  label="内置工具"
                  text="查看工具能力、参数和启用状态。点击工具展开只读定义;列表按当前全局配置生成,已有对话保留其配置。"
                />
              </span>{" "}
              <span>{tools.length}</span>
            </h3>
          </div>
          <div className="tool-resource-list">
            {tools.map((tool) => {
              const Icon =
                { read: FileSearch, bash: SquareTerminal, edit: FilePenLine, write: FilePlus2 }[tool.name] ??
                Wrench;
              const expanded = toolName === tool.name;
              return (
                <div className="tool-resource-item" key={tool.name}>
                  <button
                    className="tool-resource-row"
                    type="button"
                    aria-label={`${tool.name} ${tool.enabled ? "已启用" : "未启用"}`}
                    aria-expanded={expanded}
                    aria-controls={`tool-detail-${tool.name}`}
                    onClick={() => setToolName(expanded ? "" : tool.name)}
                  >
                    <span className="resource-avatar" aria-hidden="true">
                      <Icon size={16} />
                    </span>
                    <span className="tool-resource-summary">
                      <span>{tool.name}</span>
                      <small>{tool.promptSnippet ?? tool.description}</small>
                    </span>
                    <span className="tool-resource-status">{tool.enabled ? "已启用" : "未启用"}</span>
                    <ChevronDown size={16} className={expanded ? "rotated" : ""} aria-hidden="true" />
                  </button>
                  {expanded && (
                    <section
                      className="tool-resource-detail"
                      id={`tool-detail-${tool.name}`}
                      aria-label={`${tool.name} 工具定义`}
                    >
                      <p>{tool.description}</p>
                      <h4>
                        参数 Schema <small>只读</small>
                      </h4>
                      <pre>{JSON.stringify(tool.parameters, null, 2)}</pre>
                      {tool.promptSnippet && (
                        <>
                          <h4>工具简介</h4>
                          <pre>{tool.promptSnippet}</pre>
                        </>
                      )}
                      {!!tool.promptGuidelines?.length && (
                        <>
                          <h4>使用规则</h4>
                          <pre>{tool.promptGuidelines.join("\n")}</pre>
                        </>
                      )}
                    </section>
                  )}
                </div>
              );
            })}
          </div>
        </>
      )}
      {page === "skills" && (
        <>
          <div className="resource-group-heading">
            <h3>
              <span className="settings-label">
                已安装{" "}
                <SettingsHelp
                  label="技能"
                  text={`技能开关在新任务首次发送时生效,已有任务保留原配置。\n全局技能目录:${skills?.directories.join("、") || "正在读取…"}`}
                />
              </span>{" "}
              <span>{skills?.skills.length ?? 0}</span>
            </h3>
            <button
              className="resource-icon-button"
              type="button"
              aria-label="刷新技能"
              title="刷新技能"
              onClick={() => setRevision((r) => r + 1)}
            >
              <RefreshCcw size={16} />
            </button>
          </div>
          {skills?.diagnostics.map((d) => (
            <p className="run-error" key={d.path}>
              {d.path}: {userErrorMessage(d.message)}
            </p>
          ))}
          <div className="skill-resource-list">
            {skills?.skills.map((s) => (
              <div className="skill-resource-row" key={s.path}>
                <div className="resource-avatar" aria-hidden="true">
                  <WandSparkles size={16} />
                </div>
                <button
                  className="skill-resource-name"
                  aria-label={`${s.name} ${s.source} ${s.description}`}
                  type="button"
                  title={s.path}
                  onClick={() => setSkillPath(s.path)}
                >
                  <span>{s.name}</span>
                  <small>{s.description || "暂无描述"}</small>
                  {s.overriddenBy && <small>已被覆盖:{s.overriddenBy}</small>}
                </button>
                <label className="settings-switch">
                  <input
                    type="checkbox"
                    aria-label={`启用技能 ${s.name} ${s.source}`}
                    checked={s.enabled}
                    disabled={saving}
                    onChange={(e) => {
                      const enabled = e.target.checked;
                      void action(async () => {
                        adopt(unwrap(await window.zpi.setSkillEnabled(s.path, enabled)));
                        await refresh();
                      });
                    }}
                  />
                  <span aria-hidden="true" />
                </label>
              </div>
            ))}
            {skills?.skills.length === 0 && <p className="resource-empty">暂无技能</p>}
          </div>
          <Dialog.Root
            open={!!skillPath}
            onOpenChange={(open) => {
              if (!open) setSkillPath("");
            }}
          >
            <Dialog.Portal>
              <Dialog.Overlay className="modal-backdrop skill-detail-backdrop" />
              <Dialog.Content
                className="skill-detail-dialog"
                aria-describedby={undefined}
                onEscapeKeyDown={(event) => event.stopPropagation()}
              >
                <header>
                  <Dialog.Title>
                    {skills?.skills.find((s) => s.path === skillPath)?.name ?? "技能详情"}
                  </Dialog.Title>
                  <Dialog.Close aria-label="关闭技能详情">
                    <X size={16} />
                  </Dialog.Close>
                </header>
                <div className="skill-detail-body">
                  <p>{skills?.skills.find((s) => s.path === skillPath)?.description}</p>
                  <dl>
                    <div>
                      <dt>来源</dt>
                      <dd>{skills?.skills.find((s) => s.path === skillPath)?.source}</dd>
                    </div>
                    <div>
                      <dt>状态</dt>
                      <dd>
                        {skills?.skills.find((s) => s.path === skillPath)?.enabled ? "已启用" : "未启用"}
                      </dd>
                    </div>
                  </dl>
                  <div className="skill-detail-path">
                    <span>路径</span>
                    <code>{skillPath}</code>
                  </div>
                  <h3>SKILL.md(只读)</h3>
                  {error && <p role="alert">{error}</p>}
                  <pre>{body || "正在读取…"}</pre>
                </div>
              </Dialog.Content>
            </Dialog.Portal>
          </Dialog.Root>
        </>
      )}
    </div>
  );
}