From f37a744692707dde94d0fbc0fa689cf7b2a06919 Mon Sep 17 00:00:00 2001 From: Ryan Ward Date: Mon, 18 May 2026 12:13:05 -0500 Subject: [PATCH] Fix Warping indicator stuck after Bedrock LLM finishes responding Bedrock was calling suggest_next_prompt tool which created a SuggestPrompt action that waited forever on a oneshot channel for UI interaction that never fires in the Bedrock path, keeping the conversation permanently InProgress. Fixed by filtering the tool from the Bedrock tool list and skipping it at the stream level when the LLM calls it from context history. Also includes: Bedrock cache token tracking, cost estimation, LSP improvements, conversation usage view updates, and external config support. Co-Authored-By: Claude Opus 4.6 --- Error_20260518_112255_626.txt | 1037 +++++++++++++++++ GALAXY.md | 12 + app/src/ai/agent/conversation.rs | 14 + app/src/ai/agent_conversations_model.rs | 18 +- app/src/ai/agent_management/view.rs | 4 + app/src/ai/bedrock/client.rs | 28 + app/src/ai/bedrock/convert_request.rs | 6 +- app/src/ai/bedrock/external_config.rs | 229 ++++ app/src/ai/bedrock/external_config_tests.rs | 130 +++ app/src/ai/bedrock/mod.rs | 1 + app/src/ai/bedrock/models.rs | 37 +- app/src/ai/bedrock/stream.rs | 112 +- app/src/ai/bedrock/stream_tests.rs | 16 +- .../blocklist/controller/response_stream.rs | 2 +- app/src/ai/blocklist/mod.rs | 2 +- .../usage/conversation_usage_view.rs | 56 +- app/src/ai/blocklist/view_util.rs | 22 + app/src/ai/llms.rs | 2 +- app/src/settings_view/ai_page.rs | 2 +- app/src/terminal/view.rs | 13 + app/src/workspaces/gql_convert.rs | 5 + crates/galaxy_features/src/lib.rs | 12 + crates/lsp/src/config.rs | 46 +- crates/lsp/src/lib.rs | 8 +- crates/lsp/src/model.rs | 99 +- crates/lsp/src/service.rs | 273 ++++- crates/lsp/src/types.rs | 302 ++++- 27 files changed, 2423 insertions(+), 65 deletions(-) create mode 100644 Error_20260518_112255_626.txt create mode 100644 app/src/ai/bedrock/external_config.rs create mode 100644 app/src/ai/bedrock/external_config_tests.rs diff --git a/Error_20260518_112255_626.txt b/Error_20260518_112255_626.txt new file mode 100644 index 00000000..1236b931 --- /dev/null +++ b/Error_20260518_112255_626.txt @@ -0,0 +1,1037 @@ +{ + "bedrock": { + "context_window": [ + { + "content": { + "text": "looking at ~/GIT/personal/zed, how hard would it be to replace the warp IDE / Code Editor with Zed's code editor? LSP's, Intellisense, key gestures, etc.. \n \n Or, at minimum, just bringing (or adding) LSP's and intellisense with code following / tracking in the Warp IDE?", + "type": "text" + }, + "role": "user" + }, + { + "content": { + "parts": [ + { + "input": { + "command": "ls ~/GIT/personal/zed/" + }, + "name": "run_shell_command", + "tool_use_id": "tooluse_vz5OuMZwcx46hFdMWXIX4z", + "type": "tool_use" + }, + { + "input": { + "patterns": [ + "/Users/ryan.ward/GIT/personal/galaxy/app/src/editor*", + "/Users/ryan.ward/GIT/personal/galaxy/crates/editor/**/*.rs", + "/Users/ryan.ward/GIT/personal/galaxy/editor/**/*.rs" + ] + }, + "name": "file_glob", + "tool_use_id": "tooluse_LzMXoxi2VJsOFjPc7M8C4C", + "type": "tool_use" + } + ], + "type": "multi_part" + }, + "role": "assistant" + }, + { + "content": { + "parts": [ + { + "content": "Exit code: 0\nAGENTS.md\nCLAUDE.md\nCODE_OF_CONDUCT.md\nCONTRIBUTING.md\nCargo.lock\nCargo.toml\nDockerfile-collab\nDockerfile-collab.dockerignore\nDockerfile-cross.dockerignore\nDockerfile-distros\nDockerfile-distros.dockerignore\nGEMINI.md\nLICENSE-AGPL\nLICENSE-APACHE\nLICENSE-GPL\nProcfile\nProcfile.all\nProcfile.web\nREADME.md\nREVIEWERS.conl\nassets\nci\nclippy.toml\ncompose.yml\ncrates\ndebug.plist\ndefault.nix\ndocs\nextensions\nflake.lock\nflake.nix\nlegal\nlivekit.yaml\nlychee.toml\nnix\nrenovate.json\nrust-toolchain.toml\nrustfmt.toml\nscript\nshell.nix\ntooling\ntypos.toml", + "is_error": false, + "tool_use_id": "tooluse_vz5OuMZwcx46hFdMWXIX4z", + "type": "tool_result" + }, + { + "content": "Tool completed successfully.", + "is_error": false, + "tool_use_id": "tooluse_LzMXoxi2VJsOFjPc7M8C4C", + "type": "tool_result" + } + ], + "type": "multi_part" + }, + "role": "user" + }, + { + "content": { + "parts": [ + { + "input": { + "command": "ls ~/GIT/personal/zed/crates/ | grep -i -E \"editor|lsp|language|completion|project\" | head -30" + }, + "name": "run_shell_command", + "tool_use_id": "tooluse_6ZWGelIQN4UjqbSoCeJfqj", + "type": "tool_use" + }, + { + "input": { + "patterns": [ + "/Users/ryan.ward/GIT/personal/galaxy/editor/src/**/*.rs", + "/Users/ryan.ward/GIT/personal/galaxy/app/src/ide/**/*.rs", + "/Users/ryan.ward/GIT/personal/galaxy/app/src/ai/ide*" + ] + }, + "name": "file_glob", + "tool_use_id": "tooluse_g8NhBrDZ5YQKPh1U1cPP5V", + "type": "tool_use" + }, + { + "input": { + "command": "find /Users/ryan.ward/GIT/personal/galaxy -type d -name \"editor\" | head -10" + }, + "name": "run_shell_command", + "tool_use_id": "tooluse_ZrM6SbPRIYQN5cvaP206AA", + "type": "tool_use" + } + ], + "type": "multi_part" + }, + "role": "assistant" + }, + { + "content": { + "parts": [ + { + "content": "Exit code: 0\neditor\neditor_benchmarks\nkeymap_editor\nlanguage\nlanguage_core\nlanguage_extension\nlanguage_model\nlanguage_model_core\nlanguage_models\nlanguage_models_cloud\nlanguage_onboarding\nlanguage_selector\nlanguage_tools\nlanguages\nlsp\nproject\nproject_benchmarks\nproject_panel\nproject_symbols\nrecent_projects", + "is_error": false, + "tool_use_id": "tooluse_6ZWGelIQN4UjqbSoCeJfqj", + "type": "tool_result" + }, + { + "content": "Tool completed successfully.", + "is_error": false, + "tool_use_id": "tooluse_g8NhBrDZ5YQKPh1U1cPP5V", + "type": "tool_result" + }, + { + "content": "\n<|cursor|>", + "is_error": false, + "tool_use_id": "tooluse_ZrM6SbPRIYQN5cvaP206AA", + "type": "tool_result" + } + ], + "type": "multi_part" + }, + "role": "user" + }, + { + "content": { + "parts": [ + { + "input": { + "command": "find /Users/ryan.ward/GIT/personal/galaxy -type d -name \"editor\" -o -type d -name \"ide\" 2>/dev/null | head -20" + }, + "name": "run_shell_command", + "tool_use_id": "tooluse_OomnbdbOxtPWcGxOvs22qC", + "type": "tool_use" + }, + { + "input": { + "command": "cat /Users/ryan.ward/GIT/personal/galaxy/Cargo.toml | grep -A 2 \"editor\"" + }, + "name": "run_shell_command", + "tool_use_id": "tooluse_epIhzpMeI5HcLliDjIcGcF", + "type": "tool_use" + }, + { + "input": { + "command": "ls ~/GIT/personal/zed/crates/editor/src/ | head -40" + }, + "name": "run_shell_command", + "tool_use_id": "tooluse_NxanvkYOSKLZWKas56cBSd", + "type": "tool_use" + } + ], + "type": "multi_part" + }, + "role": "assistant" + }, + { + "content": { + "parts": [ + { + "content": "Exit code: 0\n/Users/ryan.ward/GIT/personal/galaxy/crates/editor\n/Users/ryan.ward/GIT/personal/galaxy/app/src/code/editor\n/Users/ryan.ward/GIT/personal/galaxy/app/src/ai/execution_profiles/editor\n/Users/ryan.ward/GIT/personal/galaxy/app/src/notebooks/editor\n/Users/ryan.ward/GIT/personal/galaxy/app/src/editor", + "is_error": false, + "tool_use_id": "tooluse_OomnbdbOxtPWcGxOvs22qC", + "type": "tool_result" + }, + { + "content": "Exit code: 0\n \"crates/editor\",\n \"crates/graphql\",\n \"crates/markdown_parser\",\n--\ngalaxy_editor = { path = \"crates/editor\" }\ngalaxy_features = { path = \"crates/galaxy_features\" }\nremote_server = { path = \"crates/remote_server\" }", + "is_error": false, + "tool_use_id": "tooluse_epIhzpMeI5HcLliDjIcGcF", + "type": "tool_result" + }, + { + "content": "Exit code: 0\nactions.rs\nblink_manager.rs\nbookmarks.rs\nbracket_colorization.rs\nclangd_ext.rs\ncode_actions.rs\ncode_completion_tests.rs\ncode_context_menus.rs\ncode_lens.rs\ncompletions.rs\nconfig.rs\ndiagnostics.rs\ndisplay_map\ndisplay_map.rs\ndocument_colors.rs\ndocument_symbols.rs\nedit_prediction_tests.rs\neditor.rs\neditor_block_comment_tests.rs\neditor_settings.rs\neditor_tests\neditor_tests.rs\nelement.rs\nfold.rs\nfolding_ranges.rs\ngit\ngit.rs\nhighlight_matching_bracket.rs\nhover_links.rs\nhover_popover.rs\nindent_guides.rs\ninlays\ninlays.rs\ninput.rs\nitems.rs\njsx_tag_auto_close.rs\nlinked_editing_ranges.rs\nlsp_ext.rs\nmouse_context_menu.rs\nmovement.rs", + "is_error": false, + "tool_use_id": "tooluse_NxanvkYOSKLZWKas56cBSd", + "type": "tool_result" + } + ], + "type": "multi_part" + }, + "role": "user" + }, + { + "content": { + "parts": [ + { + "input": { + "command": "ls ~/GIT/personal/zed/crates/editor/src/ | wc -l && ls ~/GIT/personal/zed/crates/lsp/src/ | head -20" + }, + "name": "run_shell_command", + "tool_use_id": "tooluse_bNHc7DMPpUQDPzFFBFlvy5", + "type": "tool_use" + }, + { + "input": { + "command": "ls /Users/ryan.ward/GIT/personal/galaxy/crates/editor/src/" + }, + "name": "run_shell_command", + "tool_use_id": "tooluse_On7202JL4XHtnHK2pv0IZE", + "type": "tool_use" + }, + { + "input": { + "command": "wc -l ~/GIT/personal/zed/crates/editor/src/editor.rs 2>/dev/null; find ~/GIT/personal/zed/crates/editor -name \"*.rs\" | xargs wc -l 2>/dev/null | tail -1" + }, + "name": "run_shell_command", + "tool_use_id": "tooluse_6SEE5moW0t8bGIafYGCitu", + "type": "tool_use" + } + ], + "type": "multi_part" + }, + "role": "assistant" + }, + { + "content": { + "parts": [ + { + "content": "Exit code: 0\n 56\ninput_handler.rs\nlsp.rs", + "is_error": false, + "tool_use_id": "tooluse_bNHc7DMPpUQDPzFFBFlvy5", + "type": "tool_result" + }, + { + "content": "Exit code: 0\ncontent\ndecoration\neditor.rs\nlib.rs\nmodel.rs\nmultiline.rs\nmultiline_tests.rs\nparallel_util.rs\nrender\nsearch.rs\nsearch_tests.rs\nselection.rs\nselection_tests.rs", + "is_error": false, + "tool_use_id": "tooluse_On7202JL4XHtnHK2pv0IZE", + "type": "tool_result" + }, + { + "content": "Exit code: 0\n 15198 /Users/ryan.ward/GIT/personal/zed/crates/editor/src/editor.rs\n 148732 total", + "is_error": false, + "tool_use_id": "tooluse_6SEE5moW0t8bGIafYGCitu", + "type": "tool_result" + } + ], + "type": "multi_part" + }, + "role": "user" + }, + { + "content": { + "parts": [ + { + "input": { + "command": "find /Users/ryan.ward/GIT/personal/galaxy/crates/editor -name \"*.rs\" | xargs wc -l 2>/dev/null | tail -1" + }, + "name": "run_shell_command", + "tool_use_id": "tooluse_nRC2z7mkP8ecACBf2gNToU", + "type": "tool_use" + }, + { + "input": { + "command": "find /Users/ryan.ward/GIT/personal/galaxy/app/src/editor -name \"*.rs\" | xargs wc -l 2>/dev/null | tail -1" + }, + "name": "run_shell_command", + "tool_use_id": "tooluse_zK8Bjvp6l0Y4Jf2FSqvi2F", + "type": "tool_use" + }, + { + "input": { + "command": "find /Users/ryan.ward/GIT/personal/galaxy/app/src/code -name \"*.rs\" | xargs wc -l 2>/dev/null | tail -1" + }, + "name": "run_shell_command", + "tool_use_id": "tooluse_PnzMBGx2d9dXLGG8bX5Srq", + "type": "tool_use" + }, + { + "input": { + "command": "find ~/GIT/personal/zed/crates/lsp -name \"*.rs\" | xargs wc -l | tail -1" + }, + "name": "run_shell_command", + "tool_use_id": "tooluse_7cHfxBmqLYlZUaixmnke59", + "type": "tool_use" + }, + { + "input": { + "command": "find ~/GIT/personal/zed/crates/language -name \"*.rs\" | xargs wc -l | tail -1" + }, + "name": "run_shell_command", + "tool_use_id": "tooluse_ZZ9xpaHl1vh6TMWBIQpyHz", + "type": "tool_use" + }, + { + "input": { + "command": "find ~/GIT/personal/zed/crates/project -name \"*.rs\" -not -path \"*/project_panel/*\" -not -path \"*/project_symbols/*\" -not -path \"*/project_benchmarks/*\" -not -path \"*/recent_projects/*\" | xargs wc -l | tail -1" + }, + "name": "run_shell_command", + "tool_use_id": "tooluse_kiRrWhrLnDuvBAg6cVMYb8", + "type": "tool_use" + } + ], + "type": "multi_part" + }, + "role": "assistant" + }, + { + "content": { + "parts": [ + { + "content": "Exit code: 0\n 61737 total", + "is_error": false, + "tool_use_id": "tooluse_nRC2z7mkP8ecACBf2gNToU", + "type": "tool_result" + }, + { + "content": "Exit code: 0\n 42718 total", + "is_error": false, + "tool_use_id": "tooluse_zK8Bjvp6l0Y4Jf2FSqvi2F", + "type": "tool_result" + }, + { + "content": "Exit code: 0\n 33911 total", + "is_error": false, + "tool_use_id": "tooluse_PnzMBGx2d9dXLGG8bX5Srq", + "type": "tool_result" + }, + { + "content": "Exit code: 0\n 2414 total", + "is_error": false, + "tool_use_id": "tooluse_7cHfxBmqLYlZUaixmnke59", + "type": "tool_result" + }, + { + "content": "Exit code: 0\n 21702 total", + "is_error": false, + "tool_use_id": "tooluse_ZZ9xpaHl1vh6TMWBIQpyHz", + "type": "tool_result" + }, + { + "content": "Exit code: 0\n 91950 total", + "is_error": false, + "tool_use_id": "tooluse_kiRrWhrLnDuvBAg6cVMYb8", + "type": "tool_result" + } + ], + "type": "multi_part" + }, + "role": "user" + }, + { + "content": { + "parts": [ + { + "input": { + "command": "head -100 /Users/ryan.ward/GIT/personal/galaxy/crates/editor/src/lib.rs" + }, + "name": "run_shell_command", + "tool_use_id": "tooluse_ypeUwQP80fdCFrTiePokWa", + "type": "tool_use" + }, + { + "input": { + "command": "cat /Users/ryan.ward/GIT/personal/galaxy/crates/editor/Cargo.toml" + }, + "name": "run_shell_command", + "tool_use_id": "tooluse_0RcLP9anNdW7kRsdKzyxl8", + "type": "tool_use" + }, + { + "input": { + "command": "head -50 ~/GIT/personal/zed/crates/editor/Cargo.toml" + }, + "name": "run_shell_command", + "tool_use_id": "tooluse_2PhKu0Th7ioeBQCD8WgBHe", + "type": "tool_use" + } + ], + "type": "multi_part" + }, + "role": "assistant" + }, + { + "content": { + "parts": [ + { + "content": "Exit code: 0\npub mod content;\npub mod decoration;\npub mod editor;\npub mod model;\npub mod multiline;\nmod parallel_util;\npub mod render;\npub mod search;\npub mod selection;", + "is_error": false, + "tool_use_id": "tooluse_ypeUwQP80fdCFrTiePokWa", + "type": "tool_result" + }, + { + "content": "Exit code: 0\n[package]\nname = \"galaxy_editor\"\nauthors = [\"Warp Team \"]\nversion = \"0.1.0\"\nedition = \"2024\"\ndescription = \"Text editing for Warp\"\nexclude = [\"test_data/*\",]\npublish.workspace = true\nlicense.workspace = true\n\n# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html\n\n[features]\ntest-util = []\n\n[dependencies]\nanyhow.workspace = true\narrayvec.workspace = true\nasset_cache.workspace = true\nasync-channel.workspace = true\nbytes.workspace = true\nenum-iterator.workspace = true\nfutures-lite.workspace = true\nfloat-cmp.workspace = true\ninstant.workspace = true\nitertools.workspace = true\nlazy_static.workspace = true\nlog.workspace = true\nmime_guess.workspace = true\nnum-traits.workspace = true\nordered-float.workspace = true\nhtml5ever.workspace = true\nmarkup5ever = \"0.35.0\"\nmarkdown_parser.workspace = true\nmermaid_to_svg.workspace = true\nparking_lot.workspace = true\nrand.workspace = true\nrangemap.workspace = true\nregex-automata = \"0.4.3\"\nregex-syntax = \"0.8.2\"\nurlocator.workspace = true\nserde.workspace = true\nserde_yaml.workspace = true\nstring-offset.workspace = true\nsum_tree.workspace = true\nthiserror.workspace = true\npathfinder_color = \"0.5.0\"\nvec1.workspace = true\ngalaxyui.workspace = true\ngalaxy_core.workspace = true\ngalaxy_util.workspace = true\nrayon.workspace = true\nline-ending.workspace = true\nvim.workspace = true\nimara-diff = \"0.2\"\n\n[dev-dependencies]\nrust-embed.workspace = true\nenclose = \"1.1.8\"\nenv_logger.workspace = true\nsum_tree = { workspace = true, features = [\"test-util\"] }\ngalaxy_core = { workspace = true, features = [\"test-util\"] }\ngalaxyui = { workspace = true, features = [\"test-util\"] }\ncriterion = { version = \"0.5.1\", features = [\"html_reports\"] }\n\n[[bench]]\nname = \"buffer_bench\"\nharness = false", + "is_error": false, + "tool_use_id": "tooluse_0RcLP9anNdW7kRsdKzyxl8", + "type": "tool_result" + }, + { + "content": "Exit code: 0\n[package]\nname = \"editor\"\nversion = \"0.1.0\"\nedition.workspace = true\npublish.workspace = true\nlicense = \"GPL-3.0-or-later\"\n\n[lints]\nworkspace = true\n\n[lib]\npath = \"src/editor.rs\"\ndoctest = false\n\n[features]\ntest-support = [\n \"text/test-support\",\n \"language/test-support\",\n \"gpui/test-support\",\n \"multi_buffer/test-support\",\n \"project/test-support\",\n \"theme/test-support\",\n \"util/test-support\",\n \"workspace/test-support\",\n \"tree-sitter-c\",\n \"tree-sitter-rust\",\n \"tree-sitter-typescript\",\n \"tree-sitter-html\",\n \"proptest\",\n \"unindent\",\n]\n\n[dependencies]\naho-corasick.workspace = true\nanyhow.workspace = true\nassets.workspace = true\nbase64.workspace = true\nbreadcrumbs.workspace = true\nclient.workspace = true\nclock.workspace = true\ncollections.workspace = true\nconvert_case.workspace = true\ndap.workspace = true\ndb.workspace = true\nbuffer_diff.workspace = true\nemojis.workspace = true\nfeature_flags.workspace = true\nfile_icons.workspace = true\nfutures.workspace = true\nfutures-lite.workspace = true", + "is_error": false, + "tool_use_id": "tooluse_2PhKu0Th7ioeBQCD8WgBHe", + "type": "tool_result" + } + ], + "type": "multi_part" + }, + "role": "user" + }, + { + "content": { + "parts": [ + { + "input": { + "command": "cat ~/GIT/personal/zed/crates/editor/Cargo.toml | grep \"dependencies\" -A 200 | grep \"^\\[\" | head -5" + }, + "name": "run_shell_command", + "tool_use_id": "tooluse_em9tqs15SXCKNr8wd3wTJS", + "type": "tool_use" + }, + { + "input": { + "command": "cat ~/GIT/personal/zed/crates/editor/Cargo.toml | grep -c \"workspace\"" + }, + "name": "run_shell_command", + "tool_use_id": "tooluse_2Q1GBb6AdJNDS96y3UV2f3", + "type": "tool_use" + }, + { + "input": { + "command": "ls /Users/ryan.ward/GIT/personal/galaxy/app/src/code/editor/" + }, + "name": "run_shell_command", + "tool_use_id": "tooluse_7WiS7KJpbI3f9wtjeyo2EG", + "type": "tool_use" + }, + { + "input": { + "command": "head -80 /Users/ryan.ward/GIT/personal/galaxy/app/src/code/editor/mod.rs 2>/dev/null || ls /Users/ryan.ward/GIT/personal/galaxy/app/src/code/" + }, + "name": "run_shell_command", + "tool_use_id": "tooluse_8H9USrs6qwWKfSnRPTFUyG", + "type": "tool_use" + } + ], + "type": "multi_part" + }, + "role": "assistant" + }, + { + "content": { + "parts": [ + { + "content": "Exit code: 0\n[dependencies]\n[dev-dependencies]\n[[bench]]\n[[bench]]", + "is_error": false, + "tool_use_id": "tooluse_em9tqs15SXCKNr8wd3wTJS", + "type": "tool_result" + }, + { + "content": "Exit code: 0\n107", + "is_error": false, + "tool_use_id": "tooluse_2Q1GBb6AdJNDS96y3UV2f3", + "type": "tool_result" + }, + { + "content": "Exit code: 0\ncomment_editor.rs\ncomment_editor_tests.rs\ncomments.rs\ndiff.rs\ndiff_tests.rs\nelement\nelement.rs\nembedded_comment.rs\nembedded_comment_tests.rs\nfind\ngoto_line\nline.rs\nline_iterator.rs\nmod.rs\nmodel.rs\nmodel_tests.rs\nnav_bar.rs\nscroll.rs\nview\nview.rs", + "is_error": false, + "tool_use_id": "tooluse_7WiS7KJpbI3f9wtjeyo2EG", + "type": "tool_result" + }, + { + "content": "Exit code: 0\n#![cfg_attr(target_family = \"wasm\", allow(dead_code, unused_imports))]\n\npub(crate) mod comment_editor;\nmod comments;\npub(super) mod diff;\nmod element;\npub mod embedded_comment;\npub mod find;\npub mod goto_line;\npub mod line;\nmod line_iterator;\npub mod model;\nmod nav_bar;\npub mod scroll;\npub mod view;\n\npub use comment_editor::{CommentEditor, CommentEditorEvent};\npub use comments::EditorCommentsModel;\npub use comments::EditorReviewComment;\npub(crate) use diff::{add_color, remove_color};\npub use element::GutterHoverTarget;\npub use nav_bar::NavBarBehavior;", + "is_error": false, + "tool_use_id": "tooluse_8H9USrs6qwWKfSnRPTFUyG", + "type": "tool_result" + } + ], + "type": "multi_part" + }, + "role": "user" + } + ], + "conversation_id": "", + "input": { + "cross_region_inference": true, + "max_tokens": 64000, + "messages": [ + { + "content": { + "text": "looking at ~/GIT/personal/zed, how hard would it be to replace the warp IDE / Code Editor with Zed's code editor? LSP's, Intellisense, key gestures, etc.. \n \n Or, at minimum, just bringing (or adding) LSP's and intellisense with code following / tracking in the Warp IDE?", + "type": "text" + }, + "role": "user" + }, + { + "content": { + "parts": [ + { + "input": { + "command": "ls ~/GIT/personal/zed/" + }, + "name": "run_shell_command", + "tool_use_id": "tooluse_vz5OuMZwcx46hFdMWXIX4z", + "type": "tool_use" + }, + { + "input": { + "patterns": [ + "/Users/ryan.ward/GIT/personal/galaxy/app/src/editor*", + "/Users/ryan.ward/GIT/personal/galaxy/crates/editor/**/*.rs", + "/Users/ryan.ward/GIT/personal/galaxy/editor/**/*.rs" + ] + }, + "name": "file_glob", + "tool_use_id": "tooluse_LzMXoxi2VJsOFjPc7M8C4C", + "type": "tool_use" + } + ], + "type": "multi_part" + }, + "role": "assistant" + }, + { + "content": { + "parts": [ + { + "content": "Exit code: 0\nAGENTS.md\nCLAUDE.md\nCODE_OF_CONDUCT.md\nCONTRIBUTING.md\nCargo.lock\nCargo.toml\nDockerfile-collab\nDockerfile-collab.dockerignore\nDockerfile-cross.dockerignore\nDockerfile-distros\nDockerfile-distros.dockerignore\nGEMINI.md\nLICENSE-AGPL\nLICENSE-APACHE\nLICENSE-GPL\nProcfile\nProcfile.all\nProcfile.web\nREADME.md\nREVIEWERS.conl\nassets\nci\nclippy.toml\ncompose.yml\ncrates\ndebug.plist\ndefault.nix\ndocs\nextensions\nflake.lock\nflake.nix\nlegal\nlivekit.yaml\nlychee.toml\nnix\nrenovate.json\nrust-toolchain.toml\nrustfmt.toml\nscript\nshell.nix\ntooling\ntypos.toml", + "is_error": false, + "tool_use_id": "tooluse_vz5OuMZwcx46hFdMWXIX4z", + "type": "tool_result" + }, + { + "content": "Tool completed successfully.", + "is_error": false, + "tool_use_id": "tooluse_LzMXoxi2VJsOFjPc7M8C4C", + "type": "tool_result" + } + ], + "type": "multi_part" + }, + "role": "user" + }, + { + "content": { + "parts": [ + { + "input": { + "command": "ls ~/GIT/personal/zed/crates/ | grep -i -E \"editor|lsp|language|completion|project\" | head -30" + }, + "name": "run_shell_command", + "tool_use_id": "tooluse_6ZWGelIQN4UjqbSoCeJfqj", + "type": "tool_use" + }, + { + "input": { + "patterns": [ + "/Users/ryan.ward/GIT/personal/galaxy/editor/src/**/*.rs", + "/Users/ryan.ward/GIT/personal/galaxy/app/src/ide/**/*.rs", + "/Users/ryan.ward/GIT/personal/galaxy/app/src/ai/ide*" + ] + }, + "name": "file_glob", + "tool_use_id": "tooluse_g8NhBrDZ5YQKPh1U1cPP5V", + "type": "tool_use" + }, + { + "input": { + "command": "find /Users/ryan.ward/GIT/personal/galaxy -type d -name \"editor\" | head -10" + }, + "name": "run_shell_command", + "tool_use_id": "tooluse_ZrM6SbPRIYQN5cvaP206AA", + "type": "tool_use" + } + ], + "type": "multi_part" + }, + "role": "assistant" + }, + { + "content": { + "parts": [ + { + "content": "Exit code: 0\neditor\neditor_benchmarks\nkeymap_editor\nlanguage\nlanguage_core\nlanguage_extension\nlanguage_model\nlanguage_model_core\nlanguage_models\nlanguage_models_cloud\nlanguage_onboarding\nlanguage_selector\nlanguage_tools\nlanguages\nlsp\nproject\nproject_benchmarks\nproject_panel\nproject_symbols\nrecent_projects", + "is_error": false, + "tool_use_id": "tooluse_6ZWGelIQN4UjqbSoCeJfqj", + "type": "tool_result" + }, + { + "content": "Tool completed successfully.", + "is_error": false, + "tool_use_id": "tooluse_g8NhBrDZ5YQKPh1U1cPP5V", + "type": "tool_result" + }, + { + "content": "\n<|cursor|>", + "is_error": false, + "tool_use_id": "tooluse_ZrM6SbPRIYQN5cvaP206AA", + "type": "tool_result" + } + ], + "type": "multi_part" + }, + "role": "user" + }, + { + "content": { + "parts": [ + { + "input": { + "command": "find /Users/ryan.ward/GIT/personal/galaxy -type d -name \"editor\" -o -type d -name \"ide\" 2>/dev/null | head -20" + }, + "name": "run_shell_command", + "tool_use_id": "tooluse_OomnbdbOxtPWcGxOvs22qC", + "type": "tool_use" + }, + { + "input": { + "command": "cat /Users/ryan.ward/GIT/personal/galaxy/Cargo.toml | grep -A 2 \"editor\"" + }, + "name": "run_shell_command", + "tool_use_id": "tooluse_epIhzpMeI5HcLliDjIcGcF", + "type": "tool_use" + }, + { + "input": { + "command": "ls ~/GIT/personal/zed/crates/editor/src/ | head -40" + }, + "name": "run_shell_command", + "tool_use_id": "tooluse_NxanvkYOSKLZWKas56cBSd", + "type": "tool_use" + } + ], + "type": "multi_part" + }, + "role": "assistant" + }, + { + "content": { + "parts": [ + { + "content": "Exit code: 0\n/Users/ryan.ward/GIT/personal/galaxy/crates/editor\n/Users/ryan.ward/GIT/personal/galaxy/app/src/code/editor\n/Users/ryan.ward/GIT/personal/galaxy/app/src/ai/execution_profiles/editor\n/Users/ryan.ward/GIT/personal/galaxy/app/src/notebooks/editor\n/Users/ryan.ward/GIT/personal/galaxy/app/src/editor", + "is_error": false, + "tool_use_id": "tooluse_OomnbdbOxtPWcGxOvs22qC", + "type": "tool_result" + }, + { + "content": "Exit code: 0\n \"crates/editor\",\n \"crates/graphql\",\n \"crates/markdown_parser\",\n--\ngalaxy_editor = { path = \"crates/editor\" }\ngalaxy_features = { path = \"crates/galaxy_features\" }\nremote_server = { path = \"crates/remote_server\" }", + "is_error": false, + "tool_use_id": "tooluse_epIhzpMeI5HcLliDjIcGcF", + "type": "tool_result" + }, + { + "content": "Exit code: 0\nactions.rs\nblink_manager.rs\nbookmarks.rs\nbracket_colorization.rs\nclangd_ext.rs\ncode_actions.rs\ncode_completion_tests.rs\ncode_context_menus.rs\ncode_lens.rs\ncompletions.rs\nconfig.rs\ndiagnostics.rs\ndisplay_map\ndisplay_map.rs\ndocument_colors.rs\ndocument_symbols.rs\nedit_prediction_tests.rs\neditor.rs\neditor_block_comment_tests.rs\neditor_settings.rs\neditor_tests\neditor_tests.rs\nelement.rs\nfold.rs\nfolding_ranges.rs\ngit\ngit.rs\nhighlight_matching_bracket.rs\nhover_links.rs\nhover_popover.rs\nindent_guides.rs\ninlays\ninlays.rs\ninput.rs\nitems.rs\njsx_tag_auto_close.rs\nlinked_editing_ranges.rs\nlsp_ext.rs\nmouse_context_menu.rs\nmovement.rs", + "is_error": false, + "tool_use_id": "tooluse_NxanvkYOSKLZWKas56cBSd", + "type": "tool_result" + } + ], + "type": "multi_part" + }, + "role": "user" + }, + { + "content": { + "parts": [ + { + "input": { + "command": "ls ~/GIT/personal/zed/crates/editor/src/ | wc -l && ls ~/GIT/personal/zed/crates/lsp/src/ | head -20" + }, + "name": "run_shell_command", + "tool_use_id": "tooluse_bNHc7DMPpUQDPzFFBFlvy5", + "type": "tool_use" + }, + { + "input": { + "command": "ls /Users/ryan.ward/GIT/personal/galaxy/crates/editor/src/" + }, + "name": "run_shell_command", + "tool_use_id": "tooluse_On7202JL4XHtnHK2pv0IZE", + "type": "tool_use" + }, + { + "input": { + "command": "wc -l ~/GIT/personal/zed/crates/editor/src/editor.rs 2>/dev/null; find ~/GIT/personal/zed/crates/editor -name \"*.rs\" | xargs wc -l 2>/dev/null | tail -1" + }, + "name": "run_shell_command", + "tool_use_id": "tooluse_6SEE5moW0t8bGIafYGCitu", + "type": "tool_use" + } + ], + "type": "multi_part" + }, + "role": "assistant" + }, + { + "content": { + "parts": [ + { + "content": "Exit code: 0\n 56\ninput_handler.rs\nlsp.rs", + "is_error": false, + "tool_use_id": "tooluse_bNHc7DMPpUQDPzFFBFlvy5", + "type": "tool_result" + }, + { + "content": "Exit code: 0\ncontent\ndecoration\neditor.rs\nlib.rs\nmodel.rs\nmultiline.rs\nmultiline_tests.rs\nparallel_util.rs\nrender\nsearch.rs\nsearch_tests.rs\nselection.rs\nselection_tests.rs", + "is_error": false, + "tool_use_id": "tooluse_On7202JL4XHtnHK2pv0IZE", + "type": "tool_result" + }, + { + "content": "Exit code: 0\n 15198 /Users/ryan.ward/GIT/personal/zed/crates/editor/src/editor.rs\n 148732 total", + "is_error": false, + "tool_use_id": "tooluse_6SEE5moW0t8bGIafYGCitu", + "type": "tool_result" + } + ], + "type": "multi_part" + }, + "role": "user" + }, + { + "content": { + "parts": [ + { + "input": { + "command": "find /Users/ryan.ward/GIT/personal/galaxy/crates/editor -name \"*.rs\" | xargs wc -l 2>/dev/null | tail -1" + }, + "name": "run_shell_command", + "tool_use_id": "tooluse_nRC2z7mkP8ecACBf2gNToU", + "type": "tool_use" + }, + { + "input": { + "command": "find /Users/ryan.ward/GIT/personal/galaxy/app/src/editor -name \"*.rs\" | xargs wc -l 2>/dev/null | tail -1" + }, + "name": "run_shell_command", + "tool_use_id": "tooluse_zK8Bjvp6l0Y4Jf2FSqvi2F", + "type": "tool_use" + }, + { + "input": { + "command": "find /Users/ryan.ward/GIT/personal/galaxy/app/src/code -name \"*.rs\" | xargs wc -l 2>/dev/null | tail -1" + }, + "name": "run_shell_command", + "tool_use_id": "tooluse_PnzMBGx2d9dXLGG8bX5Srq", + "type": "tool_use" + }, + { + "input": { + "command": "find ~/GIT/personal/zed/crates/lsp -name \"*.rs\" | xargs wc -l | tail -1" + }, + "name": "run_shell_command", + "tool_use_id": "tooluse_7cHfxBmqLYlZUaixmnke59", + "type": "tool_use" + }, + { + "input": { + "command": "find ~/GIT/personal/zed/crates/language -name \"*.rs\" | xargs wc -l | tail -1" + }, + "name": "run_shell_command", + "tool_use_id": "tooluse_ZZ9xpaHl1vh6TMWBIQpyHz", + "type": "tool_use" + }, + { + "input": { + "command": "find ~/GIT/personal/zed/crates/project -name \"*.rs\" -not -path \"*/project_panel/*\" -not -path \"*/project_symbols/*\" -not -path \"*/project_benchmarks/*\" -not -path \"*/recent_projects/*\" | xargs wc -l | tail -1" + }, + "name": "run_shell_command", + "tool_use_id": "tooluse_kiRrWhrLnDuvBAg6cVMYb8", + "type": "tool_use" + } + ], + "type": "multi_part" + }, + "role": "assistant" + }, + { + "content": { + "parts": [ + { + "content": "Exit code: 0\n 61737 total", + "is_error": false, + "tool_use_id": "tooluse_nRC2z7mkP8ecACBf2gNToU", + "type": "tool_result" + }, + { + "content": "Exit code: 0\n 42718 total", + "is_error": false, + "tool_use_id": "tooluse_zK8Bjvp6l0Y4Jf2FSqvi2F", + "type": "tool_result" + }, + { + "content": "Exit code: 0\n 33911 total", + "is_error": false, + "tool_use_id": "tooluse_PnzMBGx2d9dXLGG8bX5Srq", + "type": "tool_result" + }, + { + "content": "Exit code: 0\n 2414 total", + "is_error": false, + "tool_use_id": "tooluse_7cHfxBmqLYlZUaixmnke59", + "type": "tool_result" + }, + { + "content": "Exit code: 0\n 21702 total", + "is_error": false, + "tool_use_id": "tooluse_ZZ9xpaHl1vh6TMWBIQpyHz", + "type": "tool_result" + }, + { + "content": "Exit code: 0\n 91950 total", + "is_error": false, + "tool_use_id": "tooluse_kiRrWhrLnDuvBAg6cVMYb8", + "type": "tool_result" + } + ], + "type": "multi_part" + }, + "role": "user" + }, + { + "content": { + "parts": [ + { + "input": { + "command": "head -100 /Users/ryan.ward/GIT/personal/galaxy/crates/editor/src/lib.rs" + }, + "name": "run_shell_command", + "tool_use_id": "tooluse_ypeUwQP80fdCFrTiePokWa", + "type": "tool_use" + }, + { + "input": { + "command": "cat /Users/ryan.ward/GIT/personal/galaxy/crates/editor/Cargo.toml" + }, + "name": "run_shell_command", + "tool_use_id": "tooluse_0RcLP9anNdW7kRsdKzyxl8", + "type": "tool_use" + }, + { + "input": { + "command": "head -50 ~/GIT/personal/zed/crates/editor/Cargo.toml" + }, + "name": "run_shell_command", + "tool_use_id": "tooluse_2PhKu0Th7ioeBQCD8WgBHe", + "type": "tool_use" + } + ], + "type": "multi_part" + }, + "role": "assistant" + }, + { + "content": { + "parts": [ + { + "content": "Exit code: 0\npub mod content;\npub mod decoration;\npub mod editor;\npub mod model;\npub mod multiline;\nmod parallel_util;\npub mod render;\npub mod search;\npub mod selection;", + "is_error": false, + "tool_use_id": "tooluse_ypeUwQP80fdCFrTiePokWa", + "type": "tool_result" + }, + { + "content": "Exit code: 0\n[package]\nname = \"galaxy_editor\"\nauthors = [\"Warp Team \"]\nversion = \"0.1.0\"\nedition = \"2024\"\ndescription = \"Text editing for Warp\"\nexclude = [\"test_data/*\",]\npublish.workspace = true\nlicense.workspace = true\n\n# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html\n\n[features]\ntest-util = []\n\n[dependencies]\nanyhow.workspace = true\narrayvec.workspace = true\nasset_cache.workspace = true\nasync-channel.workspace = true\nbytes.workspace = true\nenum-iterator.workspace = true\nfutures-lite.workspace = true\nfloat-cmp.workspace = true\ninstant.workspace = true\nitertools.workspace = true\nlazy_static.workspace = true\nlog.workspace = true\nmime_guess.workspace = true\nnum-traits.workspace = true\nordered-float.workspace = true\nhtml5ever.workspace = true\nmarkup5ever = \"0.35.0\"\nmarkdown_parser.workspace = true\nmermaid_to_svg.workspace = true\nparking_lot.workspace = true\nrand.workspace = true\nrangemap.workspace = true\nregex-automata = \"0.4.3\"\nregex-syntax = \"0.8.2\"\nurlocator.workspace = true\nserde.workspace = true\nserde_yaml.workspace = true\nstring-offset.workspace = true\nsum_tree.workspace = true\nthiserror.workspace = true\npathfinder_color = \"0.5.0\"\nvec1.workspace = true\ngalaxyui.workspace = true\ngalaxy_core.workspace = true\ngalaxy_util.workspace = true\nrayon.workspace = true\nline-ending.workspace = true\nvim.workspace = true\nimara-diff = \"0.2\"\n\n[dev-dependencies]\nrust-embed.workspace = true\nenclose = \"1.1.8\"\nenv_logger.workspace = true\nsum_tree = { workspace = true, features = [\"test-util\"] }\ngalaxy_core = { workspace = true, features = [\"test-util\"] }\ngalaxyui = { workspace = true, features = [\"test-util\"] }\ncriterion = { version = \"0.5.1\", features = [\"html_reports\"] }\n\n[[bench]]\nname = \"buffer_bench\"\nharness = false", + "is_error": false, + "tool_use_id": "tooluse_0RcLP9anNdW7kRsdKzyxl8", + "type": "tool_result" + }, + { + "content": "Exit code: 0\n[package]\nname = \"editor\"\nversion = \"0.1.0\"\nedition.workspace = true\npublish.workspace = true\nlicense = \"GPL-3.0-or-later\"\n\n[lints]\nworkspace = true\n\n[lib]\npath = \"src/editor.rs\"\ndoctest = false\n\n[features]\ntest-support = [\n \"text/test-support\",\n \"language/test-support\",\n \"gpui/test-support\",\n \"multi_buffer/test-support\",\n \"project/test-support\",\n \"theme/test-support\",\n \"util/test-support\",\n \"workspace/test-support\",\n \"tree-sitter-c\",\n \"tree-sitter-rust\",\n \"tree-sitter-typescript\",\n \"tree-sitter-html\",\n \"proptest\",\n \"unindent\",\n]\n\n[dependencies]\naho-corasick.workspace = true\nanyhow.workspace = true\nassets.workspace = true\nbase64.workspace = true\nbreadcrumbs.workspace = true\nclient.workspace = true\nclock.workspace = true\ncollections.workspace = true\nconvert_case.workspace = true\ndap.workspace = true\ndb.workspace = true\nbuffer_diff.workspace = true\nemojis.workspace = true\nfeature_flags.workspace = true\nfile_icons.workspace = true\nfutures.workspace = true\nfutures-lite.workspace = true", + "is_error": false, + "tool_use_id": "tooluse_2PhKu0Th7ioeBQCD8WgBHe", + "type": "tool_result" + } + ], + "type": "multi_part" + }, + "role": "user" + }, + { + "content": { + "parts": [ + { + "input": { + "command": "cat ~/GIT/personal/zed/crates/editor/Cargo.toml | grep \"dependencies\" -A 200 | grep \"^\\[\" | head -5" + }, + "name": "run_shell_command", + "tool_use_id": "tooluse_em9tqs15SXCKNr8wd3wTJS", + "type": "tool_use" + }, + { + "input": { + "command": "cat ~/GIT/personal/zed/crates/editor/Cargo.toml | grep -c \"workspace\"" + }, + "name": "run_shell_command", + "tool_use_id": "tooluse_2Q1GBb6AdJNDS96y3UV2f3", + "type": "tool_use" + }, + { + "input": { + "command": "ls /Users/ryan.ward/GIT/personal/galaxy/app/src/code/editor/" + }, + "name": "run_shell_command", + "tool_use_id": "tooluse_7WiS7KJpbI3f9wtjeyo2EG", + "type": "tool_use" + }, + { + "input": { + "command": "head -80 /Users/ryan.ward/GIT/personal/galaxy/app/src/code/editor/mod.rs 2>/dev/null || ls /Users/ryan.ward/GIT/personal/galaxy/app/src/code/" + }, + "name": "run_shell_command", + "tool_use_id": "tooluse_8H9USrs6qwWKfSnRPTFUyG", + "type": "tool_use" + } + ], + "type": "multi_part" + }, + "role": "assistant" + }, + { + "content": { + "parts": [ + { + "content": "Exit code: 0\n[dependencies]\n[dev-dependencies]\n[[bench]]\n[[bench]]", + "is_error": false, + "tool_use_id": "tooluse_em9tqs15SXCKNr8wd3wTJS", + "type": "tool_result" + }, + { + "content": "Exit code: 0\n107", + "is_error": false, + "tool_use_id": "tooluse_2Q1GBb6AdJNDS96y3UV2f3", + "type": "tool_result" + }, + { + "content": "Exit code: 0\ncomment_editor.rs\ncomment_editor_tests.rs\ncomments.rs\ndiff.rs\ndiff_tests.rs\nelement\nelement.rs\nembedded_comment.rs\nembedded_comment_tests.rs\nfind\ngoto_line\nline.rs\nline_iterator.rs\nmod.rs\nmodel.rs\nmodel_tests.rs\nnav_bar.rs\nscroll.rs\nview\nview.rs", + "is_error": false, + "tool_use_id": "tooluse_7WiS7KJpbI3f9wtjeyo2EG", + "type": "tool_result" + }, + { + "content": "Exit code: 0\n#![cfg_attr(target_family = \"wasm\", allow(dead_code, unused_imports))]\n\npub(crate) mod comment_editor;\nmod comments;\npub(super) mod diff;\nmod element;\npub mod embedded_comment;\npub mod find;\npub mod goto_line;\npub mod line;\nmod line_iterator;\npub mod model;\nmod nav_bar;\npub mod scroll;\npub mod view;\n\npub use comment_editor::{CommentEditor, CommentEditorEvent};\npub use comments::EditorCommentsModel;\npub use comments::EditorReviewComment;\npub(crate) use diff::{add_color, remove_color};\npub use element::GutterHoverTarget;\npub use nav_bar::NavBarBehavior;", + "is_error": false, + "tool_use_id": "tooluse_8H9USrs6qwWKfSnRPTFUyG", + "type": "tool_result" + } + ], + "type": "multi_part" + }, + "role": "user" + } + ], + "model_id": "arn:aws:bedrock:us-east-1:156729649053:application-inference-profile/fma5bsw4oxhy", + "system_prompt": "You are Galaxy, an AI coding assistant embedded in a terminal application. You help users with software engineering tasks including writing code, debugging, explaining concepts, and navigating codebases.\n\n## Environment\n- Working directory: /Users/ryan.ward/GIT/personal/galaxy\n- Home directory: /Users/ryan.ward\n- OS: MacOS\n- Shell: zsh 5.9\n- Git branch: master\n- Current time (UTC): 2026-05-18T16:22:50.605091Z\n\n## Project Rules\n### Rules from /Users/ryan.ward/GIT/personal/galaxy\n# WARP.md\n\nThis file provides guidance when working with code in this repository.\n\n## Development Commands\n\n### Build and Run\n- `cargo run` - Build and run Warp locally\n- `cargo bundle --bin warp` - Bundle the main app\n\n### Running with local warp-server\nTo connect Warp client to a local warp-server instance:\n\n```bash\n# Connect to server on default port 8080\ncargo run --features with_local_server\n\n# Connect to server on custom port (e.g., 8082)\nSERVER_ROOT_URL=http://localhost:8082 WS_SERVER_URL=ws://localhost:8082/graphql/v2 cargo run --features with_local_server\n```\n\nEnvironment variables:\n- `SERVER_ROOT_URL` - HTTP endpoint (default: `http://localhost:8080`)\n- `WS_SERVER_URL` - WebSocket endpoint (default: `ws://localhost:8080/graphql/v2`)\n\n### Testing\n- `cargo nextest run --no-fail-fast --workspace --exclude command-signatures-v2` - Run tests with nextest\n- `cargo nextest run -p warp_completer --features v2` - Run completer tests with v2 features\n- `cargo test --doc` - Run doc tests\n- `cargo test` - Run standard tests for individual packages\n\n### Linting and Formatting\n- `./script/presubmit` - Run all presubmit checks (fmt, clippy, tests)\n- `cargo fmt` - Format code\n- `cargo clippy --workspace --all-targets --all-features --tests -- -D warnings` - Run clippy\n- `./script/run-clang-format.py -r --extensions 'c,h,cpp,m' ./crates/warpui/src/ ./app/src/` - Format C/C++/Obj-C code\n- `find . -name \"*.wgsl\" -exec wgslfmt --check {} +` - Check WGSL shader formatting\n\n### Bedrock Diagnostics\n- Bedrock request or stream failures automatically write `Error_.txt` to the repository root.\n- The error snapshot file includes the serialized Bedrock context window, tool definitions, protobuf request debug payload, captured Bedrock diagnostic lines, and log tails.\n- Set `GALAXY_BEDROCK_DIAGNOSTICS=1` to additionally write per-event Bedrock diagnostic logs to `bedrock-diagnostics.log` in the active Warp log directory.\n\n### Platform Setup\n- `./script/bootstrap` - Platform-specific setup (calls platform-specific bootstrap scripts)\n- `./script/install_cargo_build_deps` - Install Cargo build dependencies\n- `./script/install_cargo_test_deps` - Install Cargo test dependencies\n\n## Architecture Overview\n\nThis is a Rust-based terminal emulator with a custom UI framework called **WarpUI**.\n\n### Key Components\n\n**WarpUI Framework** (`ui/`):\n- Custom UI framework with Entity-Component-Handle pattern\n- Global `App` object owns all views/models (entities)\n- Views hold `ViewHandle` references to other views\n- `AppContext` provides temporary access to handles during render/events\n- Elements describe visual layout (Flutter-inspired)\n- Actions system for event handling\n- MouseStateHandle must be created once during construction, and then referenced/cloned anywhere we're using mouse input to track mouse changes. Inline `MouseStateHandle::default()` while rendering will cause no mouse interactions to work.\n\n**Main App** (`app/`):\n- Terminal emulation and shell management (`terminal/`)\n- AI integration including Agent Mode (`ai/`)\n- Cloud synchronization and Drive features (`drive/`)\n- Authentication and user management (`auth/`)\n- Settings and preferences (`settings/`)\n- Workspace and session management (`workspace/`)\n\n**Core Libraries**:\n- `warp_core/` - Core utilities and platform abstractions\n- `editor/` - Text editing functionality\n- `ui/` - Custom UI framework\n- `ipc/` - Inter-process communication\n- `graphql/` - GraphQL client and schema\n\n### Key Architectural Patterns\n\n1. **Entity-Handle System**: Views reference other views via handles, not direct ownership\n2. **Modular Structure**: Workspace contains multiple workspace configurations, each with terminals, notebooks, etc.\n3. **Cross-Platform**: Native implementations for macOS, Windows, Linux, plus WASM target\n4. **AI Integration**: Built-in AI assistant with context awareness and codebase indexing\n5. **Cloud Sync**: Objects can be synchronized across devices via Warp Drive\n\n### Development Guidelines\n\n**Workspace Structure**:\n- This is a Cargo workspace with 34+ member crates\n- Main binary is in `app/`, UI framework in `ui/`\n- Platform-specific code is conditionally compiled\n- Integration tests are in `integration/`\n\n**Coding Style Preferences**:\n- Avoid unnecessary type annotations, especially in closure params.\n- Avoid using too many Rust path qualifiers and use imports for concision. Place import statements at the top of the file as per convention.\n An exception to this is inside cfg-guarded code branches. In those cases, you can either embed the import into the relevant scope or just use an absolute path for one-offs.\n- If a function takes a context parameter (`AppContext`, `ViewContext`, or `ModelContext`), it should be named `ctx` and go last. The one exception is for\n functions that take a closure parameter, in which case the closure should be last.\n- Always remove unused parameters completely rather than prefixing them with `_`. Update the function signature and all call sites accordingly.\n- Prefer inline format arguments in macros like `println!`, `eprintln!`, and `format!` (for example, `eprintln!(\"{message}\")` instead of `eprintln!(\"{}\", message)`) to satisfy Clippy's `uninlined_format_args` lint.\n- Do not remove existing comments when making unrelated changes. Only remove or modify a comment if the logic it describes has changed.\n\n**Terminal Model Locking**:\n- Be extremely careful when calling `model.lock()` on the terminal model (`TerminalModel`). Acquiring multiple locks on the same model from different call sites can cause a deadlock, resulting in a UI freeze (beach ball on macOS).\n- Before adding a new `model.lock()` call, verify that no caller in the current call stack already holds the lock.\n- Prefer passing already-locked model references down the call stack rather than acquiring new locks.\n- If you must lock the model, keep the lock scope as short as possible and avoid calling other functions that might also attempt to lock.\n\n**Testing**:\n- Use `cargo nextest` for parallel test execution\n- Integration tests use custom framework in `integration/`\n- Tests should be run via presubmit script before submitting\n- Unit tests should be placed in separate files using the naming convention `${filename}_tests.rs` or `mod_test.rs`\n- Test files should be included at the end of their corresponding module with:\n ```rust\n #[cfg(test)]\n #[path = \"filename_tests.rs\"] // or \"mod_test.rs\"\n mod tests;\n ```\n\n**Pull Request Workflow**:\n- **ALWAYS** run cargo fmt and cargo clippy (the versions specified in ./script/presubmit) before opening a PR or pushing updates to an existing PR branch\n- Those commands must pass completely before creating or updating a pull request\n- Specifically, ensure `cargo fmt` and `cargo clippy` checks pass\n- If they fail, fix all issues before proceeding with the PR\n- This applies to:\n - Opening new pull requests\n - Pushing new commits to existing PR branches\n - Any branch updates that will be reviewed\n - When opening PRs, use the PR template at `.github/pull_request_template.md`\n - Add changelog entries when appropriate using the format at the bottom of the PR template. Use the following prefixes (without the `{{}}` brackets):\n - `CHANGELOG-NEW-FEATURE:` for new, relatively sizable features (use sparingly - these may get marketing/docs)\n - `CHANGELOG-IMPROVEMENT:` for new functionality of existing features\n - `CHANGELOG-BUG-FIX:` for fixes related to known bugs or regressions\n - `CHANGELOG-IMAGE:` for GCP-hosted image URLs\n - Leave changelog lines blank or remove them if no changelog entry is needed\n\n**Database**:\n- Uses Diesel ORM with SQLite\n- Migrations in `migrations/` directory\n- Schema defined in `app/src/persistence/schema.rs`\n\n**GraphQL**:\n- Schema and client code generation from `graphql/api/schema.graphql`\n- TypeScript types generated for frontend integration\n\n### Feature Flags\n\nWarp uses compile-time feature flags with a small runtime plumbing layer.\n\nHow to add a feature flag:\n- Add a new variant to `warp_core/src/features.rs` in the `FeatureFlag` enum\n- (Optional) Enable it by default for dogfood builds by listing it in `DOGFOOD_FLAGS`\n- Gate code paths with `FeatureFlag::YourFlag.is_enabled()`\n- For preview or release rollout, add to `PREVIEW_FLAGS` or `RELEASE_FLAGS` respectively (as appropriate)\n\nBest practices:\n- **Prefer runtime checks over cfg directives**: Prefer `FeatureFlag::YourFlag.is_enabled()` over `#[cfg(...)]` compile-time directives so flags can be toggled without recompilation and are easier to clean up later. Use `#[cfg(...)]` only when the code cannot compile without them (for example, platform-specific code or dependencies that do not exist when the feature is disabled).\n- Keep flags high-level and product-focused rather than per-call-site\n- Remove the flag and dead branches after launch has stabilized\n- For UI sections that expose a new feature, hide the UI behind the same flag\n\nExample:\n```rust\n#[derive(Sequence)]\npub enum FeatureFlag {\n YourNewFeature,\n}\n\n// Default-on for dogfood builds\npub const DOGFOOD_FLAGS: &[FeatureFlag] = &[\n FeatureFlag::YourNewFeature,\n];\n\n// Use in code\nif FeatureFlag::YourNewFeature.is_enabled() {\n // gated behavior\n}\n```\n\n### Exhaustive Matching\n\nWhen adding/editing match statements, avoid using the wildcard _ when at all possible. Exhaustive matching is helpful for ensuring that all variants are handled, especially when adding new variants to enums in the future.\n\n### Appearance Settings Notes\n\n- Samsung-inspired built-in themes are available as `SamsungDark` and `SamsungLight`.\n- UI font selection is persisted in `appearance.text.ui_font_name` and uses an empty string as the system-default sentinel.\n- The one-click Samsung brand preset is implemented in `app/src/settings_view/appearance_page.rs` and applies:\n - Samsung dark/light theme mapping\n - terminal + AI font defaults\n - a best-available Samsung-style UI font fallback\n\n\n## Tools\nYou have access to the following tools. Use them proactively to explore codebases and complete tasks:\n- `run_shell_command`: Execute shell commands. Use absolute paths based on the working directory.\n- `read_files`: Read file contents. Pass all files you need in a single call.\n- `apply_file_diffs`: Apply search/replace edits to files.\n- `grep`: Search for patterns in files. Pass all patterns in one call.\n- `file_glob`: Find files matching glob patterns. Pass all patterns in one call.\n- `get_tool_documentation`: Get detailed documentation for any tool or system capabilities.\n- `suggest_next_prompt`: After completing a task, suggest a follow-up action the user might want.\n\n## Guidelines\n- ALWAYS use tools to explore the codebase before answering questions about code.\n- Use absolute paths based on the working directory shown above.\n- When asked about a project, start by listing files with `file_glob` or `run_shell_command`.\n- Read relevant files before making claims about code structure or behavior.\n- Be concise and direct in responses.\n- IMPORTANT: After EVERY response, you MUST call `suggest_next_prompt` to suggest a relevant follow-up action or question the user might want to take next.\n", + "temperature": null, + "tools": [ + { + "description": "Execute a shell command and return its output.", + "input_schema": { + "properties": { + "command": { + "description": "The shell command to execute", + "type": "string" + } + }, + "required": [ + "command" + ], + "type": "object" + }, + "name": "run_shell_command" + }, + { + "description": "Find files matching glob patterns. Pass all patterns in one call.", + "input_schema": { + "properties": { + "patterns": { + "description": "Glob patterns to match", + "items": { + "type": "string" + }, + "type": "array" + } + }, + "required": [ + "patterns" + ], + "type": "object" + }, + "name": "file_glob" + } + ] + }, + "model_id": "arn:aws:bedrock:us-east-1:156729649053:application-inference-profile/fma5bsw4oxhy", + "request_id": "", + "task_id": "fa29a20f-0fce-4db8-b0b7-5c794d425634", + "tools": [ + { + "description": "Execute a shell command and return its output.", + "input_schema": { + "properties": { + "command": { + "description": "The shell command to execute", + "type": "string" + } + }, + "required": [ + "command" + ], + "type": "object" + }, + "name": "run_shell_command" + }, + { + "description": "Find files matching glob patterns. Pass all patterns in one call.", + "input_schema": { + "properties": { + "patterns": { + "description": "Glob patterns to match", + "items": { + "type": "string" + }, + "type": "array" + } + }, + "required": [ + "patterns" + ], + "type": "object" + }, + "name": "file_glob" + } + ] + }, + "captured_bedrock_lines": [ + "[2026-05-18T16:22:50.612Z][PROTOBUF][INPUT][arn:aws:bedrock:us-east-1:156729649053:application-inference-profile/fma5bsw4oxhy][PENDING][][][fa29a20f-0fce-4db8-b0b7-5c794d425634] Request { task_context: Some(TaskContext { tasks: [Task { id: \"fa29a20f-0fce-4db8-b0b7-5c794d425634\", description: \"\", dependencies: None, messages: [Message { id: \"tooluse_vz5OuMZwcx46hFdMWXIX4z\", task_id: \"fa29a20f-0fce-4db8-b0b7-5c794d425634\", request_id: \"\", timestamp: None, server_message_data: \"\", citations: [], message: Some(ToolCall(ToolCall { tool_call_id: \"tooluse_vz5OuMZwcx46hFdMWXIX4z\", tool: Some(RunShellCommand(RunShellCommand { command: \"ls ~/GIT/personal/zed/\", is_read_only: false, uses_pager: true, citations: [], is_risky: false, risk_category: Unspecified, wait_until_complete_value: None })) })) }, Message { id: \"tooluse_LzMXoxi2VJsOFjPc7M8C4C\", task_id: \"fa29a20f-0fce-4db8-b0b7-5c794d425634\", request_id: \"\", timestamp: None, server_message_data: \"\", citations: [], message: Some(ToolCall(ToolCall { tool_call_id: \"tooluse_LzMXoxi2VJsOFjPc7M8C4C\", tool: Some(FileGlob(FileGlob { patterns: [\"/Users/ryan.ward/GIT/personal/galaxy/app/src/editor*\", \"/Users/ryan.ward/GIT/personal/galaxy/crates/editor/**/*.rs\", \"/Users/ryan.ward/GIT/personal/galaxy/editor/**/*.rs\"], path: \"\" })) })) }, Message { id: \"tooluse_6ZWGelIQN4UjqbSoCeJfqj\", task_id: \"fa29a20f-0fce-4db8-b0b7-5c794d425634\", request_id: \"\", timestamp: None, server_message_data: \"\", citations: [], message: Some(ToolCall(ToolCall { tool_call_id: \"tooluse_6ZWGelIQN4UjqbSoCeJfqj\", tool: Some(RunShellCommand(RunShellCommand { command: \"ls ~/GIT/personal/zed/crates/ | grep -i -E \\\"editor|lsp|language|completion|project\\\" | head -30\", is_read_only: false, uses_pager: true, citations: [], is_risky: false, risk_category: Unspecified, wait_until_complete_value: None })) })) }, Message { id: \"tooluse_g8NhBrDZ5YQKPh1U1cPP5V\", task_id: \"fa29a20f-0fce-4db8-b0b7-5c794d425634\", request_id: \"\", timestamp: None, server_message_data: \"\", citations: [], message: Some(ToolCall(ToolCall { tool_call_id: \"tooluse_g8NhBrDZ5YQKPh1U1cPP5V\", tool: Some(FileGlob(FileGlob { patterns: [\"/Users/ryan.ward/GIT/personal/galaxy/editor/src/**/*.rs\", \"/Users/ryan.ward/GIT/personal/galaxy/app/src/ide/**/*.rs\", \"/Users/ryan.ward/GIT/personal/galaxy/app/src/ai/ide*\"], path: \"\" })) })) }, Message { id: \"tooluse_ZrM6SbPRIYQN5cvaP206AA\", task_id: \"fa29a20f-0fce-4db8-b0b7-5c794d425634\", request_id: \"\", timestamp: None, server_message_data: \"\", citations: [], message: Some(ToolCall(ToolCall { tool_call_id: \"tooluse_ZrM6SbPRIYQN5cvaP206AA\", tool: Some(RunShellCommand(RunShellCommand { command: \"find /Users/ryan.ward/GIT/personal/galaxy -type d -name \\\"editor\\\" | head -10\", is_read_only: false, uses_pager: true, citations: [], is_risky: false, risk_category: Unspecified, wait_until_complete_value: None })) })) }, Message { id: \"tooluse_OomnbdbOxtPWcGxOvs22qC\", task_id: \"fa29a20f-0fce-4db8-b0b7-5c794d425634\", request_id: \"\", timestamp: None, server_message_data: \"\", citations: [], message: Some(ToolCall(ToolCall { tool_call_id: \"tooluse_OomnbdbOxtPWcGxOvs22qC\", tool: Some(RunShellCommand(RunShellCommand { command: \"find /Users/ryan.ward/GIT/personal/galaxy -type d -name \\\"editor\\\" -o -type d -name \\\"ide\\\" 2>/dev/null | head -20\", is_read_only: false, uses_pager: true, citations: [], is_risky: false, risk_category: Unspecified, wait_until_complete_value: None })) })) }, Message { id: \"tooluse_epIhzpMeI5HcLliDjIcGcF\", task_id: \"fa29a20f-0fce-4db8-b0b7-5c794d425634\", request_id: \"\", timestamp: None, server_message_data: \"\", citations: [], message: Some(ToolCall(ToolCall { tool_call_id: \"tooluse_epIhzpMeI5HcLliDjIcGcF\", tool: Some(RunShellCommand(RunShellCommand { command: \"cat /Users/ryan.ward/GIT/personal/galaxy/Cargo.toml | grep -A 2 \\\"editor\\\"\", is_read_only: false, uses_pager: true, citations: [], is_risky: false, risk_category: Unspecified, wait_until_complete_value: None })) })) }, Message { id: \"tooluse_NxanvkYOSKLZWKas56cBSd\", task_id: \"fa29a20f-0fce-4db8-b0b7-5c794d425634\", request_id: \"\", timestamp: None, server_message_data: \"\", citations: [], message: Some(ToolCall(ToolCall { tool_call_id: \"tooluse_NxanvkYOSKLZWKas56cBSd\", tool: Some(RunShellCommand(RunShellCommand { command: \"ls ~/GIT/personal/zed/crates/editor/src/ | head -40\", is_read_only: false, uses_pager: true, citations: [], is_risky: false, risk_category: Unspecified, wait_until_complete_value: None })) })) }, Message { id: \"tooluse_bNHc7DMPpUQDPzFFBFlvy5\", task_id: \"fa29a20f-0fce-4db8-b0b7-5c794d425634\", request_id: \"\", timestamp: None, server_message_data: \"\", citations: [], message: Some(ToolCall(ToolCall { tool_call_id: \"tooluse_bNHc7DMPpUQDPzFFBFlvy5\", tool: Some(RunShellCommand(RunShellCommand { command: \"ls ~/GIT/personal/zed/crates/editor/src/ | wc -l && ls ~/GIT/personal/zed/crates/lsp/src/ | head -20\", is_read_only: false, uses_pager: true, citations: [], is_risky: false, risk_category: Unspecified, wait_until_complete_value: None })) })) }, Message { id: \"tooluse_On7202JL4XHtnHK2pv0IZE\", task_id: \"fa29a20f-0fce-4db8-b0b7-5c794d425634\", request_id: \"\", timestamp: None, server_message_data: \"\", citations: [], message: Some(ToolCall(ToolCall { tool_call_id: \"tooluse_On7202JL4XHtnHK2pv0IZE\", tool: Some(RunShellCommand(RunShellCommand { command: \"ls /Users/ryan.ward/GIT/personal/galaxy/crates/editor/src/\", is_read_only: false, uses_pager: true, citations: [], is_risky: false, risk_category: Unspecified, wait_until_complete_value: None })) })) }, Message { id: \"tooluse_6SEE5moW0t8bGIafYGCitu\", task_id: \"fa29a20f-0fce-4db8-b0b7-5c794d425634\", request_id: \"\", timestamp: None, server_message_data: \"\", citations: [], message: Some(ToolCall(ToolCall { tool_call_id: \"tooluse_6SEE5moW0t8bGIafYGCitu\", tool: Some(RunShellCommand(RunShellCommand { command: \"wc -l ~/GIT/personal/zed/crates/editor/src/editor.rs 2>/dev/null; find ~/GIT/personal/zed/crates/editor -name \\\"*.rs\\\" | xargs wc -l 2>/dev/null | tail -1\", is_read_only: false, uses_pager: true, citations: [], is_risky: false, risk_category: Unspecified, wait_until_complete_value: None })) })) }, Message { id: \"tooluse_nRC2z7mkP8ecACBf2gNToU\", task_id: \"fa29a20f-0fce-4db8-b0b7-5c794d425634\", request_id: \"\", timestamp: None, server_message_data: \"\", citations: [], message: Some(ToolCall(ToolCall { tool_call_id: \"tooluse_nRC2z7mkP8ecACBf2gNToU\", tool: Some(RunShellCommand(RunShellCommand { command: \"find /Users/ryan.ward/GIT/personal/galaxy/crates/editor -name \\\"*.rs\\\" | xargs wc -l 2>/dev/null | tail -1\", is_read_only: false, uses_pager: true, citations: [], is_risky: false, risk_category: Unspecified, wait_until_complete_value: None })) })) }, Message { id: \"tooluse_zK8Bjvp6l0Y4Jf2FSqvi2F\", task_id: \"fa29a20f-0fce-4db8-b0b7-5c794d425634\", request_id: \"\", timestamp: None, server_message_data: \"\", citations: [], message: Some(ToolCall(ToolCall { tool_call_id: \"tooluse_zK8Bjvp6l0Y4Jf2FSqvi2F\", tool: Some(RunShellCommand(RunShellCommand { command: \"find /Users/ryan.ward/GIT/personal/galaxy/app/src/editor -name \\\"*.rs\\\" | xargs wc -l 2>/dev/null | tail -1\", is_read_only: false, uses_pager: true, citations: [], is_risky: false, risk_category: Unspecified, wait_until_complete_value: None })) })) }, Message { id: \"tooluse_PnzMBGx2d9dXLGG8bX5Srq\", task_id: \"fa29a20f-0fce-4db8-b0b7-5c794d425634\", request_id: \"\", timestamp: None, server_message_data: \"\", citations: [], message: Some(ToolCall(ToolCall { tool_call_id: \"tooluse_PnzMBGx2d9dXLGG8bX5Srq\", tool: Some(RunShellCommand(RunShellCommand { command: \"find /Users/ryan.ward/GIT/personal/galaxy/app/src/code -name \\\"*.rs\\\" | xargs wc -l 2>/dev/null | tail -1\", is_read_only: false, uses_pager: true, citations: [], is_risky: false, risk_category: Unspecified, wait_until_complete_value: None })) })) }, Message { id: \"tooluse_7cHfxBmqLYlZUaixmnke59\", task_id: \"fa29a20f-0fce-4db8-b0b7-5c794d425634\", request_id: \"\", timestamp: None, server_message_data: \"\", citations: [], message: Some(ToolCall(ToolCall { tool_call_id: \"tooluse_7cHfxBmqLYlZUaixmnke59\", tool: Some(RunShellCommand(RunShellCommand { command: \"find ~/GIT/personal/zed/crates/lsp -name \\\"*.rs\\\" | xargs wc -l | tail -1\", is_read_only: false, uses_pager: true, citations: [], is_risky: false, risk_category: Unspecified, wait_until_complete_value: None })) })) }, Message { id: \"tooluse_ZZ9xpaHl1vh6TMWBIQpyHz\", task_id: \"fa29a20f-0fce-4db8-b0b7-5c794d425634\", request_id: \"\", timestamp: None, server_message_data: \"\", citations: [], message: Some(ToolCall(ToolCall { tool_call_id: \"tooluse_ZZ9xpaHl1vh6TMWBIQpyHz\", tool: Some(RunShellCommand(RunShellCommand { command: \"find ~/GIT/personal/zed/crates/language -name \\\"*.rs\\\" | xargs wc -l | tail -1\", is_read_only: false, uses_pager: true, citations: [], is_risky: false, risk_category: Unspecified, wait_until_complete_value: None })) })) }, Message { id: \"tooluse_kiRrWhrLnDuvBAg6cVMYb8\", task_id: \"fa29a20f-0fce-4db8-b0b7-5c794d425634\", request_id: \"\", timestamp: None, server_message_data: \"\", citations: [], message: Some(ToolCall(ToolCall { tool_call_id: \"tooluse_kiRrWhrLnDuvBAg6cVMYb8\", tool: Some(RunShellCommand(RunShellCommand { command: \"find ~/GIT/personal/zed/crates/project -name \\\"*.rs\\\" -not -path \\\"*/project_panel/*\\\" -not -path \\\"*/project_symbols/*\\\" -not -path \\\"*/project_benchmarks/*\\\" -not -path \\\"*/recent_projects/*\\\" | xargs wc -l | tail -1\", is_read_only: false, uses_pager: true, citations: [], is_risky: false, risk_category: Unspecified, wait_until_complete_value: None })) })) }, Message { id: \"tooluse_ypeUwQP80fdCFrTiePokWa\", task_id: \"fa29a20f-0fce-4db8-b0b7-5c794d425634\", request_id: \"\", timestamp: None, server_message_data: \"\", citations: [], message: Some(ToolCall(ToolCall { tool_call_id: \"tooluse_ypeUwQP80fdCFrTiePokWa\", tool: Some(RunShellCommand(RunShellCommand { command: \"head -100 /Users/ryan.ward/GIT/personal/galaxy/crates/editor/src/lib.rs\", is_read_only: false, uses_pager: true, citations: [], is_risky: false, risk_category: Unspecified, wait_until_complete_value: None })) })) }, Message { id: \"tooluse_0RcLP9anNdW7kRsdKzyxl8\", task_id: \"fa29a20f-0fce-4db8-b0b7-5c794d425634\", request_id: \"\", timestamp: None, server_message_data: \"\", citations: [], message: Some(ToolCall(ToolCall { tool_call_id: \"tooluse_0RcLP9anNdW7kRsdKzyxl8\", tool: Some(RunShellCommand(RunShellCommand { command: \"cat /Users/ryan.ward/GIT/personal/galaxy/crates/editor/Cargo.toml\", is_read_only: false, uses_pager: true, citations: [], is_risky: false, risk_category: Unspecified, wait_until_complete_value: None })) })) }, Message { id: \"tooluse_2PhKu0Th7ioeBQCD8WgBHe\", task_id: \"fa29a20f-0fce-4db8-b0b7-5c794d425634\", request_id: \"\", timestamp: None, server_message_data: \"\", citations: [], message: Some(ToolCall(ToolCall { tool_call_id: \"tooluse_2PhKu0Th7ioeBQCD8WgBHe\", tool: Some(RunShellCommand(RunShellCommand { command: \"head -50 ~/GIT/personal/zed/crates/editor/Cargo.toml\", is_read_only: false, uses_pager: true, citations: [], is_risky: false, risk_category: Unspecified, wait_until_complete_value: None })) })) }, Message { id: \"tooluse_em9tqs15SXCKNr8wd3wTJS\", task_id: \"fa29a20f-0fce-4db8-b0b7-5c794d425634\", request_id: \"\", timestamp: None, server_message_data: \"\", citations: [], message: Some(ToolCall(ToolCall { tool_call_id: \"tooluse_em9tqs15SXCKNr8wd3wTJS\", tool: Some(RunShellCommand(RunShellCommand { command: \"cat ~/GIT/personal/zed/crates/editor/Cargo.toml | grep \\\"dependencies\\\" -A 200 | grep \\\"^\\\\[\\\" | head -5\", is_read_only: false, uses_pager: true, citations: [], is_risky: false, risk_category: Unspecified, wait_until_complete_value: None })) })) }, Message { id: \"tooluse_2Q1GBb6AdJNDS96y3UV2f3\", task_id: \"fa29a20f-0fce-4db8-b0b7-5c794d425634\", request_id: \"\", timestamp: None, server_message_data: \"\", citations: [], message: Some(ToolCall(ToolCall { tool_call_id: \"tooluse_2Q1GBb6AdJNDS96y3UV2f3\", tool: Some(RunShellCommand(RunShellCommand { command: \"cat ~/GIT/personal/zed/crates/editor/Cargo.toml | grep -c \\\"workspace\\\"\", is_read_only: false, uses_pager: true, citations: [], is_risky: false, risk_category: Unspecified, wait_until_complete_value: None })) })) }, Message { id: \"tooluse_7WiS7KJpbI3f9wtjeyo2EG\", task_id: \"fa29a20f-0fce-4db8-b0b7-5c794d425634\", request_id: \"\", timestamp: None, server_message_data: \"\", citations: [], message: Some(ToolCall(ToolCall { tool_call_id: \"tooluse_7WiS7KJpbI3f9wtjeyo2EG\", tool: Some(RunShellCommand(RunShellCommand { command: \"ls /Users/ryan.ward/GIT/personal/galaxy/app/src/code/editor/\", is_read_only: false, uses_pager: true, citations: [], is_risky: false, risk_category: Unspecified, wait_until_complete_value: None })) })) }, Message { id: \"tooluse_8H9USrs6qwWKfSnRPTFUyG\", task_id: \"fa29a20f-0fce-4db8-b0b7-5c794d425634\", request_id: \"\", timestamp: None, server_message_data: \"\", citations: [], message: Some(ToolCall(ToolCall { tool_call_id: \"tooluse_8H9USrs6qwWKfSnRPTFUyG\", tool: Some(RunShellCommand(RunShellCommand { command: \"head -80 /Users/ryan.ward/GIT/personal/galaxy/app/src/code/editor/mod.rs 2>/dev/null || ls /Users/ryan.ward/GIT/personal/galaxy/app/src/code/\", is_read_only: false, uses_pager: true, citations: [], is_risky: false, risk_category: Unspecified, wait_until_complete_value: None })) })) }], summary: \"\", server_data: \"\" }] }), input: Some(Input { context: Some(InputContext { directory: Some(Directory { pwd: \"/Users/ryan.ward/GIT/personal/galaxy\", home: \"/Users/ryan.ward\", pwd_file_symbols_indexed: false }), operating_system: Some(OperatingSystem { platform: \"MacOS\", distribution: \"\" }), shell: Some(Shell { name: \"zsh\", version: \"5.9\" }), current_time: Some(Timestamp { seconds: 1779121370, nanos: 605091000 }), codebases: [], project_rules: [ProjectRules { root_path: \"/Users/ryan.ward/GIT/personal/galaxy\", active_rule_files: [FileContent { file_path: \"/Users/ryan.ward/GIT/personal/galaxy/WARP.md\", content: \"# WARP.md\\n\\nThis file provides guidance when working with code in this repository.\\n\\n## Development Commands\\n\\n### Build and Run\\n- `cargo run` - Build and run Warp locally\\n- `cargo bundle --bin warp` - Bundle the main app\\n\\n### Running with local warp-server\\nTo connect Warp client to a local warp-server instance:\\n\\n```bash\\n# Connect to server on default port 8080\\ncargo run --features with_local_server\\n\\n# Connect to server on custom port (e.g., 8082)\\nSERVER_ROOT_URL=http://localhost:8082 WS_SERVER_URL=ws://localhost:8082/graphql/v2 cargo run --features with_local_server\\n```\\n\\nEnvironment variables:\\n- `SERVER_ROOT_URL` - HTTP endpoint (default: `http://localhost:8080`)\\n- `WS_SERVER_URL` - WebSocket endpoint (default: `ws://localhost:8080/graphql/v2`)\\n\\n### Testing\\n- `cargo nextest run --no-fail-fast --workspace --exclude command-signatures-v2` - Run tests with nextest\\n- `cargo nextest run -p warp_completer --features v2` - Run completer tests with v2 features\\n- `cargo test --doc` - Run doc tests\\n- `cargo test` - Run standard tests for individual packages\\n\\n### Linting and Formatting\\n- `./script/presubmit` - Run all presubmit checks (fmt, clippy, tests)\\n- `cargo fmt` - Format code\\n- `cargo clippy --workspace --all-targets --all-features --tests -- -D warnings` - Run clippy\\n- `./script/run-clang-format.py -r --extensions 'c,h,cpp,m' ./crates/warpui/src/ ./app/src/` - Format C/C++/Obj-C code\\n- `find . -name \\\"*.wgsl\\\" -exec wgslfmt --check {} +` - Check WGSL shader formatting\\n\\n### Bedrock Diagnostics\\n- Bedrock request or stream failures automatically write `Error_.txt` to the repository root.\\n- The error snapshot file includes the serialized Bedrock context window, tool definitions, protobuf request debug payload, captured Bedrock diagnostic lines, and log tails.\\n- Set `GALAXY_BEDROCK_DIAGNOSTICS=1` to additionally write per-event Bedrock diagnostic logs to `bedrock-diagnostics.log` in the active Warp log directory.\\n\\n### Platform Setup\\n- `./script/bootstrap` - Platform-specific setup (calls platform-specific bootstrap scripts)\\n- `./script/install_cargo_build_deps` - Install Cargo build dependencies\\n- `./script/install_cargo_test_deps` - Install Cargo test dependencies\\n\\n## Architecture Overview\\n\\nThis is a Rust-based terminal emulator with a custom UI framework called **WarpUI**.\\n\\n### Key Components\\n\\n**WarpUI Framework** (`ui/`):\\n- Custom UI framework with Entity-Component-Handle pattern\\n- Global `App` object owns all views/models (entities)\\n- Views hold `ViewHandle` references to other views\\n- `AppContext` provides temporary access to handles during render/events\\n- Elements describe visual layout (Flutter-inspired)\\n- Actions system for event handling\\n- MouseStateHandle must be created once during construction, and then referenced/cloned anywhere we're using mouse input to track mouse changes. Inline `MouseStateHandle::default()` while rendering will cause no mouse interactions to work.\\n\\n**Main App** (`app/`):\\n- Terminal emulation and shell management (`terminal/`)\\n- AI integration including Agent Mode (`ai/`)\\n- Cloud synchronization and Drive features (`drive/`)\\n- Authentication and user management (`auth/`)\\n- Settings and preferences (`settings/`)\\n- Workspace and session management (`workspace/`)\\n\\n**Core Libraries**:\\n- `warp_core/` - Core utilities and platform abstractions\\n- `editor/` - Text editing functionality\\n- `ui/` - Custom UI framework\\n- `ipc/` - Inter-process communication\\n- `graphql/` - GraphQL client and schema\\n\\n### Key Architectural Patterns\\n\\n1. **Entity-Handle System**: Views reference other views via handles, not direct ownership\\n2. **Modular Structure**: Workspace contains multiple workspace configurations, each with terminals, notebooks, etc.\\n3. **Cross-Platform**: Native implementations for macOS, Windows, Linux, plus WASM target\\n4. **AI Integration**: Built-in AI assistant with context awareness and codebase indexing\\n5. **Cloud Sync**: Objects can be synchronized across devices via Warp Drive\\n\\n### Development Guidelines\\n\\n**Workspace Structure**:\\n- This is a Cargo workspace with 34+ member crates\\n- Main binary is in `app/`, UI framework in `ui/`\\n- Platform-specific code is conditionally compiled\\n- Integration tests are in `integration/`\\n\\n**Coding Style Preferences**:\\n- Avoid unnecessary type annotations, especially in closure params.\\n- Avoid using too many Rust path qualifiers and use imports for concision. Place import statements at the top of the file as per convention.\\n An exception to this is inside cfg-guarded code branches. In those cases, you can either embed the import into the relevant scope or just use an absolute path for one-offs.\\n- If a function takes a context parameter (`AppContext`, `ViewContext`, or `ModelContext`), it should be named `ctx` and go last. The one exception is for\\n functions that take a closure parameter, in which case the closure should be last.\\n- Always remove unused parameters completely rather than prefixing them with `_`. Update the function signature and all call sites accordingly.\\n- Prefer inline format arguments in macros like `println!`, `eprintln!`, and `format!` (for example, `eprintln!(\\\"{message}\\\")` instead of `eprintln!(\\\"{}\\\", message)`) to satisfy Clippy's `uninlined_format_args` lint.\\n- Do not remove existing comments when making unrelated changes. Only remove or modify a comment if the logic it describes has changed.\\n\\n**Terminal Model Locking**:\\n- Be extremely careful when calling `model.lock()` on the terminal model (`TerminalModel`). Acquiring multiple locks on the same model from different call sites can cause a deadlock, resulting in a UI freeze (beach ball on macOS).\\n- Before adding a new `model.lock()` call, verify that no caller in the current call stack already holds the lock.\\n- Prefer passing already-locked model references down the call stack rather than acquiring new locks.\\n- If you must lock the model, keep the lock scope as short as possible and avoid calling other functions that might also attempt to lock.\\n\\n**Testing**:\\n- Use `cargo nextest` for parallel test execution\\n- Integration tests use custom framework in `integration/`\\n- Tests should be run via presubmit script before submitting\\n- Unit tests should be placed in separate files using the naming convention `${filename}_tests.rs` or `mod_test.rs`\\n- Test files should be included at the end of their corresponding module with:\\n ```rust\\n #[cfg(test)]\\n #[path = \\\"filename_tests.rs\\\"] // or \\\"mod_test.rs\\\"\\n mod tests;\\n ```\\n\\n**Pull Request Workflow**:\\n- **ALWAYS** run cargo fmt and cargo clippy (the versions specified in ./script/presubmit) before opening a PR or pushing updates to an existing PR branch\\n- Those commands must pass completely before creating or updating a pull request\\n- Specifically, ensure `cargo fmt` and `cargo clippy` checks pass\\n- If they fail, fix all issues before proceeding with the PR\\n- This applies to:\\n - Opening new pull requests\\n - Pushing new commits to existing PR branches\\n - Any branch updates that will be reviewed\\n - When opening PRs, use the PR template at `.github/pull_request_template.md`\\n - Add changelog entries when appropriate using the format at the bottom of the PR template. Use the following prefixes (without the `{{}}` brackets):\\n - `CHANGELOG-NEW-FEATURE:` for new, relatively sizable features (use sparingly - these may get marketing/docs)\\n - `CHANGELOG-IMPROVEMENT:` for new functionality of existing features\\n - `CHANGELOG-BUG-FIX:` for fixes related to known bugs or regressions\\n - `CHANGELOG-IMAGE:` for GCP-hosted image URLs\\n - Leave changelog lines blank or remove them if no changelog entry is needed\\n\\n**Database**:\\n- Uses Diesel ORM with SQLite\\n- Migrations in `migrations/` directory\\n- Schema defined in `app/src/persistence/schema.rs`\\n\\n**GraphQL**:\\n- Schema and client code generation from `graphql/api/schema.graphql`\\n- TypeScript types generated for frontend integration\\n\\n### Feature Flags\\n\\nWarp uses compile-time feature flags with a small runtime plumbing layer.\\n\\nHow to add a feature flag:\\n- Add a new variant to `warp_core/src/features.rs` in the `FeatureFlag` enum\\n- (Optional) Enable it by default for dogfood builds by listing it in `DOGFOOD_FLAGS`\\n- Gate code paths with `FeatureFlag::YourFlag.is_enabled()`\\n- For preview or release rollout, add to `PREVIEW_FLAGS` or `RELEASE_FLAGS` respectively (as appropriate)\\n\\nBest practices:\\n- **Prefer runtime checks over cfg directives**: Prefer `FeatureFlag::YourFlag.is_enabled()` over `#[cfg(...)]` compile-time directives so flags can be toggled without recompilation and are easier to clean up later. Use `#[cfg(...)]` only when the code cannot compile without them (for example, platform-specific code or dependencies that do not exist when the feature is disabled).\\n- Keep flags high-level and product-focused rather than per-call-site\\n- Remove the flag and dead branches after launch has stabilized\\n- For UI sections that expose a new feature, hide the UI behind the same flag\\n\\nExample:\\n```rust\\n#[derive(Sequence)]\\npub enum FeatureFlag {\\n YourNewFeature,\\n}\\n\\n// Default-on for dogfood builds\\npub const DOGFOOD_FLAGS: &[FeatureFlag] = &[\\n FeatureFlag::YourNewFeature,\\n];\\n\\n// Use in code\\nif FeatureFlag::YourNewFeature.is_enabled() {\\n // gated behavior\\n}\\n```\\n\\n### Exhaustive Matching\\n\\nWhen adding/editing match statements, avoid using the wildcard _ when at all possible. Exhaustive matching is helpful for ensuring that all variants are handled, especially when adding new variants to enums in the future.\\n\\n### Appearance Settings Notes\\n\\n- Samsung-inspired built-in themes are available as `SamsungDark` and `SamsungLight`.\\n- UI font selection is persisted in `appearance.text.ui_font_name` and uses an empty string as the system-default sentinel.\\n- The one-click Samsung brand preset is implemented in `app/src/settings_view/appearance_page.rs` and applies:\\n - Samsung dark/light theme mapping\\n - terminal + AI font defaults\\n - a best-available Samsung-style UI font fallback\\n\", line_range: None }], additional_rule_file_paths: [] }], git: Some(Git { head: \"master\", branch: \"master\" }), updated_skills_context: None, updated_lsp_servers_context: None, executed_shell_commands: [], selected_text: [], images: [], files: [] }), r#type: Some(UserInputs(UserInputs { inputs: [UserInput { input: Some(ToolCallResult(ToolCallResult { tool_call_id: \"tooluse_em9tqs15SXCKNr8wd3wTJS\", result: Some(RunShellCommand(RunShellCommandResult { command: \"cat ~/GIT/personal/zed/crates/editor/Cargo.toml | grep \\\"dependencies\\\" -A 200 | grep \\\"^\\\\[\\\" | head -5\", output: \"\", exit_code: 0, result: Some(CommandFinished(ShellCommandFinished { output: \"[dependencies]\\n[dev-dependencies]\\n[[bench]]\\n[[bench]]\", exit_code: 0, command_id: \"precmd-177912119622521-20\" })) })) })) }, UserInput { input: Some(ToolCallResult(ToolCallResult { tool_call_id: \"tooluse_2Q1GBb6AdJNDS96y3UV2f3\", result: Some(RunShellCommand(RunShellCommandResult { command: \"cat ~/GIT/personal/zed/crates/editor/Cargo.toml | grep -c \\\"workspace\\\"\", output: \"\", exit_code: 0, result: Some(CommandFinished(ShellCommandFinished { output: \"107\", exit_code: 0, command_id: \"precmd-177912119622521-21\" })) })) })) }, UserInput { input: Some(ToolCallResult(ToolCallResult { tool_call_id: \"tooluse_7WiS7KJpbI3f9wtjeyo2EG\", result: Some(RunShellCommand(RunShellCommandResult { command: \"ls /Users/ryan.ward/GIT/personal/galaxy/app/src/code/editor/\", output: \"\", exit_code: 0, result: Some(CommandFinished(ShellCommandFinished { output: \"comment_editor.rs\\ncomment_editor_tests.rs\\ncomments.rs\\ndiff.rs\\ndiff_tests.rs\\nelement\\nelement.rs\\nembedded_comment.rs\\nembedded_comment_tests.rs\\nfind\\ngoto_line\\nline.rs\\nline_iterator.rs\\nmod.rs\\nmodel.rs\\nmodel_tests.rs\\nnav_bar.rs\\nscroll.rs\\nview\\nview.rs\", exit_code: 0, command_id: \"precmd-177912119622521-22\" })) })) })) }, UserInput { input: Some(ToolCallResult(ToolCallResult { tool_call_id: \"tooluse_8H9USrs6qwWKfSnRPTFUyG\", result: Some(RunShellCommand(RunShellCommandResult { command: \"head -80 /Users/ryan.ward/GIT/personal/galaxy/app/src/code/editor/mod.rs 2>/dev/null || ls /Users/ryan.ward/GIT/personal/galaxy/app/src/code/\", output: \"\", exit_code: 0, result: Some(CommandFinished(ShellCommandFinished { output: \"#![cfg_attr(target_family = \\\"wasm\\\", allow(dead_code, unused_imports))]\\n\\npub(crate) mod comment_editor;\\nmod comments;\\npub(super) mod diff;\\nmod element;\\npub mod embedded_comment;\\npub mod find;\\npub mod goto_line;\\npub mod line;\\nmod line_iterator;\\npub mod model;\\nmod nav_bar;\\npub mod scroll;\\npub mod view;\\n\\npub use comment_editor::{CommentEditor, CommentEditorEvent};\\npub use comments::EditorCommentsModel;\\npub use comments::EditorReviewComment;\\npub(crate) use diff::{add_color, remove_color};\\npub use element::GutterHoverTarget;\\npub use nav_bar::NavBarBehavior;\", exit_code: 0, command_id: \"precmd-177912119622521-23\" })) })) })) }] })) }), settings: Some(Settings { model_config: Some(ModelConfig { base: \"arn:aws:bedrock:us-east-1:156729649053:application-inference-profile/fma5bsw4oxhy\", planning: \"\", coding: \"\", cli_agent: \"arn:aws:bedrock:us-east-1:156729649053:application-inference-profile/fma5bsw4oxhy\", computer_use_agent: \"computer-use-agent-auto\", base_model_context_window_limit: 0 }), rules_enabled: true, web_context_retrieval_enabled: true, supports_parallel_tool_calls: true, use_anthropic_text_editor_tools: false, planning_enabled: true, warp_drive_context_enabled: true, supports_create_files: true, supported_tools: [Grep, FileGlob, FileGlobV2, ReadMcpResource, CallMcpTool, InitProject, OpenCodeReview, RunShellCommand, SuggestNewConversation, Subagent, WriteToLongRunningShellCommand, ReadShellCommandOutput, ReadDocuments, CreateDocuments, EditDocuments, SuggestPrompt, FetchConversation, ReadFiles, ApplyFileDiffs, SearchCodebase, UploadFileArtifact, InsertReviewComments, ReadSkill, AskUserQuestion], supports_long_running_commands: true, should_preserve_file_content_in_history: true, supports_todos_ui: true, supports_linked_code_blocks: true, supports_started_child_task_message: true, supports_suggest_prompt: true, supports_read_image_files: true, supports_reasoning_message: true, api_keys: None, autonomy_level: Supervised, isolation_level: None, web_search_enabled: true, supported_cli_agent_tools: [WriteToLongRunningShellCommand, ReadShellCommandOutput, Grep, FileGlob, FileGlobV2, TransferShellCommandControlToUser, ReadFiles, SearchCodebase], supports_v4a_file_diffs: true, supports_summarization_via_message_replacement: false, supports_bundled_skills: true, supports_research_agent: false, supports_orchestration_v2: false }), metadata: Some(Metadata { conversation_id: \"9148dd1d-da3b-4f78-8c3b-ab6513a3a87c\", logging: {}, ambient_agent_task_id: \"\", forked_from_conversation_id: \"\", parent_agent_id: \"\", agent_name: \"\" }), existing_suggestions: None, mcp_context: Some(McpContext { resources: [], tools: [], servers: [McpServer { name: \"chrome-devtools\", description: \"\", id: \"6eba1490-40f1-43c8-98a6-d3f7f8940fa2\", resources: [], tools: [McpTool { name: \"click\", description: \"Clicks on the provided element\", input_schema: Some(Struct { fields: {\"$schema\": Value { kind: Some(StringValue(\"http://json-schema.org/draft-07/schema#\")) }, \"additionalProperties\": Value { kind: Some(BoolValue(true)) }, \"properties\": Value { kind: Some(StructValue(Struct { fields: {\"dblClick\": Value { kind: Some(StructValue(Struct { fields: {\"description\": Value { kind: Some(StringValue(\"Set to true for double clicks. Default is false.\")) }, \"type\": Value { kind: Some(StringValue(\"boolean\")) }} })) }, \"includeSnapshot\": Value { kind: Some(StructValue(Struct { fields: {\"description\": Value { kind: Some(StringValue(\"Whether to include a snapshot in the response. Default is false.\")) }, \"type\": Value { kind: Some(StringValue(\"boolean\")) }} })) }, \"uid\": Value { kind: Some(StructValue(Struct { fields: {\"description\": Value { kind: Some(StringValue(\"The uid of an element on the page from the page content snapshot\")) }, \"type\": Value { kind: Some(StringValue(\"string\")) }} })) }} })) }, \"required\": Value { kind: Some(ListValue(ListValue { values: [Value { kind: Some(StringValue(\"uid\")) }] })) }, \"type\": Value { kind: Some(StringValue(\"object\")) }} }) }, McpTool { name: \"close_page\", description: \"Closes the page by its index. The last open page cannot be closed.\", input_schema: Some(Struct { fields: {\"$schema\": Value { kind: Some(StringValue(\"http://json-schema.org/draft-07/schema#\")) }, \"additionalProperties\": Value { kind: Some(BoolValue(true)) }, \"properties\": Value { kind: Some(StructValue(Struct { fields: {\"pageId\": Value { kind: Some(StructValue(Struct { fields: {\"description\": Value { kind: Some(StringValue(\"The ID of the page to close. Call list_pages to list pages.\")) }, \"type\": Value { kind: Some(StringValue(\"number\")) }} })) }} })) }, \"required\": Value { kind: Some(ListValue(ListValue { values: [Value { kind: Some(StringValue(\"pageId\")) }] })) }, \"type\": Value { kind: Some(StringValue(\"object\")) }} }) }, McpTool { name: \"drag\", description: \"Drag an element onto another element\", input_schema: Some(Struct { fields: {\"$schema\": Value { kind: Some(StringValue(\"http://json-schema.org/draft-07/schema#\")) }, \"additionalProperties\": Value { kind: Some(BoolValue(true)) }, \"properties\": Value { kind: Some(StructValue(Struct { fields: {\"from_uid\": Value { kind: Some(StructValue(Struct { fields: {\"description\": Value { kind: Some(StringValue(\"The uid of the element to drag\")) }, \"type\": Value { kind: Some(StringValue(\"string\")) }} })) }, \"includeSnapshot\": Value { kind: Some(StructValue(Struct { fields: {\"description\": Value { kind: Some(StringValue(\"Whether to include a snapshot in the response. Default is false.\")) }, \"type\": Value { kind: Some(StringValue(\"boolean\")) }} })) }, \"to_uid\": Value { kind: Some(StructValue(Struct { fields: {\"description\": Value { kind: Some(StringValue(\"The uid of the element to drop into\")) }, \"type\": Value { kind: Some(StringValue(\"string\")) }} })) }} })) }, \"required\": Value { kind: Some(ListValue(ListValue { values: [Value { kind: Some(StringValue(\"from_uid\")) }, Value { kind: Some(StringValue(\"to_uid\")) }] })) }, \"type\": Value { kind: Some(StringValue(\"object\")) }} }) }, McpTool { name: \"emulate\", description: \"Emulates various features on the selected page.\", input_schema: Some(Struct { fields: {\"$schema\": Value { kind: Some(StringValue(\"http://json-schema.org/draft-07/schema#\")) }, \"additionalProperties\": Value { kind: Some(BoolValue(true)) }, \"properties\": Value { kind: Some(StructValue(Struct { fields: {\"colorScheme\": Value { kind: Some(StructValue(Struct { fields: {\"description\": Value { kind: Some(StringValue(\"Emulate the dark or the light mode. Set to \\\"auto\\\" to reset to the default.\")) }, \"enum\": Value { kind: Some(ListValue(ListValue { values: [Value { kind: Some(StringValue(\"dark\")) }, Value { kind: Some(StringValue(\"light\")) }, Value { kind: Some(StringValue(\"auto\")) }] })) }, \"type\": Value { kind: Some(StringValue(\"string\")) }} })) }, \"cpuThrottlingRate\": Value { kind: Some(StructValue(Struct { fields: {\"description\": Value { kind: Some(StringValue(\"Represents the CPU slowdown factor. Omit or set the rate to 1 to disable throttling\")) }, \"maximum\": Value { kind: Some(NumberValue(20.0)) }, \"minimum\": Value { kind: Some(NumberValue(1.0)) }, \"type\": Value { kind: Some(StringValue(\"number\")) }} })) }, \"geolocation\": Value { kind: Some(StructValue(Struct { fields: {\"description\": Value { kind: Some(StringValue(\"Geolocation (`,`) to emulate. Latitude between -90 and 90. Longitude between -180 and 180. Omit to clear the geolocation override.\")) }, \"type\": Value { kind: Some(StringValue(\"string\")) }} })) }, \"networkConditions\": Value { kind: Some(StructValue(Struct { fields: {\"description\": Value { kind: Some(StringValue(\"Throttle network. Omit to disable throttling.\")) }, \"enum\": Value { kind: Some(ListValue(ListValue { values: [Value { kind: Some(StringValue(\"Offline\")) }, Value { kind: Some(StringValue(\"Slow 3G\")) }, Value { kind: Some(StringValue(\"Fast 3G\")) }, Value { kind: Some(StringValue(\"Slow 4G\")) }, Value { kind: Some(StringValue(\"Fast 4G\")) }] })) }, \"type\": Value { kind: Some(StringValue(\"string\")) }} })) }, \"userAgent\": Value { kind: Some(StructValue(Struct { fields: {\"description\": Value { kind: Some(StringValue(\"User agent to emulate. Set to empty string to clear the user agent override.\")) }, \"type\": Value { kind: Some(StringValue(\"string\")) }} })) }, \"viewport\": Value { kind: Some(StructValue(Struct { fields: {\"description\": Value { kind: Some(StringValue(\"Emulate device viewports 'xx[,mobile][,touch][,landscape]'. 'touch' and 'mobile' to emulate mobile devices. 'landscape' to emulate landscape mode.\")) }, \"type\": Value { kind: Some(StringValue(\"string\")) }} })) }} })) }, \"type\": Value { kind: Some(StringValue(\"object\")) }} }) }, McpTool { name: \"evaluate_script\", description: \"Evaluate a JavaScript function inside the currently selected page. Returns the response as JSON,\\nso returned values have to be JSON-serializable.\", input_schema: Some(Struct { fields: {\"$schema\": Value { kind: Some(StringValue(\"http://json-schema.org/draft-07/schema#\")) }, \"additionalProperties\": Value { kind: Some(BoolValue(true)) }, \"properties\": Value { kind: Some(StructValue(Struct { fields: {\"args\": Value { kind: Some(StructValue(Struct { fields: {\"description\": Value { kind: Some(StringValue(\"An optional list of arguments to pass to the function.\")) }, \"items\": Value { kind: Some(StructValue(Struct { fields: {\"description\": Value { kind: Some(StringValue(\"The uid of an element on the page from the page content snapshot\")) }, \"type\": Value { kind: Some(StringValue(\"string\")) }} })) }, \"type\": Value { kind: Some(StringValue(\"array\")) }} })) }, \"dialogAction\": Value { kind: Some(StructValue(Struct { fields: {\"description\": Value { kind: Some(StringValue(\"Handle dialogs while execution. \\\"accept\\\", \\\"dismiss\\\", or string for response of window.prompt. Defaults to accept.\")) }, \"type\": Value { kind: Some(StringValue(\"string\")) }} })) }, \"filePath\": Value { kind: Some(StructValue(Struct { fields: {\"description\": Value { kind: Some(StringValue(\"The absolute or relative path to a file to save the script output to. If omitted, the output is returned inline.\")) }, \"type\": Value { kind: Some(StringValue(\"string\")) }} })) }, \"function\": Value { kind: Some(StructValue(Struct { fields: {\"description\": Value { kind: Some(StringValue(\"A JavaScript function declaration to be executed by the tool in the currently selected page.\\nExample without arguments: `() => {\\n return document.title\\n}` or `async () => {\\n return await fetch(\\\"example.com\\\")\\n}`.\\nExample with arguments: `(el) => {\\n return el.innerText;\\n}`\\n\")) }, \"type\": Value { kind: Some(StringValue(\"string\")) }} })) }} })) }, \"required\": Value { kind: Some(ListValue(ListValue { values: [Value { kind: Some(StringValue(\"function\")) }] })) }, \"type\": Value { kind: Some(StringValue(\"object\")) }} }) }, McpTool { name: \"fill\", description: \"Type text into an input, text area or select an option from a element.\", input_schema: Some(Struct { fields: {\"$schema\": Value { kind: Some(StringValue(\"http://json-schema.org/draft-07/schema#\")) }, \"additionalProperties\": Value { kind: Some(BoolValue(true)) }, \"properties\": Value { kind: Some(StructValue(Struct { fields: {\"includeSnapshot\": Value { kind: Some(StructValue(Struct { fields: {\"description\": Value { kind: Some(StringValue(\"Whether to include a snapshot in the response. Default is false.\")) }, \"type\": Value { kind: Some(StringValue(\"boolean\")) }} })) }, \"uid\": Value { kind: Some(StructValue(Struct { fields: {\"description\": Value { kind: Some(StringValue(\"The uid of an element on the page from the page content snapshot\")) }, \"type\": Value { kind: Some(StringValue(\"string\")) }} })) }, \"value\": Value { kind: Some(StructValue(Struct { fields: {\"description\": Value { kind: Some(StringValue(\"The value to fill in. \\\"true\\\" or \\\"false\\\" for checkboxes and toggles, \\\"true\\\" for radio buttons.\")) }, \"type\": Value { kind: Some(StringValue(\"string\")) }} })) }} })) }, \"required\": Value { kind: Some(ListValue(ListValue { values: [Value { kind: Some(StringValue(\"uid\")) }, Value { kind: Some(StringValue(\"value\")) }] })) }, \"type\": Value { kind: Some(StringValue(\"object\")) }} }) }, McpTool { name: \"fill_form\", description: \"Fill out multiple form elements (inputs, selects, checkboxes, radios) at once. ALWAYS prefer this tool over multiple individual 'fill' or 'click' calls when interacting with forms. It is significantly faster, more reliable, and reduces turn count. Example: Fill username, password, and check \\\"Remember Me\\\" in one call.\", input_schema: Some(Struct { fields: {\"$schema\": Value { kind: Some(StringValue(\"http://json-schema.org/draft-07/schema#\")) }, \"additionalProperties\": Value { kind: Some(BoolValue(true)) }, \"properties\": Value { kind: Some(StructValue(Struct { fields: {\"elements\": Value { kind: Some(StructValue(Struct { fields: {\"description\": Value { kind: Some(StringValue(\"Elements from snapshot to fill out.\")) }, \"items\": Value { kind: Some(StructValue(Struct { fields: {\"additionalProperties\": Value { kind: Some(BoolValue(false)) }, \"properties\": Value { kind: Some(StructValue(Struct { fields: {\"uid\": Value { kind: Some(StructValue(Struct { fields: {\"description\": Value { kind: Some(StringValue(\"The uid of the element to fill out\")) }, \"type\": Value { kind: Some(StringValue(\"string\")) }} })) }, \"value\": Value { kind: Some(StructValue(Struct { fields: {\"description\": Value { kind: Some(StringValue(\"Value for the element. \\\"true\\\" or \\\"false\\\" for checkboxes and toggles, \\\"true\\\" for radio buttons.\")) }, \"type\": Value { kind: Some(StringValue(\"string\")) }} })) }} })) }, \"required\": Value { kind: Some(ListValue(ListValue { values: [Value { kind: Some(StringValue(\"uid\")) }, Value { kind: Some(StringValue(\"value\")) }] })) }, \"type\": Value { kind: Some(StringValue(\"object\")) }} })) }, \"type\": Value { kind: Some(StringValue(\"array\")) }} })) }, \"includeSnapshot\": Value { kind: Some(StructValue(Struct { fields: {\"description\": Value { kind: Some(StringValue(\"Whether to include a snapshot in the response. Default is false.\")) }, \"type\": Value { kind: Some(StringValue(\"boolean\")) }} })) }} })) }, \"required\": Value { kind: Some(ListValue(ListValue { values: [Value { kind: Some(StringValue(\"elements\")) }] })) }, \"type\": Value { kind: Some(StringValue(\"object\")) }} }) }, McpTool { name: \"get_console_message\", description: \"Gets a console message by its ID. You can get all messages by calling list_console_messages.\", input_schema: Some(Struct { fields: {\"$schema\": Value { kind: Some(StringValue(\"http://json-schema.org/draft-07/schema#\")) }, \"additionalProperties\": Value { kind: Some(BoolValue(true)) }, \"properties\": Value { kind: Some(StructValue(Struct { fields: {\"msgid\": Value { kind: Some(StructValue(Struct { fields: {\"description\": Value { kind: Some(StringValue(\"The msgid of a console message on the page from the listed console messages\")) }, \"type\": Value { kind: Some(StringValue(\"number\")) }} })) }} })) }, \"required\": Value { kind: Some(ListValue(ListValue { values: [Value { kind: Some(StringValue(\"msgid\")) }] })) }, \"type\": Value { kind: Some(StringValue(\"object\")) }} }) }, McpTool { name: \"get_network_request\", description: \"Gets a network request by an optional reqid, if omitted returns the currently selected request in the DevTools Network panel.\", input_schema: Some(Struct { fields: {\"$schema\": Value { kind: Some(StringValue(\"http://json-schema.org/draft-07/schema#\")) }, \"additionalProperties\": Value { kind: Some(BoolValue(true)) }, \"properties\": Value { kind: Some(StructValue(Struct { fields: {\"reqid\": Value { kind: Some(StructValue(Struct { fields: {\"description\": Value { kind: Some(StringValue(\"The reqid of the network request. If omitted returns the currently selected request in the DevTools Network panel.\")) }, \"type\": Value { kind: Some(StringValue(\"number\")) }} })) }, \"requestFilePath\": Value { kind: Some(StructValue(Struct { fields: {\"description\": Value { kind: Some(StringValue(\"The absolute or relative path to a .network-request file to save the request body to. If omitted, the body is returned inline.\")) }, \"type\": Value { kind: Some(StringValue(\"string\")) }} })) }, \"responseFilePath\": Value { kind: Some(StructValue(Struct { fields: {\"description\": Value { kind: Some(StringValue(\"The absolute or relative path to a .network-response file to save the response body to. If omitted, the body is returned inline.\")) }, \"type\": Value { kind: Some(StringValue(\"string\")) }} })) }} })) }, \"type\": Value { kind: Some(StringValue(\"object\")) }} }) }, McpTool { name: \"handle_dialog\", description: \"If a browser dialog was opened, use this command to handle it\", input_schema: Some(Struct { fields: {\"$schema\": Value { kind: Some(StringValue(\"http://json-schema.org/draft-07/schema#\")) }, \"additionalProperties\": Value { kind: Some(BoolValue(true)) }, \"properties\": Value { kind: Some(StructValue(Struct { fields: {\"action\": Value { kind: Some(StructValue(Struct { fields: {\"description\": Value { kind: Some(StringValue(\"Whether to dismiss or accept the dialog\")) }, \"enum\": Value { kind: Some(ListValue(ListValue { values: [Value { kind: Some(StringValue(\"accept\")) }, Value { kind: Some(StringValue(\"dismiss\")) }] })) }, \"type\": Value { kind: Some(StringValue(\"string\")) }} })) }, \"promptText\": Value { kind: Some(StructValue(Struct { fields: {\"description\": Value { kind: Some(StringValue(\"Optional prompt text to enter into the dialog.\")) }, \"type\": Value { kind: Some(StringValue(\"string\")) }} })) }} })) }, \"required\": Value { kind: Some(ListValue(ListValue { values: [Value { kind: Some(StringValue(\"action\")) }] })) }, \"type\": Value { kind: Some(StringValue(\"object\")) }} }) }, McpTool { name: \"hover\", description: \"Hover over the provided element\", input_schema: Some(Struct { fields: {\"$schema\": Value { kind: Some(StringValue(\"http://json-schema.org/draft-07/schema#\")) }, \"additionalProperties\": Value { kind: Some(BoolValue(true)) }, \"properties\": Value { kind: Some(StructValue(Struct { fields: {\"includeSnapshot\": Value { kind: Some(StructValue(Struct { fields: {\"description\": Value { kind: Some(StringValue(\"Whether to include a snapshot in the response. Default is false.\")) }, \"type\": Value { kind: Some(StringValue(\"boolean\")) }} })) }, \"uid\": Value { kind: Some(StructValue(Struct { fields: {\"description\": Value { kind: Some(StringValue(\"The uid of an element on the page from the page content snapshot\")) }, \"type\": Value { kind: Some(StringValue(\"string\")) }} })) }} })) }, \"required\": Value { kind: Some(ListValue(ListValue { values: [Value { kind: Some(StringValue(\"uid\")) }] })) }, \"type\": Value { kind: Some(StringValue(\"object\")) }} }) }, McpTool { name: \"lighthouse_audit\", description: \"Get Lighthouse score and reports for accessibility, SEO, best practices, and agentic browsing. This excludes performance. For performance audits, run performance_start_trace\", input_schema: Some(Struct { fields: {\"$schema\": Value { kind: Some(StringValue(\"http://json-schema.org/draft-07/schema#\")) }, \"additionalProperties\": Value { kind: Some(BoolValue(true)) }, \"properties\": Value { kind: Some(StructValue(Struct { fields: {\"device\": Value { kind: Some(StructValue(Struct { fields: {\"default\": Value { kind: Some(StringValue(\"desktop\")) }, \"description\": Value { kind: Some(StringValue(\"Device to emulate.\")) }, \"enum\": Value { kind: Some(ListValue(ListValue { values: [Value { kind: Some(StringValue(\"desktop\")) }, Value { kind: Some(StringValue(\"mobile\")) }] })) }, \"type\": Value { kind: Some(StringValue(\"string\")) }} })) }, \"mode\": Value { kind: Some(StructValue(Struct { fields: {\"default\": Value { kind: Some(StringValue(\"navigation\")) }, \"description\": Value { kind: Some(StringValue(\"\\\"navigation\\\" reloads & audits. \\\"snapshot\\\" analyzes current state.\")) }, \"enum\": Value { kind: Some(ListValue(ListValue { values: [Value { kind: Some(StringValue(\"navigation\")) }, Value { kind: Some(StringValue(\"snapshot\")) }] })) }, \"type\": Value { kind: Some(StringValue(\"string\")) }} })) }, \"outputDirPath\": Value { kind: Some(StructValue(Struct { fields: {\"description\": Value { kind: Some(StringValue(\"Directory for reports. If omitted, uses temporary files.\")) }, \"type\": Value { kind: Some(StringValue(\"string\")) }} })) }} })) }, \"type\": Value { kind: Some(StringValue(\"object\")) }} }) }, McpTool { name: \"list_console_messages\", description: \"List all console messages for the currently selected page since the last navigation.\", input_schema: Some(Struct { fields: {\"$schema\": Value { kind: Some(StringValue(\"http://json-schema.org/draft-07/schema#\")) }, \"additionalProperties\": Value { kind: Some(BoolValue(true)) }, \"properties\": Value { kind: Some(StructValue(Struct { fields: {\"includePreservedMessages\": Value { kind: Some(StructValue(Struct { fields: {\"default\": Value { kind: Some(BoolValue(false)) }, \"description\": Value { kind: Some(StringValue(\"Set to true to return the preserved messages over the last 3 navigations.\")) }, \"type\": Value { kind: Some(StringValue(\"boolean\")) }} })) }, \"pageIdx\": Value { kind: Some(StructValue(Struct { fields: {\"description\": Value { kind: Some(StringValue(\"Page number to return (0-based). When omitted, returns the first page.\")) }, \"minimum\": Value { kind: Some(NumberValue(0.0)) }, \"type\": Value { kind: Some(StringValue(\"integer\")) }} })) }, \"pageSize\": Value { kind: Some(StructValue(Struct { fields: {\"description\": Value { kind: Some(StringValue(\"Maximum number of messages to return. When omitted, returns all messages.\")) }, \"exclusiveMinimum\": Value { kind: Some(NumberValue(0.0)) }, \"type\": Value { kind: Some(StringValue(\"integer\")) }} })) }, \"types\": Value { kind: Some(StructValue(Struct { fields: {\"description\": Value { kind: Some(StringValue(\"Filter messages to only return messages of the specified resource types. When omitted or empty, returns all messages.\")) }, \"items\": Value { kind: Some(StructValue(Struct { fields: {\"enum\": Value { kind: Some(ListValue(ListValue { values: [Value { kind: Some(StringValue(\"log\")) }, Value { kind: Some(StringValue(\"debug\")) }, Value { kind: Some(StringValue(\"info\")) }, Value { kind: Some(StringValue(\"error\")) }, Value { kind: Some(StringValue(\"warn\")) }, Value { kind: Some(StringValue(\"dir\")) }, Value { kind: Some(StringValue(\"dirxml\")) }, Value { kind: Some(StringValue(\"table\")) }, Value { kind: Some(StringValue(\"trace\")) }, Value { kind: Some(StringValue(\"clear\")) }, Value { kind: Some(StringValue(\"startGroup\")) }, Value { kind: Some(StringValue(\"startGroupCollapsed\")) }, Value { kind: Some(StringValue(\"endGroup\")) }, Value { kind: Some(StringValue(\"assert\")) }, Value { kind: Some(StringValue(\"profile\")) }, Value { kind: Some(StringValue(\"profileEnd\")) }, Value { kind: Some(StringValue(\"count\")) }, Value { kind: Some(StringValue(\"timeEnd\")) }, Value { kind: Some(StringValue(\"verbose\")) }, Value { kind: Some(StringValue(\"issue\")) }] })) }, \"type\": Value { kind: Some(StringValue(\"string\")) }} })) }, \"type\": Value { kind: Some(StringValue(\"array\")) }} })) }} })) }, \"type\": Value { kind: Some(StringValue(\"object\")) }} }) }, McpTool { name: \"list_network_requests\", description: \"List all requests for the currently selected page since the last navigation.\", input_schema: Some(Struct { fields: {\"$schema\": Value { kind: Some(StringValue(\"http://json-schema.org/draft-07/schema#\")) }, \"additionalProperties\": Value { kind: Some(BoolValue(true)) }, \"properties\": Value { kind: Some(StructValue(Struct { fields: {\"includePreservedRequests\": Value { kind: Some(StructValue(Struct { fields: {\"default\": Value { kind: Some(BoolValue(false)) }, \"description\": Value { kind: Some(StringValue(\"Set to true to return the preserved requests over the last 3 navigations.\")) }, \"type\": Value { kind: Some(StringValue(\"boolean\")) }} })) }, \"pageIdx\": Value { kind: Some(StructValue(Struct { fields: {\"description\": Value { kind: Some(StringValue(\"Page number to return (0-based). When omitted, returns the first page.\")) }, \"minimum\": Value { kind: Some(NumberValue(0.0)) }, \"type\": Value { kind: Some(StringValue(\"integer\")) }} })) }, \"pageSize\": Value { kind: Some(StructValue(Struct { fields: {\"description\": Value { kind: Some(StringValue(\"Maximum number of requests to return. When omitted, returns all requests.\")) }, \"exclusiveMinimum\": Value { kind: Some(NumberValue(0.0)) }, \"type\": Value { kind: Some(StringValue(\"integer\")) }} })) }, \"resourceTypes\": Value { kind: Some(StructValue(Struct { fields: {\"description\": Value { kind: Some(StringValue(\"Filter requests to only return requests of the specified resource types. When omitted or empty, returns all requests.\")) }, \"items\": Value { kind: Some(StructValue(Struct { fields: {\"enum\": Value { kind: Some(ListValue(ListValue { values: [Value { kind: Some(StringValue(\"document\")) }, Value { kind: Some(StringValue(\"stylesheet\")) }, Value { kind: Some(StringValue(\"image\")) }, Value { kind: Some(StringValue(\"media\")) }, Value { kind: Some(StringValue(\"font\")) }, Value { kind: Some(StringValue(\"script\")) }, Value { kind: Some(StringValue(\"texttrack\")) }, Value { kind: Some(StringValue(\"xhr\")) }, Value { kind: Some(StringValue(\"fetch\")) }, Value { kind: Some(StringValue(\"prefetch\")) }, Value { kind: Some(StringValue(\"eventsource\")) }, Value { kind: Some(StringValue(\"websocket\")) }, Value { kind: Some(StringValue(\"manifest\")) }, Value { kind: Some(StringValue(\"signedexchange\")) }, Value { kind: Some(StringValue(\"ping\")) }, Value { kind: Some(StringValue(\"cspviolationreport\")) }, Value { kind: Some(StringValue(\"preflight\")) }, Value { kind: Some(StringValue(\"fedcm\")) }, Value { kind: Some(StringValue(\"other\")) }] })) }, \"type\": Value { kind: Some(StringValue(\"string\")) }} })) }, \"type\": Value { kind: Some(StringValue(\"array\")) }} })) }} })) }, \"type\": Value { kind: Some(StringValue(\"object\")) }} }) }, McpTool { name: \"list_pages\", description: \"Get a list of pages open in the browser.\", input_schema: Some(Struct { fields: {\"$schema\": Value { kind: Some(StringValue(\"http://json-schema.org/draft-07/schema#\")) }, \"additionalProperties\": Value { kind: Some(BoolValue(true)) }, \"properties\": Value { kind: Some(StructValue(Struct { fields: {} })) }, \"type\": Value { kind: Some(StringValue(\"object\")) }} }) }, McpTool { name: \"navigate_page\", description: \"Go to a URL, or back, forward, or reload. Use project URL if not specified otherwise.\", input_schema: Some(Struct { fields: {\"$schema\": Value { kind: Some(StringValue(\"http://json-schema.org/draft-07/schema#\")) }, \"additionalProperties\": Value { kind: Some(BoolValue(true)) }, \"properties\": Value { kind: Some(StructValue(Struct { fields: {\"handleBeforeUnload\": Value { kind: Some(StructValue(Struct { fields: {\"description\": Value { kind: Some(StringValue(\"Whether to auto accept or beforeunload dialogs triggered by this navigation. Default is accept.\")) }, \"enum\": Value { kind: Some(ListValue(ListValue { values: [Value { kind: Some(StringValue(\"accept\")) }, Value { kind: Some(StringValue(\"decline\")) }] })) }, \"type\": Value { kind: Some(StringValue(\"string\")) }} })) }, \"ignoreCache\": Value { kind: Some(StructValue(Struct { fields: {\"description\": Value { kind: Some(StringValue(\"Whether to ignore cache on reload.\")) }, \"type\": Value { kind: Some(StringValue(\"boolean\")) }} })) }, \"initScript\": Value { kind: Some(StructValue(Struct { fields: {\"description\": Value { kind: Some(StringValue(\"A JavaScript script to be executed on each new document before any other scripts for the next navigation.\")) }, \"type\": Value { kind: Some(StringValue(\"string\")) }} })) }, \"timeout\": Value { kind: Some(StructValue(Struct { fields: {\"description\": Value { kind: Some(StringValue(\"Maximum wait time in milliseconds. If set to 0, the default timeout will be used.\")) }, \"type\": Value { kind: Some(StringValue(\"integer\")) }} })) }, \"type\": Value { kind: Some(StructValue(Struct { fields: {\"description\": Value { kind: Some(StringValue(\"Navigate the page by URL, back or forward in history, or reload.\")) }, \"enum\": Value { kind: Some(ListValue(ListValue { values: [Value { kind: Some(StringValue(\"url\")) }, Value { kind: Some(StringValue(\"back\")) }, Value { kind: Some(StringValue(\"forward\")) }, Value { kind: Some(StringValue(\"reload\")) }] })) }, \"type\": Value { kind: Some(StringValue(\"string\")) }} })) }, \"url\": Value { kind: Some(StructValue(Struct { fields: {\"description\": Value { kind: Some(StringValue(\"Target URL (only type=url)\")) }, \"type\": Value { kind: Some(StringValue(\"string\")) }} })) }} })) }, \"type\": Value { kind: Some(StringValue(\"object\")) }} }) }, McpTool { name: \"new_page\", description: \"Open a new tab and load a URL. Use project URL if not specified otherwise.\", input_schema: Some(Struct { fields: {\"$schema\": Value { kind: Some(StringValue(\"http://json-schema.org/draft-07/schema#\")) }, \"additionalProperties\": Value { kind: Some(BoolValue(true)) }, \"properties\": Value { kind: Some(StructValue(Struct { fields: {\"background\": Value { kind: Some(StructValue(Struct { fields: {\"description\": Value { kind: Some(StringValue(\"Whether to open the page in the background without bringing it to the front. Default is false (foreground).\")) }, \"type\": Value { kind: Some(StringValue(\"boolean\")) }} })) }, \"isolatedContext\": Value { kind: Some(StructValue(Struct { fields: {\"description\": Value { kind: Some(StringValue(\"If specified, the page is created in an isolated browser context with the given name. Pages in the same browser context share cookies and storage. Pages in different browser contexts are fully isolated.\")) }, \"type\": Value { kind: Some(StringValue(\"string\")) }} })) }, \"timeout\": Value { kind: Some(StructValue(Struct { fields: {\"description\": Value { kind: Some(StringValue(\"Maximum wait time in milliseconds. If set to 0, the default timeout will be used.\")) }, \"type\": Value { kind: Some(StringValue(\"integer\")) }} })) }, \"url\": Value { kind: Some(StructValue(Struct { fields: {\"description\": Value { kind: Some(StringValue(\"URL to load in a new page.\")) }, \"type\": Value { kind: Some(StringValue(\"string\")) }} })) }} })) }, \"required\": Value { kind: Some(ListValue(ListValue { values: [Value { kind: Some(StringValue(\"url\")) }] })) }, \"type\": Value { kind: Some(StringValue(\"object\")) }} }) }, McpTool { name: \"performance_analyze_insight\", description: \"Provides more detailed information on a specific Performance Insight of an insight set that was highlighted in the results of a trace recording.\", input_schema: Some(Struct { fields: {\"$schema\": Value { kind: Some(StringValue(\"http://json-schema.org/draft-07/schema#\")) }, \"additionalProperties\": Value { kind: Some(BoolValue(true)) }, \"properties\": Value { kind: Some(StructValue(Struct { fields: {\"insightName\": Value { kind: Some(StructValue(Struct { fields: {\"description\": Value { kind: Some(StringValue(\"The name of the Insight you want more information on. For example: \\\"DocumentLatency\\\" or \\\"LCPBreakdown\\\"\")) }, \"type\": Value { kind: Some(StringValue(\"string\")) }} })) }, \"insightSetId\": Value { kind: Some(StructValue(Struct { fields: {\"description\": Value { kind: Some(StringValue(\"The id for the specific insight set. Only use the ids given in the \\\"Available insight sets\\\" list.\")) }, \"type\": Value { kind: Some(StringValue(\"string\")) }} })) }} })) }, \"required\": Value { kind: Some(ListValue(ListValue { values: [Value { kind: Some(StringValue(\"insightSetId\")) }, Value { kind: Some(StringValue(\"insightName\")) }] })) }, \"type\": Value { kind: Some(StringValue(\"object\")) }} }) }, McpTool { name: \"performance_start_trace\", description: \"Start a performance trace on the selected webpage. Use to find frontend performance issues, Core Web Vitals (LCP, INP, CLS), and improve page load speed.\", input_schema: Some(Struct { fields: {\"$schema\": Value { kind: Some(StringValue(\"http://json-schema.org/draft-07/schema#\")) }, \"additionalProperties\": Value { kind: Some(BoolValue(true)) }, \"properties\": Value { kind: Some(StructValue(Struct { fields: {\"autoStop\": Value { kind: Some(StructValue(Struct { fields: {\"default\": Value { kind: Some(BoolValue(true)) }, \"description\": Value { kind: Some(StringValue(\"Determines if the trace recording should be automatically stopped.\")) }, \"type\": Value { kind: Some(StringValue(\"boolean\")) }} })) }, \"filePath\": Value { kind: Some(StructValue(Struct { fields: {\"description\": Value { kind: Some(StringValue(\"The absolute file path, or a file path relative to the current working directory, to save the raw trace data. For example, trace.json.gz (compressed) or trace.json (uncompressed).\")) }, \"type\": Value { kind: Some(StringValue(\"string\")) }} })) }, \"reload\": Value { kind: Some(StructValue(Struct { fields: {\"default\": Value { kind: Some(BoolValue(true)) }, \"description\": Value { kind: Some(StringValue(\"Determines if, once tracing has started, the current selected page should be automatically reloaded. Navigate the page to the right URL using the navigate_page tool BEFORE starting the trace if reload or autoStop is set to true.\")) }, \"type\": Value { kind: Some(StringValue(\"boolean\")) }} })) }} })) }, \"type\": Value { kind: Some(StringValue(\"object\")) }} }) }, McpTool { name: \"performance_stop_trace\", description: \"Stop the active performance trace recording on the selected webpage.\", input_schema: Some(Struct { fields: {\"$schema\": Value { kind: Some(StringValue(\"http://json-schema.org/draft-07/schema#\")) }, \"additionalProperties\": Value { kind: Some(BoolValue(true)) }, \"properties\": Value { kind: Some(StructValue(Struct { fields: {\"filePath\": Value { kind: Some(StructValue(Struct { fields: {\"description\": Value { kind: Some(StringValue(\"The absolute file path, or a file path relative to the current working directory, to save the raw trace data. For example, trace.json.gz (compressed) or trace.json (uncompressed).\")) }, \"type\": Value { kind: Some(StringValue(\"string\")) }} })) }} })) }, \"type\": Value { kind: Some(StringValue(\"object\")) }} }) }, McpTool { name: \"press_key\", description: \"Press a key or key combination. Use this when other input methods like fill() cannot be used (e.g., keyboard shortcuts, navigation keys, or special key combinations).\", input_schema: Some(Struct { fields: {\"$schema\": Value { kind: Some(StringValue(\"http://json-schema.org/draft-07/schema#\")) }, \"additionalProperties\": Value { kind: Some(BoolValue(true)) }, \"properties\": Value { kind: Some(StructValue(Struct { fields: {\"includeSnapshot\": Value { kind: Some(StructValue(Struct { fields: {\"description\": Value { kind: Some(StringValue(\"Whether to include a snapshot in the response. Default is false.\")) }, \"type\": Value { kind: Some(StringValue(\"boolean\")) }} })) }, \"key\": Value { kind: Some(StructValue(Struct { fields: {\"description\": Value { kind: Some(StringValue(\"A key or a combination (e.g., \\\"Enter\\\", \\\"Control+A\\\", \\\"Control++\\\", \\\"Control+Shift+R\\\"). Modifiers: Control, Shift, Alt, Meta\")) }, \"type\": Value { kind: Some(StringValue(\"string\")) }} })) }} })) }, \"required\": Value { kind: Some(ListValue(ListValue { values: [Value { kind: Some(StringValue(\"key\")) }] })) }, \"type\": Value { kind: Some(StringValue(\"object\")) }} }) }, McpTool { name: \"resize_page\", description: \"Resizes the selected page's window so that the page has specified dimension\", input_schema: Some(Struct { fields: {\"$schema\": Value { kind: Some(StringValue(\"http://json-schema.org/draft-07/schema#\")) }, \"additionalProperties\": Value { kind: Some(BoolValue(true)) }, \"properties\": Value { kind: Some(StructValue(Struct { fields: {\"height\": Value { kind: Some(StructValue(Struct { fields: {\"description\": Value { kind: Some(StringValue(\"Page height\")) }, \"type\": Value { kind: Some(StringValue(\"number\")) }} })) }, \"width\": Value { kind: Some(StructValue(Struct { fields: {\"description\": Value { kind: Some(StringValue(\"Page width\")) }, \"type\": Value { kind: Some(StringValue(\"number\")) }} })) }} })) }, \"required\": Value { kind: Some(ListValue(ListValue { values: [Value { kind: Some(StringValue(\"width\")) }, Value { kind: Some(StringValue(\"height\")) }] })) }, \"type\": Value { kind: Some(StringValue(\"object\")) }} }) }, McpTool { name: \"select_page\", description: \"Select a page as a context for future tool calls.\", input_schema: Some(Struct { fields: {\"$schema\": Value { kind: Some(StringValue(\"http://json-schema.org/draft-07/schema#\")) }, \"additionalProperties\": Value { kind: Some(BoolValue(true)) }, \"properties\": Value { kind: Some(StructValue(Struct { fields: {\"bringToFront\": Value { kind: Some(StructValue(Struct { fields: {\"description\": Value { kind: Some(StringValue(\"Whether to focus the page and bring it to the top.\")) }, \"type\": Value { kind: Some(StringValue(\"boolean\")) }} })) }, \"pageId\": Value { kind: Some(StructValue(Struct { fields: {\"description\": Value { kind: Some(StringValue(\"The ID of the page to select. Call list_pages to get available pages.\")) }, \"type\": Value { kind: Some(StringValue(\"number\")) }} })) }} })) }, \"required\": Value { kind: Some(ListValue(ListValue { values: [Value { kind: Some(StringValue(\"pageId\")) }] })) }, \"type\": Value { kind: Some(StringValue(\"object\")) }} }) }, McpTool { name: \"take_memory_snapshot\", description: \"Capture a heap snapshot of the currently selected page. Use to analyze the memory distribution of JavaScript objects and debug memory leaks.\", input_schema: Some(Struct { fields: {\"$schema\": Value { kind: Some(StringValue(\"http://json-schema.org/draft-07/schema#\")) }, \"additionalProperties\": Value { kind: Some(BoolValue(true)) }, \"properties\": Value { kind: Some(StructValue(Struct { fields: {\"filePath\": Value { kind: Some(StructValue(Struct { fields: {\"description\": Value { kind: Some(StringValue(\"A path to a .heapsnapshot file to save the heapsnapshot to.\")) }, \"type\": Value { kind: Some(StringValue(\"string\")) }} })) }} })) }, \"required\": Value { kind: Some(ListValue(ListValue { values: [Value { kind: Some(StringValue(\"filePath\")) }] })) }, \"type\": Value { kind: Some(StringValue(\"object\")) }} }) }, McpTool { name: \"take_screenshot\", description: \"Take a screenshot of the page or element.\", input_schema: Some(Struct { fields: {\"$schema\": Value { kind: Some(StringValue(\"http://json-schema.org/draft-07/schema#\")) }, \"additionalProperties\": Value { kind: Some(BoolValue(true)) }, \"properties\": Value { kind: Some(StructValue(Struct { fields: {\"filePath\": Value { kind: Some(StructValue(Struct { fields: {\"description\": Value { kind: Some(StringValue(\"The absolute path, or a path relative to the current working directory, to save the screenshot to instead of attaching it to the response.\")) }, \"type\": Value { kind: Some(StringValue(\"string\")) }} })) }, \"format\": Value { kind: Some(StructValue(Struct { fields: {\"default\": Value { kind: Some(StringValue(\"png\")) }, \"description\": Value { kind: Some(StringValue(\"Type of format to save the screenshot as. Default is \\\"png\\\"\")) }, \"enum\": Value { kind: Some(ListValue(ListValue { values: [Value { kind: Some(StringValue(\"png\")) }, Value { kind: Some(StringValue(\"jpeg\")) }, Value { kind: Some(StringValue(\"webp\")) }] })) }, \"type\": Value { kind: Some(StringValue(\"string\")) }} })) }, \"fullPage\": Value { kind: Some(StructValue(Struct { fields: {\"description\": Value { kind: Some(StringValue(\"If set to true takes a screenshot of the full page instead of the currently visible viewport. Incompatible with uid.\")) }, \"type\": Value { kind: Some(StringValue(\"boolean\")) }} })) }, \"quality\": Value { kind: Some(StructValue(Struct { fields: {\"description\": Value { kind: Some(StringValue(\"Compression quality for JPEG and WebP formats (0-100). Higher values mean better quality but larger file sizes. Ignored for PNG format.\")) }, \"maximum\": Value { kind: Some(NumberValue(100.0)) }, \"minimum\": Value { kind: Some(NumberValue(0.0)) }, \"type\": Value { kind: Some(StringValue(\"number\")) }} })) }, \"uid\": Value { kind: Some(StructValue(Struct { fields: {\"description\": Value { kind: Some(StringValue(\"The uid of an element on the page from the page content snapshot. If omitted, takes a page screenshot.\")) }, \"type\": Value { kind: Some(StringValue(\"string\")) }} })) }} })) }, \"type\": Value { kind: Some(StringValue(\"object\")) }} }) }, McpTool { name: \"take_snapshot\", description: \"Take a text snapshot of the currently selected page based on the a11y tree. The snapshot lists page elements along with a unique\\nidentifier (uid). Always use the latest snapshot. Prefer taking a snapshot over taking a screenshot. The snapshot indicates the element selected\\nin the DevTools Elements panel (if any).\", input_schema: Some(Struct { fields: {\"$schema\": Value { kind: Some(StringValue(\"http://json-schema.org/draft-07/schema#\")) }, \"additionalProperties\": Value { kind: Some(BoolValue(true)) }, \"properties\": Value { kind: Some(StructValue(Struct { fields: {\"filePath\": Value { kind: Some(StructValue(Struct { fields: {\"description\": Value { kind: Some(StringValue(\"The absolute path, or a path relative to the current working directory, to save the snapshot to instead of attaching it to the response.\")) }, \"type\": Value { kind: Some(StringValue(\"string\")) }} })) }, \"verbose\": Value { kind: Some(StructValue(Struct { fields: {\"description\": Value { kind: Some(StringValue(\"Whether to include all possible information available in the full a11y tree. Default is false.\")) }, \"type\": Value { kind: Some(StringValue(\"boolean\")) }} })) }} })) }, \"type\": Value { kind: Some(StringValue(\"object\")) }} }) }, McpTool { name: \"type_text\", description: \"Type text using keyboard into a previously focused input\", input_schema: Some(Struct { fields: {\"$schema\": Value { kind: Some(StringValue(\"http://json-schema.org/draft-07/schema#\")) }, \"additionalProperties\": Value { kind: Some(BoolValue(true)) }, \"properties\": Value { kind: Some(StructValue(Struct { fields: {\"submitKey\": Value { kind: Some(StructValue(Struct { fields: {\"description\": Value { kind: Some(StringValue(\"Optional key to press after typing. E.g., \\\"Enter\\\", \\\"Tab\\\", \\\"Escape\\\"\")) }, \"type\": Value { kind: Some(StringValue(\"string\")) }} })) }, \"text\": Value { kind: Some(StructValue(Struct { fields: {\"description\": Value { kind: Some(StringValue(\"The text to type\")) }, \"type\": Value { kind: Some(StringValue(\"string\")) }} })) }} })) }, \"required\": Value { kind: Some(ListValue(ListValue { values: [Value { kind: Some(StringValue(\"text\")) }] })) }, \"type\": Value { kind: Some(StringValue(\"object\")) }} }) }, McpTool { name: \"upload_file\", description: \"Upload a file through a provided element.\", input_schema: Some(Struct { fields: {\"$schema\": Value { kind: Some(StringValue(\"http://json-schema.org/draft-07/schema#\")) }, \"additionalProperties\": Value { kind: Some(BoolValue(true)) }, \"properties\": Value { kind: Some(StructValue(Struct { fields: {\"filePath\": Value { kind: Some(StructValue(Struct { fields: {\"description\": Value { kind: Some(StringValue(\"The local path of the file to upload\")) }, \"type\": Value { kind: Some(StringValue(\"string\")) }} })) }, \"includeSnapshot\": Value { kind: Some(StructValue(Struct { fields: {\"description\": Value { kind: Some(StringValue(\"Whether to include a snapshot in the response. Default is false.\")) }, \"type\": Value { kind: Some(StringValue(\"boolean\")) }} })) }, \"uid\": Value { kind: Some(StructValue(Struct { fields: {\"description\": Value { kind: Some(StringValue(\"The uid of the file input element or an element that will open file chooser on the page from the page content snapshot\")) }, \"type\": Value { kind: Some(StringValue(\"string\")) }} })) }} })) }, \"required\": Value { kind: Some(ListValue(ListValue { values: [Value { kind: Some(StringValue(\"uid\")) }, Value { kind: Some(StringValue(\"filePath\")) }] })) }, \"type\": Value { kind: Some(StringValue(\"object\")) }} }) }, McpTool { name: \"wait_for\", description: \"Wait for the specified text to appear on the selected page.\", input_schema: Some(Struct { fields: {\"$schema\": Value { kind: Some(StringValue(\"http://json-schema.org/draft-07/schema#\")) }, \"additionalProperties\": Value { kind: Some(BoolValue(true)) }, \"properties\": Value { kind: Some(StructValue(Struct { fields: {\"text\": Value { kind: Some(StructValue(Struct { fields: {\"description\": Value { kind: Some(StringValue(\"Non-empty list of texts. Resolves when any value appears on the page.\")) }, \"items\": Value { kind: Some(StructValue(Struct { fields: {\"type\": Value { kind: Some(StringValue(\"string\")) }} })) }, \"minItems\": Value { kind: Some(NumberValue(1.0)) }, \"type\": Value { kind: Some(StringValue(\"array\")) }} })) }, \"timeout\": Value { kind: Some(StructValue(Struct { fields: {\"description\": Value { kind: Some(StringValue(\"Maximum wait time in milliseconds. If set to 0, the default timeout will be used.\")) }, \"type\": Value { kind: Some(StringValue(\"integer\")) }} })) }} })) }, \"required\": Value { kind: Some(ListValue(ListValue { values: [Value { kind: Some(StringValue(\"text\")) }] })) }, \"type\": Value { kind: Some(StringValue(\"object\")) }} }) }] }, McpServer { name: \"macos-control\", description: \"\", id: \"db88525f-4596-4db5-a345-59fd15792c1f\", resources: [], tools: [McpTool { name: \"screenshot\", description: \"Capture a screenshot of the entire screen or a specific app window. Returns a PNG image.\", input_schema: Some(Struct { fields: {\"$schema\": Value { kind: Some(StringValue(\"http://json-schema.org/draft-07/schema#\")) }, \"additionalProperties\": Value { kind: Some(BoolValue(false)) }, \"properties\": Value { kind: Some(StructValue(Struct { fields: {\"app\": Value { kind: Some(StructValue(Struct { fields: {\"description\": Value { kind: Some(StringValue(\"App name to capture. Omit for full screen.\")) }, \"type\": Value { kind: Some(StringValue(\"string\")) }} })) }} })) }, \"type\": Value { kind: Some(StringValue(\"object\")) }} }) }, McpTool { name: \"screen_ocr\", description: \"OCR the screen using Apple Vision. Returns every text element with pixel coordinates (x, y, centerX, centerY). Use centerX/centerY with click_at to click on any text.\", input_schema: Some(Struct { fields: {\"$schema\": Value { kind: Some(StringValue(\"http://json-schema.org/draft-07/schema#\")) }, \"additionalProperties\": Value { kind: Some(BoolValue(false)) }, \"properties\": Value { kind: Some(StructValue(Struct { fields: {\"app\": Value { kind: Some(StructValue(Struct { fields: {\"description\": Value { kind: Some(StringValue(\"App to OCR. Omit for full screen.\")) }, \"type\": Value { kind: Some(StringValue(\"string\")) }} })) }, \"compact\": Value { kind: Some(StructValue(Struct { fields: {\"description\": Value { kind: Some(StringValue(\"Return compact OCR objects (text + click coordinates + confidence).\")) }, \"type\": Value { kind: Some(StringValue(\"boolean\")) }} })) }, \"include_bounds\": Value { kind: Some(StructValue(Struct { fields: {\"description\": Value { kind: Some(StringValue(\"Include bounding boxes in results (default true).\")) }, \"type\": Value { kind: Some(StringValue(\"boolean\")) }} })) }, \"max_elements\": Value { kind: Some(StructValue(Struct { fields: {\"description\": Value { kind: Some(StringValue(\"Max OCR elements to return (for smaller/faster responses).\")) }, \"minimum\": Value { kind: Some(NumberValue(1.0)) }, \"type\": Value { kind: Some(StringValue(\"integer\")) }} })) }, \"min_confidence\": Value { kind: Some(StructValue(Struct { fields: {\"description\": Value { kind: Some(StringValue(\"Filter out OCR matches below this confidence (0.0-1.0).\")) }, \"maximum\": Value { kind: Some(NumberValue(1.0)) }, \"minimum\": Value { kind: Some(NumberValue(0.0)) }, \"type\": Value { kind: Some(StringValue(\"number\")) }} })) }} })) }, \"type\": Value { kind: Some(StringValue(\"object\")) }} }) }, McpTool { name: \"find_text_on_screen\", description: \"Find specific text on screen and get its clickable coordinates. Like Ctrl+F for the entire screen. Returns matches with centerX/centerY for use with click_at.\", input_schema: Some(Struct { fields: {\"$schema\": Value { kind: Some(StringValue(\"http://json-schema.org/draft-07/schema#\")) }, \"additionalProperties\": Value { kind: Some(BoolValue(false)) }, \"properties\": Value { kind: Some(StructValue(Struct { fields: {\"app\": Value { kind: Some(StructValue(Struct { fields: {\"description\": Value { kind: Some(StringValue(\"App to search in. Omit for full screen.\")) }, \"type\": Value { kind: Some(StringValue(\"string\")) }} })) }, \"text\": Value { kind: Some(StructValue(Struct { fields: {\"description\": Value { kind: Some(StringValue(\"Text to find (case-insensitive)\")) }, \"type\": Value { kind: Some(StringValue(\"string\")) }} })) }} })) }, \"required\": Value { kind: Some(ListValue(ListValue { values: [Value { kind: Some(StringValue(\"text\")) }] })) }, \"type\": Value { kind: Some(StringValue(\"object\")) }} }) }, McpTool { name: \"click_at\", description: \"Click at x,y screen coordinates. Returns a screenshot by default (disable with return_screenshot=false). Use screenshot + screen_ocr to find coordinates first. Prefer batch_actions when combining with other actions.\", input_schema: Some(Struct { fields: {\"$schema\": Value { kind: Some(StringValue(\"http://json-schema.org/draft-07/schema#\")) }, \"additionalProperties\": Value { kind: Some(BoolValue(false)) }, \"properties\": Value { kind: Some(StructValue(Struct { fields: {\"return_screenshot\": Value { kind: Some(StructValue(Struct { fields: {\"description\": Value { kind: Some(StringValue(\"Return a screenshot after clicking (default true). Set false for faster/smaller responses.\")) }, \"type\": Value { kind: Some(StringValue(\"boolean\")) }} })) }, \"x\": Value { kind: Some(StructValue(Struct { fields: {\"description\": Value { kind: Some(StringValue(\"X coordinate\")) }, \"type\": Value { kind: Some(StringValue(\"number\")) }} })) }, \"y\": Value { kind: Some(StructValue(Struct { fields: {\"description\": Value { kind: Some(StringValue(\"Y coordinate\")) }, \"type\": Value { kind: Some(StringValue(\"number\")) }} })) }} })) }, \"required\": Value { kind: Some(ListValue(ListValue { values: [Value { kind: Some(StringValue(\"x\")) }, Value { kind: Some(StringValue(\"y\")) }] })) }, \"type\": Value { kind: Some(StringValue(\"object\")) }} }) }, McpTool { name: \"double_click_at\", description: \"Double-click at x,y screen coordinates. Returns a screenshot by default (disable with return_screenshot=false). Prefer batch_actions when combining with other actions.\", input_schema: Some(Struct { fields: {\"$schema\": Value { kind: Some(StringValue(\"http://json-schema.org/draft-07/schema#\")) }, \"additionalProperties\": Value { kind: Some(BoolValue(false)) }, \"properties\": Value { kind: Some(StructValue(Struct { fields: {\"return_screenshot\": Value { kind: Some(StructValue(Struct { fields: {\"description\": Value { kind: Some(StringValue(\"Return a screenshot after double-clicking (default true). Set false for faster/smaller responses.\")) }, \"type\": Value { kind: Some(StringValue(\"boolean\")) }} })) }, \"x\": Value { kind: Some(StructValue(Struct { fields: {\"description\": Value { kind: Some(StringValue(\"X coordinate\")) }, \"type\": Value { kind: Some(StringValue(\"number\")) }} })) }, \"y\": Value { kind: Some(StructValue(Struct { fields: {\"description\": Value { kind: Some(StringValue(\"Y coordinate\")) }, \"type\": Value { kind: Some(StringValue(\"number\")) }} })) }} })) }, \"required\": Value { kind: Some(ListValue(ListValue { values: [Value { kind: Some(StringValue(\"x\")) }, Value { kind: Some(StringValue(\"y\")) }] })) }, \"type\": Value { kind: Some(StringValue(\"object\")) }} }) }, McpTool { name: \"type_text\", description: \"Type text using keyboard input. If app is specified, focuses that app first to ensure keystrokes go to the right place. Without app, types into the frontmost app. Prefer batch_actions when combining with other actions.\", input_schema: Some(Struct { fields: {\"$schema\": Value { kind: Some(StringValue(\"http://json-schema.org/draft-07/schema#\")) }, \"additionalProperties\": Value { kind: Some(BoolValue(false)) }, \"properties\": Value { kind: Some(StructValue(Struct { fields: {\"app\": Value { kind: Some(StructValue(Struct { fields: {\"description\": Value { kind: Some(StringValue(\"App to focus before typing (e.g. 'Google Chrome', 'Notes'). Recommended to avoid keystrokes going to the wrong app.\")) }, \"type\": Value { kind: Some(StringValue(\"string\")) }} })) }, \"text\": Value { kind: Some(StructValue(Struct { fields: {\"description\": Value { kind: Some(StringValue(\"Text to type\")) }, \"type\": Value { kind: Some(StringValue(\"string\")) }} })) }} })) }, \"required\": Value { kind: Some(ListValue(ListValue { values: [Value { kind: Some(StringValue(\"text\")) }] })) }, \"type\": Value { kind: Some(StringValue(\"object\")) }} }) }, McpTool { name: \"press_key\", description: \"Press a key combo (e.g. press 's' with ['command'] for Cmd+S). Supports a-z, 0-9, return, tab, space, delete, escape, arrows, f1-f12. If app is specified, focuses that app first. Prefer batch_actions when combining with other actions.\", input_schema: Some(Struct { fields: {\"$schema\": Value { kind: Some(StringValue(\"http://json-schema.org/draft-07/schema#\")) }, \"additionalProperties\": Value { kind: Some(BoolValue(false)) }, \"properties\": Value { kind: Some(StructValue(Struct { fields: {\"app\": Value { kind: Some(StructValue(Struct { fields: {\"description\": Value { kind: Some(StringValue(\"App to focus before pressing key (e.g. 'Google Chrome', 'Notes').\")) }, \"type\": Value { kind: Some(StringValue(\"string\")) }} })) }, \"key\": Value { kind: Some(StructValue(Struct { fields: {\"description\": Value { kind: Some(StringValue(\"Key name: a-z, 0-9, return, tab, space, delete, escape, up/down/left/right, f1-f12\")) }, \"type\": Value { kind: Some(StringValue(\"string\")) }} })) }, \"modifiers\": Value { kind: Some(StructValue(Struct { fields: {\"description\": Value { kind: Some(StringValue(\"Modifier keys: 'command', 'shift', 'option', 'control'\")) }, \"items\": Value { kind: Some(StructValue(Struct { fields: {\"type\": Value { kind: Some(StringValue(\"string\")) }} })) }, \"type\": Value { kind: Some(StringValue(\"array\")) }} })) }} })) }, \"required\": Value { kind: Some(ListValue(ListValue { values: [Value { kind: Some(StringValue(\"key\")) }] })) }, \"type\": Value { kind: Some(StringValue(\"object\")) }} }) }, McpTool { name: \"scroll\", description: \"Scroll in the frontmost application. Prefer batch_actions when combining with other actions.\", input_schema: Some(Struct { fields: {\"$schema\": Value { kind: Some(StringValue(\"http://json-schema.org/draft-07/schema#\")) }, \"additionalProperties\": Value { kind: Some(BoolValue(false)) }, \"properties\": Value { kind: Some(StructValue(Struct { fields: {\"amount\": Value { kind: Some(StructValue(Struct { fields: {\"default\": Value { kind: Some(NumberValue(3.0)) }, \"description\": Value { kind: Some(StringValue(\"Number of lines to scroll (default 3)\")) }, \"type\": Value { kind: Some(StringValue(\"number\")) }} })) }, \"direction\": Value { kind: Some(StructValue(Struct { fields: {\"description\": Value { kind: Some(StringValue(\"Scroll direction\")) }, \"enum\": Value { kind: Some(ListValue(ListValue { values: [Value { kind: Some(StringValue(\"up\")) }, Value { kind: Some(StringValue(\"down\")) }, Value { kind: Some(StringValue(\"left\")) }, Value { kind: Some(StringValue(\"right\")) }] })) }, \"type\": Value { kind: Some(StringValue(\"string\")) }} })) }} })) }, \"required\": Value { kind: Some(ListValue(ListValue { values: [Value { kind: Some(StringValue(\"direction\")) }] })) }, \"type\": Value { kind: Some(StringValue(\"object\")) }} }) }, McpTool { name: \"launch_app\", description: \"Open or focus a macOS application by name. Prefer batch_actions when combining with other actions.\", input_schema: Some(Struct { fields: {\"$schema\": Value { kind: Some(StringValue(\"http://json-schema.org/draft-07/schema#\")) }, \"additionalProperties\": Value { kind: Some(BoolValue(false)) }, \"properties\": Value { kind: Some(StructValue(Struct { fields: {\"name\": Value { kind: Some(StructValue(Struct { fields: {\"description\": Value { kind: Some(StringValue(\"Application name, e.g. 'Safari', 'Notes'\")) }, \"type\": Value { kind: Some(StringValue(\"string\")) }} })) }} })) }, \"required\": Value { kind: Some(ListValue(ListValue { values: [Value { kind: Some(StringValue(\"name\")) }] })) }, \"type\": Value { kind: Some(StringValue(\"object\")) }} }) }, McpTool { name: \"list_running_apps\", description: \"List all visible running macOS applications.\", input_schema: Some(Struct { fields: {\"properties\": Value { kind: Some(StructValue(Struct { fields: {} })) }, \"type\": Value { kind: Some(StringValue(\"object\")) }} }) }, McpTool { name: \"get_ui_elements\", description: \"Get the accessibility tree of an app window. Returns UI element roles, names, and positions.\", input_schema: Some(Struct { fields: {\"$schema\": Value { kind: Some(StringValue(\"http://json-schema.org/draft-07/schema#\")) }, \"additionalProperties\": Value { kind: Some(BoolValue(false)) }, \"properties\": Value { kind: Some(StructValue(Struct { fields: {\"app\": Value { kind: Some(StructValue(Struct { fields: {\"description\": Value { kind: Some(StringValue(\"Application process name\")) }, \"type\": Value { kind: Some(StringValue(\"string\")) }} })) }} })) }, \"required\": Value { kind: Some(ListValue(ListValue { values: [Value { kind: Some(StringValue(\"app\")) }] })) }, \"type\": Value { kind: Some(StringValue(\"object\")) }} }) }, McpTool { name: \"click_element\", description: \"Click a named UI element in an app window. Returns a screenshot by default (disable with return_screenshot=false). Use get_ui_elements to discover element names. Prefer batch_actions when combining with other actions.\", input_schema: Some(Struct { fields: {\"$schema\": Value { kind: Some(StringValue(\"http://json-schema.org/draft-07/schema#\")) }, \"additionalProperties\": Value { kind: Some(BoolValue(false)) }, \"properties\": Value { kind: Some(StructValue(Struct { fields: {\"app\": Value { kind: Some(StructValue(Struct { fields: {\"description\": Value { kind: Some(StringValue(\"Application process name\")) }, \"type\": Value { kind: Some(StringValue(\"string\")) }} })) }, \"name\": Value { kind: Some(StructValue(Struct { fields: {\"description\": Value { kind: Some(StringValue(\"Name of the UI element to click\")) }, \"type\": Value { kind: Some(StringValue(\"string\")) }} })) }, \"return_screenshot\": Value { kind: Some(StructValue(Struct { fields: {\"description\": Value { kind: Some(StringValue(\"Return a screenshot after clicking (default true). Set false for faster/smaller responses.\")) }, \"type\": Value { kind: Some(StringValue(\"boolean\")) }} })) }} })) }, \"required\": Value { kind: Some(ListValue(ListValue { values: [Value { kind: Some(StringValue(\"app\")) }, Value { kind: Some(StringValue(\"name\")) }] })) }, \"type\": Value { kind: Some(StringValue(\"object\")) }} }) }, McpTool { name: \"open_url\", description: \"Open a URL in Safari or Chrome.\", input_schema: Some(Struct { fields: {\"$schema\": Value { kind: Some(StringValue(\"http://json-schema.org/draft-07/schema#\")) }, \"additionalProperties\": Value { kind: Some(BoolValue(false)) }, \"properties\": Value { kind: Some(StructValue(Struct { fields: {\"browser\": Value { kind: Some(StructValue(Struct { fields: {\"description\": Value { kind: Some(StringValue(\"'safari' or 'chrome' (defaults to Safari)\")) }, \"type\": Value { kind: Some(StringValue(\"string\")) }} })) }, \"url\": Value { kind: Some(StructValue(Struct { fields: {\"description\": Value { kind: Some(StringValue(\"URL to open\")) }, \"type\": Value { kind: Some(StringValue(\"string\")) }} })) }} })) }, \"required\": Value { kind: Some(ListValue(ListValue { values: [Value { kind: Some(StringValue(\"url\")) }] })) }, \"type\": Value { kind: Some(StringValue(\"object\")) }} }) }, McpTool { name: \"get_clipboard\", description: \"Read the current text contents of the macOS clipboard.\", input_schema: Some(Struct { fields: {\"properties\": Value { kind: Some(StructValue(Struct { fields: {} })) }, \"type\": Value { kind: Some(StringValue(\"object\")) }} }) }, McpTool { name: \"set_clipboard\", description: \"Write text to the macOS clipboard.\", input_schema: Some(Struct { fields: {\"$schema\": Value { kind: Some(StringValue(\"http://json-schema.org/draft-07/schema#\")) }, \"additionalProperties\": Value { kind: Some(BoolValue(false)) }, \"properties\": Value { kind: Some(StructValue(Struct { fields: {\"text\": Value { kind: Some(StructValue(Struct { fields: {\"description\": Value { kind: Some(StringValue(\"Text to copy to clipboard\")) }, \"type\": Value { kind: Some(StringValue(\"string\")) }} })) }} })) }, \"required\": Value { kind: Some(ListValue(ListValue { values: [Value { kind: Some(StringValue(\"text\")) }] })) }, \"type\": Value { kind: Some(StringValue(\"object\")) }} }) }, McpTool { name: \"execute_javascript\", description: \"Run JavaScript in the active browser tab. Much faster than screenshot+OCR for web pages. Returns the result.\", input_schema: Some(Struct { fields: {\"$schema\": Value { kind: Some(StringValue(\"http://json-schema.org/draft-07/schema#\")) }, \"additionalProperties\": Value { kind: Some(BoolValue(false)) }, \"properties\": Value { kind: Some(StructValue(Struct { fields: {\"browser\": Value { kind: Some(StructValue(Struct { fields: {\"description\": Value { kind: Some(StringValue(\"'safari' or 'chrome' (defaults to Safari)\")) }, \"type\": Value { kind: Some(StringValue(\"string\")) }} })) }, \"code\": Value { kind: Some(StructValue(Struct { fields: {\"description\": Value { kind: Some(StringValue(\"JavaScript code to execute\")) }, \"type\": Value { kind: Some(StringValue(\"string\")) }} })) }} })) }, \"required\": Value { kind: Some(ListValue(ListValue { values: [Value { kind: Some(StringValue(\"code\")) }] })) }, \"type\": Value { kind: Some(StringValue(\"object\")) }} }) }, McpTool { name: \"get_page_text\", description: \"Get all visible text from the current browser page. Faster than OCR for web content.\", input_schema: Some(Struct { fields: {\"$schema\": Value { kind: Some(StringValue(\"http://json-schema.org/draft-07/schema#\")) }, \"additionalProperties\": Value { kind: Some(BoolValue(false)) }, \"properties\": Value { kind: Some(StructValue(Struct { fields: {\"browser\": Value { kind: Some(StructValue(Struct { fields: {\"description\": Value { kind: Some(StringValue(\"'safari' or 'chrome' (defaults to Safari)\")) }, \"type\": Value { kind: Some(StringValue(\"string\")) }} })) }} })) }, \"type\": Value { kind: Some(StringValue(\"object\")) }} }) }, McpTool { name: \"get_page_elements\", description: \"Get all interactive elements (buttons, inputs, selects, radios, checkboxes, links) from the current browser page. Returns structured text showing each element's type, label, value, and state. Use this instead of screenshot+OCR to understand web page content. Then use click_by_text or fill_by_label to interact.\", input_schema: Some(Struct { fields: {\"$schema\": Value { kind: Some(StringValue(\"http://json-schema.org/draft-07/schema#\")) }, \"additionalProperties\": Value { kind: Some(BoolValue(false)) }, \"properties\": Value { kind: Some(StructValue(Struct { fields: {\"browser\": Value { kind: Some(StructValue(Struct { fields: {\"description\": Value { kind: Some(StringValue(\"'safari' or 'chrome' (defaults to Safari)\")) }, \"type\": Value { kind: Some(StringValue(\"string\")) }} })) }} })) }, \"type\": Value { kind: Some(StringValue(\"object\")) }} }) }, McpTool { name: \"click_by_text\", description: \"Click a button, link, tab, radio, or checkbox by its visible text. Much more reliable than coordinate clicking. Scrolls element into view before clicking.\", input_schema: Some(Struct { fields: {\"$schema\": Value { kind: Some(StringValue(\"http://json-schema.org/draft-07/schema#\")) }, \"additionalProperties\": Value { kind: Some(BoolValue(false)) }, \"properties\": Value { kind: Some(StructValue(Struct { fields: {\"browser\": Value { kind: Some(StructValue(Struct { fields: {\"description\": Value { kind: Some(StringValue(\"'safari' or 'chrome' (defaults to Safari)\")) }, \"type\": Value { kind: Some(StringValue(\"string\")) }} })) }, \"element_type\": Value { kind: Some(StructValue(Struct { fields: {\"description\": Value { kind: Some(StringValue(\"Type of element to click (default: 'any')\")) }, \"enum\": Value { kind: Some(ListValue(ListValue { values: [Value { kind: Some(StringValue(\"button\")) }, Value { kind: Some(StringValue(\"link\")) }, Value { kind: Some(StringValue(\"tab\")) }, Value { kind: Some(StringValue(\"radio\")) }, Value { kind: Some(StringValue(\"checkbox\")) }, Value { kind: Some(StringValue(\"any\")) }] })) }, \"type\": Value { kind: Some(StringValue(\"string\")) }} })) }, \"index\": Value { kind: Some(StructValue(Struct { fields: {\"description\": Value { kind: Some(StringValue(\"Which match to click if multiple found (0-based, default: 0)\")) }, \"type\": Value { kind: Some(StringValue(\"number\")) }} })) }, \"text\": Value { kind: Some(StructValue(Struct { fields: {\"description\": Value { kind: Some(StringValue(\"Visible text to search for (case-insensitive partial match)\")) }, \"type\": Value { kind: Some(StringValue(\"string\")) }} })) }} })) }, \"required\": Value { kind: Some(ListValue(ListValue { values: [Value { kind: Some(StringValue(\"text\")) }] })) }, \"type\": Value { kind: Some(StringValue(\"object\")) }} }) }, McpTool { name: \"fill_by_label\", description: \"Fill a form field by its label text. Finds the input/textarea associated with the label and fills it. Works with React/Vue/Angular apps. On failure, lists available field labels for debugging.\", input_schema: Some(Struct { fields: {\"$schema\": Value { kind: Some(StringValue(\"http://json-schema.org/draft-07/schema#\")) }, \"additionalProperties\": Value { kind: Some(BoolValue(false)) }, \"properties\": Value { kind: Some(StructValue(Struct { fields: {\"browser\": Value { kind: Some(StructValue(Struct { fields: {\"description\": Value { kind: Some(StringValue(\"'safari' or 'chrome' (defaults to Safari)\")) }, \"type\": Value { kind: Some(StringValue(\"string\")) }} })) }, \"label\": Value { kind: Some(StructValue(Struct { fields: {\"description\": Value { kind: Some(StringValue(\"Label text of the field (case-insensitive partial match)\")) }, \"type\": Value { kind: Some(StringValue(\"string\")) }} })) }, \"value\": Value { kind: Some(StructValue(Struct { fields: {\"description\": Value { kind: Some(StringValue(\"Value to fill in\")) }, \"type\": Value { kind: Some(StringValue(\"string\")) }} })) }} })) }, \"required\": Value { kind: Some(ListValue(ListValue { values: [Value { kind: Some(StringValue(\"label\")) }, Value { kind: Some(StringValue(\"value\")) }] })) }, \"type\": Value { kind: Some(StringValue(\"object\")) }} }) }, McpTool { name: \"select_option\", description: \"Select a dropdown option by the dropdown's label and the option text. On failure, lists available options for debugging.\", input_schema: Some(Struct { fields: {\"$schema\": Value { kind: Some(StringValue(\"http://json-schema.org/draft-07/schema#\")) }, \"additionalProperties\": Value { kind: Some(BoolValue(false)) }, \"properties\": Value { kind: Some(StructValue(Struct { fields: {\"browser\": Value { kind: Some(StructValue(Struct { fields: {\"description\": Value { kind: Some(StringValue(\"'safari' or 'chrome' (defaults to Safari)\")) }, \"type\": Value { kind: Some(StringValue(\"string\")) }} })) }, \"label\": Value { kind: Some(StructValue(Struct { fields: {\"description\": Value { kind: Some(StringValue(\"Label text of the select/dropdown (case-insensitive partial match)\")) }, \"type\": Value { kind: Some(StringValue(\"string\")) }} })) }, \"option\": Value { kind: Some(StructValue(Struct { fields: {\"description\": Value { kind: Some(StringValue(\"Option text to select (case-insensitive partial match)\")) }, \"type\": Value { kind: Some(StringValue(\"string\")) }} })) }} })) }, \"required\": Value { kind: Some(ListValue(ListValue { values: [Value { kind: Some(StringValue(\"label\")) }, Value { kind: Some(StringValue(\"option\")) }] })) }, \"type\": Value { kind: Some(StringValue(\"object\")) }} }) }, McpTool { name: \"batch_actions\", description: \"PREFERRED: Always use this tool instead of calling individual action tools (click_at, type_text, press_key, launch_app, etc.) one at a time. Combine multiple steps into a single batch call — this is dramatically faster. Returns a single screenshot by default (disable with return_screenshot=false). Only use individual tools when you need to read the result of one action before deciding the next. Stops on first error. Max 20 actions per call.\\n\\nExample — open Notes and write text (1 call instead of 6):\\n [{ \\\"action\\\": \\\"launch_app\\\", \\\"name\\\": \\\"Notes\\\" }, { \\\"action\\\": \\\"key\\\", \\\"key\\\": \\\"n\\\", \\\"modifiers\\\": [\\\"command\\\"] }, { \\\"action\\\": \\\"set_clipboard\\\", \\\"text\\\": \\\"Hello\\\\nWorld\\\" }, { \\\"action\\\": \\\"key\\\", \\\"key\\\": \\\"v\\\", \\\"modifiers\\\": [\\\"command\\\"] }]\", input_schema: Some(Struct { fields: {\"$schema\": Value { kind: Some(StringValue(\"http://json-schema.org/draft-07/schema#\")) }, \"additionalProperties\": Value { kind: Some(BoolValue(false)) }, \"properties\": Value { kind: Some(StructValue(Struct { fields: {\"actions\": Value { kind: Some(StructValue(Struct { fields: {\"description\": Value { kind: Some(StringValue(\"Array of actions to execute sequentially\")) }, \"items\": Value { kind: Some(StructValue(Struct { fields: {\"anyOf\": Value { kind: Some(ListValue(ListValue { values: [Value { kind: Some(StructValue(Struct { fields: {\"additionalProperties\": Value { kind: Some(BoolValue(false)) }, \"properties\": Value { kind: Some(StructValue(Struct { fields: {\"action\": Value { kind: Some(StructValue(Struct { fields: {\"const\": Value { kind: Some(StringValue(\"click\")) }, \"type\": Value { kind: Some(StringValue(\"string\")) }} })) }, \"x\": Value { kind: Some(StructValue(Struct { fields: {\"type\": Value { kind: Some(StringValue(\"number\")) }} })) }, \"y\": Value { kind: Some(StructValue(Struct { fields: {\"type\": Value { kind: Some(StringValue(\"number\")) }} })) }} })) }, \"required\": Value { kind: Some(ListValue(ListValue { values: [Value { kind: Some(StringValue(\"action\")) }, Value { kind: Some(StringValue(\"x\")) }, Value { kind: Some(StringValue(\"y\")) }] })) }, \"type\": Value { kind: Some(StringValue(\"object\")) }} })) }, Value { kind: Some(StructValue(Struct { fields: {\"additionalProperties\": Value { kind: Some(BoolValue(false)) }, \"properties\": Value { kind: Some(StructValue(Struct { fields: {\"action\": Value { kind: Some(StructValue(Struct { fields: {\"const\": Value { kind: Some(StringValue(\"double_click\")) }, \"type\": Value { kind: Some(StringValue(\"string\")) }} })) }, \"x\": Value { kind: Some(StructValue(Struct { fields: {\"type\": Value { kind: Some(StringValue(\"number\")) }} })) }, \"y\": Value { kind: Some(StructValue(Struct { fields: {\"type\": Value { kind: Some(StringValue(\"number\")) }} })) }} })) }, \"required\": Value { kind: Some(ListValue(ListValue { values: [Value { kind: Some(StringValue(\"action\")) }, Value { kind: Some(StringValue(\"x\")) }, Value { kind: Some(StringValue(\"y\")) }] })) }, \"type\": Value { kind: Some(StringValue(\"object\")) }} })) }, Value { kind: Some(StructValue(Struct { fields: {\"additionalProperties\": Value { kind: Some(BoolValue(false)) }, \"properties\": Value { kind: Some(StructValue(Struct { fields: {\"action\": Value { kind: Some(StructValue(Struct { fields: {\"const\": Value { kind: Some(StringValue(\"click_element\")) }, \"type\": Value { kind: Some(StringValue(\"string\")) }} })) }, \"app\": Value { kind: Some(StructValue(Struct { fields: {\"type\": Value { kind: Some(StringValue(\"string\")) }} })) }, \"name\": Value { kind: Some(StructValue(Struct { fields: {\"type\": Value { kind: Some(StringValue(\"string\")) }} })) }} })) }, \"required\": Value { kind: Some(ListValue(ListValue { values: [Value { kind: Some(StringValue(\"action\")) }, Value { kind: Some(StringValue(\"app\")) }, Value { kind: Some(StringValue(\"name\")) }] })) }, \"type\": Value { kind: Some(StringValue(\"object\")) }} })) }, Value { kind: Some(StructValue(Struct { fields: {\"additionalProperties\": Value { kind: Some(BoolValue(false)) }, \"properties\": Value { kind: Some(StructValue(Struct { fields: {\"action\": Value { kind: Some(StructValue(Struct { fields: {\"const\": Value { kind: Some(StringValue(\"type\")) }, \"type\": Value { kind: Some(StringValue(\"string\")) }} })) }, \"text\": Value { kind: Some(StructValue(Struct { fields: {\"type\": Value { kind: Some(StringValue(\"string\")) }} })) }} })) }, \"required\": Value { kind: Some(ListValue(ListValue { values: [Value { kind: Some(StringValue(\"action\")) }, Value { kind: Some(StringValue(\"text\")) }] })) }, \"type\": Value { kind: Some(StringValue(\"object\")) }} })) }, Value { kind: Some(StructValue(Struct { fields: {\"additionalProperties\": Value { kind: Some(BoolValue(false)) }, \"properties\": Value { kind: Some(StructValue(Struct { fields: {\"action\": Value { kind: Some(StructValue(Struct { fields: {\"const\": Value { kind: Some(StringValue(\"key\")) }, \"type\": Value { kind: Some(StringValue(\"string\")) }} })) }, \"key\": Value { kind: Some(StructValue(Struct { fields: {\"type\": Value { kind: Some(StringValue(\"string\")) }} })) }, \"modifiers\": Value { kind: Some(StructValue(Struct { fields: {\"items\": Value { kind: Some(StructValue(Struct { fields: {\"type\": Value { kind: Some(StringValue(\"string\")) }} })) }, \"type\": Value { kind: Some(StringValue(\"array\")) }} })) }} })) }, \"required\": Value { kind: Some(ListValue(ListValue { values: [Value { kind: Some(StringValue(\"action\")) }, Value { kind: Some(StringValue(\"key\")) }] })) }, \"type\": Value { kind: Some(StringValue(\"object\")) }} })) }, Value { kind: Some(StructValue(Struct { fields: {\"additionalProperties\": Value { kind: Some(BoolValue(false)) }, \"properties\": Value { kind: Some(StructValue(Struct { fields: {\"action\": Value { kind: Some(StructValue(Struct { fields: {\"const\": Value { kind: Some(StringValue(\"scroll\")) }, \"type\": Value { kind: Some(StringValue(\"string\")) }} })) }, \"amount\": Value { kind: Some(StructValue(Struct { fields: {\"type\": Value { kind: Some(StringValue(\"number\")) }} })) }, \"direction\": Value { kind: Some(StructValue(Struct { fields: {\"enum\": Value { kind: Some(ListValue(ListValue { values: [Value { kind: Some(StringValue(\"up\")) }, Value { kind: Some(StringValue(\"down\")) }, Value { kind: Some(StringValue(\"left\")) }, Value { kind: Some(StringValue(\"right\")) }] })) }, \"type\": Value { kind: Some(StringValue(\"string\")) }} })) }} })) }, \"required\": Value { kind: Some(ListValue(ListValue { values: [Value { kind: Some(StringValue(\"action\")) }, Value { kind: Some(StringValue(\"direction\")) }] })) }, \"type\": Value { kind: Some(StringValue(\"object\")) }} })) }, Value { kind: Some(StructValue(Struct { fields: {\"additionalProperties\": Value { kind: Some(BoolValue(false)) }, \"properties\": Value { kind: Some(StructValue(Struct { fields: {\"action\": Value { kind: Some(StructValue(Struct { fields: {\"const\": Value { kind: Some(StringValue(\"launch_app\")) }, \"type\": Value { kind: Some(StringValue(\"string\")) }} })) }, \"name\": Value { kind: Some(StructValue(Struct { fields: {\"type\": Value { kind: Some(StringValue(\"string\")) }} })) }} })) }, \"required\": Value { kind: Some(ListValue(ListValue { values: [Value { kind: Some(StringValue(\"action\")) }, Value { kind: Some(StringValue(\"name\")) }] })) }, \"type\": Value { kind: Some(StringValue(\"object\")) }} })) }, Value { kind: Some(StructValue(Struct { fields: {\"additionalProperties\": Value { kind: Some(BoolValue(false)) }, \"properties\": Value { kind: Some(StructValue(Struct { fields: {\"action\": Value { kind: Some(StructValue(Struct { fields: {\"const\": Value { kind: Some(StringValue(\"open_url\")) }, \"type\": Value { kind: Some(StringValue(\"string\")) }} })) }, \"browser\": Value { kind: Some(StructValue(Struct { fields: {\"type\": Value { kind: Some(StringValue(\"string\")) }} })) }, \"url\": Value { kind: Some(StructValue(Struct { fields: {\"type\": Value { kind: Some(StringValue(\"string\")) }} })) }} })) }, \"required\": Value { kind: Some(ListValue(ListValue { values: [Value { kind: Some(StringValue(\"action\")) }, Value { kind: Some(StringValue(\"url\")) }] })) }, \"type\": Value { kind: Some(StringValue(\"object\")) }} })) }, Value { kind: Some(StructValue(Struct { fields: {\"additionalProperties\": Value { kind: Some(BoolValue(false)) }, \"properties\": Value { kind: Some(StructValue(Struct { fields: {\"action\": Value { kind: Some(StructValue(Struct { fields: {\"const\": Value { kind: Some(StringValue(\"set_clipboard\")) }, \"type\": Value { kind: Some(StringValue(\"string\")) }} })) }, \"text\": Value { kind: Some(StructValue(Struct { fields: {\"type\": Value { kind: Some(StringValue(\"string\")) }} })) }} })) }, \"required\": Value { kind: Some(ListValue(ListValue { values: [Value { kind: Some(StringValue(\"action\")) }, Value { kind: Some(StringValue(\"text\")) }] })) }, \"type\": Value { kind: Some(StringValue(\"object\")) }} })) }, Value { kind: Some(StructValue(Struct { fields: {\"additionalProperties\": Value { kind: Some(BoolValue(false)) }, \"properties\": Value { kind: Some(StructValue(Struct { fields: {\"action\": Value { kind: Some(StructValue(Struct { fields: {\"const\": Value { kind: Some(StringValue(\"execute_javascript\")) }, \"type\": Value { kind: Some(StringValue(\"string\")) }} })) }, \"browser\": Value { kind: Some(StructValue(Struct { fields: {\"type\": Value { kind: Some(StringValue(\"string\")) }} })) }, \"code\": Value { kind: Some(StructValue(Struct { fields: {\"type\": Value { kind: Some(StringValue(\"string\")) }} })) }} })) }, \"required\": Value { kind: Some(ListValue(ListValue { values: [Value { kind: Some(StringValue(\"action\")) }, Value { kind: Some(StringValue(\"code\")) }] })) }, \"type\": Value { kind: Some(StringValue(\"object\")) }} })) }, Value { kind: Some(StructValue(Struct { fields: {\"additionalProperties\": Value { kind: Some(BoolValue(false)) }, \"properties\": Value { kind: Some(StructValue(Struct { fields: {\"action\": Value { kind: Some(StructValue(Struct { fields: {\"const\": Value { kind: Some(StringValue(\"click_text\")) }, \"type\": Value { kind: Some(StringValue(\"string\")) }} })) }, \"browser\": Value { kind: Some(StructValue(Struct { fields: {\"type\": Value { kind: Some(StringValue(\"string\")) }} })) }, \"element_type\": Value { kind: Some(StructValue(Struct { fields: {\"type\": Value { kind: Some(StringValue(\"string\")) }} })) }, \"index\": Value { kind: Some(StructValue(Struct { fields: {\"type\": Value { kind: Some(StringValue(\"number\")) }} })) }, \"text\": Value { kind: Some(StructValue(Struct { fields: {\"type\": Value { kind: Some(StringValue(\"string\")) }} })) }} })) }, \"required\": Value { kind: Some(ListValue(ListValue { values: [Value { kind: Some(StringValue(\"action\")) }, Value { kind: Some(StringValue(\"text\")) }] })) }, \"type\": Value { kind: Some(StringValue(\"object\")) }} })) }, Value { kind: Some(StructValue(Struct { fields: {\"additionalProperties\": Value { kind: Some(BoolValue(false)) }, \"properties\": Value { kind: Some(StructValue(Struct { fields: {\"action\": Value { kind: Some(StructValue(Struct { fields: {\"const\": Value { kind: Some(StringValue(\"fill_label\")) }, \"type\": Value { kind: Some(StringValue(\"string\")) }} })) }, \"browser\": Value { kind: Some(StructValue(Struct { fields: {\"type\": Value { kind: Some(StringValue(\"string\")) }} })) }, \"label\": Value { kind: Some(StructValue(Struct { fields: {\"type\": Value { kind: Some(StringValue(\"string\")) }} })) }, \"value\": Value { kind: Some(StructValue(Struct { fields: {\"type\": Value { kind: Some(StringValue(\"string\")) }} })) }} })) }, \"required\": Value { kind: Some(ListValue(ListValue { values: [Value { kind: Some(StringValue(\"action\")) }, Value { kind: Some(StringValue(\"label\")) }, Value { kind: Some(StringValue(\"value\")) }] })) }, \"type\": Value { kind: Some(StringValue(\"object\")) }} })) }, Value { kind: Some(StructValue(Struct { fields: {\"additionalProperties\": Value { kind: Some(BoolValue(false)) }, \"properties\": Value { kind: Some(StructValue(Struct { fields: {\"action\": Value { kind: Some(StructValue(Struct { fields: {\"const\": Value { kind: Some(StringValue(\"select_option\")) }, \"type\": Value { kind: Some(StringValue(\"string\")) }} })) }, \"browser\": Value { kind: Some(StructValue(Struct { fields: {\"type\": Value { kind: Some(StringValue(\"string\")) }} })) }, \"label\": Value { kind: Some(StructValue(Struct { fields: {\"type\": Value { kind: Some(StringValue(\"string\")) }} })) }, \"option\": Value { kind: Some(StructValue(Struct { fields: {\"type\": Value { kind: Some(StringValue(\"string\")) }} })) }} })) }, \"required\": Value { kind: Some(ListValue(ListValue { values: [Value { kind: Some(StringValue(\"action\")) }, Value { kind: Some(StringValue(\"label\")) }, Value { kind: Some(StringValue(\"option\")) }] })) }, \"type\": Value { kind: Some(StringValue(\"object\")) }} })) }] })) }} })) }, \"maxItems\": Value { kind: Some(NumberValue(20.0)) }, \"minItems\": Value { kind: Some(NumberValue(1.0)) }, \"type\": Value { kind: Some(StringValue(\"array\")) }} })) }, \"delay_between_ms\": Value { kind: Some(StructValue(Struct { fields: {\"description\": Value { kind: Some(StringValue(\"Delay between actions in ms (default 100)\")) }, \"type\": Value { kind: Some(StringValue(\"number\")) }} })) }, \"return_screenshot\": Value { kind: Some(StructValue(Struct { fields: {\"description\": Value { kind: Some(StringValue(\"Return one screenshot at the end (default true). Set false for faster/smaller responses.\")) }, \"type\": Value { kind: Some(StringValue(\"boolean\")) }} })) }} })) }, \"required\": Value { kind: Some(ListValue(ListValue { values: [Value { kind: Some(StringValue(\"actions\")) }] })) }, \"type\": Value { kind: Some(StringValue(\"object\")) }} }) }] }] }) }", + "timestamp_local": "2026-05-18T11:22:55.626-05:00", + "timestamp_utc": "2026-05-18T16:22:55.626Z" +} \ No newline at end of file diff --git a/GALAXY.md b/GALAXY.md index 319c319a..1d0e8bfc 100644 --- a/GALAXY.md +++ b/GALAXY.md @@ -68,6 +68,18 @@ Global rules are loaded from `~/.galaxy-ai/rules/*.md` (filename = rule name, co Project rules are loaded from `GALAXY.md` or `AGENTS.md` files found in the project directory tree. +**External Config Fallback** (`app/src/ai/bedrock/external_config.rs`): +When Galaxy's own Bedrock settings are at defaults, it falls back to configurations from: +1. **Claude Code** (`~/.claude/settings.json`) — reads `env.AWS_PROFILE`, `env.AWS_REGION`, and `env.DCP_MODEL_MAP` (ARN-based model mappings) +2. **OpenCode** (`~/.config/opencode/opencode.json`) — reads `provider.amazon-bedrock.options.profile` and `.region` + +Priority: Galaxy explicit settings > Claude Code > OpenCode > hardcoded defaults. Fallback only applies when profile is `"default"` (for profile) or empty (for region/models). External model ARNs are merged with Galaxy's built-in default model list. + +**Token Usage & Cost Tracking** (`app/src/ai/bedrock/stream.rs`): +The Bedrock stream extracts full token metadata from responses: `input_tokens`, `output_tokens`, `cache_read_input_tokens`, `cache_write_input_tokens`. These flow through `build_stream_finished` → `TokenUsage` struct → `conversation.update_cost_and_usage_for_request()`. Cost is estimated using Sonnet-tier Bedrock pricing as a conservative default. Displayed in: +- **Agent management cards** — total token count in metadata row +- **Conversation usage footer** — full breakdown (input/output/cache read/cache write) + estimated cost + ### Key Architectural Patterns 1. **Entity-Handle System**: Views reference other views via handles, not direct ownership diff --git a/app/src/ai/agent/conversation.rs b/app/src/ai/agent/conversation.rs index 50a44124..cd2b7cd3 100644 --- a/app/src/ai/agent/conversation.rs +++ b/app/src/ai/agent/conversation.rs @@ -3062,6 +3062,20 @@ impl AIConversation { self.total_token_usage_by_model.values().cloned().collect() } + pub fn total_tokens(&self) -> u32 { + self.total_token_usage_by_model + .values() + .map(Self::total_tokens_for_usage) + .sum() + } + + pub fn total_cost_cents(&self) -> f32 { + self.total_token_usage_by_model + .values() + .map(|u| u.cost_in_cents) + .sum() + } + fn total_tokens_for_usage(usage: &TokenUsage) -> u32 { usage.total_input + usage.output + usage.input_cache_read + usage.input_cache_write } diff --git a/app/src/ai/agent_conversations_model.rs b/app/src/ai/agent_conversations_model.rs index c67aa86d..73ed4a7c 100644 --- a/app/src/ai/agent_conversations_model.rs +++ b/app/src/ai/agent_conversations_model.rs @@ -4,7 +4,9 @@ use crate::ai::agent::conversation::{AIConversationId, ConversationStatus}; use crate::ai::ambient_agents::AmbientAgentTaskId; use crate::ai::ambient_agents::{AgentSource, AmbientAgentTask, AmbientAgentTaskState}; use crate::ai::artifacts::Artifact; -use crate::ai::blocklist::{format_credits, BlocklistAIHistoryEvent, BlocklistAIHistoryModel}; +use crate::ai::blocklist::{ + format_credits, format_token_count, BlocklistAIHistoryEvent, BlocklistAIHistoryModel, +}; use crate::ai::cloud_environments::CloudAmbientAgentEnvironment; use crate::ai::conversation_navigation::ConversationNavigationData; use crate::auth::auth_manager::{AuthManager, AuthManagerEvent}; @@ -511,6 +513,20 @@ impl ConversationOrTask<'_> { self.request_usage(app).map(format_credits) } + pub fn display_total_tokens(&self, app: &AppContext) -> Option { + match self { + ConversationOrTask::Task(_) => None, + ConversationOrTask::Conversation(metadata) => { + let history_model = BlocklistAIHistoryModel::as_ref(app); + history_model + .conversation(&metadata.nav_data.id) + .map(|conv| conv.total_tokens()) + .filter(|&t| t > 0) + .map(format_token_count) + } + } + } + pub fn last_updated(&self) -> DateTime { match self { ConversationOrTask::Task(task) => task.updated_at, diff --git a/app/src/ai/agent_management/view.rs b/app/src/ai/agent_management/view.rs index 9543b936..71be8d75 100644 --- a/app/src/ai/agent_management/view.rs +++ b/app/src/ai/agent_management/view.rs @@ -1824,6 +1824,10 @@ impl AgentManagementView { metadata_parts.push(format!("Credits used: {usage}")); } + if let Some(tokens) = card_data.display_total_tokens(app) { + metadata_parts.push(format!("Tokens: {tokens}")); + } + let metadata_text = metadata_parts.join(" • "); Text::new(metadata_text, font_family, font_size) diff --git a/app/src/ai/bedrock/client.rs b/app/src/ai/bedrock/client.rs index 5816f5d1..cca193f5 100644 --- a/app/src/ai/bedrock/client.rs +++ b/app/src/ai/bedrock/client.rs @@ -7,6 +7,7 @@ use aws_sdk_bedrockruntime::Client as BedrockRuntimeClient; use crate::settings::ai::BedrockAuthMethod; +use super::external_config::ExternalBedrockConfig; use super::convert::{build_converse_request, ConversationMessage, ToolDefinition}; use super::diagnostic::BedrockDiagnosticLogger; use super::models::apply_cross_region_prefix; @@ -29,6 +30,33 @@ pub struct BedrockClientConfig { pub fallback_to_warp: bool, } +impl BedrockClientConfig { + /// Applies external config (from Claude Code / OpenCode) as fallback values + /// when Galaxy's own settings are at their defaults. + pub fn with_external_fallbacks(mut self) -> Self { + let external = ExternalBedrockConfig::load(); + if external.is_empty() { + return self; + } + + if self.profile == "default" { + if let Some(profile) = external.profile { + log::info!("[bedrock] Using profile from external config: {profile}"); + self.profile = profile; + } + } + + if self.region.is_empty() { + if let Some(region) = external.region { + log::info!("[bedrock] Using region from external config: {region}"); + self.region = region; + } + } + + self + } +} + #[derive(Debug, thiserror::Error)] pub enum BedrockError { #[error("Bedrock credentials not configured")] diff --git a/app/src/ai/bedrock/convert_request.rs b/app/src/ai/bedrock/convert_request.rs index e615b8b1..b7758ef3 100644 --- a/app/src/ai/bedrock/convert_request.rs +++ b/app/src/ai/bedrock/convert_request.rs @@ -872,6 +872,11 @@ pub fn extract_tools(request: &api::Request) -> Vec { tools = default_tool_definitions(); } + // Filter out suggest_next_prompt — its action executor waits on a oneshot + // channel for UI interaction that never fires in the Bedrock path, causing + // the conversation to stay InProgress forever. + tools.retain(|t| t.name != "suggest_next_prompt"); + tools } @@ -963,7 +968,6 @@ fn default_tool_definitions() -> Vec { tool_definition_for_name("apply_file_diffs"), tool_definition_for_name("grep"), tool_definition_for_name("file_glob"), - tool_definition_for_name("suggest_next_prompt"), ] } diff --git a/app/src/ai/bedrock/external_config.rs b/app/src/ai/bedrock/external_config.rs new file mode 100644 index 00000000..12534784 --- /dev/null +++ b/app/src/ai/bedrock/external_config.rs @@ -0,0 +1,229 @@ +use std::collections::HashMap; +use std::path::PathBuf; + +use crate::settings::ai::BedrockModelConfig; + +#[derive(Debug, Clone, Default)] +pub struct ExternalBedrockConfig { + pub profile: Option, + pub region: Option, + pub models: Vec, +} + +impl ExternalBedrockConfig { + pub fn load() -> Self { + let claude_config = load_claude_code_config(); + let opencode_config = load_opencode_config(); + + // Claude Code takes priority over OpenCode since it has richer model mappings + Self { + profile: claude_config + .profile + .or(opencode_config.profile), + region: claude_config + .region + .or(opencode_config.region), + models: if claude_config.models.is_empty() { + opencode_config.models + } else { + claude_config.models + }, + } + } + + pub fn is_empty(&self) -> bool { + self.profile.is_none() && self.region.is_none() && self.models.is_empty() + } +} + +fn load_claude_code_config() -> ExternalBedrockConfig { + let path = dirs::home_dir() + .map(|h| h.join(".claude").join("settings.json")) + .unwrap_or_default(); + + parse_claude_code_config(path) +} + +fn parse_claude_code_config(path: PathBuf) -> ExternalBedrockConfig { + let contents = match std::fs::read_to_string(&path) { + Ok(c) => c, + Err(_) => return ExternalBedrockConfig::default(), + }; + + let json: serde_json::Value = match serde_json::from_str(&contents) { + Ok(v) => v, + Err(_) => return ExternalBedrockConfig::default(), + }; + + let env = match json.get("env").and_then(|v| v.as_object()) { + Some(e) => e, + None => return ExternalBedrockConfig::default(), + }; + + let profile = env + .get("AWS_PROFILE") + .and_then(|v| v.as_str()) + .map(|s| s.to_string()); + + let region = env + .get("AWS_REGION") + .and_then(|v| v.as_str()) + .map(|s| s.to_string()); + + let models = parse_claude_code_model_map(env); + + ExternalBedrockConfig { + profile, + region, + models, + } +} + +fn parse_claude_code_model_map( + env: &serde_json::Map, +) -> Vec { + let model_map_str = match env.get("DCP_MODEL_MAP").and_then(|v| v.as_str()) { + Some(s) => s, + None => return Vec::new(), + }; + + let model_map: HashMap = match serde_json::from_str(model_map_str) { + Ok(m) => m, + Err(_) => return Vec::new(), + }; + + model_map + .into_iter() + .map(|(source_id, arn)| { + let display_name = derive_display_name(&source_id); + BedrockModelConfig { + model_id: arn, + display_name, + vision_supported: true, + } + }) + .collect() +} + +fn derive_display_name(model_id: &str) -> String { + // Strip region prefix (e.g. "us." from "us.anthropic.claude-opus-4-6-v1") + let id = model_id + .strip_prefix("us.") + .or_else(|| model_id.strip_prefix("eu.")) + .or_else(|| model_id.strip_prefix("ap.")) + .or_else(|| model_id.strip_prefix("global.")) + .unwrap_or(model_id); + + // Strip vendor prefix + let id = id + .strip_prefix("anthropic.") + .or_else(|| id.strip_prefix("amazon.")) + .or_else(|| id.strip_prefix("meta.")) + .unwrap_or(id); + + // Convert model slug to display name + // e.g. "claude-opus-4-6-v1" -> "Claude Opus 4.6" + // e.g. "claude-sonnet-4-5-20250929-v1:0" -> "Claude Sonnet 4.5" + prettify_model_slug(id) +} + +fn prettify_model_slug(slug: &str) -> String { + // Remove version suffixes like "-v1:0", "-v1", "-v2:0" + let slug = slug + .split("-v") + .next() + .unwrap_or(slug); + + // Remove date suffixes like "-20250514" or "-20251001" + let parts: Vec<&str> = slug.split('-').collect(); + let mut cleaned: Vec<&str> = Vec::new(); + let mut i = 0; + while i < parts.len() { + // Skip parts that look like dates (8 digits) + if parts[i].len() == 8 && parts[i].chars().all(|c| c.is_ascii_digit()) { + i += 1; + continue; + } + cleaned.push(parts[i]); + i += 1; + } + + // Try to detect version numbers: consecutive single-digit parts become "X.Y" + let mut result = String::new(); + let mut i = 0; + while i < cleaned.len() { + if i > 0 { + result.push(' '); + } + + // Check if this and the next part form a version number (e.g. "4" "6" -> "4.6") + if cleaned[i].len() == 1 + && cleaned[i].chars().all(|c| c.is_ascii_digit()) + && i + 1 < cleaned.len() + && cleaned[i + 1].len() == 1 + && cleaned[i + 1].chars().all(|c| c.is_ascii_digit()) + { + result.push_str(cleaned[i]); + result.push('.'); + result.push_str(cleaned[i + 1]); + i += 2; + continue; + } + + // Capitalize first letter of each word + let word = cleaned[i]; + let mut chars = word.chars(); + if let Some(first) = chars.next() { + result.push(first.to_ascii_uppercase()); + result.extend(chars); + } + i += 1; + } + + result +} + +fn load_opencode_config() -> ExternalBedrockConfig { + let path = dirs::home_dir() + .map(|h| h.join(".config").join("opencode").join("opencode.json")) + .unwrap_or_default(); + + parse_opencode_config(path) +} + +fn parse_opencode_config(path: PathBuf) -> ExternalBedrockConfig { + let contents = match std::fs::read_to_string(&path) { + Ok(c) => c, + Err(_) => return ExternalBedrockConfig::default(), + }; + + let json: serde_json::Value = match serde_json::from_str(&contents) { + Ok(v) => v, + Err(_) => return ExternalBedrockConfig::default(), + }; + + let bedrock_opts = json + .get("provider") + .and_then(|p| p.get("amazon-bedrock")) + .and_then(|b| b.get("options")); + + let profile = bedrock_opts + .and_then(|o| o.get("profile")) + .and_then(|v| v.as_str()) + .map(|s| s.to_string()); + + let region = bedrock_opts + .and_then(|o| o.get("region")) + .and_then(|v| v.as_str()) + .map(|s| s.to_string()); + + ExternalBedrockConfig { + profile, + region, + models: Vec::new(), + } +} + +#[cfg(test)] +#[path = "external_config_tests.rs"] +mod tests; diff --git a/app/src/ai/bedrock/external_config_tests.rs b/app/src/ai/bedrock/external_config_tests.rs new file mode 100644 index 00000000..355e2850 --- /dev/null +++ b/app/src/ai/bedrock/external_config_tests.rs @@ -0,0 +1,130 @@ +use std::io::Write; + +use tempfile::NamedTempFile; + +use super::*; + +#[test] +fn test_parse_claude_code_config_extracts_profile_and_region() { + let mut file = NamedTempFile::new().unwrap(); + write!( + file, + r#"{{ + "env": {{ + "AWS_PROFILE": "coding-assistant", + "AWS_REGION": "us-west-2", + "CLAUDE_CODE_USE_BEDROCK": "1" + }} + }}"# + ) + .unwrap(); + + let config = parse_claude_code_config(file.path().to_path_buf()); + assert_eq!(config.profile, Some("coding-assistant".to_string())); + assert_eq!(config.region, Some("us-west-2".to_string())); +} + +#[test] +fn test_parse_claude_code_config_extracts_model_map() { + let mut file = NamedTempFile::new().unwrap(); + write!( + file, + r#"{{ + "env": {{ + "AWS_PROFILE": "test", + "AWS_REGION": "us-east-1", + "DCP_MODEL_MAP": "{{\"us.anthropic.claude-opus-4-6-v1\": \"arn:aws:bedrock:us-east-1:123456:application-inference-profile/abc123\"}}" + }} + }}"# + ) + .unwrap(); + + let config = parse_claude_code_config(file.path().to_path_buf()); + assert_eq!(config.models.len(), 1); + assert_eq!( + config.models[0].model_id, + "arn:aws:bedrock:us-east-1:123456:application-inference-profile/abc123" + ); + assert!(config.models[0].vision_supported); +} + +#[test] +fn test_parse_claude_code_config_missing_file() { + let config = parse_claude_code_config(PathBuf::from("/nonexistent/path/settings.json")); + assert!(config.is_empty()); +} + +#[test] +fn test_parse_opencode_config_extracts_bedrock_options() { + let mut file = NamedTempFile::new().unwrap(); + write!( + file, + r#"{{ + "provider": {{ + "amazon-bedrock": {{ + "options": {{ + "region": "eu-west-1", + "profile": "my-profile" + }} + }} + }} + }}"# + ) + .unwrap(); + + let config = parse_opencode_config(file.path().to_path_buf()); + assert_eq!(config.profile, Some("my-profile".to_string())); + assert_eq!(config.region, Some("eu-west-1".to_string())); +} + +#[test] +fn test_parse_opencode_config_missing_file() { + let config = parse_opencode_config(PathBuf::from("/nonexistent/opencode.json")); + assert!(config.is_empty()); +} + +#[test] +fn test_claude_code_takes_priority_over_opencode() { + // When both configs provide a profile, Claude Code wins + let claude = ExternalBedrockConfig { + profile: Some("claude-profile".to_string()), + region: Some("us-east-1".to_string()), + models: Vec::new(), + }; + let opencode = ExternalBedrockConfig { + profile: Some("opencode-profile".to_string()), + region: Some("eu-west-1".to_string()), + models: Vec::new(), + }; + + let merged = ExternalBedrockConfig { + profile: claude.profile.or(opencode.profile), + region: claude.region.or(opencode.region), + models: Vec::new(), + }; + + assert_eq!(merged.profile, Some("claude-profile".to_string())); + assert_eq!(merged.region, Some("us-east-1".to_string())); +} + +#[test] +fn test_prettify_model_slug() { + assert_eq!(prettify_model_slug("claude-opus-4-6"), "Claude Opus 4.6"); + assert_eq!( + prettify_model_slug("claude-sonnet-4-5-20250929"), + "Claude Sonnet 4.5" + ); + assert_eq!(prettify_model_slug("claude-haiku-4-5"), "Claude Haiku 4.5"); +} + +#[test] +fn test_derive_display_name_strips_prefixes() { + assert_eq!( + derive_display_name("us.anthropic.claude-opus-4-6-v1"), + "Claude Opus 4.6" + ); + assert_eq!( + derive_display_name("global.anthropic.claude-haiku-4-5-20251001-v1:0"), + "Claude Haiku 4.5" + ); +} diff --git a/app/src/ai/bedrock/mod.rs b/app/src/ai/bedrock/mod.rs index 3f7b71ef..efaa81b3 100644 --- a/app/src/ai/bedrock/mod.rs +++ b/app/src/ai/bedrock/mod.rs @@ -3,6 +3,7 @@ pub mod convert; pub mod convert_request; pub mod diagnostic; pub mod discovery; +pub mod external_config; pub mod models; pub mod stream; diff --git a/app/src/ai/bedrock/models.rs b/app/src/ai/bedrock/models.rs index 6fa2e0b3..e6584040 100644 --- a/app/src/ai/bedrock/models.rs +++ b/app/src/ai/bedrock/models.rs @@ -1,5 +1,7 @@ use crate::settings::ai::BedrockModelConfig; +use super::external_config::ExternalBedrockConfig; + pub struct DefaultModel { pub model_id: &'static str, pub display_name: &'static str, @@ -55,18 +57,43 @@ pub const DEFAULT_BEDROCK_MODELS: &[DefaultModel] = &[ ]; pub fn get_effective_models(user_models: &[BedrockModelConfig]) -> Vec { - if user_models.is_empty() { - DEFAULT_BEDROCK_MODELS + if !user_models.is_empty() { + return user_models.to_vec(); + } + + // Fall back to models from external configs (Claude Code / OpenCode) + let external = ExternalBedrockConfig::load(); + if !external.models.is_empty() { + log::info!( + "[bedrock] Using {} model(s) from external config", + external.models.len() + ); + // Merge external models with defaults so the user still sees all defaults + let mut models = external.models; + let defaults: Vec = DEFAULT_BEDROCK_MODELS .iter() .map(|m| BedrockModelConfig { model_id: m.model_id.to_string(), display_name: m.display_name.to_string(), vision_supported: m.vision_supported, }) - .collect() - } else { - user_models.to_vec() + .collect(); + for default in defaults { + if !models.iter().any(|m| m.model_id == default.model_id) { + models.push(default); + } + } + return models; } + + DEFAULT_BEDROCK_MODELS + .iter() + .map(|m| BedrockModelConfig { + model_id: m.model_id.to_string(), + display_name: m.display_name.to_string(), + vision_supported: m.vision_supported, + }) + .collect() } pub fn apply_cross_region_prefix(model_id: &str, region: &str) -> String { diff --git a/app/src/ai/bedrock/stream.rs b/app/src/ai/bedrock/stream.rs index 41c4f7fc..774975a0 100644 --- a/app/src/ai/bedrock/stream.rs +++ b/app/src/ai/bedrock/stream.rs @@ -55,6 +55,8 @@ pub fn bedrock_stream_to_response_events( let mut _has_tool_calls = false; let mut input_tokens: i32 = 0; let mut output_tokens: i32 = 0; + let mut cache_read_input_tokens: i32 = 0; + let mut cache_write_input_tokens: i32 = 0; let mut stop_reason = stream_finished::Reason::Done(stream_finished::Done {}); // Track full assistant text and tool calls for bedrock_message_history @@ -155,31 +157,41 @@ pub fn bedrock_stream_to_response_events( StreamEvent::ContentBlockStop(_) => { log::info!("[bedrock-debug] Event #{event_count}: ContentBlockStop (tool_use_id={:?})", if current_tool_use_id.is_empty() { "none" } else { ¤t_tool_use_id }); if !current_tool_use_id.is_empty() { - log::debug!("[bedrock] Tool call complete: {} ({})", current_tool_name, current_tool_use_id); - // Track for bedrock_message_history - let input_json: serde_json::Value = serde_json::from_str(¤t_tool_input_json) - .unwrap_or(serde_json::Value::Object(serde_json::Map::new())); - history_tool_calls.push(ContentPart::ToolUse { - tool_use_id: current_tool_use_id.clone(), - name: current_tool_name.clone(), - input: input_json, - }); - if let Some(ref logger) = diagnostic_logger { - logger.log_stream_event(&format!( - "ToolCall: name={}, id={}, input={}", - current_tool_name, current_tool_use_id, current_tool_input_json - )); + // Skip suggest_next_prompt — its executor hangs forever + // waiting for UI interaction that doesn't exist in the + // Bedrock path. + if current_tool_name == "suggest_next_prompt" { + log::info!("[bedrock] Skipping suggest_next_prompt tool call"); + current_tool_use_id.clear(); + current_tool_name.clear(); + current_tool_input_json.clear(); + } else { + log::debug!("[bedrock] Tool call complete: {} ({})", current_tool_name, current_tool_use_id); + // Track for bedrock_message_history + let input_json: serde_json::Value = serde_json::from_str(¤t_tool_input_json) + .unwrap_or(serde_json::Value::Object(serde_json::Map::new())); + history_tool_calls.push(ContentPart::ToolUse { + tool_use_id: current_tool_use_id.clone(), + name: current_tool_name.clone(), + input: input_json, + }); + if let Some(ref logger) = diagnostic_logger { + logger.log_stream_event(&format!( + "ToolCall: name={}, id={}, input={}", + current_tool_name, current_tool_use_id, current_tool_input_json + )); + } + let tool_msg = build_tool_call_message( + &task_id, + ¤t_tool_use_id, + ¤t_tool_name, + ¤t_tool_input_json, + ); + yield Ok(tool_msg); + current_tool_use_id.clear(); + current_tool_name.clear(); + current_tool_input_json.clear(); } - let tool_msg = build_tool_call_message( - &task_id, - ¤t_tool_use_id, - ¤t_tool_name, - ¤t_tool_input_json, - ); - yield Ok(tool_msg); - current_tool_use_id.clear(); - current_tool_name.clear(); - current_tool_input_json.clear(); } } StreamEvent::MessageStop(stop) => { @@ -204,6 +216,8 @@ pub fn bedrock_stream_to_response_events( if let Some(usage) = metadata.usage() { input_tokens = usage.input_tokens(); output_tokens = usage.output_tokens(); + cache_read_input_tokens = usage.cache_read_input_tokens().unwrap_or(0); + cache_write_input_tokens = usage.cache_write_input_tokens().unwrap_or(0); } } _ => { @@ -265,7 +279,9 @@ pub fn bedrock_stream_to_response_events( } } - log::info!("[bedrock] Stream finished: input_tokens={input_tokens}, output_tokens={output_tokens}"); + log::info!( + "[bedrock] Stream finished: input_tokens={input_tokens}, output_tokens={output_tokens}, cache_read={cache_read_input_tokens}, cache_write={cache_write_input_tokens}" + ); // Build and store the assistant message into bedrock_messages_sent // so the controller can persist it as part of conversation history. @@ -317,7 +333,13 @@ pub fn bedrock_stream_to_response_events( }; logger.log_result_success(input_tokens, output_tokens, stop_reason_str); } - let finished_event = build_stream_finished(stop_reason, input_tokens, output_tokens); + let finished_event = build_stream_finished( + stop_reason, + input_tokens, + output_tokens, + cache_read_input_tokens, + cache_write_input_tokens, + ); yield Ok(finished_event); }; @@ -365,8 +387,11 @@ pub(super) fn build_stream_finished( reason: stream_finished::Reason, input_tokens: i32, output_tokens: i32, + cache_read_input_tokens: i32, + cache_write_input_tokens: i32, ) -> ResponseEvent { - let total_tokens = (input_tokens + output_tokens) as u32; + let total_tokens = + (input_tokens + output_tokens + cache_read_input_tokens + cache_write_input_tokens) as u32; let mut byok_token_usage = std::collections::HashMap::new(); if total_tokens > 0 { @@ -381,6 +406,20 @@ pub(super) fn build_stream_finished( ); } + let token_usage = vec![stream_finished::TokenUsage { + model_id: "bedrock".to_string(), + total_input: input_tokens as u32, + output: output_tokens as u32, + input_cache_read: cache_read_input_tokens as u32, + input_cache_write: cache_write_input_tokens as u32, + cost_in_cents: estimate_cost_cents( + input_tokens as u32, + output_tokens as u32, + cache_read_input_tokens as u32, + cache_write_input_tokens as u32, + ), + }]; + #[allow(deprecated)] let conversation_usage_metadata = Some(stream_finished::ConversationUsageMetadata { context_window_usage: 0.0, @@ -396,7 +435,7 @@ pub(super) fn build_stream_finished( r#type: Some(api::response_event::Type::Finished( api::response_event::StreamFinished { reason: Some(reason), - token_usage: vec![], + token_usage, should_refresh_model_config: false, request_cost: None, conversation_usage_metadata, @@ -405,6 +444,23 @@ pub(super) fn build_stream_finished( } } +/// Estimates cost in cents based on Anthropic Claude Bedrock pricing. +/// Uses Sonnet-tier pricing as a conservative default since we don't +/// know the exact model at this layer. +/// Pricing (per 1M tokens): input $3, output $15, cache_read $0.30, cache_write $3.75 +fn estimate_cost_cents( + input_tokens: u32, + output_tokens: u32, + cache_read_tokens: u32, + cache_write_tokens: u32, +) -> f32 { + let input_cost = input_tokens as f64 * 0.3 / 100_000.0; + let output_cost = output_tokens as f64 * 1.5 / 100_000.0; + let cache_read_cost = cache_read_tokens as f64 * 0.03 / 100_000.0; + let cache_write_cost = cache_write_tokens as f64 * 0.375 / 100_000.0; + (input_cost + output_cost + cache_read_cost + cache_write_cost) as f32 +} + fn build_add_agent_output_message( task_id: &str, message_id: &str, diff --git a/app/src/ai/bedrock/stream_tests.rs b/app/src/ai/bedrock/stream_tests.rs index 0d1b863c..cc9f41fa 100644 --- a/app/src/ai/bedrock/stream_tests.rs +++ b/app/src/ai/bedrock/stream_tests.rs @@ -19,7 +19,7 @@ fn test_build_stream_init_has_valid_ids() { #[test] fn test_build_stream_finished_done_reason() { let reason = stream_finished::Reason::Done(stream_finished::Done {}); - let event = build_stream_finished(reason, 100, 50); + let event = build_stream_finished(reason, 100, 50, 20, 10); match event.r#type { Some(api::response_event::Type::Finished(finished)) => { @@ -35,8 +35,16 @@ fn test_build_stream_finished_done_reason() { .get("bedrock") .unwrap() .total_tokens, - 150 + 180 ); + // Verify token_usage includes cache breakdown + assert_eq!(finished.token_usage.len(), 1); + let usage = &finished.token_usage[0]; + assert_eq!(usage.total_input, 100); + assert_eq!(usage.output, 50); + assert_eq!(usage.input_cache_read, 20); + assert_eq!(usage.input_cache_write, 10); + assert!(usage.cost_in_cents > 0.0); } other => panic!("Expected Finished event, got {:?}", other), } @@ -45,7 +53,7 @@ fn test_build_stream_finished_done_reason() { #[test] fn test_build_stream_finished_max_token_limit() { let reason = stream_finished::Reason::MaxTokenLimit(stream_finished::ReachedMaxTokenLimit {}); - let event = build_stream_finished(reason, 200, 100); + let event = build_stream_finished(reason, 200, 100, 0, 0); match event.r#type { Some(api::response_event::Type::Finished(finished)) => { @@ -61,7 +69,7 @@ fn test_build_stream_finished_max_token_limit() { #[test] fn test_build_stream_finished_other_reason() { let reason = stream_finished::Reason::Other(stream_finished::Other {}); - let event = build_stream_finished(reason, 0, 0); + let event = build_stream_finished(reason, 0, 0, 0, 0); match event.r#type { Some(api::response_event::Type::Finished(finished)) => { diff --git a/app/src/ai/blocklist/controller/response_stream.rs b/app/src/ai/blocklist/controller/response_stream.rs index 27a55a58..afc14ded 100644 --- a/app/src/ai/blocklist/controller/response_stream.rs +++ b/app/src/ai/blocklist/controller/response_stream.rs @@ -101,7 +101,7 @@ impl ResponseStream { secret_access_key: settings.bedrock_secret_access_key.value().clone(), cross_region_inference: *settings.bedrock_cross_region_inference.value(), fallback_to_warp: *settings.bedrock_fallback_to_warp.value(), - }) + }.with_external_fallbacks()) } pub fn new( diff --git a/app/src/ai/blocklist/mod.rs b/app/src/ai/blocklist/mod.rs index b762e024..4e147683 100644 --- a/app/src/ai/blocklist/mod.rs +++ b/app/src/ai/blocklist/mod.rs @@ -67,7 +67,7 @@ pub(crate) use view_util::{ NEW_AGENT_PANE_LABEL, }; -pub(crate) use view_util::format_credits; +pub(crate) use view_util::{format_credits, format_token_count}; pub use crate::ai::blocklist::block::{secret_redaction, AIBlockResponseRating, TextLocation}; pub use block::keyboard_navigable_buttons; diff --git a/app/src/ai/blocklist/usage/conversation_usage_view.rs b/app/src/ai/blocklist/usage/conversation_usage_view.rs index ce17a689..9f1e00fa 100644 --- a/app/src/ai/blocklist/usage/conversation_usage_view.rs +++ b/app/src/ai/blocklist/usage/conversation_usage_view.rs @@ -1,5 +1,5 @@ use crate::ai::blocklist::usage::render_context_window_usage_icon; -use crate::ai::blocklist::view_util::format_credits; +use crate::ai::blocklist::view_util::{format_cost_cents, format_credits, format_token_count}; use crate::appearance::Appearance; use crate::persistence::model::{ token_usage_category_display_name, ModelTokenUsage, FULL_TERMINAL_USE_CATEGORY, @@ -37,6 +37,11 @@ pub struct ConversationUsageInfo { pub lines_added: i32, pub lines_removed: i32, pub commands_executed: i32, + pub total_input_tokens: u32, + pub total_output_tokens: u32, + pub total_cache_read_tokens: u32, + pub total_cache_write_tokens: u32, + pub estimated_cost_cents: f32, } /// Timing information for the last set of agent responses @@ -263,6 +268,55 @@ impl ConversationUsageView { ); } + // Token usage section + let total_tokens = self.usage_info.total_input_tokens + + self.usage_info.total_output_tokens + + self.usage_info.total_cache_read_tokens + + self.usage_info.total_cache_write_tokens; + if total_tokens > 0 { + labels.push(render_label_text("Total tokens", appearance)); + values.push(render_value_text( + format_token_count(total_tokens), + appearance, + )); + + labels.push(render_label_text(" Input", appearance)); + values.push(render_value_text( + format_token_count(self.usage_info.total_input_tokens), + appearance, + )); + + labels.push(render_label_text(" Output", appearance)); + values.push(render_value_text( + format_token_count(self.usage_info.total_output_tokens), + appearance, + )); + + if self.usage_info.total_cache_read_tokens > 0 { + labels.push(render_label_text(" Cache read", appearance)); + values.push(render_value_text( + format_token_count(self.usage_info.total_cache_read_tokens), + appearance, + )); + } + + if self.usage_info.total_cache_write_tokens > 0 { + labels.push(render_label_text(" Cache write", appearance)); + values.push(render_value_text( + format_token_count(self.usage_info.total_cache_write_tokens), + appearance, + )); + } + + if self.usage_info.estimated_cost_cents > 0.0 { + labels.push(render_label_text("Estimated cost", appearance)); + values.push(render_value_text( + format_cost_cents(self.usage_info.estimated_cost_cents), + appearance, + )); + } + } + labels.push(render_label_text("Context window used", appearance)); let context_usage_str = format!("{}%", (self.usage_info.context_window_usage * 100.).round()); diff --git a/app/src/ai/blocklist/view_util.rs b/app/src/ai/blocklist/view_util.rs index 373df3c6..64984541 100644 --- a/app/src/ai/blocklist/view_util.rs +++ b/app/src/ai/blocklist/view_util.rs @@ -152,6 +152,28 @@ pub fn get_ai_block_overflow_menu_element_position_id(view_id: EntityId) -> Stri } /// Formats credit count to display as whole numbers when the value is effectively a whole number, +pub fn format_token_count(tokens: u32) -> String { + if tokens >= 1_000_000 { + format!("{:.1}M tokens", tokens as f64 / 1_000_000.0) + } else if tokens >= 1_000 { + format!("{:.1}k tokens", tokens as f64 / 1_000.0) + } else { + format!("{tokens} tokens") + } +} + +pub fn format_cost_cents(cents: f32) -> String { + if cents >= 100.0 { + format!("${:.2}", cents / 100.0) + } else if cents >= 1.0 { + format!("{:.1}\u{00A2}", cents) + } else if cents > 0.0 { + format!("{:.2}\u{00A2}", cents) + } else { + "$0.00".to_string() + } +} + /// otherwise displays with one decimal place. /// Returns a formatted string with proper pluralization ("credit" vs "credits"). pub fn format_credits(credits: f32) -> String { diff --git a/app/src/ai/llms.rs b/app/src/ai/llms.rs index dd9113ac..5a97655a 100644 --- a/app/src/ai/llms.rs +++ b/app/src/ai/llms.rs @@ -605,7 +605,7 @@ impl LLMPreferences { secret_access_key: settings.bedrock_secret_access_key.value().clone(), cross_region_inference: *settings.bedrock_cross_region_inference.value(), fallback_to_warp: *settings.bedrock_fallback_to_warp.value(), - }; + }.with_external_fallbacks(); ctx.spawn( async move { discover_inference_profiles(&config).await }, diff --git a/app/src/settings_view/ai_page.rs b/app/src/settings_view/ai_page.rs index 6185b471..50283980 100644 --- a/app/src/settings_view/ai_page.rs +++ b/app/src/settings_view/ai_page.rs @@ -2743,7 +2743,7 @@ impl TypedActionView for AISettingsPageView { secret_access_key: ai_settings.bedrock_secret_access_key.value().clone(), cross_region_inference: *ai_settings.bedrock_cross_region_inference.value(), fallback_to_warp: *ai_settings.bedrock_fallback_to_warp.value(), - }; + }.with_external_fallbacks(); ctx.spawn( async move { discover_inference_profiles(&config).await }, diff --git a/app/src/terminal/view.rs b/app/src/terminal/view.rs index d905f123..c244b015 100644 --- a/app/src/terminal/view.rs +++ b/app/src/terminal/view.rs @@ -5627,6 +5627,14 @@ impl TerminalView { let wall_to_wall_response_time_ms = conversation.wall_to_wall_response_time_since_last_query(); + let token_usage_list = conversation.total_token_usage(); + let total_input_tokens: u32 = token_usage_list.iter().map(|u| u.total_input).sum(); + let total_output_tokens: u32 = token_usage_list.iter().map(|u| u.output).sum(); + let total_cache_read_tokens: u32 = token_usage_list.iter().map(|u| u.input_cache_read).sum(); + let total_cache_write_tokens: u32 = + token_usage_list.iter().map(|u| u.input_cache_write).sum(); + let estimated_cost_cents: f32 = token_usage_list.iter().map(|u| u.cost_in_cents).sum(); + let conversation_usage_info = ConversationUsageInfo { credits_spent: conversation.credits_spent(), credits_spent_for_last_block: conversation.credits_spent_for_last_block(), @@ -5637,6 +5645,11 @@ impl TerminalView { lines_added: tool_usage.apply_file_diff_stats.lines_added, lines_removed: tool_usage.apply_file_diff_stats.lines_removed, commands_executed: tool_usage.run_command_stats.commands_executed, + total_input_tokens, + total_output_tokens, + total_cache_read_tokens, + total_cache_write_tokens, + estimated_cost_cents, }; let timing_info = TimingInfo { diff --git a/app/src/workspaces/gql_convert.rs b/app/src/workspaces/gql_convert.rs index deedc58b..dff5b5e4 100644 --- a/app/src/workspaces/gql_convert.rs +++ b/app/src/workspaces/gql_convert.rs @@ -264,6 +264,11 @@ impl From<&gql_usage::ConversationUsage> for ConversationUsageInfo { lines_added: tool.apply_file_diff_stats.lines_added, lines_removed: tool.apply_file_diff_stats.lines_removed, commands_executed: tool.run_command_stats.commands_executed, + total_input_tokens: 0, + total_output_tokens: 0, + total_cache_read_tokens: 0, + total_cache_write_tokens: 0, + estimated_cost_cents: 0.0, } } } diff --git a/crates/galaxy_features/src/lib.rs b/crates/galaxy_features/src/lib.rs index 57717e77..0ceca833 100644 --- a/crates/galaxy_features/src/lib.rs +++ b/crates/galaxy_features/src/lib.rs @@ -831,6 +831,18 @@ pub enum FeatureFlag { VerticalTabsSummaryMode, CloudModeInputV2, + + /// Enables LSP-based code completion (autocomplete) in the code editor. + LspCompletion, + + /// Enables LSP code actions (quick fixes, refactorings) in the code editor. + LspCodeActions, + + /// Enables LSP-based symbol rename in the code editor. + LspRename, + + /// Enables LSP signature help (parameter hints) in the code editor. + LspSignatureHelp, } static FLAG_STATES: [AtomicBool; cardinality::()] = diff --git a/crates/lsp/src/config.rs b/crates/lsp/src/config.rs index 71cb9cac..03cb6e07 100644 --- a/crates/lsp/src/config.rs +++ b/crates/lsp/src/config.rs @@ -6,10 +6,14 @@ use anyhow::Result; #[cfg(not(target_arch = "wasm32"))] use command::r#async::Command; use lsp_types::{ - ClientCapabilities, ClientInfo, DidChangeWatchedFilesClientCapabilities, GotoCapability, - HoverClientCapabilities, InitializeParams, MarkupKind, PublishDiagnosticsClientCapabilities, - TextDocumentClientCapabilities, TextDocumentSyncClientCapabilities, Uri, - WindowClientCapabilities, WorkDoneProgressParams, WorkspaceClientCapabilities, WorkspaceFolder, + ClientCapabilities, ClientInfo, CodeActionClientCapabilities, + CompletionClientCapabilities, CompletionItemCapability, + CompletionItemCapabilityResolveSupport, DidChangeWatchedFilesClientCapabilities, + GotoCapability, HoverClientCapabilities, InitializeParams, MarkupKind, + PublishDiagnosticsClientCapabilities, RenameClientCapabilities, + SignatureHelpClientCapabilities, TextDocumentClientCapabilities, + TextDocumentSyncClientCapabilities, Uri, WindowClientCapabilities, WorkDoneProgressParams, + WorkspaceClientCapabilities, WorkspaceFolder, }; use crate::supported_servers::LSPServerType; @@ -301,14 +305,31 @@ fn default_client_capabilities() -> ClientCapabilities { will_save_wait_until: Some(false), did_save: Some(true), }), + completion: Some(CompletionClientCapabilities { + dynamic_registration: Some(false), + completion_item: Some(CompletionItemCapability { + snippet_support: Some(true), + documentation_format: Some(vec![ + MarkupKind::Markdown, + MarkupKind::PlainText, + ]), + resolve_support: Some(CompletionItemCapabilityResolveSupport { + properties: vec![ + "documentation".into(), + "detail".into(), + "additionalTextEdits".into(), + ], + }), + ..Default::default() + }), + ..Default::default() + }), definition: Some(GotoCapability { dynamic_registration: Some(false), link_support: Some(true), }), hover: Some(HoverClientCapabilities { dynamic_registration: Some(false), - // Request Markdown content from the LSP for hover responses. - // This enables proper syntax highlighting in hover tooltips. content_format: Some(vec![MarkupKind::Markdown, MarkupKind::PlainText]), }), publish_diagnostics: Some(PublishDiagnosticsClientCapabilities { @@ -316,6 +337,19 @@ fn default_client_capabilities() -> ClientCapabilities { related_information: Some(true), ..Default::default() }), + signature_help: Some(SignatureHelpClientCapabilities { + dynamic_registration: Some(false), + ..Default::default() + }), + rename: Some(RenameClientCapabilities { + dynamic_registration: Some(false), + prepare_support: Some(true), + ..Default::default() + }), + code_action: Some(CodeActionClientCapabilities { + dynamic_registration: Some(false), + ..Default::default() + }), ..Default::default() }), ..Default::default() diff --git a/crates/lsp/src/lib.rs b/crates/lsp/src/lib.rs index ccedc88b..c48226a1 100644 --- a/crates/lsp/src/lib.rs +++ b/crates/lsp/src/lib.rs @@ -23,14 +23,18 @@ pub use config::{default_init_params, LanguageId, LspServerConfig}; pub use jsonrpc::{JsonRpcService, ServerNotificationEvent, Transport}; pub use lsp_types::{ notification::{self}, - Position, Range, + CompletionItem, Position, Range, }; pub use manager::{LspManagerModel, LspManagerModelEvent}; pub use model::{ BackgroundTaskInfo, DocumentDiagnostics, LanguageServerId, LspEvent, LspServerModel, LspState, }; pub use service::LspService; -pub use types::{HoverContents, HoverResult, MarkupKind, ReferenceLocation}; +pub use types::{ + CodeActionData, CompletionItemData, CompletionKind, CompletionResult, CompletionTrigger, + FileEdits, HoverContents, HoverResult, MarkupKind, ParameterInfo, ParameterLabel, + PrepareRenameResult, ReferenceLocation, RenameResult, SignatureHelpResult, SignatureInfo, +}; #[derive(Copy, Clone, Debug, PartialEq, Eq)] pub enum LspServerLogLevel { diff --git a/crates/lsp/src/model.rs b/crates/lsp/src/model.rs index 1107a818..4d7d90f3 100644 --- a/crates/lsp/src/model.rs +++ b/crates/lsp/src/model.rs @@ -3,16 +3,17 @@ use crate::{ server_repo_watcher::LspRepoWatcher, supported_servers::LSPServerType, types::{ - DefinitionLocation, DocumentVersion, HoverResult, Location, ReferenceLocation, - TextDocumentContentChangeEvent, TextEdit, WatchedFileChangeEvent, + CodeActionData, CompletionResult, CompletionTrigger, DefinitionLocation, DocumentVersion, + HoverResult, Location, PrepareRenameResult, ReferenceLocation, RenameResult, + SignatureHelpResult, TextDocumentContentChangeEvent, TextEdit, WatchedFileChangeEvent, }, LspServerConfig, LspServerLogLevel, LspService, }; use instant::Instant; use lsp_types::{ notification::{self, Notification}, - FormattingOptions, NumberOrString, ProgressParams, ProgressParamsValue, - PublishDiagnosticsParams, WorkDoneProgress, + CompletionItem, CompletionTriggerKind, FormattingOptions, NumberOrString, ProgressParams, + ProgressParamsValue, PublishDiagnosticsParams, Range as LspRange, WorkDoneProgress, }; use std::{ collections::HashMap, @@ -735,6 +736,96 @@ impl LspServerModel { .await }) } + + pub fn completion( + &self, + path: PathBuf, + position: Location, + trigger: CompletionTrigger, + ) -> Result>>> { + let service = self.service()?; + let (trigger_kind, trigger_character) = match trigger { + CompletionTrigger::Invoked => (CompletionTriggerKind::INVOKED, None), + CompletionTrigger::TriggerCharacter(ch) => { + (CompletionTriggerKind::TRIGGER_CHARACTER, Some(ch)) + } + CompletionTrigger::Incomplete => { + (CompletionTriggerKind::TRIGGER_FOR_INCOMPLETE_COMPLETIONS, None) + } + }; + Ok(async move { + service + .text_document() + .completion(&path, position.into_lsp(), Some(trigger_kind), trigger_character) + .await + }) + } + + pub fn completion_resolve( + &self, + item: CompletionItem, + ) -> Result>> { + let service = self.service()?; + Ok(async move { service.text_document().completion_resolve(item).await }) + } + + pub fn signature_help( + &self, + path: PathBuf, + position: Location, + ) -> Result>>> { + let service = self.service()?; + Ok(async move { + service + .text_document() + .signature_help(&path, position.into_lsp()) + .await + }) + } + + pub fn code_actions( + &self, + path: PathBuf, + range: LspRange, + diagnostics: Vec, + ) -> Result>>> { + let service = self.service()?; + Ok(async move { + service + .text_document() + .code_action(&path, range, diagnostics) + .await + }) + } + + pub fn prepare_rename( + &self, + path: PathBuf, + position: Location, + ) -> Result>>> { + let service = self.service()?; + Ok(async move { + service + .text_document() + .prepare_rename(&path, position.into_lsp()) + .await + }) + } + + pub fn rename( + &self, + path: PathBuf, + position: Location, + new_name: String, + ) -> Result>>> { + let service = self.service()?; + Ok(async move { + service + .text_document() + .rename(&path, position.into_lsp(), new_name) + .await + }) + } } impl Entity for LspServerModel { diff --git a/crates/lsp/src/service.rs b/crates/lsp/src/service.rs index 52108e6b..243fda43 100644 --- a/crates/lsp/src/service.rs +++ b/crates/lsp/src/service.rs @@ -7,8 +7,9 @@ use std::{ use crate::{ config::{lsp_uri_to_path, path_to_lsp_uri, LanguageId}, types::{ - HoverResult, LspDefinitionLocation, ReferenceLocation, TextDocumentContentChangeEvent, - TextEdit, WatchedFileChangeEvent, + CodeActionData, CompletionResult, FileEdits, HoverResult, Location, LspDefinitionLocation, + PrepareRenameResult, Range, ReferenceLocation, RenameResult, SignatureHelpResult, + TextDocumentContentChangeEvent, TextEdit, WatchedFileChangeEvent, }, LspServerLogLevel, }; @@ -19,12 +20,14 @@ use jsonrpc::{JsonRpcService, RequestId, ServerNotificationEvent}; use lsp_types::{ notification::{self, Notification}, request::{self, Request}, - CancelParams, DidChangeTextDocumentParams, DidChangeWatchedFilesParams, - DidChangeWatchedFilesRegistrationOptions, DidCloseTextDocumentParams, - DidOpenTextDocumentParams, DocumentFormattingParams, FileChangeType, FileSystemWatcher, - FormattingOptions, GlobPattern, GotoDefinitionParams, GotoDefinitionResponse, HoverParams, - InitializeParams, InitializedParams, NumberOrString, OneOf, Position, ReferenceParams, - RegistrationParams, RelativePattern, TextDocumentIdentifier, TextDocumentItem, + CancelParams, CodeActionContext, CodeActionParams, CompletionContext, CompletionItem, + CompletionParams, CompletionTriggerKind, DidChangeTextDocumentParams, + DidChangeWatchedFilesParams, DidChangeWatchedFilesRegistrationOptions, + DidCloseTextDocumentParams, DidOpenTextDocumentParams, DocumentFormattingParams, + FileChangeType, FileSystemWatcher, FormattingOptions, GlobPattern, GotoDefinitionParams, + GotoDefinitionResponse, HoverParams, InitializeParams, InitializedParams, NumberOrString, + OneOf, Position, Range as LspRange, ReferenceParams, RegistrationParams, RelativePattern, + RenameParams, SignatureHelpParams, TextDocumentIdentifier, TextDocumentItem, TextDocumentPositionParams, UnregistrationParams, VersionedTextDocumentIdentifier, WatchKind, }; use serde_json::Value; @@ -739,4 +742,258 @@ impl<'a> TextDocumentService<'a> { .filter_map(|loc| ReferenceLocation::try_from(loc).ok()) .collect()) } + + pub async fn completion( + &self, + path: &Path, + position: Position, + trigger: Option, + trigger_character: Option, + ) -> anyhow::Result> { + let uri = path_to_lsp_uri(path)?; + + let completion_params = CompletionParams { + text_document_position: TextDocumentPositionParams { + text_document: TextDocumentIdentifier { uri }, + position, + }, + work_done_progress_params: Default::default(), + partial_result_params: Default::default(), + context: Some(CompletionContext { + trigger_kind: trigger.unwrap_or(CompletionTriggerKind::INVOKED), + trigger_character, + }), + }; + + let result = self + .service + .send_request::(completion_params) + .await; + + if let Err(e) = &result { + self.service.log_to_server_log( + LspServerLogLevel::Error, + format!("textDocument/completion failed: {e}"), + ); + } + + Ok(result?.map(Into::into)) + } + + pub async fn completion_resolve( + &self, + item: CompletionItem, + ) -> anyhow::Result { + let result = self + .service + .send_request::(item) + .await; + + if let Err(e) = &result { + self.service.log_to_server_log( + LspServerLogLevel::Error, + format!("completionItem/resolve failed: {e}"), + ); + } + + result + } + + pub async fn signature_help( + &self, + path: &Path, + position: Position, + ) -> anyhow::Result> { + let uri = path_to_lsp_uri(path)?; + + let params = SignatureHelpParams { + text_document_position_params: TextDocumentPositionParams { + text_document: TextDocumentIdentifier { uri }, + position, + }, + work_done_progress_params: Default::default(), + context: None, + }; + + let result = self + .service + .send_request::(params) + .await; + + if let Err(e) = &result { + self.service.log_to_server_log( + LspServerLogLevel::Error, + format!("textDocument/signatureHelp failed: {e}"), + ); + } + + Ok(result?.map(Into::into)) + } + + pub async fn code_action( + &self, + path: &Path, + range: LspRange, + diagnostics: Vec, + ) -> anyhow::Result> { + let uri = path_to_lsp_uri(path)?; + + let params = CodeActionParams { + text_document: TextDocumentIdentifier { uri }, + range, + context: CodeActionContext { + diagnostics, + only: None, + trigger_kind: None, + }, + work_done_progress_params: Default::default(), + partial_result_params: Default::default(), + }; + + let result = self + .service + .send_request::(params) + .await; + + if let Err(e) = &result { + self.service.log_to_server_log( + LspServerLogLevel::Error, + format!("textDocument/codeAction failed: {e}"), + ); + } + + Ok(result? + .unwrap_or_default() + .into_iter() + .map(Into::into) + .collect()) + } + + pub async fn prepare_rename( + &self, + path: &Path, + position: Position, + ) -> anyhow::Result> { + let uri = path_to_lsp_uri(path)?; + + let params = TextDocumentPositionParams { + text_document: TextDocumentIdentifier { uri }, + position, + }; + + let result = self + .service + .send_request::(params) + .await; + + if let Err(e) = &result { + self.service.log_to_server_log( + LspServerLogLevel::Error, + format!("textDocument/prepareRename failed: {e}"), + ); + } + + Ok(result?.map(|response| match response { + lsp_types::PrepareRenameResponse::Range(range) => PrepareRenameResult { + range: range.into(), + placeholder: None, + }, + lsp_types::PrepareRenameResponse::RangeWithPlaceholder { range, placeholder } => { + PrepareRenameResult { + range: range.into(), + placeholder: Some(placeholder), + } + } + lsp_types::PrepareRenameResponse::DefaultBehavior { .. } => PrepareRenameResult { + range: Range { + start: Location { line: 0, column: 0 }, + end: Location { line: 0, column: 0 }, + }, + placeholder: None, + }, + })) + } + + pub async fn rename( + &self, + path: &Path, + position: Position, + new_name: String, + ) -> anyhow::Result> { + let uri = path_to_lsp_uri(path)?; + + let params = RenameParams { + text_document_position: TextDocumentPositionParams { + text_document: TextDocumentIdentifier { uri }, + position, + }, + new_name, + work_done_progress_params: Default::default(), + }; + + let result = self + .service + .send_request::(params) + .await; + + if let Err(e) = &result { + self.service.log_to_server_log( + LspServerLogLevel::Error, + format!("textDocument/rename failed: {e}"), + ); + } + + let workspace_edit = match result? { + Some(edit) => edit, + None => return Ok(None), + }; + + let file_edits = workspace_edit_to_file_edits(workspace_edit)?; + Ok(Some(RenameResult { edits: file_edits })) + } +} + +fn workspace_edit_to_file_edits( + workspace_edit: lsp_types::WorkspaceEdit, +) -> anyhow::Result> { + let mut result: Vec = Vec::new(); + + if let Some(changes) = workspace_edit.changes { + for (uri, edits) in changes { + let path = lsp_uri_to_path(&uri)?; + result.push(FileEdits { + path, + edits: edits.into_iter().map(Into::into).collect(), + }); + } + } + + if let Some(document_changes) = workspace_edit.document_changes { + match document_changes { + lsp_types::DocumentChanges::Edits(edits) => { + for edit in edits { + let path = lsp_uri_to_path(&edit.text_document.uri)?; + result.push(FileEdits { + path, + edits: edit.edits.into_iter().map(|e| match e { + lsp_types::OneOf::Left(text_edit) => text_edit.into(), + lsp_types::OneOf::Right(annotated) => { + lsp_types::TextEdit { + range: annotated.text_edit.range, + new_text: annotated.text_edit.new_text, + } + .into() + } + }).collect(), + }); + } + } + lsp_types::DocumentChanges::Operations(_) => { + // File create/rename/delete operations are not supported yet + log::warn!("workspace/rename returned file operations which are not yet supported"); + } + } + } + + Ok(result) } diff --git a/crates/lsp/src/types.rs b/crates/lsp/src/types.rs index ff080d2d..9ddcea6e 100644 --- a/crates/lsp/src/types.rs +++ b/crates/lsp/src/types.rs @@ -1,7 +1,8 @@ use std::path::PathBuf; use lsp_types::{ - FileChangeType, FileEvent, Location as LspLocation, LocationLink, Position as LspPosition, + CompletionItem, CompletionItemKind, CompletionResponse, FileChangeType, FileEvent, + InsertTextFormat, Location as LspLocation, LocationLink, Position as LspPosition, Range as LspRange, }; @@ -141,6 +142,7 @@ impl TryFrom for ReferenceLocation { } /// Edit returned by the LSP. +#[derive(Debug, Clone)] pub struct TextEdit { pub range: Range, pub text: String, @@ -278,3 +280,301 @@ impl WatchedFileChangeEvent { }) } } + +/// The kind of a completion item, mapped from LSP's CompletionItemKind. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum CompletionKind { + Text, + Method, + Function, + Constructor, + Field, + Variable, + Class, + Interface, + Module, + Property, + Unit, + Value, + Enum, + Keyword, + Snippet, + Color, + File, + Reference, + Folder, + EnumMember, + Constant, + Struct, + Event, + Operator, + TypeParameter, +} + +impl From for CompletionKind { + fn from(kind: CompletionItemKind) -> Self { + match kind { + CompletionItemKind::TEXT => Self::Text, + CompletionItemKind::METHOD => Self::Method, + CompletionItemKind::FUNCTION => Self::Function, + CompletionItemKind::CONSTRUCTOR => Self::Constructor, + CompletionItemKind::FIELD => Self::Field, + CompletionItemKind::VARIABLE => Self::Variable, + CompletionItemKind::CLASS => Self::Class, + CompletionItemKind::INTERFACE => Self::Interface, + CompletionItemKind::MODULE => Self::Module, + CompletionItemKind::PROPERTY => Self::Property, + CompletionItemKind::UNIT => Self::Unit, + CompletionItemKind::VALUE => Self::Value, + CompletionItemKind::ENUM => Self::Enum, + CompletionItemKind::KEYWORD => Self::Keyword, + CompletionItemKind::SNIPPET => Self::Snippet, + CompletionItemKind::COLOR => Self::Color, + CompletionItemKind::FILE => Self::File, + CompletionItemKind::REFERENCE => Self::Reference, + CompletionItemKind::FOLDER => Self::Folder, + CompletionItemKind::ENUM_MEMBER => Self::EnumMember, + CompletionItemKind::CONSTANT => Self::Constant, + CompletionItemKind::STRUCT => Self::Struct, + CompletionItemKind::EVENT => Self::Event, + CompletionItemKind::OPERATOR => Self::Operator, + CompletionItemKind::TYPE_PARAMETER => Self::TypeParameter, + _ => Self::Text, + } + } +} + +/// A single completion item returned from an LSP completion request. +#[derive(Debug, Clone)] +pub struct CompletionItemData { + /// The label displayed in the completion menu. + pub label: String, + /// Additional detail (e.g. type signature), displayed dimmed. + pub detail: Option, + /// The kind of completion (function, variable, etc.). + pub kind: Option, + /// Text used for filtering. Falls back to `label` if None. + pub filter_text: Option, + /// Text to sort completions by. Falls back to `label` if None. + pub sort_text: Option, + /// The text to insert when this completion is accepted. + pub insert_text: String, + /// Whether the insert text is a snippet (contains tab stops like $1, ${2:placeholder}). + pub is_snippet: bool, + /// The range of text to replace when applying this completion. + /// If None, the editor should replace the current word prefix. + pub text_edit_range: Option, + /// Additional text edits (e.g. auto-imports) applied alongside the main insertion. + pub additional_edits: Vec, + /// The original LSP CompletionItem, preserved for resolve requests. + pub raw_item: CompletionItem, +} + +impl CompletionItemData { + pub fn effective_filter_text(&self) -> &str { + self.filter_text.as_deref().unwrap_or(&self.label) + } + + pub fn effective_sort_text(&self) -> &str { + self.sort_text.as_deref().unwrap_or(&self.label) + } +} + +/// The result of a completion request. +#[derive(Debug, Clone)] +pub struct CompletionResult { + /// The completion items. + pub items: Vec, + /// Whether the list is incomplete (server may have more results if the user continues typing). + pub is_incomplete: bool, +} + +impl From for CompletionResult { + fn from(response: CompletionResponse) -> Self { + match response { + CompletionResponse::Array(items) => Self { + items: items.into_iter().map(completion_item_to_data).collect(), + is_incomplete: false, + }, + CompletionResponse::List(list) => Self { + items: list + .items + .into_iter() + .map(completion_item_to_data) + .collect(), + is_incomplete: list.is_incomplete, + }, + } + } +} + +fn completion_item_to_data(item: CompletionItem) -> CompletionItemData { + let is_snippet = item.insert_text_format == Some(InsertTextFormat::SNIPPET); + + let (insert_text, text_edit_range) = if let Some(ref text_edit) = item.text_edit { + match text_edit { + lsp_types::CompletionTextEdit::Edit(edit) => { + (edit.new_text.clone(), Some(edit.range.into())) + } + lsp_types::CompletionTextEdit::InsertAndReplace(edit) => { + (edit.new_text.clone(), Some(edit.insert.into())) + } + } + } else { + let text = item + .insert_text + .clone() + .unwrap_or_else(|| item.label.clone()); + (text, None) + }; + + let additional_edits = item + .additional_text_edits + .as_ref() + .map(|edits| edits.iter().map(|e| TextEdit::from(e.clone())).collect()) + .unwrap_or_default(); + + CompletionItemData { + label: item.label.clone(), + detail: item.detail.clone(), + kind: item.kind.map(Into::into), + filter_text: item.filter_text.clone(), + sort_text: item.sort_text.clone(), + insert_text, + is_snippet, + text_edit_range, + additional_edits, + raw_item: item, + } +} + +/// Trigger context for a completion request. +#[derive(Debug, Clone)] +pub enum CompletionTrigger { + /// User explicitly invoked completion (e.g. Ctrl+Space). + Invoked, + /// A trigger character was typed (e.g. '.', '::', '->'). + TriggerCharacter(String), + /// Re-triggered for an incomplete completion list as the user continues typing. + Incomplete, +} + +/// The result of a signature help request. +#[derive(Debug, Clone)] +pub struct SignatureHelpResult { + pub signatures: Vec, + pub active_signature: Option, + pub active_parameter: Option, +} + +#[derive(Debug, Clone)] +pub struct SignatureInfo { + pub label: String, + pub documentation: Option, + pub parameters: Vec, +} + +#[derive(Debug, Clone)] +pub struct ParameterInfo { + pub label: ParameterLabel, + pub documentation: Option, +} + +#[derive(Debug, Clone)] +pub enum ParameterLabel { + Simple(String), + Offsets(u32, u32), +} + +impl From for SignatureHelpResult { + fn from(help: lsp_types::SignatureHelp) -> Self { + Self { + signatures: help.signatures.into_iter().map(Into::into).collect(), + active_signature: help.active_signature.map(|s| s as usize), + active_parameter: help.active_parameter.map(|p| p as usize), + } + } +} + +impl From for SignatureInfo { + fn from(sig: lsp_types::SignatureInformation) -> Self { + Self { + label: sig.label, + documentation: sig.documentation.map(|doc| match doc { + lsp_types::Documentation::String(s) => s, + lsp_types::Documentation::MarkupContent(m) => m.value, + }), + parameters: sig + .parameters + .unwrap_or_default() + .into_iter() + .map(Into::into) + .collect(), + } + } +} + +impl From for ParameterInfo { + fn from(param: lsp_types::ParameterInformation) -> Self { + Self { + label: match param.label { + lsp_types::ParameterLabel::Simple(s) => ParameterLabel::Simple(s), + lsp_types::ParameterLabel::LabelOffsets(offsets) => { + ParameterLabel::Offsets(offsets[0], offsets[1]) + } + }, + documentation: param.documentation.map(|doc| match doc { + lsp_types::Documentation::String(s) => s, + lsp_types::Documentation::MarkupContent(m) => m.value, + }), + } + } +} + +/// A code action returned from the LSP. +#[derive(Debug, Clone)] +pub struct CodeActionData { + pub title: String, + pub kind: Option, + pub is_preferred: bool, + pub raw: lsp_types::CodeActionOrCommand, +} + +impl From for CodeActionData { + fn from(action_or_cmd: lsp_types::CodeActionOrCommand) -> Self { + match &action_or_cmd { + lsp_types::CodeActionOrCommand::CodeAction(action) => Self { + title: action.title.clone(), + kind: action.kind.as_ref().map(|k| k.as_str().to_string()), + is_preferred: action.is_preferred.unwrap_or(false), + raw: action_or_cmd, + }, + lsp_types::CodeActionOrCommand::Command(cmd) => Self { + title: cmd.title.clone(), + kind: None, + is_preferred: false, + raw: action_or_cmd, + }, + } + } +} + +/// The result of a prepare-rename request. +#[derive(Debug, Clone)] +pub struct PrepareRenameResult { + pub range: Range, + pub placeholder: Option, +} + +/// The result of a rename request — a set of edits across files. +#[derive(Debug, Clone)] +pub struct RenameResult { + pub edits: Vec, +} + +/// Edits for a single file as part of a workspace edit. +#[derive(Debug, Clone)] +pub struct FileEdits { + pub path: PathBuf, + pub edits: Vec, +}