@oh-my-pi/pi-coding-agent 3.20.1 → 3.24.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 (123) hide show
  1. package/CHANGELOG.md +107 -8
  2. package/docs/custom-tools.md +3 -3
  3. package/docs/extensions.md +226 -220
  4. package/docs/hooks.md +2 -2
  5. package/docs/sdk.md +50 -53
  6. package/examples/custom-tools/README.md +2 -17
  7. package/examples/extensions/README.md +76 -74
  8. package/examples/extensions/todo.ts +2 -5
  9. package/examples/hooks/custom-compaction.ts +2 -4
  10. package/examples/hooks/handoff.ts +1 -1
  11. package/examples/hooks/qna.ts +1 -1
  12. package/examples/sdk/02-custom-model.ts +1 -1
  13. package/examples/sdk/README.md +7 -11
  14. package/package.json +6 -6
  15. package/src/cli/args.ts +9 -6
  16. package/src/cli/file-processor.ts +1 -1
  17. package/src/cli/list-models.ts +1 -1
  18. package/src/core/agent-session.ts +16 -5
  19. package/src/core/auth-storage.ts +1 -1
  20. package/src/core/compaction/branch-summarization.ts +2 -2
  21. package/src/core/compaction/compaction.ts +2 -2
  22. package/src/core/compaction/utils.ts +1 -1
  23. package/src/core/custom-tools/types.ts +1 -1
  24. package/src/core/custom-tools/wrapper.ts +0 -1
  25. package/src/core/extensions/index.ts +1 -6
  26. package/src/core/extensions/runner.ts +1 -1
  27. package/src/core/extensions/types.ts +1 -1
  28. package/src/core/extensions/wrapper.ts +1 -8
  29. package/src/core/file-mentions.ts +5 -8
  30. package/src/core/hooks/runner.ts +2 -2
  31. package/src/core/hooks/types.ts +1 -1
  32. package/src/core/messages.ts +1 -1
  33. package/src/core/model-registry.ts +1 -1
  34. package/src/core/model-resolver.ts +1 -1
  35. package/src/core/sdk.ts +64 -105
  36. package/src/core/session-manager.ts +18 -22
  37. package/src/core/settings-manager.ts +66 -1
  38. package/src/core/slash-commands.ts +12 -5
  39. package/src/core/system-prompt.ts +49 -36
  40. package/src/core/title-generator.ts +2 -2
  41. package/src/core/tools/ask.ts +98 -4
  42. package/src/core/tools/bash-interceptor.ts +11 -4
  43. package/src/core/tools/bash.ts +121 -5
  44. package/src/core/tools/context.ts +7 -0
  45. package/src/core/tools/edit-diff.ts +73 -24
  46. package/src/core/tools/edit.ts +221 -34
  47. package/src/core/tools/exa/render.ts +4 -16
  48. package/src/core/tools/find.ts +149 -5
  49. package/src/core/tools/gemini-image.ts +279 -56
  50. package/src/core/tools/git.ts +17 -3
  51. package/src/core/tools/grep.ts +185 -5
  52. package/src/core/tools/index.test.ts +180 -0
  53. package/src/core/tools/index.ts +96 -242
  54. package/src/core/tools/ls.ts +133 -5
  55. package/src/core/tools/lsp/index.ts +32 -29
  56. package/src/core/tools/lsp/render.ts +21 -22
  57. package/src/core/tools/notebook.ts +112 -4
  58. package/src/core/tools/output.ts +175 -15
  59. package/src/core/tools/read.ts +127 -25
  60. package/src/core/tools/render-utils.ts +241 -0
  61. package/src/core/tools/renderers.ts +40 -828
  62. package/src/core/tools/review.ts +26 -25
  63. package/src/core/tools/rulebook.ts +11 -3
  64. package/src/core/tools/task/agents.ts +28 -7
  65. package/src/core/tools/task/discovery.ts +0 -6
  66. package/src/core/tools/task/executor.ts +264 -254
  67. package/src/core/tools/task/index.ts +48 -208
  68. package/src/core/tools/task/render.ts +26 -11
  69. package/src/core/tools/task/types.ts +7 -12
  70. package/src/core/tools/task/worker-protocol.ts +17 -0
  71. package/src/core/tools/task/worker.ts +238 -0
  72. package/src/core/tools/truncate.ts +27 -1
  73. package/src/core/tools/web-fetch.ts +25 -49
  74. package/src/core/tools/web-search/index.ts +132 -46
  75. package/src/core/tools/web-search/providers/anthropic.ts +7 -2
  76. package/src/core/tools/web-search/providers/exa.ts +2 -1
  77. package/src/core/tools/web-search/providers/perplexity.ts +6 -1
  78. package/src/core/tools/web-search/render.ts +6 -4
  79. package/src/core/tools/web-search/types.ts +13 -0
  80. package/src/core/tools/write.ts +96 -14
  81. package/src/core/voice.ts +1 -1
  82. package/src/discovery/helpers.test.ts +1 -1
  83. package/src/index.ts +5 -16
  84. package/src/main.ts +5 -5
  85. package/src/modes/interactive/components/assistant-message.ts +1 -1
  86. package/src/modes/interactive/components/custom-message.ts +1 -1
  87. package/src/modes/interactive/components/extensions/inspector-panel.ts +25 -22
  88. package/src/modes/interactive/components/extensions/state-manager.ts +12 -0
  89. package/src/modes/interactive/components/footer.ts +1 -1
  90. package/src/modes/interactive/components/hook-message.ts +1 -1
  91. package/src/modes/interactive/components/model-selector.ts +1 -1
  92. package/src/modes/interactive/components/oauth-selector.ts +1 -1
  93. package/src/modes/interactive/components/settings-defs.ts +49 -0
  94. package/src/modes/interactive/components/status-line.ts +1 -1
  95. package/src/modes/interactive/components/tool-execution.ts +93 -538
  96. package/src/modes/interactive/interactive-mode.ts +19 -7
  97. package/src/modes/interactive/theme/theme.ts +4 -4
  98. package/src/modes/print-mode.ts +1 -1
  99. package/src/modes/rpc/rpc-client.ts +1 -1
  100. package/src/modes/rpc/rpc-types.ts +1 -1
  101. package/src/prompts/system-prompt.md +4 -0
  102. package/src/prompts/task.md +0 -7
  103. package/src/prompts/tools/gemini-image.md +5 -1
  104. package/src/prompts/tools/output.md +6 -2
  105. package/src/prompts/tools/task.md +68 -0
  106. package/src/prompts/tools/web-fetch.md +1 -0
  107. package/src/prompts/tools/web-search.md +2 -0
  108. package/src/utils/image-convert.ts +8 -2
  109. package/src/utils/image-magick.ts +247 -0
  110. package/src/utils/image-resize.ts +53 -13
  111. package/examples/custom-tools/question/index.ts +0 -84
  112. package/examples/custom-tools/subagent/README.md +0 -172
  113. package/examples/custom-tools/subagent/agents/planner.md +0 -37
  114. package/examples/custom-tools/subagent/agents/scout.md +0 -50
  115. package/examples/custom-tools/subagent/agents/worker.md +0 -24
  116. package/examples/custom-tools/subagent/agents.ts +0 -156
  117. package/examples/custom-tools/subagent/commands/implement-and-review.md +0 -10
  118. package/examples/custom-tools/subagent/commands/implement.md +0 -10
  119. package/examples/custom-tools/subagent/commands/scout-and-plan.md +0 -9
  120. package/examples/custom-tools/subagent/index.ts +0 -1002
  121. package/examples/sdk/05-tools.ts +0 -94
  122. package/examples/sdk/12-full-control.ts +0 -95
  123. package/src/prompts/browser.md +0 -71
@@ -1,11 +1,27 @@
1
1
  import { readFileSync, type Stats, statSync } from "node:fs";
2
2
  import nodePath from "node:path";
3
3
  import type { AgentTool } from "@oh-my-pi/pi-agent-core";
4
+ import type { Component } from "@oh-my-pi/pi-tui";
5
+ import { Text } from "@oh-my-pi/pi-tui";
4
6
  import { Type } from "@sinclair/typebox";
5
7
  import type { Subprocess } from "bun";
8
+ import { getLanguageFromPath, type Theme } from "../../modes/interactive/theme/theme";
6
9
  import grepDescription from "../../prompts/tools/grep.md" with { type: "text" };
7
10
  import { ensureTool } from "../../utils/tools-manager";
11
+ import type { RenderResultOptions } from "../custom-tools/types";
12
+ import type { ToolSession } from "./index";
8
13
  import { resolveToCwd } from "./path-utils";
14
+ import {
15
+ formatCount,
16
+ formatEmptyMessage,
17
+ formatErrorMessage,
18
+ formatExpandHint,
19
+ formatMeta,
20
+ formatMoreItems,
21
+ formatScope,
22
+ formatTruncationSuffix,
23
+ PREVIEW_LIMITS,
24
+ } from "./render-utils";
9
25
  import {
10
26
  DEFAULT_MAX_BYTES,
11
27
  formatSize,
@@ -64,7 +80,7 @@ export interface GrepToolDetails {
64
80
  error?: string;
65
81
  }
66
82
 
67
- export function createGrepTool(cwd: string): AgentTool<typeof grepSchema> {
83
+ export function createGrepTool(session: ToolSession): AgentTool<typeof grepSchema> {
68
84
  return {
69
85
  name: "grep",
70
86
  label: "Grep",
@@ -112,9 +128,9 @@ export function createGrepTool(cwd: string): AgentTool<typeof grepSchema> {
112
128
  throw new Error("ripgrep (rg) is not available and could not be downloaded");
113
129
  }
114
130
 
115
- const searchPath = resolveToCwd(searchDir || ".", cwd);
131
+ const searchPath = resolveToCwd(searchDir || ".", session.cwd);
116
132
  const scopePath = (() => {
117
- const relative = nodePath.relative(cwd, searchPath).replace(/\\/g, "/");
133
+ const relative = nodePath.relative(session.cwd, searchPath).replace(/\\/g, "/");
118
134
  return relative.length === 0 ? "." : relative;
119
135
  })();
120
136
  let searchStat: Stats;
@@ -580,5 +596,169 @@ export function createGrepTool(cwd: string): AgentTool<typeof grepSchema> {
580
596
  };
581
597
  }
582
598
 
583
- /** Default grep tool using process.cwd() - for backwards compatibility */
584
- export const grepTool = createGrepTool(process.cwd());
599
+ // =============================================================================
600
+ // TUI Renderer
601
+ // =============================================================================
602
+
603
+ interface GrepRenderArgs {
604
+ pattern: string;
605
+ path?: string;
606
+ glob?: string;
607
+ type?: string;
608
+ ignoreCase?: boolean;
609
+ caseSensitive?: boolean;
610
+ literal?: boolean;
611
+ multiline?: boolean;
612
+ context?: number;
613
+ limit?: number;
614
+ outputMode?: string;
615
+ }
616
+
617
+ const COLLAPSED_LIST_LIMIT = PREVIEW_LIMITS.COLLAPSED_ITEMS;
618
+ const COLLAPSED_TEXT_LIMIT = PREVIEW_LIMITS.COLLAPSED_LINES * 2;
619
+
620
+ export const grepToolRenderer = {
621
+ renderCall(args: GrepRenderArgs, uiTheme: Theme): Component {
622
+ const label = uiTheme.fg("toolTitle", uiTheme.bold("Grep"));
623
+ let text = `${label} ${uiTheme.fg("accent", args.pattern || "?")}`;
624
+
625
+ const meta: string[] = [];
626
+ if (args.path) meta.push(`in ${args.path}`);
627
+ if (args.glob) meta.push(`glob:${args.glob}`);
628
+ if (args.type) meta.push(`type:${args.type}`);
629
+ if (args.outputMode && args.outputMode !== "files_with_matches") meta.push(`mode:${args.outputMode}`);
630
+ if (args.caseSensitive) {
631
+ meta.push("case:sensitive");
632
+ } else if (args.ignoreCase) {
633
+ meta.push("case:insensitive");
634
+ }
635
+ if (args.literal) meta.push("literal");
636
+ if (args.multiline) meta.push("multiline");
637
+ if (args.context !== undefined) meta.push(`context:${args.context}`);
638
+ if (args.limit !== undefined) meta.push(`limit:${args.limit}`);
639
+
640
+ text += formatMeta(meta, uiTheme);
641
+
642
+ return new Text(text, 0, 0);
643
+ },
644
+
645
+ renderResult(
646
+ result: { content: Array<{ type: string; text?: string }>; details?: GrepToolDetails },
647
+ { expanded }: RenderResultOptions,
648
+ uiTheme: Theme,
649
+ ): Component {
650
+ const details = result.details;
651
+
652
+ if (details?.error) {
653
+ return new Text(formatErrorMessage(details.error, uiTheme), 0, 0);
654
+ }
655
+
656
+ const hasDetailedData = details?.matchCount !== undefined || details?.fileCount !== undefined;
657
+
658
+ if (!hasDetailedData) {
659
+ const textContent = result.content?.find((c) => c.type === "text")?.text;
660
+ if (!textContent || textContent === "No matches found") {
661
+ return new Text(formatEmptyMessage("No matches found", uiTheme), 0, 0);
662
+ }
663
+
664
+ const lines = textContent.split("\n").filter((line) => line.trim() !== "");
665
+ const maxLines = expanded ? lines.length : Math.min(lines.length, COLLAPSED_TEXT_LIMIT);
666
+ const displayLines = lines.slice(0, maxLines);
667
+ const remaining = lines.length - maxLines;
668
+ const hasMore = remaining > 0;
669
+
670
+ const icon = uiTheme.styledSymbol("status.success", "success");
671
+ const summary = formatCount("item", lines.length);
672
+ const expandHint = formatExpandHint(expanded, hasMore, uiTheme);
673
+ let text = `${icon} ${uiTheme.fg("dim", summary)}${expandHint}`;
674
+
675
+ for (let i = 0; i < displayLines.length; i++) {
676
+ const isLast = i === displayLines.length - 1 && remaining === 0;
677
+ const branch = isLast ? uiTheme.tree.last : uiTheme.tree.branch;
678
+ text += `\n ${uiTheme.fg("dim", branch)} ${uiTheme.fg("toolOutput", displayLines[i])}`;
679
+ }
680
+
681
+ if (remaining > 0) {
682
+ text += `\n ${uiTheme.fg("dim", uiTheme.tree.last)} ${uiTheme.fg("muted", formatMoreItems(remaining, "item", uiTheme))}`;
683
+ }
684
+
685
+ return new Text(text, 0, 0);
686
+ }
687
+
688
+ const matchCount = details?.matchCount ?? 0;
689
+ const fileCount = details?.fileCount ?? 0;
690
+ const mode = details?.mode ?? "files_with_matches";
691
+ const truncated = details?.truncated ?? details?.truncation?.truncated ?? false;
692
+ const files = details?.files ?? [];
693
+
694
+ if (matchCount === 0) {
695
+ return new Text(formatEmptyMessage("No matches found", uiTheme), 0, 0);
696
+ }
697
+
698
+ const icon = uiTheme.styledSymbol("status.success", "success");
699
+ const summaryParts =
700
+ mode === "files_with_matches"
701
+ ? [formatCount("file", fileCount)]
702
+ : [formatCount("match", matchCount), formatCount("file", fileCount)];
703
+ const summaryText = summaryParts.join(uiTheme.sep.dot);
704
+ const scopeLabel = formatScope(details?.scopePath, uiTheme);
705
+
706
+ const fileEntries: Array<{ path: string; count?: number }> = details?.fileMatches?.length
707
+ ? details.fileMatches.map((entry) => ({ path: entry.path, count: entry.count }))
708
+ : files.map((path) => ({ path }));
709
+ const maxFiles = expanded ? fileEntries.length : Math.min(fileEntries.length, COLLAPSED_LIST_LIMIT);
710
+ const hasMoreFiles = fileEntries.length > maxFiles;
711
+ const expandHint = formatExpandHint(expanded, hasMoreFiles, uiTheme);
712
+
713
+ let text = `${icon} ${uiTheme.fg("dim", summaryText)}${formatTruncationSuffix(truncated, uiTheme)}${scopeLabel}${expandHint}`;
714
+
715
+ const truncationReasons: string[] = [];
716
+ if (details?.matchLimitReached) {
717
+ truncationReasons.push(`limit ${details.matchLimitReached} matches`);
718
+ }
719
+ if (details?.headLimitReached) {
720
+ truncationReasons.push(`head limit ${details.headLimitReached}`);
721
+ }
722
+ if (details?.truncation?.truncated) {
723
+ truncationReasons.push("size limit");
724
+ }
725
+ if (details?.linesTruncated) {
726
+ truncationReasons.push("line length");
727
+ }
728
+
729
+ const hasTruncation = truncationReasons.length > 0;
730
+
731
+ if (fileEntries.length > 0) {
732
+ for (let i = 0; i < maxFiles; i++) {
733
+ const entry = fileEntries[i];
734
+ const isLast = i === maxFiles - 1 && !hasMoreFiles && !hasTruncation;
735
+ const branch = isLast ? uiTheme.tree.last : uiTheme.tree.branch;
736
+ const isDir = entry.path.endsWith("/");
737
+ const entryPath = isDir ? entry.path.slice(0, -1) : entry.path;
738
+ const lang = isDir ? undefined : getLanguageFromPath(entryPath);
739
+ const entryIcon = isDir
740
+ ? uiTheme.fg("accent", uiTheme.icon.folder)
741
+ : uiTheme.fg("muted", uiTheme.getLangIcon(lang));
742
+ const countLabel =
743
+ entry.count !== undefined
744
+ ? ` ${uiTheme.fg("dim", `(${entry.count} match${entry.count !== 1 ? "es" : ""})`)}`
745
+ : "";
746
+ text += `\n ${uiTheme.fg("dim", branch)} ${entryIcon} ${uiTheme.fg("accent", entry.path)}${countLabel}`;
747
+ }
748
+
749
+ if (hasMoreFiles) {
750
+ const moreFilesBranch = hasTruncation ? uiTheme.tree.branch : uiTheme.tree.last;
751
+ text += `\n ${uiTheme.fg("dim", moreFilesBranch)} ${uiTheme.fg(
752
+ "muted",
753
+ formatMoreItems(fileEntries.length - maxFiles, "file", uiTheme),
754
+ )}`;
755
+ }
756
+ }
757
+
758
+ if (hasTruncation) {
759
+ text += `\n ${uiTheme.fg("dim", uiTheme.tree.last)} ${uiTheme.fg("warning", `truncated: ${truncationReasons.join(", ")}`)}`;
760
+ }
761
+
762
+ return new Text(text, 0, 0);
763
+ },
764
+ };
@@ -0,0 +1,180 @@
1
+ import { describe, expect, it } from "bun:test";
2
+ import { BUILTIN_TOOLS, createTools, HIDDEN_TOOLS, type ToolSession } from "./index";
3
+
4
+ function createTestSession(overrides: Partial<ToolSession> = {}): ToolSession {
5
+ return {
6
+ cwd: "/tmp/test",
7
+ hasUI: false,
8
+ rulebookRules: [],
9
+ getSessionFile: () => null,
10
+ getSessionSpawns: () => "*",
11
+ ...overrides,
12
+ };
13
+ }
14
+
15
+ describe("createTools", () => {
16
+ it("creates all builtin tools by default", async () => {
17
+ const session = createTestSession();
18
+ const tools = await createTools(session);
19
+ const names = tools.map((t) => t.name);
20
+
21
+ // Core tools should always be present
22
+ expect(names).toContain("bash");
23
+ expect(names).toContain("read");
24
+ expect(names).toContain("edit");
25
+ expect(names).toContain("write");
26
+ expect(names).toContain("grep");
27
+ expect(names).toContain("find");
28
+ expect(names).toContain("ls");
29
+ expect(names).toContain("lsp");
30
+ expect(names).toContain("notebook");
31
+ expect(names).toContain("task");
32
+ expect(names).toContain("output");
33
+ expect(names).toContain("web_fetch");
34
+ expect(names).toContain("web_search");
35
+ });
36
+
37
+ it("respects requested tool subset", async () => {
38
+ const session = createTestSession();
39
+ const tools = await createTools(session, ["read", "write"]);
40
+ const names = tools.map((t) => t.name);
41
+
42
+ expect(names).toEqual(["read", "write"]);
43
+ });
44
+
45
+ it("includes hidden tools when explicitly requested", async () => {
46
+ const session = createTestSession();
47
+ const tools = await createTools(session, ["report_finding"]);
48
+ const names = tools.map((t) => t.name);
49
+
50
+ expect(names).toEqual(["report_finding"]);
51
+ });
52
+
53
+ it("excludes ask tool when hasUI is false", async () => {
54
+ const session = createTestSession({ hasUI: false });
55
+ const tools = await createTools(session);
56
+ const names = tools.map((t) => t.name);
57
+
58
+ expect(names).not.toContain("ask");
59
+ });
60
+
61
+ it("includes ask tool when hasUI is true", async () => {
62
+ const session = createTestSession({ hasUI: true });
63
+ const tools = await createTools(session);
64
+ const names = tools.map((t) => t.name);
65
+
66
+ expect(names).toContain("ask");
67
+ });
68
+
69
+ it("excludes rulebook tool when no rules provided", async () => {
70
+ const session = createTestSession({ rulebookRules: [] });
71
+ const tools = await createTools(session);
72
+ const names = tools.map((t) => t.name);
73
+
74
+ expect(names).not.toContain("rulebook");
75
+ });
76
+
77
+ it("includes rulebook tool when rules provided", async () => {
78
+ const session = createTestSession({
79
+ rulebookRules: [
80
+ {
81
+ path: "/test/rule.md",
82
+ name: "Test Rule",
83
+ content: "Test content",
84
+ description: "A test rule",
85
+ _source: { provider: "test", providerName: "Test", path: "/test", level: "project" },
86
+ },
87
+ ],
88
+ });
89
+ const tools = await createTools(session);
90
+ const names = tools.map((t) => t.name);
91
+
92
+ expect(names).toContain("rulebook");
93
+ });
94
+
95
+ it("excludes git tool when disabled in settings", async () => {
96
+ const session = createTestSession({
97
+ settings: {
98
+ getImageAutoResize: () => true,
99
+ getLspFormatOnWrite: () => true,
100
+ getLspDiagnosticsOnWrite: () => true,
101
+ getLspDiagnosticsOnEdit: () => false,
102
+ getEditFuzzyMatch: () => true,
103
+ getGitToolEnabled: () => false,
104
+ getBashInterceptorEnabled: () => true,
105
+ },
106
+ });
107
+ const tools = await createTools(session);
108
+ const names = tools.map((t) => t.name);
109
+
110
+ expect(names).not.toContain("git");
111
+ });
112
+
113
+ it("includes git tool when enabled in settings", async () => {
114
+ const session = createTestSession({
115
+ settings: {
116
+ getImageAutoResize: () => true,
117
+ getLspFormatOnWrite: () => true,
118
+ getLspDiagnosticsOnWrite: () => true,
119
+ getLspDiagnosticsOnEdit: () => false,
120
+ getEditFuzzyMatch: () => true,
121
+ getGitToolEnabled: () => true,
122
+ getBashInterceptorEnabled: () => true,
123
+ },
124
+ });
125
+ const tools = await createTools(session);
126
+ const names = tools.map((t) => t.name);
127
+
128
+ expect(names).toContain("git");
129
+ });
130
+
131
+ it("includes git tool when no settings provided (default enabled)", async () => {
132
+ const session = createTestSession({ settings: undefined });
133
+ const tools = await createTools(session);
134
+ const names = tools.map((t) => t.name);
135
+
136
+ expect(names).toContain("git");
137
+ });
138
+
139
+ it("always includes output tool when task tool is present", async () => {
140
+ const session = createTestSession();
141
+ const tools = await createTools(session);
142
+ const names = tools.map((t) => t.name);
143
+
144
+ // Both should be present together
145
+ expect(names).toContain("task");
146
+ expect(names).toContain("output");
147
+ });
148
+
149
+ it("BUILTIN_TOOLS contains all expected tools", () => {
150
+ const expectedTools = [
151
+ "ask",
152
+ "bash",
153
+ "edit",
154
+ "find",
155
+ "git",
156
+ "grep",
157
+ "ls",
158
+ "lsp",
159
+ "notebook",
160
+ "output",
161
+ "read",
162
+ "rulebook",
163
+ "task",
164
+ "web_fetch",
165
+ "web_search",
166
+ "write",
167
+ ];
168
+
169
+ for (const tool of expectedTools) {
170
+ expect(BUILTIN_TOOLS).toHaveProperty(tool);
171
+ }
172
+
173
+ // Ensure we haven't missed any
174
+ expect(Object.keys(BUILTIN_TOOLS).sort()).toEqual(expectedTools.sort());
175
+ });
176
+
177
+ it("HIDDEN_TOOLS contains review tools", () => {
178
+ expect(Object.keys(HIDDEN_TOOLS).sort()).toEqual(["report_finding", "submit_review"]);
179
+ });
180
+ });