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.
Files changed (136) hide show
  1. package/LICENSE +21 -0
  2. package/LICENSE.compact-agent-activity +21 -0
  3. package/LICENSE.mermaid +21 -0
  4. package/README.md +428 -2
  5. package/README_zh.md +429 -0
  6. package/client/activity/Activity.test.ts +220 -0
  7. package/client/activity/Activity.tsx +784 -0
  8. package/client/activity/detail/detail.test.ts +102 -0
  9. package/client/activity/detail/dispatch.tsx +94 -0
  10. package/client/activity/detail/file.tsx +93 -0
  11. package/client/activity/detail/index.tsx +39 -0
  12. package/client/activity/detail/parts.tsx +221 -0
  13. package/client/activity/detail/search.tsx +59 -0
  14. package/client/activity/detail/terminal.tsx +53 -0
  15. package/client/activity/detail/texts.tsx +42 -0
  16. package/client/activity/disclosure.ts +49 -0
  17. package/client/activity/dom.test.ts +52 -0
  18. package/client/activity/dom.ts +138 -0
  19. package/client/activity/group-store.test.ts +126 -0
  20. package/client/activity/group-store.ts +265 -0
  21. package/client/activity/highlight.test.ts +116 -0
  22. package/client/activity/highlight.ts +418 -0
  23. package/client/activity/history.test.ts +602 -0
  24. package/client/activity/history.ts +239 -0
  25. package/client/activity/index.ts +74 -0
  26. package/client/activity/latest-marker.test.ts +41 -0
  27. package/client/activity/latest-marker.ts +70 -0
  28. package/client/activity/styles.ts +535 -0
  29. package/client/activity/tools/child-agent.tsx +308 -0
  30. package/client/activity/tools/exa.tsx +99 -0
  31. package/client/activity/tools/github.tsx +501 -0
  32. package/client/activity/tools/list.tsx +102 -0
  33. package/client/activity/tools/lists.test.ts +193 -0
  34. package/client/activity/tools/paseo/agent.tsx +384 -0
  35. package/client/activity/tools/paseo/browser.tsx +322 -0
  36. package/client/activity/tools/paseo/index.tsx +79 -0
  37. package/client/activity/tools/paseo/parts.tsx +133 -0
  38. package/client/activity/tools/paseo/provider.tsx +220 -0
  39. package/client/activity/tools/paseo/schedule.tsx +246 -0
  40. package/client/activity/tools/paseo/speak.tsx +20 -0
  41. package/client/activity/tools/paseo/terminal.tsx +214 -0
  42. package/client/activity/tools/paseo/workspace.tsx +162 -0
  43. package/client/activity/tools/shared.tsx +431 -0
  44. package/client/activity/transform.test.ts +28 -0
  45. package/client/activity/transform.ts +43 -0
  46. package/client/agent-pills.ts +56 -0
  47. package/client/dom.ts +75 -0
  48. package/client/enhance/controller.ts +111 -0
  49. package/client/enhance/dom.test.ts +28 -0
  50. package/client/enhance/dom.ts +101 -0
  51. package/client/enhance/index.ts +95 -0
  52. package/client/enhance/pill.tsx +19 -0
  53. package/client/enhance/state.ts +58 -0
  54. package/client/feature.ts +22 -0
  55. package/client/features.test.ts +149 -0
  56. package/client/features.ts +83 -0
  57. package/client/locale.ts +45 -0
  58. package/client/mermaid/diagram.tsx +438 -0
  59. package/client/mermaid/index.ts +27 -0
  60. package/client/mermaid/item.tsx +28 -0
  61. package/client/mermaid/markdown.tsx +217 -0
  62. package/client/mermaid/registration.ts +32 -0
  63. package/client/mermaid/toolbar.tsx +175 -0
  64. package/client/mermaid/viewer.tsx +314 -0
  65. package/client/readonly/dom.test.ts +66 -0
  66. package/client/readonly/dom.ts +32 -0
  67. package/client/readonly/index.ts +27 -0
  68. package/client/registration.test.ts +243 -0
  69. package/client/settings.tsx +438 -0
  70. package/client/sticky/StickyBar.tsx +144 -0
  71. package/client/sticky/dom.test.ts +230 -0
  72. package/client/sticky/dom.ts +517 -0
  73. package/client/sticky/index.ts +38 -0
  74. package/client/sticky/pill.tsx +70 -0
  75. package/client/timeline-pager.test.ts +121 -0
  76. package/client/timeline-pager.ts +123 -0
  77. package/index.client.tsx +44 -0
  78. package/index.server.ts +12 -0
  79. package/package.json +42 -4
  80. package/paseo-plugin.json +6 -0
  81. package/server/enhance/index.ts +63 -0
  82. package/server/enhance/manifest.ts +176 -0
  83. package/server/enhance/probe.test.ts +215 -0
  84. package/server/enhance/probe.ts +118 -0
  85. package/server/enhance/provider.ts +149 -0
  86. package/shared/activity/child-agent.test.ts +27 -0
  87. package/shared/activity/child-agent.ts +26 -0
  88. package/shared/activity/diff.test.ts +74 -0
  89. package/shared/activity/diff.ts +207 -0
  90. package/shared/activity/exa.test.ts +66 -0
  91. package/shared/activity/exa.ts +183 -0
  92. package/shared/activity/file-kind.test.ts +13 -0
  93. package/shared/activity/file-kind.ts +112 -0
  94. package/shared/activity/github.test.ts +56 -0
  95. package/shared/activity/github.ts +174 -0
  96. package/shared/activity/palette.test.ts +26 -0
  97. package/shared/activity/palette.ts +106 -0
  98. package/shared/activity/parse.test.ts +39 -0
  99. package/shared/activity/parse.ts +115 -0
  100. package/shared/activity/paseo-tools.test.ts +163 -0
  101. package/shared/activity/paseo-tools.ts +497 -0
  102. package/shared/activity/text.test.ts +85 -0
  103. package/shared/activity/text.ts +202 -0
  104. package/shared/activity/timeline.test.ts +26 -0
  105. package/shared/activity/timeline.ts +108 -0
  106. package/shared/activity/tool-presentation.test.ts +154 -0
  107. package/shared/activity/tool-presentation.ts +352 -0
  108. package/shared/enhance/contract.ts +105 -0
  109. package/shared/enhance/decide.ts +97 -0
  110. package/shared/enhance/enhance.test.ts +188 -0
  111. package/shared/enhance/failure.ts +40 -0
  112. package/shared/enhance/index.ts +8 -0
  113. package/shared/enhance/normalize.ts +16 -0
  114. package/shared/enhance/parse.ts +79 -0
  115. package/shared/enhance/policy.ts +48 -0
  116. package/shared/enhance/prompt.ts +134 -0
  117. package/shared/enhance/run.ts +84 -0
  118. package/shared/i18n.test.ts +81 -0
  119. package/shared/i18n.ts +300 -0
  120. package/shared/mermaid/flowchart.test.ts +169 -0
  121. package/shared/mermaid/flowchart.ts +271 -0
  122. package/shared/mermaid/layout.test.ts +305 -0
  123. package/shared/mermaid/layout.ts +496 -0
  124. package/shared/mermaid/parse.test.ts +110 -0
  125. package/shared/mermaid/parse.ts +193 -0
  126. package/shared/mermaid/segment.test.ts +55 -0
  127. package/shared/mermaid/segment.ts +95 -0
  128. package/shared/mermaid/sequence.test.ts +68 -0
  129. package/shared/mermaid/sequence.ts +130 -0
  130. package/shared/mermaid/transform.test.ts +52 -0
  131. package/shared/mermaid/transform.ts +45 -0
  132. package/shared/mermaid/zoom.test.ts +134 -0
  133. package/shared/mermaid/zoom.ts +114 -0
  134. package/shared/settings.ts +41 -0
  135. package/shared/sticky/history.test.ts +91 -0
  136. 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
+ }