Z ZPI 文档 v0.3.8
介绍页

源码

packages/desktop/src/renderer/PaneTabOverview.tsx

tsx202 行查看原文
// Adapted from ZCode SidePaneTabOverview and sidePaneTabSearch (Apache-2.0).
import * as Popover from "@radix-ui/react-popover";
import { ActionHint } from "@zpi-dev/ui";
import { Command } from "cmdk";
import { ChevronsDown, Search, X } from "lucide-react";
import { useEffect, useMemo, useState } from "react";
import { PaneTabIcon } from "./PaneTabIcon.tsx";
import {
  activatePaneTab,
  closeTab,
  type PaneTab,
  paneTask,
  reopenPaneTab,
  usePane,
  visiblePaneTabs,
} from "./pane-store.ts";

function formatTaskRelativeTime(timestamp: number, now: number) {
  const minutes = Math.floor((now - timestamp) / 60000);
  return minutes < 1
    ? "刚刚"
    : minutes < 60
      ? `${minutes} 分钟前`
      : minutes < 1440
        ? `${Math.floor(minutes / 60)} 小时前`
        : `${Math.floor(minutes / 1440)} 天前`;
}

function rank(tabs: PaneTab[], query: string) {
  const parts = query.trim().toLocaleLowerCase().split(/\s+/).filter(Boolean);
  if (!parts.length) return tabs;
  return tabs
    .map((tab, index) => {
      const title = tab.title.toLocaleLowerCase();
      const hint = (
        tab.type === "changes"
          ? (tab.path ?? "")
          : tab.type === "browser"
            ? tab.state.url
            : "cwd" in tab
              ? (tab.cwd ?? "")
              : ""
      ).toLocaleLowerCase();
      const typeLabel =
        tab.type === "changes" && tab.reviewSource
          ? "审查"
          : tab.type === "browser"
            ? "浏览器"
            : tab.type === "terminal" || tab.type === "background-task"
              ? "终端"
              : ["file-manager", "file", "changes"].includes(tab.type)
                ? "文件"
                : tab.type === "new-tab"
                  ? "新标签页"
                  : tab.type.startsWith("side-chat")
                    ? "辅助对话"
                    : "任务";
      const all = `${title} ${hint} ${typeLabel}`;
      const score = parts.every((part) => all.includes(part))
        ? parts.reduce(
            (score, part) =>
              score +
              (title.startsWith(part)
                ? 120
                : title.split(/[\s/_.:-]+/).some((word) => word.startsWith(part))
                  ? 90
                  : title.includes(part)
                    ? 70
                    : hint.includes(part)
                      ? 40
                      : 20),
            0,
          )
        : -1;
      return { tab, index, score };
    })
    .filter(({ score }) => score >= 0)
    .sort((a, b) => b.score - a.score || a.index - b.index)
    .map(({ tab }) => tab);
}
export function PaneTabOverview({ sessionId }: { sessionId?: string }) {
  const [open, setOpen] = useState(false);
  const [query, setQuery] = useState("");
  const [now, setNow] = useState(Date.now);
  const state = usePane();
  useEffect(() => {
    if (!open) return;
    setNow(Date.now());
    const timer = setInterval(() => setNow(Date.now()), 60_000);
    return () => clearInterval(timer);
  }, [open]);
  const tabs = useMemo(
    () => rank(visiblePaneTabs(sessionId, state.tabs), query),
    [sessionId, state.tabs, query],
  );
  const closed = useMemo(
    () =>
      rank(
        visiblePaneTabs(
          sessionId,
          state.recentClosed.map(({ tab }) => tab),
        ),
        query,
      ),
    [sessionId, state.recentClosed, query],
  );
  const choose = (id: string, closed: boolean) => {
    if (closed) paneTask(reopenPaneTab(id));
    else activatePaneTab(id);
    setOpen(false);
    setQuery("");
  };
  return (
    <Popover.Root
      open={open}
      onOpenChange={(value) => {
        setOpen(value);
        if (!value) setQuery("");
      }}
    >
      <ActionHint label="搜索标签页" appearance="control" side="bottom">
        <Popover.Trigger className="pane-header-action" aria-label="搜索标签页">
          <ChevronsDown size={16} />
        </Popover.Trigger>
      </ActionHint>
      <Popover.Portal>
        <Popover.Content
          className="pane-tab-overview"
          align="start"
          sideOffset={4}
          onCloseAutoFocus={(event) => event.preventDefault()}
        >
          <Command shouldFilter={false} label="搜索标签页">
            <div className="pane-overview-search">
              <Search size={16} />
              <Command.Input
                value={query}
                onValueChange={setQuery}
                placeholder="搜索标签页..."
                aria-label="搜索标签页..."
              />
            </div>
            <Command.List>
              {(
                [
                  [false, tabs, "打开的标签页"],
                  [true, closed, "最近关闭的标签页"],
                ] as const
              ).map(([isClosed, tabs, title]) =>
                tabs.length || (!isClosed && !closed.length) ? (
                  <Command.Group key={title} heading={title}>
                    {!tabs.length && <Command.Empty>没有找到标签页。</Command.Empty>}
                    {tabs.map((tab) => (
                      <Command.Item
                        key={tab.id}
                        value={`${isClosed}:${tab.id}`}
                        onSelect={() => choose(tab.id, isClosed)}
                        data-active={(!isClosed && state.active === tab.id) || undefined}
                      >
                        <span className="pane-overview-icon">
                          <PaneTabIcon tab={tab} />
                        </span>
                        <span className="pane-overview-title">{tab.title}</span>
                        <span className="pane-overview-time">
                          {formatTaskRelativeTime(
                            isClosed
                              ? (state.recentClosed.find(({ tab: closed }) => closed.id === tab.id)
                                  ?.closedAt ?? now)
                              : (tab.openedAt ?? now),
                            now,
                          )}
                          {isClosed ? "关闭" : "打开"}
                        </span>
                        {!isClosed && (
                          <button
                            aria-label={`关闭标签 ${tab.title}`}
                            onPointerDown={(event) => {
                              event.preventDefault();
                              event.stopPropagation();
                            }}
                            onClick={(event) => {
                              event.preventDefault();
                              event.stopPropagation();
                              paneTask(closeTab(tab.id));
                            }}
                          >
                            <X size={12} />
                          </button>
                        )}
                      </Command.Item>
                    ))}
                  </Command.Group>
                ) : null,
              )}
            </Command.List>
          </Command>
        </Popover.Content>
      </Popover.Portal>
    </Popover.Root>
  );
}