Restructure dash (#372)

* Restructure dash

* WIP

* WIP

* refactor trigger UI: extract PR summary card, add mentions section, rename labels

Co-authored-by: Cursor <cursoragent@cursor.com>

* clean up console UI: remove info icons from section descriptions, rename mentions trigger

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix review feedback: layout, terminology, form scope

- extract console sidebar sections to module-level constant
- align three-column layout breakpoints to xl (match sidebar visibility)
- fix mixed shell/bash terminology in beta page
- scope FormProvider to trigger sections only, restore autoComplete="off"

Co-authored-by: Cursor <cursoragent@cursor.com>

* Bump

---------

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
Colin McDonnell
2026-02-23 23:34:29 +00:00
committed by pullfrog[bot]
parent b8a0d799ee
commit a7bd746f21
53 changed files with 672 additions and 672 deletions
+24 -24
View File
@@ -37,10 +37,10 @@ function buildDisallowedTools(ctx: AgentRunContext): string[] {
const disallowed: string[] = [];
if (ctx.payload.web === "disabled") disallowed.push("WebFetch");
if (ctx.payload.search === "disabled") disallowed.push("WebSearch");
// both "disabled" and "restricted" block native bash
// "restricted" means use MCP bash tool instead
const bash = ctx.payload.bash;
if (bash !== "enabled") disallowed.push("Bash");
// both "disabled" and "restricted" block native shell
// "restricted" means use MCP shell tool instead
const shell = ctx.payload.shell;
if (shell !== "enabled") disallowed.push("Bash");
// always block native file tools (use MCP file_read/file_write instead)
disallowed.push("Read", "Write", "Edit", "MultiEdit");
// block built-in subagent spawning — delegation is handled by gh_pullfrog/delegate
@@ -131,8 +131,8 @@ export const claude = agent({
let finalOutput = "";
const usageContainer: UsageContainer = { value: null };
// Track bash tool IDs to identify when bash tool results come back
const bashToolIds = new Set<string>();
// track shell tool IDs to identify when shell tool results come back
const shellToolIds = new Set<string>();
const thinkingTimer = new ThinkingTimer();
const result = await spawn({
@@ -164,7 +164,7 @@ export const claude = agent({
const handler = messageHandlers[message.type];
if (handler) {
await handler(message as never, bashToolIds, thinkingTimer, usageContainer);
await handler(message as never, shellToolIds, thinkingTimer, usageContainer);
}
} catch {
// ignore parse errors - might be non-JSON output
@@ -213,7 +213,7 @@ type SDKMessageType = SDKMessage["type"];
type SDKMessageHandler<type extends SDKMessageType = SDKMessageType> = (
data: Extract<SDKMessage, { type: type }>,
bashToolIds: Set<string>,
shellToolIds: Set<string>,
thinkingTimer: ThinkingTimer,
usageContainer: UsageContainer
) => void | Promise<void>;
@@ -223,15 +223,15 @@ type SDKMessageHandlers = {
};
const messageHandlers: SDKMessageHandlers = {
assistant: (data, bashToolIds, thinkingTimer, _usageContainer) => {
assistant: (data, shellToolIds, thinkingTimer, _usageContainer) => {
if (data.message?.content) {
for (const content of data.message.content) {
if (content.type === "text" && content.text?.trim()) {
log.box(content.text.trim(), { title: "Claude" });
} else if (content.type === "tool_use") {
// Track bash tool IDs
// track shell tool IDs (Claude's native tool is named "bash")
if (content.name === "bash" && content.id) {
bashToolIds.add(content.id);
shellToolIds.add(content.id);
}
thinkingTimer.markToolCall();
@@ -243,7 +243,7 @@ const messageHandlers: SDKMessageHandlers = {
}
}
},
user: (data, bashToolIds, thinkingTimer, _usageContainer) => {
user: (data, shellToolIds, thinkingTimer, _usageContainer) => {
if (data.message?.content) {
for (const content of data.message.content) {
if (typeof content === "string") {
@@ -253,7 +253,7 @@ const messageHandlers: SDKMessageHandlers = {
thinkingTimer.markToolResult();
const toolUseId = content.tool_use_id;
const isBashTool = toolUseId && bashToolIds.has(toolUseId);
const isShellTool = toolUseId && shellToolIds.has(toolUseId);
const outputContent =
typeof content.content === "string"
@@ -270,9 +270,9 @@ const messageHandlers: SDKMessageHandlers = {
.join("\n")
: String(content.content);
if (isBashTool) {
// Log bash output in a collapsed group
log.startGroup(`bash output`);
if (isShellTool) {
// Log shell output in a collapsed group
log.startGroup(`shell output`);
if (content.is_error) {
log.info(outputContent);
} else {
@@ -280,18 +280,18 @@ const messageHandlers: SDKMessageHandlers = {
}
log.endGroup();
// Clean up the tracked ID
bashToolIds.delete(toolUseId);
shellToolIds.delete(toolUseId);
} else if (content.is_error) {
log.info(`Tool error: ${outputContent}`);
} else {
// log successful non-bash tool result at debug level
// log successful non-shell tool result at debug level
log.debug(`tool output: ${outputContent}`);
}
}
}
}
},
result: async (data, _bashToolIds, _thinkingTimer, usageContainer) => {
result: async (data, _shellToolIds, _thinkingTimer, usageContainer) => {
if (data.subtype === "success") {
const usage = data.usage;
const inputTokens = usage?.input_tokens || 0;
@@ -333,9 +333,9 @@ const messageHandlers: SDKMessageHandlers = {
log.info(`Failed: ${JSON.stringify(data)}`);
}
},
system: (_data, _bashToolIds, _thinkingTimer, _usageContainer) => {},
stream_event: (_data, _bashToolIds, _thinkingTimer, _usageContainer) => {},
tool_progress: (_data, _bashToolIds, _thinkingTimer, _usageContainer) => {},
tool_use_summary: (_data, _bashToolIds, _thinkingTimer, _usageContainer) => {},
auth_status: (_data, _bashToolIds, _thinkingTimer, _usageContainer) => {},
system: (_data, _shellToolIds, _thinkingTimer, _usageContainer) => {},
stream_event: (_data, _shellToolIds, _thinkingTimer, _usageContainer) => {},
tool_progress: (_data, _shellToolIds, _thinkingTimer, _usageContainer) => {},
tool_use_summary: (_data, _shellToolIds, _thinkingTimer, _usageContainer) => {},
auth_status: (_data, _shellToolIds, _thinkingTimer, _usageContainer) => {},
};
+9 -9
View File
@@ -77,11 +77,11 @@ function writeCodexConfig(ctx: AgentRunContext): string {
const mcpServerSections = [`[mcp_servers.${ghPullfrogMcpName}]\nurl = "${ctx.mcpServerUrl}"`];
// build features section for tool control
// disable native shell if bash is "disabled" or "restricted"
// when "restricted", agent uses MCP bash tool which filters secrets
const bash = ctx.payload.bash;
// disable native shell if shell is "disabled" or "restricted"
// when "restricted", agent uses MCP shell tool which filters secrets
const shell = ctx.payload.shell;
const features: string[] = [];
if (bash !== "enabled") {
if (shell !== "enabled") {
features.push("shell_tool = false");
features.push("unified_exec = false");
}
@@ -114,7 +114,7 @@ ${mcpServerSections.join("\n\n")}
);
log.info(
`» Codex config written to ${configPath} (shell: ${bash === "enabled" ? "enabled" : "disabled"}, project trusted: ${cwd})`
`» Codex config written to ${configPath} (shell: ${shell === "enabled" ? "enabled" : "disabled"}, project trusted: ${cwd})`
);
return codexDir;
@@ -210,13 +210,13 @@ export const codex = agent({
const commandExecutionIds = new Set<string>();
const thinkingTimer = new ThinkingTimer();
// when bash is restricted/disabled, filter sensitive env vars from the codex process.
// when shell is restricted/disabled, filter sensitive env vars from the codex process.
// defense-in-depth: codex 0.99.0's shell_command_tool feature flag is unreliable,
// so native shell commands may still run. filtering the process env ensures secrets
// (matching *_TOKEN, *_KEY, *_SECRET, etc.) are not accessible even if native shell
// bypasses the MCP bash tool's filterEnv.
// bypasses the MCP shell tool's filterEnv.
// API key is explicitly re-added since codex needs it for API calls.
const baseEnv = ctx.payload.bash === "enabled" ? process.env : filterEnv();
const baseEnv = ctx.payload.shell === "enabled" ? process.env : filterEnv();
const env: NodeJS.ProcessEnv = {
...baseEnv,
CODEX_HOME: codexDir,
@@ -386,7 +386,7 @@ function createMessageHandlers(): {
const isTracked = commandExecutionIds.has(item.id);
if (isTracked) {
thinkingTimer.markToolResult();
log.startGroup(`bash output`);
log.startGroup(`shell output`);
if (item.status === "failed" || (item.exit_code !== undefined && item.exit_code !== 0)) {
log.info(item.aggregated_output || "Command failed");
} else {
+3 -3
View File
@@ -189,7 +189,7 @@ export const cursor = agent({
},
tool_call: (event: CursorToolCallEvent) => {
if (event.subtype === "started") {
// handle both MCP tools and built-in tools (bash, WebFetch, etc)
// handle both MCP tools and built-in tools (shell, WebFetch, etc)
const mcpToolCall = event.tool_call?.mcpToolCall;
const builtinToolCall = (event.tool_call as any)?.builtinToolCall;
@@ -414,11 +414,11 @@ function configureCursorTools(ctx: AgentRunContext): void {
mkdirSync(cursorConfigDir, { recursive: true });
// build deny list based on tool permissions
const bash = ctx.payload.bash;
const shell = ctx.payload.shell;
const deny: string[] = [];
if (ctx.payload.search === "disabled") deny.push("WebSearch");
// both "disabled" and "restricted" block native shell
if (bash !== "enabled") deny.push("Shell(*)");
if (shell !== "enabled") deny.push("Shell(*)");
// always block native file tools (use MCP file_read/file_write instead)
deny.push("Read(*)", "Write(*)", "StrReplace(*)", "EditNotebook(*)", "Delete(*)");
// block built-in subagent spawning — delegation is handled by gh_pullfrog/delegate
+2 -2
View File
@@ -405,9 +405,9 @@ function configureGeminiSettings(ctx: AgentRunContext): string {
};
// build tools.exclude based on permissions (v0.3.0+ nested format)
const bash = ctx.payload.bash;
const shell = ctx.payload.shell;
const exclude: string[] = [];
if (bash !== "enabled") exclude.push("run_shell_command");
if (shell !== "enabled") exclude.push("run_shell_command");
if (ctx.payload.web === "disabled") exclude.push("web_fetch");
if (ctx.payload.search === "disabled") exclude.push("google_web_search");
// always block native file tools (use MCP file_read/file_write instead)
+3 -3
View File
@@ -150,7 +150,7 @@ export const opencode = agent({
const event = JSON.parse(trimmed) as OpenCodeEvent;
eventCount++;
// debug log all events to diagnose ordering and missing MCP/bash tool calls
// debug log all events to diagnose ordering and missing MCP/shell tool calls
log.debug(JSON.stringify(event, null, 2));
const timeSinceLastActivity = getIdleMs();
@@ -309,11 +309,11 @@ function configureOpenCode(ctx: AgentRunContext): void {
// build permission object based on tool permissions
// note: OpenCode has no built-in web search tool
const bash = ctx.payload.bash;
const shell = ctx.payload.shell;
const permission = {
edit: "deny",
read: "deny",
bash: bash !== "enabled" ? "deny" : "allow",
bash: shell !== "enabled" ? "deny" : "allow",
webfetch: ctx.payload.web === "disabled" ? "deny" : "allow",
external_directory: "deny",
};
+1 -1
View File
@@ -47,7 +47,7 @@ export const agent = <const input extends AgentInput>(input: input): defineAgent
log.info(`» web: ${ctx.payload.web}`);
log.info(`» search: ${ctx.payload.search}`);
log.info(`» push: ${ctx.payload.push}`);
log.info(`» bash: ${ctx.payload.bash}`);
log.info(`» shell: ${ctx.payload.shell}`);
log.debug(`» payload: ${JSON.stringify(ctx.payload, null, 2)}`);
return input.run(ctx);