@spzxg/cli-ui 0.1.0 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +76 -14
- package/dist/{cli-ui-embed-BeBZKDZO.js → cli-ui-embed-hutDQhkT.js} +13316 -11962
- package/dist/{cli-ui-index-CuwqVeUk.js → cli-ui-index-C8TOk7M3.js} +1 -1
- package/dist/cli-ui.css +1 -1
- package/dist/cli-ui.js +15 -9
- package/package.json +1 -1
- package/src/board/BrowserPane.tsx +418 -0
- package/src/board/ChangesPane.tsx +142 -0
- package/src/board/CliBoard.tsx +246 -0
- package/src/board/FileViewer.tsx +257 -0
- package/src/board/FilesPane.tsx +109 -0
- package/src/board/blocks.tsx +96 -0
- package/src/board/fileKind.ts +81 -0
- package/src/chat/CliChat.tsx +186 -63
- package/src/core/client.ts +221 -10
- package/src/core/config.ts +4 -2
- package/src/core/conversation.ts +81 -13
- package/src/core/provider.tsx +33 -19
- package/src/core/store.ts +168 -84
- package/src/core/types.ts +109 -2
- package/src/elements.tsx +51 -7
- package/src/embed.ts +15 -4
- package/src/index.ts +11 -5
- package/src/msglist/CliMsgList.tsx +119 -25
- package/src/styles.css +124 -0
- package/src/ui/Markdown.tsx +35 -6
- package/src/ui/fileLinks.ts +46 -0
- package/src/ui/transcriptModel.ts +19 -1
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
// Agent 消息里指向本地文件的链接([报告](report.md)、/Users/…/a.ts、file:///…、src/a.ts:12)。
|
|
2
|
+
// 识别规则移植自 Operator 的 app/orchestrator/fileLinks.ts:原样渲染的话浏览器会把它当成宿主网站根目录下的地址,打不开。
|
|
3
|
+
// Operator 把它改写到任务的文件接口;这里由宿主 / 后端给出地址(fileUrl),给不出就显示成不可点的文字,不留坏链接。
|
|
4
|
+
import { createContext } from "react";
|
|
5
|
+
|
|
6
|
+
/** 本地文件 → 能打开的地址;返回 null 表示打不开(显示成文字)。 */
|
|
7
|
+
export type FileUrlResolver = (path: string, kind: "link" | "image") => string | null;
|
|
8
|
+
export const FileLinkContext = createContext<FileUrlResolver | null>(null);
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* 点了本地文件链接:交给看板 / 弹窗打开,返回 true 表示接住了(链接不再按网址跳转)。
|
|
12
|
+
* 按住 ⌘/Ctrl/Shift 点、或中键点,仍然按网址在新窗口打开。
|
|
13
|
+
*/
|
|
14
|
+
export type FileOpenHandler = (path: string) => boolean;
|
|
15
|
+
export const FileOpenContext = createContext<FileOpenHandler | null>(null);
|
|
16
|
+
|
|
17
|
+
const SCHEME = /^[a-z][a-z0-9+.-]*:/i;
|
|
18
|
+
const WIN_ABS = /^[a-z]:[\\/]/i;
|
|
19
|
+
|
|
20
|
+
function normalize(p: string): string {
|
|
21
|
+
const abs = p.startsWith("/");
|
|
22
|
+
const out: string[] = [];
|
|
23
|
+
for (const seg of p.split("/")) {
|
|
24
|
+
if (!seg || seg === ".") continue;
|
|
25
|
+
if (seg === ".." && out.length && out[out.length - 1] !== "..") out.pop();
|
|
26
|
+
else if (seg === ".." && abs) continue;
|
|
27
|
+
else out.push(seg);
|
|
28
|
+
}
|
|
29
|
+
return (abs ? "/" : "") + out.join("/");
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/** 链接指向本地文件时返回路径(相对工作目录或绝对路径);网址、锚点等返回 null。规则与 Operator 一致。 */
|
|
33
|
+
export function localFilePath(href: string | undefined | null): string | null {
|
|
34
|
+
let h = String(href ?? "").trim();
|
|
35
|
+
if (!h || h.startsWith("#") || h.startsWith("//") || h.startsWith("/api/") || h.startsWith("/view?")) return null;
|
|
36
|
+
if (/^file:/i.test(h)) h = h.replace(/^file:(\/\/localhost)?(\/\/)?/i, "");
|
|
37
|
+
else if (SCHEME.test(h) && !WIN_ABS.test(h)) return null;
|
|
38
|
+
h = h.replace(/[?#].*$/, "");
|
|
39
|
+
try { h = decodeURI(h); } catch { /* 保持原样 */ }
|
|
40
|
+
h = h.replace(/\\/g, "/");
|
|
41
|
+
h = h.replace(/:(\d+)(:\d+)?$/, "");
|
|
42
|
+
if (!h) return null;
|
|
43
|
+
if (h === "~" || h.startsWith("~/")) return h;
|
|
44
|
+
if (h.startsWith("/") || WIN_ABS.test(h)) return WIN_ABS.test(h) ? h : normalize(h);
|
|
45
|
+
return normalize(h);
|
|
46
|
+
}
|
|
@@ -16,12 +16,30 @@ const toolCache = new WeakMap<Msg, ToolData>();
|
|
|
16
16
|
export function toolDataOf(m: Msg): ToolData {
|
|
17
17
|
let data = toolCache.get(m);
|
|
18
18
|
if (!data) {
|
|
19
|
-
try { data = JSON.parse(m.content)
|
|
19
|
+
try { data = sanitizeTool(JSON.parse(m.content)); } catch { data = { title: m.content }; }
|
|
20
20
|
toolCache.set(m, data);
|
|
21
21
|
}
|
|
22
22
|
return data;
|
|
23
23
|
}
|
|
24
24
|
|
|
25
|
+
/**
|
|
26
|
+
* 工具卡片的数据来自节点,格式不对的部分丢掉而不是让整个对话渲染崩掉:
|
|
27
|
+
* 提问卡片要有 questions 数组,浏览器输入卡片要有 fields 数组,其余字段按类型保留。
|
|
28
|
+
*/
|
|
29
|
+
function sanitizeTool(raw: unknown): ToolData {
|
|
30
|
+
if (!raw || typeof raw !== "object" || Array.isArray(raw)) return { title: typeof raw === "string" ? raw : String(raw) };
|
|
31
|
+
const d = { ...(raw as Record<string, unknown>) } as Record<string, unknown> & ToolData;
|
|
32
|
+
if (typeof d.title !== "string") d.title = d.title == null ? "" : String(d.title);
|
|
33
|
+
for (const k of ["detail", "result"] as const) if (d[k] != null && typeof d[k] !== "string") d[k] = JSON.stringify(d[k]);
|
|
34
|
+
const ask = d.ask as { questions?: unknown } | undefined;
|
|
35
|
+
if (ask && (typeof ask !== "object" || !Array.isArray(ask.questions))) delete d.ask;
|
|
36
|
+
const input = d.input as { fields?: unknown; id?: unknown } | undefined;
|
|
37
|
+
if (input && (typeof input !== "object" || !Array.isArray(input.fields))) delete d.input;
|
|
38
|
+
if (d.diff && !Array.isArray(d.diff)) delete d.diff;
|
|
39
|
+
if (d.peek && typeof d.peek !== "object") delete d.peek;
|
|
40
|
+
return d;
|
|
41
|
+
}
|
|
42
|
+
|
|
25
43
|
/** 从最新一端找这个 id 的消息(实时更新都落在末尾)。 */
|
|
26
44
|
export function indexFromEnd(arr: Msg[], id: string): number {
|
|
27
45
|
for (let i = arr.length - 1; i >= 0; i--) if (arr[i].id === id) return i;
|