paseo-plugin-chat-view 0.0.0-stage → 0.1.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/LICENSE +21 -0
- package/LICENSE.compact-agent-activity +21 -0
- package/LICENSE.mermaid +21 -0
- package/README.md +428 -2
- package/README_zh.md +429 -0
- package/client/activity/Activity.test.ts +220 -0
- package/client/activity/Activity.tsx +784 -0
- package/client/activity/detail/detail.test.ts +102 -0
- package/client/activity/detail/dispatch.tsx +94 -0
- package/client/activity/detail/file.tsx +93 -0
- package/client/activity/detail/index.tsx +39 -0
- package/client/activity/detail/parts.tsx +221 -0
- package/client/activity/detail/search.tsx +59 -0
- package/client/activity/detail/terminal.tsx +53 -0
- package/client/activity/detail/texts.tsx +42 -0
- package/client/activity/disclosure.ts +49 -0
- package/client/activity/dom.test.ts +52 -0
- package/client/activity/dom.ts +138 -0
- package/client/activity/group-store.test.ts +126 -0
- package/client/activity/group-store.ts +265 -0
- package/client/activity/highlight.test.ts +116 -0
- package/client/activity/highlight.ts +418 -0
- package/client/activity/history.test.ts +602 -0
- package/client/activity/history.ts +239 -0
- package/client/activity/index.ts +74 -0
- package/client/activity/latest-marker.test.ts +41 -0
- package/client/activity/latest-marker.ts +70 -0
- package/client/activity/styles.ts +535 -0
- package/client/activity/tools/child-agent.tsx +308 -0
- package/client/activity/tools/exa.tsx +99 -0
- package/client/activity/tools/github.tsx +501 -0
- package/client/activity/tools/list.tsx +102 -0
- package/client/activity/tools/lists.test.ts +193 -0
- package/client/activity/tools/paseo/agent.tsx +384 -0
- package/client/activity/tools/paseo/browser.tsx +322 -0
- package/client/activity/tools/paseo/index.tsx +79 -0
- package/client/activity/tools/paseo/parts.tsx +133 -0
- package/client/activity/tools/paseo/provider.tsx +220 -0
- package/client/activity/tools/paseo/schedule.tsx +246 -0
- package/client/activity/tools/paseo/speak.tsx +20 -0
- package/client/activity/tools/paseo/terminal.tsx +214 -0
- package/client/activity/tools/paseo/workspace.tsx +162 -0
- package/client/activity/tools/shared.tsx +431 -0
- package/client/activity/transform.test.ts +28 -0
- package/client/activity/transform.ts +43 -0
- package/client/agent-pills.ts +56 -0
- package/client/dom.ts +75 -0
- package/client/enhance/controller.ts +111 -0
- package/client/enhance/dom.test.ts +28 -0
- package/client/enhance/dom.ts +101 -0
- package/client/enhance/index.ts +95 -0
- package/client/enhance/pill.tsx +19 -0
- package/client/enhance/state.ts +58 -0
- package/client/feature.ts +22 -0
- package/client/features.test.ts +149 -0
- package/client/features.ts +83 -0
- package/client/locale.ts +45 -0
- package/client/mermaid/diagram.tsx +438 -0
- package/client/mermaid/index.ts +27 -0
- package/client/mermaid/item.tsx +28 -0
- package/client/mermaid/markdown.tsx +217 -0
- package/client/mermaid/registration.ts +32 -0
- package/client/mermaid/toolbar.tsx +175 -0
- package/client/mermaid/viewer.tsx +314 -0
- package/client/readonly/dom.test.ts +66 -0
- package/client/readonly/dom.ts +32 -0
- package/client/readonly/index.ts +27 -0
- package/client/registration.test.ts +243 -0
- package/client/settings.tsx +438 -0
- package/client/sticky/StickyBar.tsx +144 -0
- package/client/sticky/dom.test.ts +230 -0
- package/client/sticky/dom.ts +517 -0
- package/client/sticky/index.ts +38 -0
- package/client/sticky/pill.tsx +70 -0
- package/client/timeline-pager.test.ts +121 -0
- package/client/timeline-pager.ts +123 -0
- package/index.client.tsx +44 -0
- package/index.server.ts +12 -0
- package/package.json +42 -4
- package/paseo-plugin.json +6 -0
- package/server/enhance/index.ts +63 -0
- package/server/enhance/manifest.ts +176 -0
- package/server/enhance/probe.test.ts +215 -0
- package/server/enhance/probe.ts +118 -0
- package/server/enhance/provider.ts +149 -0
- package/shared/activity/child-agent.test.ts +27 -0
- package/shared/activity/child-agent.ts +26 -0
- package/shared/activity/diff.test.ts +74 -0
- package/shared/activity/diff.ts +207 -0
- package/shared/activity/exa.test.ts +66 -0
- package/shared/activity/exa.ts +183 -0
- package/shared/activity/file-kind.test.ts +13 -0
- package/shared/activity/file-kind.ts +112 -0
- package/shared/activity/github.test.ts +56 -0
- package/shared/activity/github.ts +174 -0
- package/shared/activity/palette.test.ts +26 -0
- package/shared/activity/palette.ts +106 -0
- package/shared/activity/parse.test.ts +39 -0
- package/shared/activity/parse.ts +115 -0
- package/shared/activity/paseo-tools.test.ts +163 -0
- package/shared/activity/paseo-tools.ts +497 -0
- package/shared/activity/text.test.ts +85 -0
- package/shared/activity/text.ts +202 -0
- package/shared/activity/timeline.test.ts +26 -0
- package/shared/activity/timeline.ts +108 -0
- package/shared/activity/tool-presentation.test.ts +154 -0
- package/shared/activity/tool-presentation.ts +352 -0
- package/shared/enhance/contract.ts +105 -0
- package/shared/enhance/decide.ts +97 -0
- package/shared/enhance/enhance.test.ts +188 -0
- package/shared/enhance/failure.ts +40 -0
- package/shared/enhance/index.ts +8 -0
- package/shared/enhance/normalize.ts +16 -0
- package/shared/enhance/parse.ts +79 -0
- package/shared/enhance/policy.ts +48 -0
- package/shared/enhance/prompt.ts +134 -0
- package/shared/enhance/run.ts +84 -0
- package/shared/i18n.test.ts +81 -0
- package/shared/i18n.ts +300 -0
- package/shared/mermaid/flowchart.test.ts +169 -0
- package/shared/mermaid/flowchart.ts +271 -0
- package/shared/mermaid/layout.test.ts +305 -0
- package/shared/mermaid/layout.ts +496 -0
- package/shared/mermaid/parse.test.ts +110 -0
- package/shared/mermaid/parse.ts +193 -0
- package/shared/mermaid/segment.test.ts +55 -0
- package/shared/mermaid/segment.ts +95 -0
- package/shared/mermaid/sequence.test.ts +68 -0
- package/shared/mermaid/sequence.ts +130 -0
- package/shared/mermaid/transform.test.ts +52 -0
- package/shared/mermaid/transform.ts +45 -0
- package/shared/mermaid/zoom.test.ts +134 -0
- package/shared/mermaid/zoom.ts +114 -0
- package/shared/settings.ts +41 -0
- package/shared/sticky/history.test.ts +91 -0
- package/shared/sticky/history.ts +86 -0
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
import { renderToStaticMarkup } from "react-dom/server";
|
|
3
|
+
import { expect, it, vi } from "vitest";
|
|
4
|
+
import { createToolCallData } from "../../../shared/activity/timeline";
|
|
5
|
+
import { resolveActivityPalette } from "../../../shared/activity/palette";
|
|
6
|
+
import { useActivityStyles } from "../styles";
|
|
7
|
+
import { DetailBody } from "./index";
|
|
8
|
+
|
|
9
|
+
vi.mock("react-native", async () => await import("react-native-web"));
|
|
10
|
+
vi.mock("@getpaseo/plugin/client/react-native", async () => ({
|
|
11
|
+
ScrollView: (await import("react-native-web")).ScrollView,
|
|
12
|
+
Icon: () => null,
|
|
13
|
+
useRevealedText: (text: string) => text,
|
|
14
|
+
}));
|
|
15
|
+
|
|
16
|
+
const theme = {
|
|
17
|
+
colors: {
|
|
18
|
+
foreground: "#222222",
|
|
19
|
+
foregroundMuted: "#777777",
|
|
20
|
+
border: "#dddddd",
|
|
21
|
+
surface0: "#ffffff",
|
|
22
|
+
surface1: "#eeeeee",
|
|
23
|
+
accent: "#0066cc",
|
|
24
|
+
statusSuccess: "#00aa44",
|
|
25
|
+
statusWarning: "#cc8800",
|
|
26
|
+
statusDanger: "#cc2222",
|
|
27
|
+
},
|
|
28
|
+
};
|
|
29
|
+
const palette = resolveActivityPalette("default", theme.colors as never);
|
|
30
|
+
|
|
31
|
+
function Body({ toolName, detail }: { toolName: string; detail: unknown }) {
|
|
32
|
+
const styles = useActivityStyles(theme as never, palette);
|
|
33
|
+
const data = createToolCallData({
|
|
34
|
+
type: "tool_call",
|
|
35
|
+
callId: "call",
|
|
36
|
+
name: toolName,
|
|
37
|
+
status: "completed",
|
|
38
|
+
error: null,
|
|
39
|
+
detail,
|
|
40
|
+
} as never);
|
|
41
|
+
return React.createElement(DetailBody, { data, theme, palette, styles } as never);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
const render = (toolName: string, detail: unknown) =>
|
|
45
|
+
renderToStaticMarkup(React.createElement(Body, { toolName, detail }));
|
|
46
|
+
|
|
47
|
+
// 每个 detail 类型都要落到自己的渲染器:断言各自独有的文案,而不是"渲染成功"。
|
|
48
|
+
const cases: Array<[string, string, unknown, string]> = [
|
|
49
|
+
[
|
|
50
|
+
"shell",
|
|
51
|
+
"exec_command",
|
|
52
|
+
{ type: "shell", command: "npm test", cwd: "/tmp", output: "ok" },
|
|
53
|
+
"Command",
|
|
54
|
+
],
|
|
55
|
+
[
|
|
56
|
+
"read",
|
|
57
|
+
"read_file",
|
|
58
|
+
{ type: "read", filePath: "src/a.ts", content: "const a = 1;" },
|
|
59
|
+
"Contents",
|
|
60
|
+
],
|
|
61
|
+
[
|
|
62
|
+
"write",
|
|
63
|
+
"write_file",
|
|
64
|
+
{ type: "write", filePath: "src/a.ts", content: "x" },
|
|
65
|
+
"Written contents",
|
|
66
|
+
],
|
|
67
|
+
[
|
|
68
|
+
"edit",
|
|
69
|
+
"edit_file",
|
|
70
|
+
{ type: "edit", filePath: "src/a.ts", unifiedDiff: "@@ -1 +1 @@\n-old\n+new\n" },
|
|
71
|
+
"Diff",
|
|
72
|
+
],
|
|
73
|
+
[
|
|
74
|
+
"search",
|
|
75
|
+
"search_files",
|
|
76
|
+
{ type: "search", query: "needle", filePaths: ["a.ts"] },
|
|
77
|
+
"Query: needle",
|
|
78
|
+
],
|
|
79
|
+
["fetch", "fetch_url", { type: "fetch", url: "https://x.test", result: "body" }, "Result"],
|
|
80
|
+
[
|
|
81
|
+
"worktree_setup",
|
|
82
|
+
"worktree_setup",
|
|
83
|
+
{ type: "worktree_setup", branchName: "b", worktreePath: "/tmp/w" },
|
|
84
|
+
"Branch: b",
|
|
85
|
+
],
|
|
86
|
+
[
|
|
87
|
+
"sub_agent",
|
|
88
|
+
"task",
|
|
89
|
+
{ type: "sub_agent", subAgentType: "explore", log: "[read] a.ts" },
|
|
90
|
+
"Activity log",
|
|
91
|
+
],
|
|
92
|
+
["plain_text", "speak", { type: "plain_text", text: "hello" }, "hello"],
|
|
93
|
+
["plan", "update_plan", { type: "plan", text: "- one\n- two" }, "- one"],
|
|
94
|
+
["unknown", "mystery_tool", { type: "unknown", input: { a: 1 }, output: [1, 2] }, "Input"],
|
|
95
|
+
];
|
|
96
|
+
|
|
97
|
+
it.each(cases)(
|
|
98
|
+
"renders %s details through its own renderer",
|
|
99
|
+
(_type, toolName, detail, expected) => {
|
|
100
|
+
expect(render(toolName, detail)).toContain(expected);
|
|
101
|
+
},
|
|
102
|
+
);
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
|
|
3
|
+
import { exaToolKind } from "../../../shared/activity/exa";
|
|
4
|
+
import { githubToolKind } from "../../../shared/activity/github";
|
|
5
|
+
import { paseoToolLeafName } from "../../../shared/activity/paseo-tools";
|
|
6
|
+
import { extractCodeInput, formatUnknownValue } from "../../../shared/activity/text";
|
|
7
|
+
import { ExaToolDetail } from "../tools/exa";
|
|
8
|
+
import { GithubToolDetail } from "../tools/github";
|
|
9
|
+
import { PaseoToolDetail } from "../tools/paseo/index";
|
|
10
|
+
import { DetailRenderProps, HighlightedCodeBlock } from "./parts";
|
|
11
|
+
|
|
12
|
+
/** Unknown payloads: hand them to the tool-specific panels, or show raw JSON. */
|
|
13
|
+
export function UnknownDetail({
|
|
14
|
+
data,
|
|
15
|
+
detail,
|
|
16
|
+
theme,
|
|
17
|
+
palette,
|
|
18
|
+
styles,
|
|
19
|
+
}: DetailRenderProps<"unknown">) {
|
|
20
|
+
if (githubToolKind(data.name)) {
|
|
21
|
+
return (
|
|
22
|
+
<GithubToolDetail
|
|
23
|
+
toolName={data.name}
|
|
24
|
+
input={detail.input}
|
|
25
|
+
output={detail.output}
|
|
26
|
+
theme={theme}
|
|
27
|
+
palette={palette}
|
|
28
|
+
styles={styles}
|
|
29
|
+
/>
|
|
30
|
+
);
|
|
31
|
+
}
|
|
32
|
+
if (exaToolKind(data.name)) {
|
|
33
|
+
return (
|
|
34
|
+
<ExaToolDetail
|
|
35
|
+
toolName={data.name}
|
|
36
|
+
input={detail.input}
|
|
37
|
+
output={detail.output}
|
|
38
|
+
theme={theme}
|
|
39
|
+
styles={styles}
|
|
40
|
+
/>
|
|
41
|
+
);
|
|
42
|
+
}
|
|
43
|
+
if (paseoToolLeafName(data.name)) {
|
|
44
|
+
return (
|
|
45
|
+
<PaseoToolDetail
|
|
46
|
+
toolName={data.name}
|
|
47
|
+
input={detail.input}
|
|
48
|
+
output={detail.output}
|
|
49
|
+
theme={theme}
|
|
50
|
+
palette={palette}
|
|
51
|
+
styles={styles}
|
|
52
|
+
/>
|
|
53
|
+
);
|
|
54
|
+
}
|
|
55
|
+
const codeInput = extractCodeInput(data.name, detail.input);
|
|
56
|
+
if (codeInput) {
|
|
57
|
+
return (
|
|
58
|
+
<>
|
|
59
|
+
<HighlightedCodeBlock
|
|
60
|
+
code={codeInput.code}
|
|
61
|
+
language={codeInput.language}
|
|
62
|
+
label="Input"
|
|
63
|
+
styles={styles}
|
|
64
|
+
theme={theme}
|
|
65
|
+
/>
|
|
66
|
+
<HighlightedCodeBlock
|
|
67
|
+
code={formatUnknownValue(detail.output)}
|
|
68
|
+
language="json"
|
|
69
|
+
label="Output"
|
|
70
|
+
styles={styles}
|
|
71
|
+
theme={theme}
|
|
72
|
+
/>
|
|
73
|
+
</>
|
|
74
|
+
);
|
|
75
|
+
}
|
|
76
|
+
return (
|
|
77
|
+
<>
|
|
78
|
+
<HighlightedCodeBlock
|
|
79
|
+
code={formatUnknownValue(detail.input)}
|
|
80
|
+
language="json"
|
|
81
|
+
label="Input"
|
|
82
|
+
styles={styles}
|
|
83
|
+
theme={theme}
|
|
84
|
+
/>
|
|
85
|
+
<HighlightedCodeBlock
|
|
86
|
+
code={formatUnknownValue(detail.output)}
|
|
87
|
+
language="json"
|
|
88
|
+
label="Output"
|
|
89
|
+
styles={styles}
|
|
90
|
+
theme={theme}
|
|
91
|
+
/>
|
|
92
|
+
</>
|
|
93
|
+
);
|
|
94
|
+
}
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
import { Text } from "react-native";
|
|
3
|
+
|
|
4
|
+
import { MAX_DIFF_CHARS } from "../../../shared/activity/diff";
|
|
5
|
+
import { DetailRenderProps, DiffBlock, HighlightedCodeBlock, PathRow } from "./parts";
|
|
6
|
+
|
|
7
|
+
/** Everything that shows file contents: reads, writes, and edits as a diff. */
|
|
8
|
+
export function ReadDetail({ data, detail, theme, styles }: DetailRenderProps<"read">) {
|
|
9
|
+
return (
|
|
10
|
+
<>
|
|
11
|
+
<PathRow icon={data.presentation.fileIcon ?? "Eye"} path={detail.filePath} styles={styles} />
|
|
12
|
+
{detail.content ? (
|
|
13
|
+
<HighlightedCodeBlock
|
|
14
|
+
code={detail.content}
|
|
15
|
+
language={data.presentation.language ?? "text"}
|
|
16
|
+
label="Contents"
|
|
17
|
+
styles={styles}
|
|
18
|
+
theme={theme}
|
|
19
|
+
/>
|
|
20
|
+
) : (
|
|
21
|
+
<Text style={styles.empty}>No file contents returned.</Text>
|
|
22
|
+
)}
|
|
23
|
+
</>
|
|
24
|
+
);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export function WriteDetail({ data, detail, theme, styles }: DetailRenderProps<"write">) {
|
|
28
|
+
return (
|
|
29
|
+
<>
|
|
30
|
+
<PathRow
|
|
31
|
+
icon={data.presentation.fileIcon ?? "Pencil"}
|
|
32
|
+
path={detail.filePath}
|
|
33
|
+
styles={styles}
|
|
34
|
+
/>
|
|
35
|
+
{detail.content ? (
|
|
36
|
+
<HighlightedCodeBlock
|
|
37
|
+
code={detail.content}
|
|
38
|
+
language={data.presentation.language ?? "text"}
|
|
39
|
+
label="Written contents"
|
|
40
|
+
styles={styles}
|
|
41
|
+
theme={theme}
|
|
42
|
+
/>
|
|
43
|
+
) : null}
|
|
44
|
+
</>
|
|
45
|
+
);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export function EditDetail({ data, detail, theme, styles, palette }: DetailRenderProps<"edit">) {
|
|
49
|
+
const isOversized =
|
|
50
|
+
(detail.unifiedDiff?.length ??
|
|
51
|
+
(detail.oldString?.length ?? 0) + (detail.newString?.length ?? 0)) > MAX_DIFF_CHARS;
|
|
52
|
+
|
|
53
|
+
if (isOversized && detail.unifiedDiff === undefined) {
|
|
54
|
+
return (
|
|
55
|
+
<>
|
|
56
|
+
<PathRow
|
|
57
|
+
icon={data.presentation.fileIcon ?? "Pencil"}
|
|
58
|
+
path={detail.filePath}
|
|
59
|
+
styles={styles}
|
|
60
|
+
/>
|
|
61
|
+
{detail.oldString ? (
|
|
62
|
+
<HighlightedCodeBlock
|
|
63
|
+
code={detail.oldString}
|
|
64
|
+
language={data.presentation.language ?? "text"}
|
|
65
|
+
label="Before"
|
|
66
|
+
styles={styles}
|
|
67
|
+
theme={theme}
|
|
68
|
+
/>
|
|
69
|
+
) : null}
|
|
70
|
+
{detail.newString ? (
|
|
71
|
+
<HighlightedCodeBlock
|
|
72
|
+
code={detail.newString}
|
|
73
|
+
language={data.presentation.language ?? "text"}
|
|
74
|
+
label="After"
|
|
75
|
+
styles={styles}
|
|
76
|
+
theme={theme}
|
|
77
|
+
/>
|
|
78
|
+
) : null}
|
|
79
|
+
</>
|
|
80
|
+
);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
return (
|
|
84
|
+
<>
|
|
85
|
+
<PathRow
|
|
86
|
+
icon={data.presentation.fileIcon ?? "Pencil"}
|
|
87
|
+
path={detail.filePath}
|
|
88
|
+
styles={styles}
|
|
89
|
+
/>
|
|
90
|
+
<DiffBlock detail={detail} palette={palette} styles={styles} />
|
|
91
|
+
</>
|
|
92
|
+
);
|
|
93
|
+
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
import { Text } from "react-native";
|
|
3
|
+
|
|
4
|
+
import { FetchDetail, SearchDetail } from "./search";
|
|
5
|
+
import { EditDetail, ReadDetail, WriteDetail } from "./file";
|
|
6
|
+
import { ShellDetail, WorktreeSetupDetail } from "./terminal";
|
|
7
|
+
import { PlainTextDetail, PlanDetail, SubAgentDetail } from "./texts";
|
|
8
|
+
import { UnknownDetail } from "./dispatch";
|
|
9
|
+
import { asToolCallDetail, type DetailRenderProps } from "./parts";
|
|
10
|
+
|
|
11
|
+
/** The detail module's surface: the dispatcher plus the primitives the activity rows reuse. */
|
|
12
|
+
export { DetailLabel, HighlightedCodeBlock, asToolCallDetail } from "./parts";
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* One renderer per detail type, so a new protocol detail cannot be forgotten: the record stops
|
|
16
|
+
* compiling until it is registered.
|
|
17
|
+
*/
|
|
18
|
+
const RENDERERS: {
|
|
19
|
+
[K in DetailRenderProps["detail"]["type"]]: (props: DetailRenderProps<K>) => React.ReactNode;
|
|
20
|
+
} = {
|
|
21
|
+
shell: ShellDetail,
|
|
22
|
+
read: ReadDetail,
|
|
23
|
+
write: WriteDetail,
|
|
24
|
+
edit: EditDetail,
|
|
25
|
+
search: SearchDetail,
|
|
26
|
+
fetch: FetchDetail,
|
|
27
|
+
worktree_setup: WorktreeSetupDetail,
|
|
28
|
+
sub_agent: SubAgentDetail,
|
|
29
|
+
plain_text: PlainTextDetail,
|
|
30
|
+
plan: PlanDetail,
|
|
31
|
+
unknown: UnknownDetail,
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
export function DetailBody(props: Omit<DetailRenderProps, "detail">): React.ReactNode {
|
|
35
|
+
const detail = asToolCallDetail(props.data.detail);
|
|
36
|
+
if (!detail) return <Text style={props.styles.empty}>Tool details unavailable.</Text>;
|
|
37
|
+
const render = RENDERERS[detail.type] as (props: DetailRenderProps) => React.ReactNode;
|
|
38
|
+
return render({ ...props, detail });
|
|
39
|
+
}
|
|
@@ -0,0 +1,221 @@
|
|
|
1
|
+
import type { ToolCallDetail } from "@getpaseo/protocol/agent-types";
|
|
2
|
+
import React, { useMemo, useState, type ReactNode } from "react";
|
|
3
|
+
import { Icon, ScrollView } from "@getpaseo/plugin/client/react-native";
|
|
4
|
+
import { Pressable, Text, View, type TextStyle } from "react-native";
|
|
5
|
+
|
|
6
|
+
import type { ActivityPalette } from "../../../shared/activity/palette";
|
|
7
|
+
import { diffLinesForDetail, type DiffLine } from "../../../shared/activity/diff";
|
|
8
|
+
import { PREVIEW_LINES, previewText } from "../../../shared/activity/text";
|
|
9
|
+
import type { ToolCallItemData } from "../../../shared/activity/timeline";
|
|
10
|
+
import { useHighlightTokens, type HighlightToken } from "../highlight";
|
|
11
|
+
import { useActivityStyles, type Theme } from "../styles";
|
|
12
|
+
|
|
13
|
+
export type ActivityStyles = ReturnType<typeof useActivityStyles>;
|
|
14
|
+
|
|
15
|
+
/** Everything a per-case detail renderer needs; `detail` is narrowed to the case it renders. */
|
|
16
|
+
export interface DetailRenderProps<K extends ToolCallDetail["type"] = ToolCallDetail["type"]> {
|
|
17
|
+
data: ToolCallItemData;
|
|
18
|
+
detail: Extract<ToolCallDetail, { type: K }>;
|
|
19
|
+
theme: Theme;
|
|
20
|
+
palette: ActivityPalette;
|
|
21
|
+
styles: ActivityStyles;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/** Detail payloads arrive from the wire, so the shape is only known after a runtime check. */
|
|
25
|
+
export function asToolCallDetail(value: ToolCallItemData["detail"]): ToolCallDetail | null {
|
|
26
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) return null;
|
|
27
|
+
const type = Reflect.get(value, "type");
|
|
28
|
+
if (typeof type !== "string") return null;
|
|
29
|
+
return value as unknown as ToolCallDetail;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export function DetailLabel({ children, style }: { children: ReactNode; style: TextStyle }) {
|
|
33
|
+
return <Text style={style}>{children}</Text>;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export function PathRow({
|
|
37
|
+
icon,
|
|
38
|
+
path,
|
|
39
|
+
styles,
|
|
40
|
+
}: {
|
|
41
|
+
icon: string;
|
|
42
|
+
path: string;
|
|
43
|
+
styles: ActivityStyles;
|
|
44
|
+
}) {
|
|
45
|
+
return (
|
|
46
|
+
<View style={styles.pathRow}>
|
|
47
|
+
<Icon name={icon} color={styles.pathText.color} size={12} />
|
|
48
|
+
<Text selectable style={styles.pathText}>
|
|
49
|
+
{path}
|
|
50
|
+
</Text>
|
|
51
|
+
</View>
|
|
52
|
+
);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function TokenizedLines({ lines, styles }: { lines: HighlightToken[][]; styles: ActivityStyles }) {
|
|
56
|
+
return (
|
|
57
|
+
<View>
|
|
58
|
+
{lines.map((line, lineIndex) => (
|
|
59
|
+
<Text key={`line-${lineIndex}`} selectable style={styles.codeLine}>
|
|
60
|
+
{line.length === 0
|
|
61
|
+
? " "
|
|
62
|
+
: line.map((token, tokenIndex) => {
|
|
63
|
+
const tokenStyle: TextStyle = {
|
|
64
|
+
color: token.color ?? styles.codeLine.color,
|
|
65
|
+
...(token.fontStyle && token.fontStyle & 1 ? { fontStyle: "italic" } : {}),
|
|
66
|
+
...(token.fontStyle && token.fontStyle & 2 ? { fontWeight: "700" } : {}),
|
|
67
|
+
...(token.fontStyle && token.fontStyle & 4
|
|
68
|
+
? { textDecorationLine: "underline" }
|
|
69
|
+
: {}),
|
|
70
|
+
};
|
|
71
|
+
return (
|
|
72
|
+
<Text key={`${lineIndex}-${tokenIndex}`} style={tokenStyle}>
|
|
73
|
+
{token.content}
|
|
74
|
+
</Text>
|
|
75
|
+
);
|
|
76
|
+
})}
|
|
77
|
+
</Text>
|
|
78
|
+
))}
|
|
79
|
+
</View>
|
|
80
|
+
);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
export function HighlightedCodeBlock({
|
|
84
|
+
code,
|
|
85
|
+
language,
|
|
86
|
+
theme,
|
|
87
|
+
label,
|
|
88
|
+
styles,
|
|
89
|
+
}: {
|
|
90
|
+
code: string;
|
|
91
|
+
language: string;
|
|
92
|
+
theme: Theme;
|
|
93
|
+
label?: string;
|
|
94
|
+
styles: ActivityStyles;
|
|
95
|
+
}) {
|
|
96
|
+
const [showAll, setShowAll] = useState(false);
|
|
97
|
+
const preview = useMemo(() => previewText(code), [code]);
|
|
98
|
+
const displayCode = showAll ? code : preview.text;
|
|
99
|
+
const tokens = useHighlightTokens(displayCode, language, theme.colors);
|
|
100
|
+
|
|
101
|
+
return (
|
|
102
|
+
<View style={styles.section}>
|
|
103
|
+
{label ? <DetailLabel style={styles.detailLabel}>{label}</DetailLabel> : null}
|
|
104
|
+
<ScrollView horizontal nestedScrollEnabled style={styles.codeScroll}>
|
|
105
|
+
<View style={styles.codeSurface}>
|
|
106
|
+
{tokens ? (
|
|
107
|
+
<TokenizedLines lines={tokens} styles={styles} />
|
|
108
|
+
) : (
|
|
109
|
+
<Text selectable style={styles.codeLine}>
|
|
110
|
+
{displayCode || " "}
|
|
111
|
+
</Text>
|
|
112
|
+
)}
|
|
113
|
+
</View>
|
|
114
|
+
</ScrollView>
|
|
115
|
+
{preview.truncated ? (
|
|
116
|
+
<Pressable
|
|
117
|
+
accessibilityRole="button"
|
|
118
|
+
onPress={() => setShowAll(!showAll)}
|
|
119
|
+
style={styles.showMoreButton}
|
|
120
|
+
>
|
|
121
|
+
<Text style={styles.showMoreText}>
|
|
122
|
+
{showAll
|
|
123
|
+
? "Show less"
|
|
124
|
+
: `Show all (${preview.totalLines} lines, ${preview.totalChars.toLocaleString()} chars)`}
|
|
125
|
+
</Text>
|
|
126
|
+
</Pressable>
|
|
127
|
+
) : null}
|
|
128
|
+
</View>
|
|
129
|
+
);
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
export function DiffBlock({
|
|
133
|
+
detail,
|
|
134
|
+
palette,
|
|
135
|
+
styles,
|
|
136
|
+
}: {
|
|
137
|
+
detail: Extract<ToolCallDetail, { type: "edit" }>;
|
|
138
|
+
palette: ActivityPalette;
|
|
139
|
+
styles: ActivityStyles;
|
|
140
|
+
}) {
|
|
141
|
+
const [showAll, setShowAll] = useState(false);
|
|
142
|
+
const lines = useMemo(() => diffLinesForDetail(detail), [detail]);
|
|
143
|
+
const isTruncated = lines.length > PREVIEW_LINES;
|
|
144
|
+
const displayLines = showAll || !isTruncated ? lines : lines.slice(0, PREVIEW_LINES);
|
|
145
|
+
|
|
146
|
+
return (
|
|
147
|
+
<View style={styles.section}>
|
|
148
|
+
<DetailLabel style={styles.detailLabel}>Diff</DetailLabel>
|
|
149
|
+
<ScrollView horizontal nestedScrollEnabled style={styles.codeScroll}>
|
|
150
|
+
<View style={styles.diffSurface}>
|
|
151
|
+
{displayLines.length === 0 ? (
|
|
152
|
+
<Text style={styles.empty}>No changed lines.</Text>
|
|
153
|
+
) : (
|
|
154
|
+
displayLines.map((line, index) => (
|
|
155
|
+
<DiffRow
|
|
156
|
+
key={`${line.kind}-${index}`}
|
|
157
|
+
line={line}
|
|
158
|
+
palette={palette}
|
|
159
|
+
styles={styles}
|
|
160
|
+
/>
|
|
161
|
+
))
|
|
162
|
+
)}
|
|
163
|
+
</View>
|
|
164
|
+
</ScrollView>
|
|
165
|
+
{isTruncated ? (
|
|
166
|
+
<Pressable
|
|
167
|
+
accessibilityRole="button"
|
|
168
|
+
onPress={() => setShowAll(!showAll)}
|
|
169
|
+
style={styles.showMoreButton}
|
|
170
|
+
>
|
|
171
|
+
<Text style={styles.showMoreText}>
|
|
172
|
+
{showAll ? "Show less" : `Show all (${lines.length} lines)`}
|
|
173
|
+
</Text>
|
|
174
|
+
</Pressable>
|
|
175
|
+
) : null}
|
|
176
|
+
</View>
|
|
177
|
+
);
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
export function DiffRow({
|
|
181
|
+
line,
|
|
182
|
+
palette,
|
|
183
|
+
styles,
|
|
184
|
+
}: {
|
|
185
|
+
line: DiffLine;
|
|
186
|
+
palette: ActivityPalette;
|
|
187
|
+
styles: ActivityStyles;
|
|
188
|
+
}) {
|
|
189
|
+
const rowStyle =
|
|
190
|
+
line.kind === "add"
|
|
191
|
+
? styles.diffAdded
|
|
192
|
+
: line.kind === "remove"
|
|
193
|
+
? styles.diffRemoved
|
|
194
|
+
: line.kind === "meta"
|
|
195
|
+
? styles.diffMeta
|
|
196
|
+
: undefined;
|
|
197
|
+
const marker =
|
|
198
|
+
line.kind === "add" ? "+" : line.kind === "remove" ? "-" : line.kind === "meta" ? "" : " ";
|
|
199
|
+
const markerColor =
|
|
200
|
+
line.kind === "add"
|
|
201
|
+
? palette.statusColors.completed
|
|
202
|
+
: line.kind === "remove"
|
|
203
|
+
? palette.statusColors.failed
|
|
204
|
+
: line.kind === "meta"
|
|
205
|
+
? palette.categoryColors.search
|
|
206
|
+
: palette.categoryColors.unknown;
|
|
207
|
+
return (
|
|
208
|
+
<View style={[styles.diffLine, rowStyle]}>
|
|
209
|
+
<Text style={[styles.diffMarker, { color: markerColor }]}>{marker}</Text>
|
|
210
|
+
<Text
|
|
211
|
+
selectable
|
|
212
|
+
style={[
|
|
213
|
+
styles.diffText,
|
|
214
|
+
{ color: line.kind === "meta" ? markerColor : styles.codeLine.color },
|
|
215
|
+
]}
|
|
216
|
+
>
|
|
217
|
+
{line.text || " "}
|
|
218
|
+
</Text>
|
|
219
|
+
</View>
|
|
220
|
+
);
|
|
221
|
+
}
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
import { Text, View } from "react-native";
|
|
3
|
+
|
|
4
|
+
import { fileIconForPath } from "../../../shared/activity/file-kind";
|
|
5
|
+
import { DetailRenderProps, HighlightedCodeBlock, PathRow } from "./parts";
|
|
6
|
+
|
|
7
|
+
/** Search and fetch results, including web hits and page bodies. */
|
|
8
|
+
export function SearchDetail({ detail, theme, styles }: DetailRenderProps<"search">) {
|
|
9
|
+
return (
|
|
10
|
+
<>
|
|
11
|
+
<Text style={styles.detailText}>Query: {detail.query}</Text>
|
|
12
|
+
{detail.filePaths?.map((filePath) => (
|
|
13
|
+
<PathRow key={filePath} icon={fileIconForPath(filePath)} path={filePath} styles={styles} />
|
|
14
|
+
))}
|
|
15
|
+
{detail.content ? (
|
|
16
|
+
<HighlightedCodeBlock code={detail.content} language="text" styles={styles} theme={theme} />
|
|
17
|
+
) : null}
|
|
18
|
+
{detail.webResults?.map((result) => (
|
|
19
|
+
<View key={result.url} style={styles.section}>
|
|
20
|
+
<Text selectable style={styles.detailText}>
|
|
21
|
+
{result.title}
|
|
22
|
+
</Text>
|
|
23
|
+
<Text selectable style={styles.mutedText}>
|
|
24
|
+
{result.url}
|
|
25
|
+
</Text>
|
|
26
|
+
</View>
|
|
27
|
+
))}
|
|
28
|
+
{detail.annotations?.map((annotation, index) => (
|
|
29
|
+
<Text key={`${annotation}-${index}`} selectable style={styles.mutedText}>
|
|
30
|
+
{annotation}
|
|
31
|
+
</Text>
|
|
32
|
+
))}
|
|
33
|
+
</>
|
|
34
|
+
);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export function FetchDetail({ detail, theme, styles }: DetailRenderProps<"fetch">) {
|
|
38
|
+
return (
|
|
39
|
+
<>
|
|
40
|
+
<Text selectable style={styles.detailText}>
|
|
41
|
+
{detail.url}
|
|
42
|
+
</Text>
|
|
43
|
+
{detail.result ? (
|
|
44
|
+
<HighlightedCodeBlock
|
|
45
|
+
code={detail.result}
|
|
46
|
+
language="text"
|
|
47
|
+
label="Result"
|
|
48
|
+
styles={styles}
|
|
49
|
+
theme={theme}
|
|
50
|
+
/>
|
|
51
|
+
) : null}
|
|
52
|
+
{detail.code !== undefined ? (
|
|
53
|
+
<Text style={styles.mutedText}>
|
|
54
|
+
HTTP {detail.code} {detail.codeText ?? ""}
|
|
55
|
+
</Text>
|
|
56
|
+
) : null}
|
|
57
|
+
</>
|
|
58
|
+
);
|
|
59
|
+
}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
import { Text } from "react-native";
|
|
3
|
+
|
|
4
|
+
import { DetailRenderProps, HighlightedCodeBlock } from "./parts";
|
|
5
|
+
|
|
6
|
+
/** Command execution and the worktree setup that precedes it. */
|
|
7
|
+
export function ShellDetail({ detail, theme, styles }: DetailRenderProps<"shell">) {
|
|
8
|
+
return (
|
|
9
|
+
<>
|
|
10
|
+
<HighlightedCodeBlock
|
|
11
|
+
code={`$ ${detail.command}`}
|
|
12
|
+
language="bash"
|
|
13
|
+
label="Command"
|
|
14
|
+
styles={styles}
|
|
15
|
+
theme={theme}
|
|
16
|
+
/>
|
|
17
|
+
{detail.output ? (
|
|
18
|
+
<HighlightedCodeBlock
|
|
19
|
+
code={detail.output}
|
|
20
|
+
language="ansi"
|
|
21
|
+
label="Output"
|
|
22
|
+
styles={styles}
|
|
23
|
+
theme={theme}
|
|
24
|
+
/>
|
|
25
|
+
) : null}
|
|
26
|
+
{detail.exitCode !== undefined && detail.exitCode !== null ? (
|
|
27
|
+
<Text style={styles.mutedText}>Exit code {detail.exitCode}</Text>
|
|
28
|
+
) : null}
|
|
29
|
+
</>
|
|
30
|
+
);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export function WorktreeSetupDetail({
|
|
34
|
+
detail,
|
|
35
|
+
theme,
|
|
36
|
+
styles,
|
|
37
|
+
}: DetailRenderProps<"worktree_setup">) {
|
|
38
|
+
return (
|
|
39
|
+
<>
|
|
40
|
+
<Text style={styles.detailText}>Branch: {detail.branchName}</Text>
|
|
41
|
+
<Text style={styles.mutedText}>Path: {detail.worktreePath}</Text>
|
|
42
|
+
{detail.log ? (
|
|
43
|
+
<HighlightedCodeBlock
|
|
44
|
+
code={detail.log}
|
|
45
|
+
language="ansi"
|
|
46
|
+
label="Setup log"
|
|
47
|
+
styles={styles}
|
|
48
|
+
theme={theme}
|
|
49
|
+
/>
|
|
50
|
+
) : null}
|
|
51
|
+
</>
|
|
52
|
+
);
|
|
53
|
+
}
|