Files
hack/tools/scripts/llm-key-hunter/results/_cache/content/chapzin_jcode-harness.json
T

1 line
1.1 MiB
Plaintext

{"repo": "chapzin/jcode-harness", "files": {"src/cli/args/tests.rs": {"19e08804f268cacb6cc278dd2eb9050e8fff8af9": {"t": 1785745845.8220546, "n": 12531, "h": "515483e567b7668b", "c": "use super::*;\nuse crate::cli::provider_init::ProviderChoice;\n\n#[test]\nfn test_provider_choice_aliases_parse() {\n let args = Args::try_parse_from([\"jcode\", \"--provider\", \"z.ai\", \"run\", \"smoke\"]).unwrap();\n assert_eq!(args.provider, ProviderChoice::Zai);\n\n let args =\n Args::try_parse_from([\"jcode\", \"--provider\", \"kimi-for-coding\", \"run\", \"smoke\"]).unwrap();\n assert_eq!(args.provider, ProviderChoice::Kimi);\n\n let args =\n Args::try_parse_from([\"jcode\", \"--provider\", \"cerebrascode\", \"run\", \"smoke\"]).unwrap();\n assert_eq!(args.provider, ProviderChoice::Cerebras);\n\n let args = Args::try_parse_from([\"jcode\", \"--provider\", \"compat\", \"run\", \"smoke\"]).unwrap();\n assert_eq!(args.provider, ProviderChoice::OpenaiCompatible);\n\n let args = Args::try_parse_from([\"jcode\", \"--provider\", \"bailian\", \"run\", \"smoke\"]).unwrap();\n assert_eq!(args.provider, ProviderChoice::AlibabaCodingPlan);\n\n let args = Args::try_parse_from([\"jcode\", \"--provider\", \"together\", \"run\", \"smoke\"]).unwrap();\n assert_eq!(args.provider, ProviderChoice::TogetherAi);\n\n let args = Args::try_parse_from([\"jcode\", \"--provider\", \"grok\", \"run\", \"smoke\"]).unwrap();\n assert_eq!(args.provider, ProviderChoice::Xai);\n\n let args = Args::try_parse_from([\"jcode\", \"--provider\", \"cgc\", \"run\", \"smoke\"]).unwrap();\n assert_eq!(args.provider, ProviderChoice::Comtegra);\n}\n\n#[test]\nfn model_list_subcommand_parses() {\n let args = Args::try_parse_from([\"jcode\", \"model\", \"list\", \"--json\", \"--verbose\"]).unwrap();\n match args.command {\n Some(Command::Model(ModelCommand::List { json, verbose })) => {\n assert!(json);\n assert!(verbose);\n }\n other => panic!(\"unexpected command: {:?}\", other),\n }\n}\n\n#[test]\nfn session_rename_subcommand_parses() {\n let args = Args::try_parse_from([\n \"jcode\",\n \"session\",\n \"rename\",\n \"fox\",\n \"release planning\",\n \"--json\",\n ])\n .unwrap();\n match args.command {\n Some(Command::Session(SessionCommand::Rename {\n session,\n name,\n clear,\n json,\n })) => {\n assert_eq!(session, \"fox\");\n assert_eq!(name.as_deref(), Some(\"release planning\"));\n assert!(!clear);\n assert!(json);\n }\n other => panic!(\"unexpected command: {:?}\", other),\n }\n\n let args = Args::try_parse_from([\"jcode\", \"session\", \"rename\", \"fox\", \"--clear\"]).unwrap();\n match args.command {\n Some(Command::Session(SessionCommand::Rename {\n session,\n name,\n clear,\n json,\n })) => {\n assert_eq!(session, \"fox\");\n assert!(name.is_none());\n assert!(clear);\n assert!(!json);\n }\n other => panic!(\"unexpected command: {:?}\", other),\n }\n}\n\n#[test]\nfn login_no_browser_flag_parses() {\n let args = Args::try_parse_from([\"jcode\", \"login\", \"--no-browser\"]).unwrap();\n match args.command {\n Some(Command::Login {\n account,\n no_browser,\n print_auth_url,\n callback_url,\n auth_code,\n json,\n complete,\n google_access_tier,\n api_base,\n api_key,\n api_key_env,\n }) => {\n assert!(account.is_none());\n assert!(no_browser);\n assert!(!print_auth_url);\n assert!(callback_url.is_none());\n assert!(auth_code.is_none());\n assert!(!json);\n assert!(!complete);\n assert!(google_access_tier.is_none());\n assert!(api_base.is_none());\n assert!(api_key.is_none());\n assert!(api_key_env.is_none());\n }\n other => panic!(\"unexpected command: {:?}\", other),\n }\n\n let args = Args::try_parse_from([\"jcode\", \"login\", \"--headless\"]).unwrap();\n match args.command {\n Some(Command::Login { no_browser, .. }) => assert!(no_browser),\n other => panic!(\"unexpected command: {:?}\", other),\n }\n}\n\n#[test]\nfn login_openai_compatible_scriptable_flags_parse() {\n let args = Args::try_parse_from([\n \"jcode\",\n \"--provider\",\n \"openai-compatible\",\n \"--model\",\n \"deepseek-v4-flash\",\n \"login\",\n \"--api-base\",\n \"https://api.deepseek.com\",\n \"--api-key-env\",\n \"DEEPSEEK_API_KEY\",\n ])\n .unwrap();\n assert_eq!(args.provider, ProviderChoice::OpenaiCompatible);\n assert_eq!(args.model.as_deref(), Some(\"deepseek-v4-flash\"));\n match args.command {\n Some(Command::Login {\n api_base,\n api_key_env,\n ..\n }) => {\n assert_eq!(api_base.as_deref(), Some(\"https://api.deepseek.com\"));\n assert_eq!(api_key_env.as_deref(), Some(\"DEEPSEEK_API_KEY\"));\n }\n other => panic!(\"unexpected command: {:?}\", other),\n }\n}\n\n#[test]\nfn login_openai_compatible_accepts_global_provider_and_model_after_subcommand() {\n let args = Args::try_parse_from([\n \"jcode\",\n \"login\",\n \"--provider\",\n \"openai-compatible\",\n \"--api-base\",\n \"https://api.deepseek.com\",\n \"--model\",\n \"deepseek-v4-flash\",\n ])\n .unwrap();\n\n assert_eq!(args.provider, ProviderChoice::OpenaiCompatible);\n assert_eq!(args.model.as_deref(), Some(\"deepseek-v4-flash\"));\n match args.command {\n Some(Command::Login { api_base, .. }) => {\n assert_eq!(api_base.as_deref(), Some(\"https://api.deepseek.com\"));\n }\n other => panic!(\"unexpected command: {:?}\", other),\n }\n}\n\n#[test]\nfn login_scriptable_flags_parse() {\n let args = Args::try_parse_from([\"jcode\", \"login\", \"--print-auth-url\", \"--json\"]).unwrap();\n match args.command {\n Some(Command::Login {\n print_auth_url,\n json,\n callback_url,\n auth_code,\n complete,\n google_access_tier,\n ..\n }) => {\n assert!(print_auth_url);\n assert!(json);\n assert!(callback_url.is_none());\n assert!(auth_code.is_none());\n assert!(!complete);\n assert!(google_access_tier.is_none());\n }\n other => panic!(\"unexpected command: {:?}\", other),\n }\n\n let args = Args::try_parse_from([\n \"jcode\",\n \"login\",\n \"--callback-url\",\n \"http://localhost:1455/auth/callback?code=x&state=y\",\n ])\n .unwrap();\n match args.command {\n Some(Command::Login { callback_url, .. }) => {\n assert_eq!(\n callback_url.as_deref(),\n Some(\"http://localhost:1455/auth/callback?code=x&state=y\")\n );\n }\n other => panic!(\"unexpected command: {:?}\", other),\n }\n\n let args = Args::try_parse_from([\"jcode\", \"login\", \"--auth-code\", \"abc123\"]).unwrap();\n match args.command {\n Some(Command::Login { auth_code, .. }) => {\n assert_eq!(auth_code.as_deref(), Some(\"abc123\"));\n }\n other => panic!(\"unexpected command: {:?}\", other),\n }\n\n let args = Args::try_parse_from([\n \"jcode\",\n \"login\",\n \"--complete\",\n \"--google-access-tier\",\n \"readonly\",\n ])\n .unwrap();\n match args.command {\n Some(Command::Login {\n complete,\n google_access_tier,\n ..\n }) => {\n assert!(complete);\n assert_eq!(google_access_tier, Some(GoogleAccessTierArg::Readonly));\n }\n other => panic!(\"unexpected command: {:?}\", other),\n }\n}\n\n#[test]\nfn quiet_global_flag_parses() {\n let args = Args::try_parse_from([\"jcode\", \"--quiet\", \"model\", \"list\"]).unwrap();\n assert!(args.quiet);\n}\n\n#[test]\nfn run_json_subcommand_parses() {\n let args = Args::try_parse_from([\"jcode\", \"run\", \"--json\", \"hello\"]).unwrap();\n match args.command {\n Some(Command::Run {\n json,\n ndjson,\n message,\n }) => {\n assert!(json);\n assert!(!ndjson);\n assert_eq!(message, \"hello\");\n }\n other => panic!(\"unexpected command: {:?}\", other),\n }\n}\n\n#[test]\nfn run_ndjson_subcommand_parses() {\n let args = Args::try_parse_from([\"jcode\", \"run\", \"--ndjson\", \"hello\"]).unwrap();\n match args.command {\n Some(Command::Run {\n json,\n ndjson,\n message,\n }) => {\n assert!(!json);\n assert!(ndjson);\n assert_eq!(message, \"hello\");\n }\n other => panic!(\"unexpected command: {:?}\", other),\n }\n}\n\n#[test]\nfn version_subcommand_parses() {\n let args = Args::try_parse_from([\"jcode\", \"version\", \"--json\"]).unwrap();\n match args.command {\n Some(Command::Version { json }) => assert!(json),\n other => panic!(\"unexpected command: {:?}\", other),\n }\n}\n\n#[test]\nfn usage_subcommand_parses() {\n let args = Args::try_parse_from([\"jcode\", \"usage\", \"--json\"]).unwrap();\n match args.command {\n Some(Command::Usage { json }) => assert!(json),\n other => panic!(\"unexpected command: {:?}\", other),\n }\n}\n\n#[test]\nfn auth_status_subcommand_parses() {\n let args = Args::try_parse_from([\"jcode\", \"auth\", \"status\", \"--json\"]).unwrap();\n match args.command {\n Some(Command::Auth(AuthCommand::Status { json })) => assert!(json),\n other => panic!(\"unexpected command: {:?}\", other),\n }\n}\n\n#[test]\nfn auth_doctor_subcommand_parses() {\n let args = Args::try_parse_from([\"jcode\", \"auth\", \"doctor\", \"openai\", \"--validate\", \"--json\"])\n .unwrap();\n match args.command {\n Some(Command::Auth(AuthCommand::Doctor {\n provider,\n validate,\n json,\n })) => {\n assert_eq!(provider.as_deref(), Some(\"openai\"));\n assert!(validate);\n assert!(json);\n }\n other => panic!(\"unexpected command: {:?}\", other),\n }\n}\n\n#[test]\nfn provider_list_subcommand_parses() {\n let args = Args::try_parse_from([\"jcode\", \"provider\", \"list\", \"--json\"]).unwrap();\n match args.command {\n Some(Command::Provider(ProviderCommand::List { json })) => assert!(json),\n other => panic!(\"unexpected command: {:?}\", other),\n }\n}\n\n#[test]\nfn provider_current_subcommand_parses() {\n let args = Args::try_parse_from([\"jcode\", \"provider\", \"current\", \"--json\"]).unwrap();\n match args.command {\n Some(Command::Provider(ProviderCommand::Current { json })) => assert!(json),\n other => panic!(\"unexpected command: {:?}\", other),\n }\n}\n\n#[test]\nfn provider_add_subcommand_parses_agent_friendly_flags() {\n let args = Args::try_parse_from([\n \"jcode\",\n \"provider\",\n \"add\",\n \"my-api\",\n \"--base-url\",\n \"https://llm.example.com/v1\",\n \"--model\",\n \"model-a\",\n \"--context-window\",\n \"128000\",\n \"--api-key-stdin\",\n \"--auth\",\n \"bearer\",\n \"--set-default\",\n \"--json\",\n ])\n .unwrap();\n\n match args.command {\n Some(Command::Provider(ProviderCommand::Add {\n name,\n base_url,\n model,\n context_window,\n api_key_stdin,\n auth,\n set_default,\n json,\n ..\n })) => {\n assert_eq!(name, \"my-api\");\n assert_eq!(base_url, \"https://llm.example.com/v1\");\n assert_eq!(model, \"model-a\");\n assert_eq!(context_window, Some(128000));\n assert!(api_key_stdin);\n assert_eq!(auth, Some(ProviderAuthArg::Bearer));\n assert!(set_default);\n assert!(json);\n }\n other => panic!(\"unexpected command: {:?}\", other),\n }\n}\n\n#[test]\nfn restart_save_subcommand_parses() {\n let args = Args::try_parse_from([\"jcode\", \"restart\", \"save\"]).unwrap();\n match args.command {\n Some(Command::Restart {\n action: RestartCommand::Save {\n auto_restore: false,\n },\n }) => {}\n other => panic!(\"unexpected command: {:?}\", other),\n }\n}\n\n#[test]\nfn restart_save_auto_restore_flag_parses() {\n let args = Args::try_parse_from([\"jcode\", \"restart\", \"save\", \"--auto-restore\"]).unwrap();\n match args.command {\n Some(Command::Restart {\n action: RestartCommand::Save { auto_restore: true },\n }) => {}\n other => panic!(\"unexpected command: {:?}\", other),\n }\n}\n"}, "e48894603c6459e0d52df43c69861463b1941e4b": {"t": 1785757202.3711276, "n": 12531, "h": "515483e567b7668b", "c": "use super::*;\nuse crate::cli::provider_init::ProviderChoice;\n\n#[test]\nfn test_provider_choice_aliases_parse() {\n let args = Args::try_parse_from([\"jcode\", \"--provider\", \"z.ai\", \"run\", \"smoke\"]).unwrap();\n assert_eq!(args.provider, ProviderChoice::Zai);\n\n let args =\n Args::try_parse_from([\"jcode\", \"--provider\", \"kimi-for-coding\", \"run\", \"smoke\"]).unwrap();\n assert_eq!(args.provider, ProviderChoice::Kimi);\n\n let args =\n Args::try_parse_from([\"jcode\", \"--provider\", \"cerebrascode\", \"run\", \"smoke\"]).unwrap();\n assert_eq!(args.provider, ProviderChoice::Cerebras);\n\n let args = Args::try_parse_from([\"jcode\", \"--provider\", \"compat\", \"run\", \"smoke\"]).unwrap();\n assert_eq!(args.provider, ProviderChoice::OpenaiCompatible);\n\n let args = Args::try_parse_from([\"jcode\", \"--provider\", \"bailian\", \"run\", \"smoke\"]).unwrap();\n assert_eq!(args.provider, ProviderChoice::AlibabaCodingPlan);\n\n let args = Args::try_parse_from([\"jcode\", \"--provider\", \"together\", \"run\", \"smoke\"]).unwrap();\n assert_eq!(args.provider, ProviderChoice::TogetherAi);\n\n let args = Args::try_parse_from([\"jcode\", \"--provider\", \"grok\", \"run\", \"smoke\"]).unwrap();\n assert_eq!(args.provider, ProviderChoice::Xai);\n\n let args = Args::try_parse_from([\"jcode\", \"--provider\", \"cgc\", \"run\", \"smoke\"]).unwrap();\n assert_eq!(args.provider, ProviderChoice::Comtegra);\n}\n\n#[test]\nfn model_list_subcommand_parses() {\n let args = Args::try_parse_from([\"jcode\", \"model\", \"list\", \"--json\", \"--verbose\"]).unwrap();\n match args.command {\n Some(Command::Model(ModelCommand::List { json, verbose })) => {\n assert!(json);\n assert!(verbose);\n }\n other => panic!(\"unexpected command: {:?}\", other),\n }\n}\n\n#[test]\nfn session_rename_subcommand_parses() {\n let args = Args::try_parse_from([\n \"jcode\",\n \"session\",\n \"rename\",\n \"fox\",\n \"release planning\",\n \"--json\",\n ])\n .unwrap();\n match args.command {\n Some(Command::Session(SessionCommand::Rename {\n session,\n name,\n clear,\n json,\n })) => {\n assert_eq!(session, \"fox\");\n assert_eq!(name.as_deref(), Some(\"release planning\"));\n assert!(!clear);\n assert!(json);\n }\n other => panic!(\"unexpected command: {:?}\", other),\n }\n\n let args = Args::try_parse_from([\"jcode\", \"session\", \"rename\", \"fox\", \"--clear\"]).unwrap();\n match args.command {\n Some(Command::Session(SessionCommand::Rename {\n session,\n name,\n clear,\n json,\n })) => {\n assert_eq!(session, \"fox\");\n assert!(name.is_none());\n assert!(clear);\n assert!(!json);\n }\n other => panic!(\"unexpected command: {:?}\", other),\n }\n}\n\n#[test]\nfn login_no_browser_flag_parses() {\n let args = Args::try_parse_from([\"jcode\", \"login\", \"--no-browser\"]).unwrap();\n match args.command {\n Some(Command::Login {\n account,\n no_browser,\n print_auth_url,\n callback_url,\n auth_code,\n json,\n complete,\n google_access_tier,\n api_base,\n api_key,\n api_key_env,\n }) => {\n assert!(account.is_none());\n assert!(no_browser);\n assert!(!print_auth_url);\n assert!(callback_url.is_none());\n assert!(auth_code.is_none());\n assert!(!json);\n assert!(!complete);\n assert!(google_access_tier.is_none());\n assert!(api_base.is_none());\n assert!(api_key.is_none());\n assert!(api_key_env.is_none());\n }\n other => panic!(\"unexpected command: {:?}\", other),\n }\n\n let args = Args::try_parse_from([\"jcode\", \"login\", \"--headless\"]).unwrap();\n match args.command {\n Some(Command::Login { no_browser, .. }) => assert!(no_browser),\n other => panic!(\"unexpected command: {:?}\", other),\n }\n}\n\n#[test]\nfn login_openai_compatible_scriptable_flags_parse() {\n let args = Args::try_parse_from([\n \"jcode\",\n \"--provider\",\n \"openai-compatible\",\n \"--model\",\n \"deepseek-v4-flash\",\n \"login\",\n \"--api-base\",\n \"https://api.deepseek.com\",\n \"--api-key-env\",\n \"DEEPSEEK_API_KEY\",\n ])\n .unwrap();\n assert_eq!(args.provider, ProviderChoice::OpenaiCompatible);\n assert_eq!(args.model.as_deref(), Some(\"deepseek-v4-flash\"));\n match args.command {\n Some(Command::Login {\n api_base,\n api_key_env,\n ..\n }) => {\n assert_eq!(api_base.as_deref(), Some(\"https://api.deepseek.com\"));\n assert_eq!(api_key_env.as_deref(), Some(\"DEEPSEEK_API_KEY\"));\n }\n other => panic!(\"unexpected command: {:?}\", other),\n }\n}\n\n#[test]\nfn login_openai_compatible_accepts_global_provider_and_model_after_subcommand() {\n let args = Args::try_parse_from([\n \"jcode\",\n \"login\",\n \"--provider\",\n \"openai-compatible\",\n \"--api-base\",\n \"https://api.deepseek.com\",\n \"--model\",\n \"deepseek-v4-flash\",\n ])\n .unwrap();\n\n assert_eq!(args.provider, ProviderChoice::OpenaiCompatible);\n assert_eq!(args.model.as_deref(), Some(\"deepseek-v4-flash\"));\n match args.command {\n Some(Command::Login { api_base, .. }) => {\n assert_eq!(api_base.as_deref(), Some(\"https://api.deepseek.com\"));\n }\n other => panic!(\"unexpected command: {:?}\", other),\n }\n}\n\n#[test]\nfn login_scriptable_flags_parse() {\n let args = Args::try_parse_from([\"jcode\", \"login\", \"--print-auth-url\", \"--json\"]).unwrap();\n match args.command {\n Some(Command::Login {\n print_auth_url,\n json,\n callback_url,\n auth_code,\n complete,\n google_access_tier,\n ..\n }) => {\n assert!(print_auth_url);\n assert!(json);\n assert!(callback_url.is_none());\n assert!(auth_code.is_none());\n assert!(!complete);\n assert!(google_access_tier.is_none());\n }\n other => panic!(\"unexpected command: {:?}\", other),\n }\n\n let args = Args::try_parse_from([\n \"jcode\",\n \"login\",\n \"--callback-url\",\n \"http://localhost:1455/auth/callback?code=x&state=y\",\n ])\n .unwrap();\n match args.command {\n Some(Command::Login { callback_url, .. }) => {\n assert_eq!(\n callback_url.as_deref(),\n Some(\"http://localhost:1455/auth/callback?code=x&state=y\")\n );\n }\n other => panic!(\"unexpected command: {:?}\", other),\n }\n\n let args = Args::try_parse_from([\"jcode\", \"login\", \"--auth-code\", \"abc123\"]).unwrap();\n match args.command {\n Some(Command::Login { auth_code, .. }) => {\n assert_eq!(auth_code.as_deref(), Some(\"abc123\"));\n }\n other => panic!(\"unexpected command: {:?}\", other),\n }\n\n let args = Args::try_parse_from([\n \"jcode\",\n \"login\",\n \"--complete\",\n \"--google-access-tier\",\n \"readonly\",\n ])\n .unwrap();\n match args.command {\n Some(Command::Login {\n complete,\n google_access_tier,\n ..\n }) => {\n assert!(complete);\n assert_eq!(google_access_tier, Some(GoogleAccessTierArg::Readonly));\n }\n other => panic!(\"unexpected command: {:?}\", other),\n }\n}\n\n#[test]\nfn quiet_global_flag_parses() {\n let args = Args::try_parse_from([\"jcode\", \"--quiet\", \"model\", \"list\"]).unwrap();\n assert!(args.quiet);\n}\n\n#[test]\nfn run_json_subcommand_parses() {\n let args = Args::try_parse_from([\"jcode\", \"run\", \"--json\", \"hello\"]).unwrap();\n match args.command {\n Some(Command::Run {\n json,\n ndjson,\n message,\n }) => {\n assert!(json);\n assert!(!ndjson);\n assert_eq!(message, \"hello\");\n }\n other => panic!(\"unexpected command: {:?}\", other),\n }\n}\n\n#[test]\nfn run_ndjson_subcommand_parses() {\n let args = Args::try_parse_from([\"jcode\", \"run\", \"--ndjson\", \"hello\"]).unwrap();\n match args.command {\n Some(Command::Run {\n json,\n ndjson,\n message,\n }) => {\n assert!(!json);\n assert!(ndjson);\n assert_eq!(message, \"hello\");\n }\n other => panic!(\"unexpected command: {:?}\", other),\n }\n}\n\n#[test]\nfn version_subcommand_parses() {\n let args = Args::try_parse_from([\"jcode\", \"version\", \"--json\"]).unwrap();\n match args.command {\n Some(Command::Version { json }) => assert!(json),\n other => panic!(\"unexpected command: {:?}\", other),\n }\n}\n\n#[test]\nfn usage_subcommand_parses() {\n let args = Args::try_parse_from([\"jcode\", \"usage\", \"--json\"]).unwrap();\n match args.command {\n Some(Command::Usage { json }) => assert!(json),\n other => panic!(\"unexpected command: {:?}\", other),\n }\n}\n\n#[test]\nfn auth_status_subcommand_parses() {\n let args = Args::try_parse_from([\"jcode\", \"auth\", \"status\", \"--json\"]).unwrap();\n match args.command {\n Some(Command::Auth(AuthCommand::Status { json })) => assert!(json),\n other => panic!(\"unexpected command: {:?}\", other),\n }\n}\n\n#[test]\nfn auth_doctor_subcommand_parses() {\n let args = Args::try_parse_from([\"jcode\", \"auth\", \"doctor\", \"openai\", \"--validate\", \"--json\"])\n .unwrap();\n match args.command {\n Some(Command::Auth(AuthCommand::Doctor {\n provider,\n validate,\n json,\n })) => {\n assert_eq!(provider.as_deref(), Some(\"openai\"));\n assert!(validate);\n assert!(json);\n }\n other => panic!(\"unexpected command: {:?}\", other),\n }\n}\n\n#[test]\nfn provider_list_subcommand_parses() {\n let args = Args::try_parse_from([\"jcode\", \"provider\", \"list\", \"--json\"]).unwrap();\n match args.command {\n Some(Command::Provider(ProviderCommand::List { json })) => assert!(json),\n other => panic!(\"unexpected command: {:?}\", other),\n }\n}\n\n#[test]\nfn provider_current_subcommand_parses() {\n let args = Args::try_parse_from([\"jcode\", \"provider\", \"current\", \"--json\"]).unwrap();\n match args.command {\n Some(Command::Provider(ProviderCommand::Current { json })) => assert!(json),\n other => panic!(\"unexpected command: {:?}\", other),\n }\n}\n\n#[test]\nfn provider_add_subcommand_parses_agent_friendly_flags() {\n let args = Args::try_parse_from([\n \"jcode\",\n \"provider\",\n \"add\",\n \"my-api\",\n \"--base-url\",\n \"https://llm.example.com/v1\",\n \"--model\",\n \"model-a\",\n \"--context-window\",\n \"128000\",\n \"--api-key-stdin\",\n \"--auth\",\n \"bearer\",\n \"--set-default\",\n \"--json\",\n ])\n .unwrap();\n\n match args.command {\n Some(Command::Provider(ProviderCommand::Add {\n name,\n base_url,\n model,\n context_window,\n api_key_stdin,\n auth,\n set_default,\n json,\n ..\n })) => {\n assert_eq!(name, \"my-api\");\n assert_eq!(base_url, \"https://llm.example.com/v1\");\n assert_eq!(model, \"model-a\");\n assert_eq!(context_window, Some(128000));\n assert!(api_key_stdin);\n assert_eq!(auth, Some(ProviderAuthArg::Bearer));\n assert!(set_default);\n assert!(json);\n }\n other => panic!(\"unexpected command: {:?}\", other),\n }\n}\n\n#[test]\nfn restart_save_subcommand_parses() {\n let args = Args::try_parse_from([\"jcode\", \"restart\", \"save\"]).unwrap();\n match args.command {\n Some(Command::Restart {\n action: RestartCommand::Save {\n auto_restore: false,\n },\n }) => {}\n other => panic!(\"unexpected command: {:?}\", other),\n }\n}\n\n#[test]\nfn restart_save_auto_restore_flag_parses() {\n let args = Args::try_parse_from([\"jcode\", \"restart\", \"save\", \"--auto-restore\"]).unwrap();\n match args.command {\n Some(Command::Restart {\n action: RestartCommand::Save { auto_restore: true },\n }) => {}\n other => panic!(\"unexpected command: {:?}\", other),\n }\n}\n"}, "f7dd546e85c39e96499852a5bdeda35ae4497946": {"t": 1785757203.7892213, "n": 11414, "h": "390224dc69fd0157", "c": "use super::*;\nuse crate::cli::provider_init::ProviderChoice;\n\n#[test]\nfn test_provider_choice_aliases_parse() {\n let args = Args::try_parse_from([\"jcode\", \"--provider\", \"z.ai\", \"run\", \"smoke\"]).unwrap();\n assert_eq!(args.provider, ProviderChoice::Zai);\n\n let args =\n Args::try_parse_from([\"jcode\", \"--provider\", \"kimi-for-coding\", \"run\", \"smoke\"]).unwrap();\n assert_eq!(args.provider, ProviderChoice::Kimi);\n\n let args =\n Args::try_parse_from([\"jcode\", \"--provider\", \"cerebrascode\", \"run\", \"smoke\"]).unwrap();\n assert_eq!(args.provider, ProviderChoice::Cerebras);\n\n let args = Args::try_parse_from([\"jcode\", \"--provider\", \"compat\", \"run\", \"smoke\"]).unwrap();\n assert_eq!(args.provider, ProviderChoice::OpenaiCompatible);\n\n let args = Args::try_parse_from([\"jcode\", \"--provider\", \"bailian\", \"run\", \"smoke\"]).unwrap();\n assert_eq!(args.provider, ProviderChoice::AlibabaCodingPlan);\n\n let args = Args::try_parse_from([\"jcode\", \"--provider\", \"together\", \"run\", \"smoke\"]).unwrap();\n assert_eq!(args.provider, ProviderChoice::TogetherAi);\n\n let args = Args::try_parse_from([\"jcode\", \"--provider\", \"grok\", \"run\", \"smoke\"]).unwrap();\n assert_eq!(args.provider, ProviderChoice::Xai);\n\n let args = Args::try_parse_from([\"jcode\", \"--provider\", \"cgc\", \"run\", \"smoke\"]).unwrap();\n assert_eq!(args.provider, ProviderChoice::Comtegra);\n}\n\n#[test]\nfn model_list_subcommand_parses() {\n let args = Args::try_parse_from([\"jcode\", \"model\", \"list\", \"--json\", \"--verbose\"]).unwrap();\n match args.command {\n Some(Command::Model(ModelCommand::List { json, verbose })) => {\n assert!(json);\n assert!(verbose);\n }\n other => panic!(\"unexpected command: {:?}\", other),\n }\n}\n\n#[test]\nfn login_no_browser_flag_parses() {\n let args = Args::try_parse_from([\"jcode\", \"login\", \"--no-browser\"]).unwrap();\n match args.command {\n Some(Command::Login {\n account,\n no_browser,\n print_auth_url,\n callback_url,\n auth_code,\n json,\n complete,\n google_access_tier,\n api_base,\n api_key,\n api_key_env,\n }) => {\n assert!(account.is_none());\n assert!(no_browser);\n assert!(!print_auth_url);\n assert!(callback_url.is_none());\n assert!(auth_code.is_none());\n assert!(!json);\n assert!(!complete);\n assert!(google_access_tier.is_none());\n assert!(api_base.is_none());\n assert!(api_key.is_none());\n assert!(api_key_env.is_none());\n }\n other => panic!(\"unexpected command: {:?}\", other),\n }\n\n let args = Args::try_parse_from([\"jcode\", \"login\", \"--headless\"]).unwrap();\n match args.command {\n Some(Command::Login { no_browser, .. }) => assert!(no_browser),\n other => panic!(\"unexpected command: {:?}\", other),\n }\n}\n\n#[test]\nfn login_openai_compatible_scriptable_flags_parse() {\n let args = Args::try_parse_from([\n \"jcode\",\n \"--provider\",\n \"openai-compatible\",\n \"--model\",\n \"deepseek-v4-flash\",\n \"login\",\n \"--api-base\",\n \"https://api.deepseek.com\",\n \"--api-key-env\",\n \"DEEPSEEK_API_KEY\",\n ])\n .unwrap();\n assert_eq!(args.provider, ProviderChoice::OpenaiCompatible);\n assert_eq!(args.model.as_deref(), Some(\"deepseek-v4-flash\"));\n match args.command {\n Some(Command::Login {\n api_base,\n api_key_env,\n ..\n }) => {\n assert_eq!(api_base.as_deref(), Some(\"https://api.deepseek.com\"));\n assert_eq!(api_key_env.as_deref(), Some(\"DEEPSEEK_API_KEY\"));\n }\n other => panic!(\"unexpected command: {:?}\", other),\n }\n}\n\n#[test]\nfn login_openai_compatible_accepts_global_provider_and_model_after_subcommand() {\n let args = Args::try_parse_from([\n \"jcode\",\n \"login\",\n \"--provider\",\n \"openai-compatible\",\n \"--api-base\",\n \"https://api.deepseek.com\",\n \"--model\",\n \"deepseek-v4-flash\",\n ])\n .unwrap();\n\n assert_eq!(args.provider, ProviderChoice::OpenaiCompatible);\n assert_eq!(args.model.as_deref(), Some(\"deepseek-v4-flash\"));\n match args.command {\n Some(Command::Login { api_base, .. }) => {\n assert_eq!(api_base.as_deref(), Some(\"https://api.deepseek.com\"));\n }\n other => panic!(\"unexpected command: {:?}\", other),\n }\n}\n\n#[test]\nfn login_scriptable_flags_parse() {\n let args = Args::try_parse_from([\"jcode\", \"login\", \"--print-auth-url\", \"--json\"]).unwrap();\n match args.command {\n Some(Command::Login {\n print_auth_url,\n json,\n callback_url,\n auth_code,\n complete,\n google_access_tier,\n ..\n }) => {\n assert!(print_auth_url);\n assert!(json);\n assert!(callback_url.is_none());\n assert!(auth_code.is_none());\n assert!(!complete);\n assert!(google_access_tier.is_none());\n }\n other => panic!(\"unexpected command: {:?}\", other),\n }\n\n let args = Args::try_parse_from([\n \"jcode\",\n \"login\",\n \"--callback-url\",\n \"http://localhost:1455/auth/callback?code=x&state=y\",\n ])\n .unwrap();\n match args.command {\n Some(Command::Login { callback_url, .. }) => {\n assert_eq!(\n callback_url.as_deref(),\n Some(\"http://localhost:1455/auth/callback?code=x&state=y\")\n );\n }\n other => panic!(\"unexpected command: {:?}\", other),\n }\n\n let args = Args::try_parse_from([\"jcode\", \"login\", \"--auth-code\", \"abc123\"]).unwrap();\n match args.command {\n Some(Command::Login { auth_code, .. }) => {\n assert_eq!(auth_code.as_deref(), Some(\"abc123\"));\n }\n other => panic!(\"unexpected command: {:?}\", other),\n }\n\n let args = Args::try_parse_from([\n \"jcode\",\n \"login\",\n \"--complete\",\n \"--google-access-tier\",\n \"readonly\",\n ])\n .unwrap();\n match args.command {\n Some(Command::Login {\n complete,\n google_access_tier,\n ..\n }) => {\n assert!(complete);\n assert_eq!(google_access_tier, Some(GoogleAccessTierArg::Readonly));\n }\n other => panic!(\"unexpected command: {:?}\", other),\n }\n}\n\n#[test]\nfn quiet_global_flag_parses() {\n let args = Args::try_parse_from([\"jcode\", \"--quiet\", \"model\", \"list\"]).unwrap();\n assert!(args.quiet);\n}\n\n#[test]\nfn run_json_subcommand_parses() {\n let args = Args::try_parse_from([\"jcode\", \"run\", \"--json\", \"hello\"]).unwrap();\n match args.command {\n Some(Command::Run {\n json,\n ndjson,\n message,\n }) => {\n assert!(json);\n assert!(!ndjson);\n assert_eq!(message, \"hello\");\n }\n other => panic!(\"unexpected command: {:?}\", other),\n }\n}\n\n#[test]\nfn run_ndjson_subcommand_parses() {\n let args = Args::try_parse_from([\"jcode\", \"run\", \"--ndjson\", \"hello\"]).unwrap();\n match args.command {\n Some(Command::Run {\n json,\n ndjson,\n message,\n }) => {\n assert!(!json);\n assert!(ndjson);\n assert_eq!(message, \"hello\");\n }\n other => panic!(\"unexpected command: {:?}\", other),\n }\n}\n\n#[test]\nfn version_subcommand_parses() {\n let args = Args::try_parse_from([\"jcode\", \"version\", \"--json\"]).unwrap();\n match args.command {\n Some(Command::Version { json }) => assert!(json),\n other => panic!(\"unexpected command: {:?}\", other),\n }\n}\n\n#[test]\nfn usage_subcommand_parses() {\n let args = Args::try_parse_from([\"jcode\", \"usage\", \"--json\"]).unwrap();\n match args.command {\n Some(Command::Usage { json }) => assert!(json),\n other => panic!(\"unexpected command: {:?}\", other),\n }\n}\n\n#[test]\nfn auth_status_subcommand_parses() {\n let args = Args::try_parse_from([\"jcode\", \"auth\", \"status\", \"--json\"]).unwrap();\n match args.command {\n Some(Command::Auth(AuthCommand::Status { json })) => assert!(json),\n other => panic!(\"unexpected command: {:?}\", other),\n }\n}\n\n#[test]\nfn auth_doctor_subcommand_parses() {\n let args = Args::try_parse_from([\"jcode\", \"auth\", \"doctor\", \"openai\", \"--validate\", \"--json\"])\n .unwrap();\n match args.command {\n Some(Command::Auth(AuthCommand::Doctor {\n provider,\n validate,\n json,\n })) => {\n assert_eq!(provider.as_deref(), Some(\"openai\"));\n assert!(validate);\n assert!(json);\n }\n other => panic!(\"unexpected command: {:?}\", other),\n }\n}\n\n#[test]\nfn provider_list_subcommand_parses() {\n let args = Args::try_parse_from([\"jcode\", \"provider\", \"list\", \"--json\"]).unwrap();\n match args.command {\n Some(Command::Provider(ProviderCommand::List { json })) => assert!(json),\n other => panic!(\"unexpected command: {:?}\", other),\n }\n}\n\n#[test]\nfn provider_current_subcommand_parses() {\n let args = Args::try_parse_from([\"jcode\", \"provider\", \"current\", \"--json\"]).unwrap();\n match args.command {\n Some(Command::Provider(ProviderCommand::Current { json })) => assert!(json),\n other => panic!(\"unexpected command: {:?}\", other),\n }\n}\n\n#[test]\nfn provider_add_subcommand_parses_agent_friendly_flags() {\n let args = Args::try_parse_from([\n \"jcode\",\n \"provider\",\n \"add\",\n \"my-api\",\n \"--base-url\",\n \"https://llm.example.com/v1\",\n \"--model\",\n \"model-a\",\n \"--context-window\",\n \"128000\",\n \"--api-key-stdin\",\n \"--auth\",\n \"bearer\",\n \"--set-default\",\n \"--json\",\n ])\n .unwrap();\n\n match args.command {\n Some(Command::Provider(ProviderCommand::Add {\n name,\n base_url,\n model,\n context_window,\n api_key_stdin,\n auth,\n set_default,\n json,\n ..\n })) => {\n assert_eq!(name, \"my-api\");\n assert_eq!(base_url, \"https://llm.example.com/v1\");\n assert_eq!(model, \"model-a\");\n assert_eq!(context_window, Some(128000));\n assert!(api_key_stdin);\n assert_eq!(auth, Some(ProviderAuthArg::Bearer));\n assert!(set_default);\n assert!(json);\n }\n other => panic!(\"unexpected command: {:?}\", other),\n }\n}\n\n#[test]\nfn restart_save_subcommand_parses() {\n let args = Args::try_parse_from([\"jcode\", \"restart\", \"save\"]).unwrap();\n match args.command {\n Some(Command::Restart {\n action: RestartCommand::Save {\n auto_restore: false,\n },\n }) => {}\n other => panic!(\"unexpected command: {:?}\", other),\n }\n}\n\n#[test]\nfn restart_save_auto_restore_flag_parses() {\n let args = Args::try_parse_from([\"jcode\", \"restart\", \"save\", \"--auto-restore\"]).unwrap();\n match args.command {\n Some(Command::Restart {\n action: RestartCommand::Save { auto_restore: true },\n }) => {}\n other => panic!(\"unexpected command: {:?}\", other),\n }\n}\n"}, "da6d314a46f4d447ff91d66cd338243b5672e0a7": {"t": 1785757205.1857014, "n": 11262, "h": "f224486768483412", "c": "use super::*;\nuse crate::cli::provider_init::ProviderChoice;\n\n#[test]\nfn test_provider_choice_aliases_parse() {\n let args = Args::try_parse_from([\"jcode\", \"--provider\", \"z.ai\", \"run\", \"smoke\"]).unwrap();\n assert_eq!(args.provider, ProviderChoice::Zai);\n\n let args =\n Args::try_parse_from([\"jcode\", \"--provider\", \"kimi-for-coding\", \"run\", \"smoke\"]).unwrap();\n assert_eq!(args.provider, ProviderChoice::Kimi);\n\n let args =\n Args::try_parse_from([\"jcode\", \"--provider\", \"cerebrascode\", \"run\", \"smoke\"]).unwrap();\n assert_eq!(args.provider, ProviderChoice::Cerebras);\n\n let args = Args::try_parse_from([\"jcode\", \"--provider\", \"compat\", \"run\", \"smoke\"]).unwrap();\n assert_eq!(args.provider, ProviderChoice::OpenaiCompatible);\n\n let args = Args::try_parse_from([\"jcode\", \"--provider\", \"bailian\", \"run\", \"smoke\"]).unwrap();\n assert_eq!(args.provider, ProviderChoice::AlibabaCodingPlan);\n\n let args = Args::try_parse_from([\"jcode\", \"--provider\", \"together\", \"run\", \"smoke\"]).unwrap();\n assert_eq!(args.provider, ProviderChoice::TogetherAi);\n\n let args = Args::try_parse_from([\"jcode\", \"--provider\", \"grok\", \"run\", \"smoke\"]).unwrap();\n assert_eq!(args.provider, ProviderChoice::Xai);\n}\n\n#[test]\nfn model_list_subcommand_parses() {\n let args = Args::try_parse_from([\"jcode\", \"model\", \"list\", \"--json\", \"--verbose\"]).unwrap();\n match args.command {\n Some(Command::Model(ModelCommand::List { json, verbose })) => {\n assert!(json);\n assert!(verbose);\n }\n other => panic!(\"unexpected command: {:?}\", other),\n }\n}\n\n#[test]\nfn login_no_browser_flag_parses() {\n let args = Args::try_parse_from([\"jcode\", \"login\", \"--no-browser\"]).unwrap();\n match args.command {\n Some(Command::Login {\n account,\n no_browser,\n print_auth_url,\n callback_url,\n auth_code,\n json,\n complete,\n google_access_tier,\n api_base,\n api_key,\n api_key_env,\n }) => {\n assert!(account.is_none());\n assert!(no_browser);\n assert!(!print_auth_url);\n assert!(callback_url.is_none());\n assert!(auth_code.is_none());\n assert!(!json);\n assert!(!complete);\n assert!(google_access_tier.is_none());\n assert!(api_base.is_none());\n assert!(api_key.is_none());\n assert!(api_key_env.is_none());\n }\n other => panic!(\"unexpected command: {:?}\", other),\n }\n\n let args = Args::try_parse_from([\"jcode\", \"login\", \"--headless\"]).unwrap();\n match args.command {\n Some(Command::Login { no_browser, .. }) => assert!(no_browser),\n other => panic!(\"unexpected command: {:?}\", other),\n }\n}\n\n#[test]\nfn login_openai_compatible_scriptable_flags_parse() {\n let args = Args::try_parse_from([\n \"jcode\",\n \"--provider\",\n \"openai-compatible\",\n \"--model\",\n \"deepseek-v4-flash\",\n \"login\",\n \"--api-base\",\n \"https://api.deepseek.com\",\n \"--api-key-env\",\n \"DEEPSEEK_API_KEY\",\n ])\n .unwrap();\n assert_eq!(args.provider, ProviderChoice::OpenaiCompatible);\n assert_eq!(args.model.as_deref(), Some(\"deepseek-v4-flash\"));\n match args.command {\n Some(Command::Login {\n api_base,\n api_key_env,\n ..\n }) => {\n assert_eq!(api_base.as_deref(), Some(\"https://api.deepseek.com\"));\n assert_eq!(api_key_env.as_deref(), Some(\"DEEPSEEK_API_KEY\"));\n }\n other => panic!(\"unexpected command: {:?}\", other),\n }\n}\n\n#[test]\nfn login_openai_compatible_accepts_global_provider_and_model_after_subcommand() {\n let args = Args::try_parse_from([\n \"jcode\",\n \"login\",\n \"--provider\",\n \"openai-compatible\",\n \"--api-base\",\n \"https://api.deepseek.com\",\n \"--model\",\n \"deepseek-v4-flash\",\n ])\n .unwrap();\n\n assert_eq!(args.provider, ProviderChoice::OpenaiCompatible);\n assert_eq!(args.model.as_deref(), Some(\"deepseek-v4-flash\"));\n match args.command {\n Some(Command::Login { api_base, .. }) => {\n assert_eq!(api_base.as_deref(), Some(\"https://api.deepseek.com\"));\n }\n other => panic!(\"unexpected command: {:?}\", other),\n }\n}\n\n#[test]\nfn login_scriptable_flags_parse() {\n let args = Args::try_parse_from([\"jcode\", \"login\", \"--print-auth-url\", \"--json\"]).unwrap();\n match args.command {\n Some(Command::Login {\n print_auth_url,\n json,\n callback_url,\n auth_code,\n complete,\n google_access_tier,\n ..\n }) => {\n assert!(print_auth_url);\n assert!(json);\n assert!(callback_url.is_none());\n assert!(auth_code.is_none());\n assert!(!complete);\n assert!(google_access_tier.is_none());\n }\n other => panic!(\"unexpected command: {:?}\", other),\n }\n\n let args = Args::try_parse_from([\n \"jcode\",\n \"login\",\n \"--callback-url\",\n \"http://localhost:1455/auth/callback?code=x&state=y\",\n ])\n .unwrap();\n match args.command {\n Some(Command::Login { callback_url, .. }) => {\n assert_eq!(\n callback_url.as_deref(),\n Some(\"http://localhost:1455/auth/callback?code=x&state=y\")\n );\n }\n other => panic!(\"unexpected command: {:?}\", other),\n }\n\n let args = Args::try_parse_from([\"jcode\", \"login\", \"--auth-code\", \"abc123\"]).unwrap();\n match args.command {\n Some(Command::Login { auth_code, .. }) => {\n assert_eq!(auth_code.as_deref(), Some(\"abc123\"));\n }\n other => panic!(\"unexpected command: {:?}\", other),\n }\n\n let args = Args::try_parse_from([\n \"jcode\",\n \"login\",\n \"--complete\",\n \"--google-access-tier\",\n \"readonly\",\n ])\n .unwrap();\n match args.command {\n Some(Command::Login {\n complete,\n google_access_tier,\n ..\n }) => {\n assert!(complete);\n assert_eq!(google_access_tier, Some(GoogleAccessTierArg::Readonly));\n }\n other => panic!(\"unexpected command: {:?}\", other),\n }\n}\n\n#[test]\nfn quiet_global_flag_parses() {\n let args = Args::try_parse_from([\"jcode\", \"--quiet\", \"model\", \"list\"]).unwrap();\n assert!(args.quiet);\n}\n\n#[test]\nfn run_json_subcommand_parses() {\n let args = Args::try_parse_from([\"jcode\", \"run\", \"--json\", \"hello\"]).unwrap();\n match args.command {\n Some(Command::Run {\n json,\n ndjson,\n message,\n }) => {\n assert!(json);\n assert!(!ndjson);\n assert_eq!(message, \"hello\");\n }\n other => panic!(\"unexpected command: {:?}\", other),\n }\n}\n\n#[test]\nfn run_ndjson_subcommand_parses() {\n let args = Args::try_parse_from([\"jcode\", \"run\", \"--ndjson\", \"hello\"]).unwrap();\n match args.command {\n Some(Command::Run {\n json,\n ndjson,\n message,\n }) => {\n assert!(!json);\n assert!(ndjson);\n assert_eq!(message, \"hello\");\n }\n other => panic!(\"unexpected command: {:?}\", other),\n }\n}\n\n#[test]\nfn version_subcommand_parses() {\n let args = Args::try_parse_from([\"jcode\", \"version\", \"--json\"]).unwrap();\n match args.command {\n Some(Command::Version { json }) => assert!(json),\n other => panic!(\"unexpected command: {:?}\", other),\n }\n}\n\n#[test]\nfn usage_subcommand_parses() {\n let args = Args::try_parse_from([\"jcode\", \"usage\", \"--json\"]).unwrap();\n match args.command {\n Some(Command::Usage { json }) => assert!(json),\n other => panic!(\"unexpected command: {:?}\", other),\n }\n}\n\n#[test]\nfn auth_status_subcommand_parses() {\n let args = Args::try_parse_from([\"jcode\", \"auth\", \"status\", \"--json\"]).unwrap();\n match args.command {\n Some(Command::Auth(AuthCommand::Status { json })) => assert!(json),\n other => panic!(\"unexpected command: {:?}\", other),\n }\n}\n\n#[test]\nfn auth_doctor_subcommand_parses() {\n let args = Args::try_parse_from([\"jcode\", \"auth\", \"doctor\", \"openai\", \"--validate\", \"--json\"])\n .unwrap();\n match args.command {\n Some(Command::Auth(AuthCommand::Doctor {\n provider,\n validate,\n json,\n })) => {\n assert_eq!(provider.as_deref(), Some(\"openai\"));\n assert!(validate);\n assert!(json);\n }\n other => panic!(\"unexpected command: {:?}\", other),\n }\n}\n\n#[test]\nfn provider_list_subcommand_parses() {\n let args = Args::try_parse_from([\"jcode\", \"provider\", \"list\", \"--json\"]).unwrap();\n match args.command {\n Some(Command::Provider(ProviderCommand::List { json })) => assert!(json),\n other => panic!(\"unexpected command: {:?}\", other),\n }\n}\n\n#[test]\nfn provider_current_subcommand_parses() {\n let args = Args::try_parse_from([\"jcode\", \"provider\", \"current\", \"--json\"]).unwrap();\n match args.command {\n Some(Command::Provider(ProviderCommand::Current { json })) => assert!(json),\n other => panic!(\"unexpected command: {:?}\", other),\n }\n}\n\n#[test]\nfn provider_add_subcommand_parses_agent_friendly_flags() {\n let args = Args::try_parse_from([\n \"jcode\",\n \"provider\",\n \"add\",\n \"my-api\",\n \"--base-url\",\n \"https://llm.example.com/v1\",\n \"--model\",\n \"model-a\",\n \"--context-window\",\n \"128000\",\n \"--api-key-stdin\",\n \"--auth\",\n \"bearer\",\n \"--set-default\",\n \"--json\",\n ])\n .unwrap();\n\n match args.command {\n Some(Command::Provider(ProviderCommand::Add {\n name,\n base_url,\n model,\n context_window,\n api_key_stdin,\n auth,\n set_default,\n json,\n ..\n })) => {\n assert_eq!(name, \"my-api\");\n assert_eq!(base_url, \"https://llm.example.com/v1\");\n assert_eq!(model, \"model-a\");\n assert_eq!(context_window, Some(128000));\n assert!(api_key_stdin);\n assert_eq!(auth, Some(ProviderAuthArg::Bearer));\n assert!(set_default);\n assert!(json);\n }\n other => panic!(\"unexpected command: {:?}\", other),\n }\n}\n\n#[test]\nfn restart_save_subcommand_parses() {\n let args = Args::try_parse_from([\"jcode\", \"restart\", \"save\"]).unwrap();\n match args.command {\n Some(Command::Restart {\n action: RestartCommand::Save {\n auto_restore: false,\n },\n }) => {}\n other => panic!(\"unexpected command: {:?}\", other),\n }\n}\n\n#[test]\nfn restart_save_auto_restore_flag_parses() {\n let args = Args::try_parse_from([\"jcode\", \"restart\", \"save\", \"--auto-restore\"]).unwrap();\n match args.command {\n Some(Command::Restart {\n action: RestartCommand::Save { auto_restore: true },\n }) => {}\n other => panic!(\"unexpected command: {:?}\", other),\n }\n}\n"}, "af78b620d648efdda3a5e41da617ffd2d49f302b": {"t": 1785757206.6764288, "n": 10091, "h": "500e71ae819facb7", "c": "use super::*;\nuse crate::cli::provider_init::ProviderChoice;\n\n#[test]\nfn test_provider_choice_aliases_parse() {\n let args = Args::try_parse_from([\"jcode\", \"--provider\", \"z.ai\", \"run\", \"smoke\"]).unwrap();\n assert_eq!(args.provider, ProviderChoice::Zai);\n\n let args =\n Args::try_parse_from([\"jcode\", \"--provider\", \"kimi-for-coding\", \"run\", \"smoke\"]).unwrap();\n assert_eq!(args.provider, ProviderChoice::Kimi);\n\n let args =\n Args::try_parse_from([\"jcode\", \"--provider\", \"cerebrascode\", \"run\", \"smoke\"]).unwrap();\n assert_eq!(args.provider, ProviderChoice::Cerebras);\n\n let args = Args::try_parse_from([\"jcode\", \"--provider\", \"compat\", \"run\", \"smoke\"]).unwrap();\n assert_eq!(args.provider, ProviderChoice::OpenaiCompatible);\n\n let args = Args::try_parse_from([\"jcode\", \"--provider\", \"bailian\", \"run\", \"smoke\"]).unwrap();\n assert_eq!(args.provider, ProviderChoice::AlibabaCodingPlan);\n\n let args = Args::try_parse_from([\"jcode\", \"--provider\", \"together\", \"run\", \"smoke\"]).unwrap();\n assert_eq!(args.provider, ProviderChoice::TogetherAi);\n\n let args = Args::try_parse_from([\"jcode\", \"--provider\", \"grok\", \"run\", \"smoke\"]).unwrap();\n assert_eq!(args.provider, ProviderChoice::Xai);\n}\n\n#[test]\nfn model_list_subcommand_parses() {\n let args = Args::try_parse_from([\"jcode\", \"model\", \"list\", \"--json\", \"--verbose\"]).unwrap();\n match args.command {\n Some(Command::Model(ModelCommand::List { json, verbose })) => {\n assert!(json);\n assert!(verbose);\n }\n other => panic!(\"unexpected command: {:?}\", other),\n }\n}\n\n#[test]\nfn login_no_browser_flag_parses() {\n let args = Args::try_parse_from([\"jcode\", \"login\", \"--no-browser\"]).unwrap();\n match args.command {\n Some(Command::Login {\n account,\n no_browser,\n print_auth_url,\n callback_url,\n auth_code,\n json,\n complete,\n google_access_tier,\n api_base,\n api_key,\n api_key_env,\n }) => {\n assert!(account.is_none());\n assert!(no_browser);\n assert!(!print_auth_url);\n assert!(callback_url.is_none());\n assert!(auth_code.is_none());\n assert!(!json);\n assert!(!complete);\n assert!(google_access_tier.is_none());\n assert!(api_base.is_none());\n assert!(api_key.is_none());\n assert!(api_key_env.is_none());\n }\n other => panic!(\"unexpected command: {:?}\", other),\n }\n\n let args = Args::try_parse_from([\"jcode\", \"login\", \"--headless\"]).unwrap();\n match args.command {\n Some(Command::Login { no_browser, .. }) => assert!(no_browser),\n other => panic!(\"unexpected command: {:?}\", other),\n }\n}\n\n#[test]\nfn login_openai_compatible_scriptable_flags_parse() {\n let args = Args::try_parse_from([\n \"jcode\",\n \"--provider\",\n \"openai-compatible\",\n \"--model\",\n \"deepseek-v4-flash\",\n \"login\",\n \"--api-base\",\n \"https://api.deepseek.com\",\n \"--api-key-env\",\n \"DEEPSEEK_API_KEY\",\n ])\n .unwrap();\n assert_eq!(args.provider, ProviderChoice::OpenaiCompatible);\n assert_eq!(args.model.as_deref(), Some(\"deepseek-v4-flash\"));\n match args.command {\n Some(Command::Login {\n api_base,\n api_key_env,\n ..\n }) => {\n assert_eq!(api_base.as_deref(), Some(\"https://api.deepseek.com\"));\n assert_eq!(api_key_env.as_deref(), Some(\"DEEPSEEK_API_KEY\"));\n }\n other => panic!(\"unexpected command: {:?}\", other),\n }\n}\n\n#[test]\nfn login_openai_compatible_accepts_global_provider_and_model_after_subcommand() {\n let args = Args::try_parse_from([\n \"jcode\",\n \"login\",\n \"--provider\",\n \"openai-compatible\",\n \"--api-base\",\n \"https://api.deepseek.com\",\n \"--model\",\n \"deepseek-v4-flash\",\n ])\n .unwrap();\n\n assert_eq!(args.provider, ProviderChoice::OpenaiCompatible);\n assert_eq!(args.model.as_deref(), Some(\"deepseek-v4-flash\"));\n match args.command {\n Some(Command::Login { api_base, .. }) => {\n assert_eq!(api_base.as_deref(), Some(\"https://api.deepseek.com\"));\n }\n other => panic!(\"unexpected command: {:?}\", other),\n }\n}\n\n#[test]\nfn login_scriptable_flags_parse() {\n let args = Args::try_parse_from([\"jcode\", \"login\", \"--print-auth-url\", \"--json\"]).unwrap();\n match args.command {\n Some(Command::Login {\n print_auth_url,\n json,\n callback_url,\n auth_code,\n complete,\n google_access_tier,\n ..\n }) => {\n assert!(print_auth_url);\n assert!(json);\n assert!(callback_url.is_none());\n assert!(auth_code.is_none());\n assert!(!complete);\n assert!(google_access_tier.is_none());\n }\n other => panic!(\"unexpected command: {:?}\", other),\n }\n\n let args = Args::try_parse_from([\n \"jcode\",\n \"login\",\n \"--callback-url\",\n \"http://localhost:1455/auth/callback?code=x&state=y\",\n ])\n .unwrap();\n match args.command {\n Some(Command::Login { callback_url, .. }) => {\n assert_eq!(\n callback_url.as_deref(),\n Some(\"http://localhost:1455/auth/callback?code=x&state=y\")\n );\n }\n other => panic!(\"unexpected command: {:?}\", other),\n }\n\n let args = Args::try_parse_from([\"jcode\", \"login\", \"--auth-code\", \"abc123\"]).unwrap();\n match args.command {\n Some(Command::Login { auth_code, .. }) => {\n assert_eq!(auth_code.as_deref(), Some(\"abc123\"));\n }\n other => panic!(\"unexpected command: {:?}\", other),\n }\n\n let args = Args::try_parse_from([\n \"jcode\",\n \"login\",\n \"--complete\",\n \"--google-access-tier\",\n \"readonly\",\n ])\n .unwrap();\n match args.command {\n Some(Command::Login {\n complete,\n google_access_tier,\n ..\n }) => {\n assert!(complete);\n assert_eq!(google_access_tier, Some(GoogleAccessTierArg::Readonly));\n }\n other => panic!(\"unexpected command: {:?}\", other),\n }\n}\n\n#[test]\nfn quiet_global_flag_parses() {\n let args = Args::try_parse_from([\"jcode\", \"--quiet\", \"model\", \"list\"]).unwrap();\n assert!(args.quiet);\n}\n\n#[test]\nfn run_json_subcommand_parses() {\n let args = Args::try_parse_from([\"jcode\", \"run\", \"--json\", \"hello\"]).unwrap();\n match args.command {\n Some(Command::Run {\n json,\n ndjson,\n message,\n }) => {\n assert!(json);\n assert!(!ndjson);\n assert_eq!(message, \"hello\");\n }\n other => panic!(\"unexpected command: {:?}\", other),\n }\n}\n\n#[test]\nfn run_ndjson_subcommand_parses() {\n let args = Args::try_parse_from([\"jcode\", \"run\", \"--ndjson\", \"hello\"]).unwrap();\n match args.command {\n Some(Command::Run {\n json,\n ndjson,\n message,\n }) => {\n assert!(!json);\n assert!(ndjson);\n assert_eq!(message, \"hello\");\n }\n other => panic!(\"unexpected command: {:?}\", other),\n }\n}\n\n#[test]\nfn version_subcommand_parses() {\n let args = Args::try_parse_from([\"jcode\", \"version\", \"--json\"]).unwrap();\n match args.command {\n Some(Command::Version { json }) => assert!(json),\n other => panic!(\"unexpected command: {:?}\", other),\n }\n}\n\n#[test]\nfn usage_subcommand_parses() {\n let args = Args::try_parse_from([\"jcode\", \"usage\", \"--json\"]).unwrap();\n match args.command {\n Some(Command::Usage { json }) => assert!(json),\n other => panic!(\"unexpected command: {:?}\", other),\n }\n}\n\n#[test]\nfn auth_status_subcommand_parses() {\n let args = Args::try_parse_from([\"jcode\", \"auth\", \"status\", \"--json\"]).unwrap();\n match args.command {\n Some(Command::Auth(AuthCommand::Status { json })) => assert!(json),\n other => panic!(\"unexpected command: {:?}\", other),\n }\n}\n\n#[test]\nfn auth_doctor_subcommand_parses() {\n let args = Args::try_parse_from([\"jcode\", \"auth\", \"doctor\", \"openai\", \"--validate\", \"--json\"])\n .unwrap();\n match args.command {\n Some(Command::Auth(AuthCommand::Doctor {\n provider,\n validate,\n json,\n })) => {\n assert_eq!(provider.as_deref(), Some(\"openai\"));\n assert!(validate);\n assert!(json);\n }\n other => panic!(\"unexpected command: {:?}\", other),\n }\n}\n\n#[test]\nfn provider_list_subcommand_parses() {\n let args = Args::try_parse_from([\"jcode\", \"provider\", \"list\", \"--json\"]).unwrap();\n match args.command {\n Some(Command::Provider(ProviderCommand::List { json })) => assert!(json),\n other => panic!(\"unexpected command: {:?}\", other),\n }\n}\n\n#[test]\nfn provider_current_subcommand_parses() {\n let args = Args::try_parse_from([\"jcode\", \"provider\", \"current\", \"--json\"]).unwrap();\n match args.command {\n Some(Command::Provider(ProviderCommand::Current { json })) => assert!(json),\n other => panic!(\"unexpected command: {:?}\", other),\n }\n}\n\n#[test]\nfn restart_save_subcommand_parses() {\n let args = Args::try_parse_from([\"jcode\", \"restart\", \"save\"]).unwrap();\n match args.command {\n Some(Command::Restart {\n action: RestartCommand::Save {\n auto_restore: false,\n },\n }) => {}\n other => panic!(\"unexpected command: {:?}\", other),\n }\n}\n\n#[test]\nfn restart_save_auto_restore_flag_parses() {\n let args = Args::try_parse_from([\"jcode\", \"restart\", \"save\", \"--auto-restore\"]).unwrap();\n match args.command {\n Some(Command::Restart {\n action: RestartCommand::Save { auto_restore: true },\n }) => {}\n other => panic!(\"unexpected command: {:?}\", other),\n }\n}\n"}, "50619a00d59b6916ece8e135e345c5d15a654980": {"t": 1785757208.0373795, "n": 9384, "h": "6f9dd452730e7e09", "c": "use super::*;\nuse crate::cli::provider_init::ProviderChoice;\n\n#[test]\nfn test_provider_choice_aliases_parse() {\n let args = Args::try_parse_from([\"jcode\", \"--provider\", \"z.ai\", \"run\", \"smoke\"]).unwrap();\n assert_eq!(args.provider, ProviderChoice::Zai);\n\n let args =\n Args::try_parse_from([\"jcode\", \"--provider\", \"kimi-for-coding\", \"run\", \"smoke\"]).unwrap();\n assert_eq!(args.provider, ProviderChoice::Kimi);\n\n let args =\n Args::try_parse_from([\"jcode\", \"--provider\", \"cerebrascode\", \"run\", \"smoke\"]).unwrap();\n assert_eq!(args.provider, ProviderChoice::Cerebras);\n\n let args = Args::try_parse_from([\"jcode\", \"--provider\", \"compat\", \"run\", \"smoke\"]).unwrap();\n assert_eq!(args.provider, ProviderChoice::OpenaiCompatible);\n\n let args = Args::try_parse_from([\"jcode\", \"--provider\", \"bailian\", \"run\", \"smoke\"]).unwrap();\n assert_eq!(args.provider, ProviderChoice::AlibabaCodingPlan);\n\n let args = Args::try_parse_from([\"jcode\", \"--provider\", \"together\", \"run\", \"smoke\"]).unwrap();\n assert_eq!(args.provider, ProviderChoice::TogetherAi);\n\n let args = Args::try_parse_from([\"jcode\", \"--provider\", \"grok\", \"run\", \"smoke\"]).unwrap();\n assert_eq!(args.provider, ProviderChoice::Xai);\n}\n\n#[test]\nfn model_list_subcommand_parses() {\n let args = Args::try_parse_from([\"jcode\", \"model\", \"list\", \"--json\", \"--verbose\"]).unwrap();\n match args.command {\n Some(Command::Model(ModelCommand::List { json, verbose })) => {\n assert!(json);\n assert!(verbose);\n }\n other => panic!(\"unexpected command: {:?}\", other),\n }\n}\n\n#[test]\nfn login_no_browser_flag_parses() {\n let args = Args::try_parse_from([\"jcode\", \"login\", \"--no-browser\"]).unwrap();\n match args.command {\n Some(Command::Login {\n account,\n no_browser,\n print_auth_url,\n callback_url,\n auth_code,\n json,\n complete,\n google_access_tier,\n api_base,\n api_key,\n api_key_env,\n }) => {\n assert!(account.is_none());\n assert!(no_browser);\n assert!(!print_auth_url);\n assert!(callback_url.is_none());\n assert!(auth_code.is_none());\n assert!(!json);\n assert!(!complete);\n assert!(google_access_tier.is_none());\n assert!(api_base.is_none());\n assert!(api_key.is_none());\n assert!(api_key_env.is_none());\n }\n other => panic!(\"unexpected command: {:?}\", other),\n }\n\n let args = Args::try_parse_from([\"jcode\", \"login\", \"--headless\"]).unwrap();\n match args.command {\n Some(Command::Login { no_browser, .. }) => assert!(no_browser),\n other => panic!(\"unexpected command: {:?}\", other),\n }\n}\n\n#[test]\nfn login_openai_compatible_scriptable_flags_parse() {\n let args = Args::try_parse_from([\n \"jcode\",\n \"--provider\",\n \"openai-compatible\",\n \"--model\",\n \"deepseek-v4-flash\",\n \"login\",\n \"--api-base\",\n \"https://api.deepseek.com\",\n \"--api-key-env\",\n \"DEEPSEEK_API_KEY\",\n ])\n .unwrap();\n assert_eq!(args.provider, ProviderChoice::OpenaiCompatible);\n assert_eq!(args.model.as_deref(), Some(\"deepseek-v4-flash\"));\n match args.command {\n Some(Command::Login {\n api_base,\n api_key_env,\n ..\n }) => {\n assert_eq!(api_base.as_deref(), Some(\"https://api.deepseek.com\"));\n assert_eq!(api_key_env.as_deref(), Some(\"DEEPSEEK_API_KEY\"));\n }\n other => panic!(\"unexpected command: {:?}\", other),\n }\n}\n\n#[test]\nfn login_scriptable_flags_parse() {\n let args = Args::try_parse_from([\"jcode\", \"login\", \"--print-auth-url\", \"--json\"]).unwrap();\n match args.command {\n Some(Command::Login {\n print_auth_url,\n json,\n callback_url,\n auth_code,\n complete,\n google_access_tier,\n ..\n }) => {\n assert!(print_auth_url);\n assert!(json);\n assert!(callback_url.is_none());\n assert!(auth_code.is_none());\n assert!(!complete);\n assert!(google_access_tier.is_none());\n }\n other => panic!(\"unexpected command: {:?}\", other),\n }\n\n let args = Args::try_parse_from([\n \"jcode\",\n \"login\",\n \"--callback-url\",\n \"http://localhost:1455/auth/callback?code=x&state=y\",\n ])\n .unwrap();\n match args.command {\n Some(Command::Login { callback_url, .. }) => {\n assert_eq!(\n callback_url.as_deref(),\n Some(\"http://localhost:1455/auth/callback?code=x&state=y\")\n );\n }\n other => panic!(\"unexpected command: {:?}\", other),\n }\n\n let args = Args::try_parse_from([\"jcode\", \"login\", \"--auth-code\", \"abc123\"]).unwrap();\n match args.command {\n Some(Command::Login { auth_code, .. }) => {\n assert_eq!(auth_code.as_deref(), Some(\"abc123\"));\n }\n other => panic!(\"unexpected command: {:?}\", other),\n }\n\n let args = Args::try_parse_from([\n \"jcode\",\n \"login\",\n \"--complete\",\n \"--google-access-tier\",\n \"readonly\",\n ])\n .unwrap();\n match args.command {\n Some(Command::Login {\n complete,\n google_access_tier,\n ..\n }) => {\n assert!(complete);\n assert_eq!(google_access_tier, Some(GoogleAccessTierArg::Readonly));\n }\n other => panic!(\"unexpected command: {:?}\", other),\n }\n}\n\n#[test]\nfn quiet_global_flag_parses() {\n let args = Args::try_parse_from([\"jcode\", \"--quiet\", \"model\", \"list\"]).unwrap();\n assert!(args.quiet);\n}\n\n#[test]\nfn run_json_subcommand_parses() {\n let args = Args::try_parse_from([\"jcode\", \"run\", \"--json\", \"hello\"]).unwrap();\n match args.command {\n Some(Command::Run {\n json,\n ndjson,\n message,\n }) => {\n assert!(json);\n assert!(!ndjson);\n assert_eq!(message, \"hello\");\n }\n other => panic!(\"unexpected command: {:?}\", other),\n }\n}\n\n#[test]\nfn run_ndjson_subcommand_parses() {\n let args = Args::try_parse_from([\"jcode\", \"run\", \"--ndjson\", \"hello\"]).unwrap();\n match args.command {\n Some(Command::Run {\n json,\n ndjson,\n message,\n }) => {\n assert!(!json);\n assert!(ndjson);\n assert_eq!(message, \"hello\");\n }\n other => panic!(\"unexpected command: {:?}\", other),\n }\n}\n\n#[test]\nfn version_subcommand_parses() {\n let args = Args::try_parse_from([\"jcode\", \"version\", \"--json\"]).unwrap();\n match args.command {\n Some(Command::Version { json }) => assert!(json),\n other => panic!(\"unexpected command: {:?}\", other),\n }\n}\n\n#[test]\nfn usage_subcommand_parses() {\n let args = Args::try_parse_from([\"jcode\", \"usage\", \"--json\"]).unwrap();\n match args.command {\n Some(Command::Usage { json }) => assert!(json),\n other => panic!(\"unexpected command: {:?}\", other),\n }\n}\n\n#[test]\nfn auth_status_subcommand_parses() {\n let args = Args::try_parse_from([\"jcode\", \"auth\", \"status\", \"--json\"]).unwrap();\n match args.command {\n Some(Command::Auth(AuthCommand::Status { json })) => assert!(json),\n other => panic!(\"unexpected command: {:?}\", other),\n }\n}\n\n#[test]\nfn auth_doctor_subcommand_parses() {\n let args = Args::try_parse_from([\"jcode\", \"auth\", \"doctor\", \"openai\", \"--validate\", \"--json\"])\n .unwrap();\n match args.command {\n Some(Command::Auth(AuthCommand::Doctor {\n provider,\n validate,\n json,\n })) => {\n assert_eq!(provider.as_deref(), Some(\"openai\"));\n assert!(validate);\n assert!(json);\n }\n other => panic!(\"unexpected command: {:?}\", other),\n }\n}\n\n#[test]\nfn provider_list_subcommand_parses() {\n let args = Args::try_parse_from([\"jcode\", \"provider\", \"list\", \"--json\"]).unwrap();\n match args.command {\n Some(Command::Provider(ProviderCommand::List { json })) => assert!(json),\n other => panic!(\"unexpected command: {:?}\", other),\n }\n}\n\n#[test]\nfn provider_current_subcommand_parses() {\n let args = Args::try_parse_from([\"jcode\", \"provider\", \"current\", \"--json\"]).unwrap();\n match args.command {\n Some(Command::Provider(ProviderCommand::Current { json })) => assert!(json),\n other => panic!(\"unexpected command: {:?}\", other),\n }\n}\n\n#[test]\nfn restart_save_subcommand_parses() {\n let args = Args::try_parse_from([\"jcode\", \"restart\", \"save\"]).unwrap();\n match args.command {\n Some(Command::Restart {\n action: RestartCommand::Save {\n auto_restore: false,\n },\n }) => {}\n other => panic!(\"unexpected command: {:?}\", other),\n }\n}\n\n#[test]\nfn restart_save_auto_restore_flag_parses() {\n let args = Args::try_parse_from([\"jcode\", \"restart\", \"save\", \"--auto-restore\"]).unwrap();\n match args.command {\n Some(Command::Restart {\n action: RestartCommand::Save { auto_restore: true },\n }) => {}\n other => panic!(\"unexpected command: {:?}\", other),\n }\n}\n"}, "c4bc4a54a8478aa6c1a9ca93dbc107ccaa9ed86e": {"t": 1785757209.7163596, "n": 8329, "h": "0c1d41b574bd773d", "c": "use super::*;\nuse crate::cli::provider_init::ProviderChoice;\n\n#[test]\nfn test_provider_choice_aliases_parse() {\n let args = Args::try_parse_from([\"jcode\", \"--provider\", \"z.ai\", \"run\", \"smoke\"]).unwrap();\n assert_eq!(args.provider, ProviderChoice::Zai);\n\n let args =\n Args::try_parse_from([\"jcode\", \"--provider\", \"kimi-for-coding\", \"run\", \"smoke\"]).unwrap();\n assert_eq!(args.provider, ProviderChoice::Kimi);\n\n let args =\n Args::try_parse_from([\"jcode\", \"--provider\", \"cerebrascode\", \"run\", \"smoke\"]).unwrap();\n assert_eq!(args.provider, ProviderChoice::Cerebras);\n\n let args = Args::try_parse_from([\"jcode\", \"--provider\", \"compat\", \"run\", \"smoke\"]).unwrap();\n assert_eq!(args.provider, ProviderChoice::OpenaiCompatible);\n\n let args = Args::try_parse_from([\"jcode\", \"--provider\", \"bailian\", \"run\", \"smoke\"]).unwrap();\n assert_eq!(args.provider, ProviderChoice::AlibabaCodingPlan);\n\n let args = Args::try_parse_from([\"jcode\", \"--provider\", \"together\", \"run\", \"smoke\"]).unwrap();\n assert_eq!(args.provider, ProviderChoice::TogetherAi);\n\n let args = Args::try_parse_from([\"jcode\", \"--provider\", \"grok\", \"run\", \"smoke\"]).unwrap();\n assert_eq!(args.provider, ProviderChoice::Xai);\n}\n\n#[test]\nfn model_list_subcommand_parses() {\n let args = Args::try_parse_from([\"jcode\", \"model\", \"list\", \"--json\", \"--verbose\"]).unwrap();\n match args.command {\n Some(Command::Model(ModelCommand::List { json, verbose })) => {\n assert!(json);\n assert!(verbose);\n }\n other => panic!(\"unexpected command: {:?}\", other),\n }\n}\n\n#[test]\nfn login_no_browser_flag_parses() {\n let args = Args::try_parse_from([\"jcode\", \"login\", \"--no-browser\"]).unwrap();\n match args.command {\n Some(Command::Login {\n account,\n no_browser,\n print_auth_url,\n callback_url,\n auth_code,\n json,\n complete,\n google_access_tier,\n }) => {\n assert!(account.is_none());\n assert!(no_browser);\n assert!(!print_auth_url);\n assert!(callback_url.is_none());\n assert!(auth_code.is_none());\n assert!(!json);\n assert!(!complete);\n assert!(google_access_tier.is_none());\n }\n other => panic!(\"unexpected command: {:?}\", other),\n }\n\n let args = Args::try_parse_from([\"jcode\", \"login\", \"--headless\"]).unwrap();\n match args.command {\n Some(Command::Login { no_browser, .. }) => assert!(no_browser),\n other => panic!(\"unexpected command: {:?}\", other),\n }\n}\n\n#[test]\nfn login_scriptable_flags_parse() {\n let args = Args::try_parse_from([\"jcode\", \"login\", \"--print-auth-url\", \"--json\"]).unwrap();\n match args.command {\n Some(Command::Login {\n print_auth_url,\n json,\n callback_url,\n auth_code,\n complete,\n google_access_tier,\n ..\n }) => {\n assert!(print_auth_url);\n assert!(json);\n assert!(callback_url.is_none());\n assert!(auth_code.is_none());\n assert!(!complete);\n assert!(google_access_tier.is_none());\n }\n other => panic!(\"unexpected command: {:?}\", other),\n }\n\n let args = Args::try_parse_from([\n \"jcode\",\n \"login\",\n \"--callback-url\",\n \"http://localhost:1455/auth/callback?code=x&state=y\",\n ])\n .unwrap();\n match args.command {\n Some(Command::Login { callback_url, .. }) => {\n assert_eq!(\n callback_url.as_deref(),\n Some(\"http://localhost:1455/auth/callback?code=x&state=y\")\n );\n }\n other => panic!(\"unexpected command: {:?}\", other),\n }\n\n let args = Args::try_parse_from([\"jcode\", \"login\", \"--auth-code\", \"abc123\"]).unwrap();\n match args.command {\n Some(Command::Login { auth_code, .. }) => {\n assert_eq!(auth_code.as_deref(), Some(\"abc123\"));\n }\n other => panic!(\"unexpected command: {:?}\", other),\n }\n\n let args = Args::try_parse_from([\n \"jcode\",\n \"login\",\n \"--complete\",\n \"--google-access-tier\",\n \"readonly\",\n ])\n .unwrap();\n match args.command {\n Some(Command::Login {\n complete,\n google_access_tier,\n ..\n }) => {\n assert!(complete);\n assert_eq!(google_access_tier, Some(GoogleAccessTierArg::Readonly));\n }\n other => panic!(\"unexpected command: {:?}\", other),\n }\n}\n\n#[test]\nfn quiet_global_flag_parses() {\n let args = Args::try_parse_from([\"jcode\", \"--quiet\", \"model\", \"list\"]).unwrap();\n assert!(args.quiet);\n}\n\n#[test]\nfn run_json_subcommand_parses() {\n let args = Args::try_parse_from([\"jcode\", \"run\", \"--json\", \"hello\"]).unwrap();\n match args.command {\n Some(Command::Run {\n json,\n ndjson,\n message,\n }) => {\n assert!(json);\n assert!(!ndjson);\n assert_eq!(message, \"hello\");\n }\n other => panic!(\"unexpected command: {:?}\", other),\n }\n}\n\n#[test]\nfn run_ndjson_subcommand_parses() {\n let args = Args::try_parse_from([\"jcode\", \"run\", \"--ndjson\", \"hello\"]).unwrap();\n match args.command {\n Some(Command::Run {\n json,\n ndjson,\n message,\n }) => {\n assert!(!json);\n assert!(ndjson);\n assert_eq!(message, \"hello\");\n }\n other => panic!(\"unexpected command: {:?}\", other),\n }\n}\n\n#[test]\nfn version_subcommand_parses() {\n let args = Args::try_parse_from([\"jcode\", \"version\", \"--json\"]).unwrap();\n match args.command {\n Some(Command::Version { json }) => assert!(json),\n other => panic!(\"unexpected command: {:?}\", other),\n }\n}\n\n#[test]\nfn usage_subcommand_parses() {\n let args = Args::try_parse_from([\"jcode\", \"usage\", \"--json\"]).unwrap();\n match args.command {\n Some(Command::Usage { json }) => assert!(json),\n other => panic!(\"unexpected command: {:?}\", other),\n }\n}\n\n#[test]\nfn auth_status_subcommand_parses() {\n let args = Args::try_parse_from([\"jcode\", \"auth\", \"status\", \"--json\"]).unwrap();\n match args.command {\n Some(Command::Auth(AuthCommand::Status { json })) => assert!(json),\n other => panic!(\"unexpected command: {:?}\", other),\n }\n}\n\n#[test]\nfn auth_doctor_subcommand_parses() {\n let args = Args::try_parse_from([\"jcode\", \"auth\", \"doctor\", \"openai\", \"--validate\", \"--json\"])\n .unwrap();\n match args.command {\n Some(Command::Auth(AuthCommand::Doctor {\n provider,\n validate,\n json,\n })) => {\n assert_eq!(provider.as_deref(), Some(\"openai\"));\n assert!(validate);\n assert!(json);\n }\n other => panic!(\"unexpected command: {:?}\", other),\n }\n}\n\n#[test]\nfn provider_list_subcommand_parses() {\n let args = Args::try_parse_from([\"jcode\", \"provider\", \"list\", \"--json\"]).unwrap();\n match args.command {\n Some(Command::Provider(ProviderCommand::List { json })) => assert!(json),\n other => panic!(\"unexpected command: {:?}\", other),\n }\n}\n\n#[test]\nfn provider_current_subcommand_parses() {\n let args = Args::try_parse_from([\"jcode\", \"provider\", \"current\", \"--json\"]).unwrap();\n match args.command {\n Some(Command::Provider(ProviderCommand::Current { json })) => assert!(json),\n other => panic!(\"unexpected command: {:?}\", other),\n }\n}\n\n#[test]\nfn restart_save_subcommand_parses() {\n let args = Args::try_parse_from([\"jcode\", \"restart\", \"save\"]).unwrap();\n match args.command {\n Some(Command::Restart {\n action: RestartCommand::Save {\n auto_restore: false,\n },\n }) => {}\n other => panic!(\"unexpected command: {:?}\", other),\n }\n}\n\n#[test]\nfn restart_save_auto_restore_flag_parses() {\n let args = Args::try_parse_from([\"jcode\", \"restart\", \"save\", \"--auto-restore\"]).unwrap();\n match args.command {\n Some(Command::Restart {\n action: RestartCommand::Save { auto_restore: true },\n }) => {}\n other => panic!(\"unexpected command: {:?}\", other),\n }\n}\n"}}, "src/message.rs": {"19e08804f268cacb6cc278dd2eb9050e8fff8af9": {"t": 1785745870.5754573, "n": 10410, "h": "7bcad34729df01de", "c": "use crate::bus::{BackgroundTaskCompleted, BackgroundTaskProgressEvent, BackgroundTaskStatus};\nuse base64::Engine as _;\nuse regex::Regex;\nuse std::collections::HashSet;\nuse std::path::Path;\nuse std::sync::OnceLock;\n\npub use jcode_message_types::{\n CacheControl, ConnectionPhase, ContentBlock, InputShellResult, Message, Role, StreamEvent,\n TOOL_OUTPUT_MISSING_TEXT, ToolCall, ToolDefinition, ends_with_fresh_user_turn,\n extend_stable_hash, messages_with_dynamic_system_context, sanitize_tool_id,\n stable_message_hash,\n};\n\nmod notifications;\n\npub use notifications::{\n ParsedBackgroundTaskNotification, ParsedBackgroundTaskProgressNotification,\n background_task_display_label, background_task_status_notice,\n format_background_task_notification_markdown, format_background_task_progress_markdown,\n format_input_shell_result_markdown, input_shell_status_notice,\n parse_background_task_notification_markdown,\n parse_background_task_progress_notification_markdown,\n};\n\nfn compile_static_regex(pattern: &str) -> Option<Regex> {\n match Regex::new(pattern) {\n Ok(regex) => Some(regex),\n Err(err) => {\n eprintln!(\"jcode: failed to compile static regex: {err}\");\n None\n }\n }\n}\n\nfn compile_static_regexes(patterns: &[&str]) -> Vec<Regex> {\n patterns\n .iter()\n .filter_map(|pattern| compile_static_regex(pattern))\n .collect()\n}\n\n/// Redact likely secrets from persisted tool output.\n///\n/// This is a best-effort safeguard for local session history files. It targets\n/// high-confidence token/key patterns and common `KEY=VALUE` assignments used by\n/// auth flows.\npub fn redact_secrets(text: &str) -> String {\n // Fast path to avoid regex work for most tool outputs.\n let lower = text.to_ascii_lowercase();\n\n if !text.contains(\"sk-\")\n && !text.contains(\"ghp_\")\n && !text.contains(\"github_pat_\")\n && !text.contains(\"AIza\")\n && !text.contains(\"ya29.\")\n && !text.contains(\"xox\")\n && !lower.contains(\"bearer \")\n && !lower.contains(\"api_key\")\n && !lower.contains(\"authorization\")\n && !lower.contains(\"password\")\n && !lower.contains(\"token\")\n {\n return text.to_string();\n }\n\n static DIRECT_PATTERNS: OnceLock<Vec<Regex>> = OnceLock::new();\n static ASSIGNMENT_PATTERNS: OnceLock<Vec<Regex>> = OnceLock::new();\n\n let direct_patterns = DIRECT_PATTERNS.get_or_init(|| {\n compile_static_regexes(&[\n r\"sk-ant-(?:oat|ort)01-[A-Za-z0-9_-]{20,}\",\n r\"sk-or-v1-[A-Za-z0-9_-]{20,}\",\n r\"ghp_[A-Za-z0-9]{20,}\",\n r\"github_pat_[A-Za-z0-9_]{20,}\",\n r\"ya29\\.[A-Za-z0-9._-]{20,}\",\n r\"AIza[0-9A-Za-z_-]{20,}\",\n r\"xox[baprs]-[A-Za-z0-9-]{10,}\",\n r\"(?i)\\bBearer\\s+[A-Za-z0-9._~+/=-]{16,}\",\n ])\n });\n\n let assignment_patterns = ASSIGNMENT_PATTERNS.get_or_init(|| {\n compile_static_regexes(&[\n r\"(?m)^\\s*(OPENROUTER_API_KEY\\s*=\\s*)[^\\r\\n]+\",\n r\"(?m)^\\s*(OPENCODE_API_KEY\\s*=\\s*)[^\\r\\n]+\",\n r\"(?m)^\\s*(OPENCODE_GO_API_KEY\\s*=\\s*)[^\\r\\n]+\",\n r\"(?m)^\\s*(ZHIPU_API_KEY\\s*=\\s*)[^\\r\\n]+\",\n r\"(?m)^\\s*(ZAI_API_KEY\\s*=\\s*)[^\\r\\n]+\",\n r\"(?m)^\\s*(302AI_API_KEY\\s*=\\s*)[^\\r\\n]+\",\n r\"(?m)^\\s*(BASETEN_API_KEY\\s*=\\s*)[^\\r\\n]+\",\n r\"(?m)^\\s*(CORTECS_API_KEY\\s*=\\s*)[^\\r\\n]+\",\n r\"(?m)^\\s*(DEEPSEEK_API_KEY\\s*=\\s*)[^\\r\\n]+\",\n r\"(?m)^\\s*(FIRMWARE_API_KEY\\s*=\\s*)[^\\r\\n]+\",\n r\"(?m)^\\s*(HF_TOKEN\\s*=\\s*)[^\\r\\n]+\",\n r\"(?m)^\\s*(MOONSHOT_API_KEY\\s*=\\s*)[^\\r\\n]+\",\n r\"(?m)^\\s*(NEBIUS_API_KEY\\s*=\\s*)[^\\r\\n]+\",\n r\"(?m)^\\s*(SCALEWAY_API_KEY\\s*=\\s*)[^\\r\\n]+\",\n r\"(?m)^\\s*(STACKIT_API_KEY\\s*=\\s*)[^\\r\\n]+\",\n r\"(?m)^\\s*(GROQ_API_KEY\\s*=\\s*)[^\\r\\n]+\",\n r\"(?m)^\\s*(MISTRAL_API_KEY\\s*=\\s*)[^\\r\\n]+\",\n r\"(?m)^\\s*(PERPLEXITY_API_KEY\\s*=\\s*)[^\\r\\n]+\",\n r\"(?m)^\\s*(TOGETHER_API_KEY\\s*=\\s*)[^\\r\\n]+\",\n r\"(?m)^\\s*(DEEPINFRA_API_KEY\\s*=\\s*)[^\\r\\n]+\",\n r\"(?m)^\\s*(XAI_API_KEY\\s*=\\s*)[^\\r\\n]+\",\n r\"(?m)^\\s*(LMSTUDIO_API_KEY\\s*=\\s*)[^\\r\\n]+\",\n r\"(?m)^\\s*(OLLAMA_API_KEY\\s*=\\s*)[^\\r\\n]+\",\n r\"(?m)^\\s*(CHUTES_API_KEY\\s*=\\s*)[^\\r\\n]+\",\n r\"(?m)^\\s*(CEREBRAS_API_KEY\\s*=\\s*)[^\\r\\n]+\",\n r\"(?m)^\\s*(OPENAI_COMPAT_API_KEY\\s*=\\s*)[^\\r\\n]+\",\n r\"(?m)^\\s*(ANTHROPIC_API_KEY\\s*=\\s*)[^\\r\\n]+\",\n r\"(?m)^\\s*(OPENAI_API_KEY\\s*=\\s*)[^\\r\\n]+\",\n r\"(?m)^\\s*(AZURE_OPENAI_API_KEY\\s*=\\s*)[^\\r\\n]+\",\n r\"(?m)^\\s*(CURSOR_API_KEY\\s*=\\s*)[^\\r\\n]+\",\n r\"(?m)^\\s*(GITHUB_TOKEN\\s*=\\s*)[^\\r\\n]+\",\n r\"(?im)^(\\s*(?:authorization|proxy-authorization)\\s*[:=]\\s*)[^\\r\\n]+\",\n r\"(?im)^(\\s*[A-Z0-9_]*(?:PASSWORD|TOKEN|SECRET)[A-Z0-9_]*\\s*=\\s*)[^\\r\\n]+\",\n r#\"(?im)(\\\"[A-Za-z0-9_-]*(?:password|token|secret)[A-Za-z0-9_-]*\\\"\\s*:\\s*)\\\"[^\\\"\\r\\n]{4,}\\\"\"#,\n ])\n });\n\n let mut redacted = text.to_string();\n let mut redacted_keys: HashSet<String> = [\n \"OPENROUTER_API_KEY\",\n \"OPENCODE_API_KEY\",\n \"OPENCODE_GO_API_KEY\",\n \"ZHIPU_API_KEY\",\n \"ZAI_API_KEY\",\n \"302AI_API_KEY\",\n \"BASETEN_API_KEY\",\n \"CORTECS_API_KEY\",\n \"DEEPSEEK_API_KEY\",\n \"FIRMWARE_API_KEY\",\n \"HF_TOKEN\",\n \"MOONSHOT_API_KEY\",\n \"NEBIUS_API_KEY\",\n \"SCALEWAY_API_KEY\",\n \"STACKIT_API_KEY\",\n \"GROQ_API_KEY\",\n \"MISTRAL_API_KEY\",\n \"PERPLEXITY_API_KEY\",\n \"TOGETHER_API_KEY\",\n \"DEEPINFRA_API_KEY\",\n \"XAI_API_KEY\",\n \"LMSTUDIO_API_KEY\",\n \"OLLAMA_API_KEY\",\n \"CHUTES_API_KEY\",\n \"CEREBRAS_API_KEY\",\n \"OPENAI_COMPAT_API_KEY\",\n \"ANTHROPIC_API_KEY\",\n \"OPENAI_API_KEY\",\n \"AZURE_OPENAI_API_KEY\",\n \"CURSOR_API_KEY\",\n \"GITHUB_TOKEN\",\n ]\n .iter()\n .map(|k| (*k).to_string())\n .collect();\n\n for re in direct_patterns {\n redacted = re.replace_all(&redacted, \"[REDACTED_SECRET]\").into_owned();\n }\n\n for re in assignment_patterns {\n redacted = re\n .replace_all(&redacted, \"${1}[REDACTED_SECRET]\")\n .into_owned();\n }\n\n // Also redact custom API key variable names configured at runtime.\n for source in [\n \"JCODE_OPENROUTER_API_KEY_NAME\",\n \"JCODE_OPENAI_COMPAT_API_KEY_NAME\",\n ] {\n let Some(key_name) = std::env::var(source)\n .ok()\n .map(|v| v.trim().to_string())\n .filter(|v| !v.is_empty())\n else {\n continue;\n };\n\n if !key_name\n .chars()\n .all(|c| c.is_ascii_uppercase() || c.is_ascii_digit() || c == '_')\n {\n continue;\n }\n if !redacted_keys.insert(key_name.clone()) {\n continue;\n }\n\n let pattern = format!(r\"(?m)^\\s*({}\\s*=\\s*)[^\\r\\n]+\", regex::escape(&key_name));\n if let Ok(re) = Regex::new(&pattern) {\n redacted = re\n .replace_all(&redacted, \"${1}[REDACTED_SECRET]\")\n .into_owned();\n }\n }\n\n redacted\n}\n\npub const GENERATED_IMAGE_TOOL_NAME: &str = \"image_generation\";\npub const GENERATED_IMAGE_MAX_AUTO_VISION_BYTES: u64 = 20 * 1024 * 1024;\n\npub fn generated_image_tool_input(\n path: &str,\n metadata_path: Option<&str>,\n output_format: &str,\n revised_prompt: Option<&str>,\n) -> serde_json::Value {\n serde_json::json!({\n \"path\": path,\n \"metadata_path\": metadata_path,\n \"output_format\": output_format,\n \"revised_prompt\": revised_prompt,\n })\n}\n\npub fn generated_image_summary(\n path: &str,\n metadata_path: Option<&str>,\n output_format: &str,\n revised_prompt: Option<&str>,\n) -> String {\n let mut summary = format!(\"Generated image ({}) saved to `{}`.\", output_format, path);\n if let Some(metadata_path) = metadata_path {\n summary.push_str(&format!(\"\\nMetadata saved to `{}`.\", metadata_path));\n }\n if let Some(revised_prompt) = revised_prompt.filter(|prompt| !prompt.trim().is_empty()) {\n summary.push_str(\"\\n\\nRevised prompt:\\n\");\n summary.push_str(revised_prompt.trim());\n }\n summary\n}\n\npub fn generated_image_visual_context_blocks(\n path: &str,\n metadata_path: Option<&str>,\n output_format: &str,\n revised_prompt: Option<&str>,\n) -> Option<Vec<ContentBlock>> {\n let path_ref = Path::new(path);\n let metadata = std::fs::metadata(path_ref).ok()?;\n if !metadata.is_file() || metadata.len() > GENERATED_IMAGE_MAX_AUTO_VISION_BYTES {\n return None;\n }\n\n let data = std::fs::read(path_ref).ok()?;\n let media_type = generated_image_media_type(path_ref, output_format).to_string();\n let data_b64 = base64::engine::general_purpose::STANDARD.encode(data);\n let mut reminder = format!(\n \"<system-reminder>\\nA provider-native image generation call created `{}`. Jcode attached the image pixels as visual context for future turns because the active provider supports image input and the file is under the safe {} MB limit.\\nFormat: {}\",\n path,\n GENERATED_IMAGE_MAX_AUTO_VISION_BYTES / 1024 / 1024,\n output_format,\n );\n if let Some(metadata_path) = metadata_path.filter(|value| !value.trim().is_empty()) {\n reminder.push_str(&format!(\"\\nMetadata: {}\", metadata_path));\n }\n if let Some(revised_prompt) = revised_prompt.filter(|value| !value.trim().is_empty()) {\n reminder.push_str(\"\\nRevised prompt:\\n\");\n reminder.push_str(revised_prompt.trim());\n }\n reminder.push_str(\"\\n</system-reminder>\");\n\n Some(vec![\n ContentBlock::Text {\n text: reminder,\n cache_control: None,\n },\n ContentBlock::Image {\n media_type,\n data: data_b64,\n },\n ])\n}\n\nfn generated_image_media_type(path: &Path, output_format: &str) -> &'static str {\n let ext = path\n .extension()\n .and_then(|value| value.to_str())\n .unwrap_or(output_format)\n .to_ascii_lowercase();\n match ext.as_str() {\n \"jpg\" | \"jpeg\" => \"image/jpeg\",\n \"webp\" => \"image/webp\",\n \"gif\" => \"image/gif\",\n \"bmp\" => \"image/bmp\",\n \"ico\" => \"image/x-icon\",\n _ => \"image/png\",\n }\n}\n\n#[cfg(test)]\nmod tests;\n"}, "1fa14ac154707fe32c6ca90c67c880ec3eea0a22": {"t": 1785757202.1485162, "n": 10410, "h": "7bcad34729df01de", "c": "use crate::bus::{BackgroundTaskCompleted, BackgroundTaskProgressEvent, BackgroundTaskStatus};\nuse base64::Engine as _;\nuse regex::Regex;\nuse std::collections::HashSet;\nuse std::path::Path;\nuse std::sync::OnceLock;\n\npub use jcode_message_types::{\n CacheControl, ConnectionPhase, ContentBlock, InputShellResult, Message, Role, StreamEvent,\n TOOL_OUTPUT_MISSING_TEXT, ToolCall, ToolDefinition, ends_with_fresh_user_turn,\n extend_stable_hash, messages_with_dynamic_system_context, sanitize_tool_id,\n stable_message_hash,\n};\n\nmod notifications;\n\npub use notifications::{\n ParsedBackgroundTaskNotification, ParsedBackgroundTaskProgressNotification,\n background_task_display_label, background_task_status_notice,\n format_background_task_notification_markdown, format_background_task_progress_markdown,\n format_input_shell_result_markdown, input_shell_status_notice,\n parse_background_task_notification_markdown,\n parse_background_task_progress_notification_markdown,\n};\n\nfn compile_static_regex(pattern: &str) -> Option<Regex> {\n match Regex::new(pattern) {\n Ok(regex) => Some(regex),\n Err(err) => {\n eprintln!(\"jcode: failed to compile static regex: {err}\");\n None\n }\n }\n}\n\nfn compile_static_regexes(patterns: &[&str]) -> Vec<Regex> {\n patterns\n .iter()\n .filter_map(|pattern| compile_static_regex(pattern))\n .collect()\n}\n\n/// Redact likely secrets from persisted tool output.\n///\n/// This is a best-effort safeguard for local session history files. It targets\n/// high-confidence token/key patterns and common `KEY=VALUE` assignments used by\n/// auth flows.\npub fn redact_secrets(text: &str) -> String {\n // Fast path to avoid regex work for most tool outputs.\n let lower = text.to_ascii_lowercase();\n\n if !text.contains(\"sk-\")\n && !text.contains(\"ghp_\")\n && !text.contains(\"github_pat_\")\n && !text.contains(\"AIza\")\n && !text.contains(\"ya29.\")\n && !text.contains(\"xox\")\n && !lower.contains(\"bearer \")\n && !lower.contains(\"api_key\")\n && !lower.contains(\"authorization\")\n && !lower.contains(\"password\")\n && !lower.contains(\"token\")\n {\n return text.to_string();\n }\n\n static DIRECT_PATTERNS: OnceLock<Vec<Regex>> = OnceLock::new();\n static ASSIGNMENT_PATTERNS: OnceLock<Vec<Regex>> = OnceLock::new();\n\n let direct_patterns = DIRECT_PATTERNS.get_or_init(|| {\n compile_static_regexes(&[\n r\"sk-ant-(?:oat|ort)01-[A-Za-z0-9_-]{20,}\",\n r\"sk-or-v1-[A-Za-z0-9_-]{20,}\",\n r\"ghp_[A-Za-z0-9]{20,}\",\n r\"github_pat_[A-Za-z0-9_]{20,}\",\n r\"ya29\\.[A-Za-z0-9._-]{20,}\",\n r\"AIza[0-9A-Za-z_-]{20,}\",\n r\"xox[baprs]-[A-Za-z0-9-]{10,}\",\n r\"(?i)\\bBearer\\s+[A-Za-z0-9._~+/=-]{16,}\",\n ])\n });\n\n let assignment_patterns = ASSIGNMENT_PATTERNS.get_or_init(|| {\n compile_static_regexes(&[\n r\"(?m)^\\s*(OPENROUTER_API_KEY\\s*=\\s*)[^\\r\\n]+\",\n r\"(?m)^\\s*(OPENCODE_API_KEY\\s*=\\s*)[^\\r\\n]+\",\n r\"(?m)^\\s*(OPENCODE_GO_API_KEY\\s*=\\s*)[^\\r\\n]+\",\n r\"(?m)^\\s*(ZHIPU_API_KEY\\s*=\\s*)[^\\r\\n]+\",\n r\"(?m)^\\s*(ZAI_API_KEY\\s*=\\s*)[^\\r\\n]+\",\n r\"(?m)^\\s*(302AI_API_KEY\\s*=\\s*)[^\\r\\n]+\",\n r\"(?m)^\\s*(BASETEN_API_KEY\\s*=\\s*)[^\\r\\n]+\",\n r\"(?m)^\\s*(CORTECS_API_KEY\\s*=\\s*)[^\\r\\n]+\",\n r\"(?m)^\\s*(DEEPSEEK_API_KEY\\s*=\\s*)[^\\r\\n]+\",\n r\"(?m)^\\s*(FIRMWARE_API_KEY\\s*=\\s*)[^\\r\\n]+\",\n r\"(?m)^\\s*(HF_TOKEN\\s*=\\s*)[^\\r\\n]+\",\n r\"(?m)^\\s*(MOONSHOT_API_KEY\\s*=\\s*)[^\\r\\n]+\",\n r\"(?m)^\\s*(NEBIUS_API_KEY\\s*=\\s*)[^\\r\\n]+\",\n r\"(?m)^\\s*(SCALEWAY_API_KEY\\s*=\\s*)[^\\r\\n]+\",\n r\"(?m)^\\s*(STACKIT_API_KEY\\s*=\\s*)[^\\r\\n]+\",\n r\"(?m)^\\s*(GROQ_API_KEY\\s*=\\s*)[^\\r\\n]+\",\n r\"(?m)^\\s*(MISTRAL_API_KEY\\s*=\\s*)[^\\r\\n]+\",\n r\"(?m)^\\s*(PERPLEXITY_API_KEY\\s*=\\s*)[^\\r\\n]+\",\n r\"(?m)^\\s*(TOGETHER_API_KEY\\s*=\\s*)[^\\r\\n]+\",\n r\"(?m)^\\s*(DEEPINFRA_API_KEY\\s*=\\s*)[^\\r\\n]+\",\n r\"(?m)^\\s*(XAI_API_KEY\\s*=\\s*)[^\\r\\n]+\",\n r\"(?m)^\\s*(LMSTUDIO_API_KEY\\s*=\\s*)[^\\r\\n]+\",\n r\"(?m)^\\s*(OLLAMA_API_KEY\\s*=\\s*)[^\\r\\n]+\",\n r\"(?m)^\\s*(CHUTES_API_KEY\\s*=\\s*)[^\\r\\n]+\",\n r\"(?m)^\\s*(CEREBRAS_API_KEY\\s*=\\s*)[^\\r\\n]+\",\n r\"(?m)^\\s*(OPENAI_COMPAT_API_KEY\\s*=\\s*)[^\\r\\n]+\",\n r\"(?m)^\\s*(ANTHROPIC_API_KEY\\s*=\\s*)[^\\r\\n]+\",\n r\"(?m)^\\s*(OPENAI_API_KEY\\s*=\\s*)[^\\r\\n]+\",\n r\"(?m)^\\s*(AZURE_OPENAI_API_KEY\\s*=\\s*)[^\\r\\n]+\",\n r\"(?m)^\\s*(CURSOR_API_KEY\\s*=\\s*)[^\\r\\n]+\",\n r\"(?m)^\\s*(GITHUB_TOKEN\\s*=\\s*)[^\\r\\n]+\",\n r\"(?im)^(\\s*(?:authorization|proxy-authorization)\\s*[:=]\\s*)[^\\r\\n]+\",\n r\"(?im)^(\\s*[A-Z0-9_]*(?:PASSWORD|TOKEN|SECRET)[A-Z0-9_]*\\s*=\\s*)[^\\r\\n]+\",\n r#\"(?im)(\\\"[A-Za-z0-9_-]*(?:password|token|secret)[A-Za-z0-9_-]*\\\"\\s*:\\s*)\\\"[^\\\"\\r\\n]{4,}\\\"\"#,\n ])\n });\n\n let mut redacted = text.to_string();\n let mut redacted_keys: HashSet<String> = [\n \"OPENROUTER_API_KEY\",\n \"OPENCODE_API_KEY\",\n \"OPENCODE_GO_API_KEY\",\n \"ZHIPU_API_KEY\",\n \"ZAI_API_KEY\",\n \"302AI_API_KEY\",\n \"BASETEN_API_KEY\",\n \"CORTECS_API_KEY\",\n \"DEEPSEEK_API_KEY\",\n \"FIRMWARE_API_KEY\",\n \"HF_TOKEN\",\n \"MOONSHOT_API_KEY\",\n \"NEBIUS_API_KEY\",\n \"SCALEWAY_API_KEY\",\n \"STACKIT_API_KEY\",\n \"GROQ_API_KEY\",\n \"MISTRAL_API_KEY\",\n \"PERPLEXITY_API_KEY\",\n \"TOGETHER_API_KEY\",\n \"DEEPINFRA_API_KEY\",\n \"XAI_API_KEY\",\n \"LMSTUDIO_API_KEY\",\n \"OLLAMA_API_KEY\",\n \"CHUTES_API_KEY\",\n \"CEREBRAS_API_KEY\",\n \"OPENAI_COMPAT_API_KEY\",\n \"ANTHROPIC_API_KEY\",\n \"OPENAI_API_KEY\",\n \"AZURE_OPENAI_API_KEY\",\n \"CURSOR_API_KEY\",\n \"GITHUB_TOKEN\",\n ]\n .iter()\n .map(|k| (*k).to_string())\n .collect();\n\n for re in direct_patterns {\n redacted = re.replace_all(&redacted, \"[REDACTED_SECRET]\").into_owned();\n }\n\n for re in assignment_patterns {\n redacted = re\n .replace_all(&redacted, \"${1}[REDACTED_SECRET]\")\n .into_owned();\n }\n\n // Also redact custom API key variable names configured at runtime.\n for source in [\n \"JCODE_OPENROUTER_API_KEY_NAME\",\n \"JCODE_OPENAI_COMPAT_API_KEY_NAME\",\n ] {\n let Some(key_name) = std::env::var(source)\n .ok()\n .map(|v| v.trim().to_string())\n .filter(|v| !v.is_empty())\n else {\n continue;\n };\n\n if !key_name\n .chars()\n .all(|c| c.is_ascii_uppercase() || c.is_ascii_digit() || c == '_')\n {\n continue;\n }\n if !redacted_keys.insert(key_name.clone()) {\n continue;\n }\n\n let pattern = format!(r\"(?m)^\\s*({}\\s*=\\s*)[^\\r\\n]+\", regex::escape(&key_name));\n if let Ok(re) = Regex::new(&pattern) {\n redacted = re\n .replace_all(&redacted, \"${1}[REDACTED_SECRET]\")\n .into_owned();\n }\n }\n\n redacted\n}\n\npub const GENERATED_IMAGE_TOOL_NAME: &str = \"image_generation\";\npub const GENERATED_IMAGE_MAX_AUTO_VISION_BYTES: u64 = 20 * 1024 * 1024;\n\npub fn generated_image_tool_input(\n path: &str,\n metadata_path: Option<&str>,\n output_format: &str,\n revised_prompt: Option<&str>,\n) -> serde_json::Value {\n serde_json::json!({\n \"path\": path,\n \"metadata_path\": metadata_path,\n \"output_format\": output_format,\n \"revised_prompt\": revised_prompt,\n })\n}\n\npub fn generated_image_summary(\n path: &str,\n metadata_path: Option<&str>,\n output_format: &str,\n revised_prompt: Option<&str>,\n) -> String {\n let mut summary = format!(\"Generated image ({}) saved to `{}`.\", output_format, path);\n if let Some(metadata_path) = metadata_path {\n summary.push_str(&format!(\"\\nMetadata saved to `{}`.\", metadata_path));\n }\n if let Some(revised_prompt) = revised_prompt.filter(|prompt| !prompt.trim().is_empty()) {\n summary.push_str(\"\\n\\nRevised prompt:\\n\");\n summary.push_str(revised_prompt.trim());\n }\n summary\n}\n\npub fn generated_image_visual_context_blocks(\n path: &str,\n metadata_path: Option<&str>,\n output_format: &str,\n revised_prompt: Option<&str>,\n) -> Option<Vec<ContentBlock>> {\n let path_ref = Path::new(path);\n let metadata = std::fs::metadata(path_ref).ok()?;\n if !metadata.is_file() || metadata.len() > GENERATED_IMAGE_MAX_AUTO_VISION_BYTES {\n return None;\n }\n\n let data = std::fs::read(path_ref).ok()?;\n let media_type = generated_image_media_type(path_ref, output_format).to_string();\n let data_b64 = base64::engine::general_purpose::STANDARD.encode(data);\n let mut reminder = format!(\n \"<system-reminder>\\nA provider-native image generation call created `{}`. Jcode attached the image pixels as visual context for future turns because the active provider supports image input and the file is under the safe {} MB limit.\\nFormat: {}\",\n path,\n GENERATED_IMAGE_MAX_AUTO_VISION_BYTES / 1024 / 1024,\n output_format,\n );\n if let Some(metadata_path) = metadata_path.filter(|value| !value.trim().is_empty()) {\n reminder.push_str(&format!(\"\\nMetadata: {}\", metadata_path));\n }\n if let Some(revised_prompt) = revised_prompt.filter(|value| !value.trim().is_empty()) {\n reminder.push_str(\"\\nRevised prompt:\\n\");\n reminder.push_str(revised_prompt.trim());\n }\n reminder.push_str(\"\\n</system-reminder>\");\n\n Some(vec![\n ContentBlock::Text {\n text: reminder,\n cache_control: None,\n },\n ContentBlock::Image {\n media_type,\n data: data_b64,\n },\n ])\n}\n\nfn generated_image_media_type(path: &Path, output_format: &str) -> &'static str {\n let ext = path\n .extension()\n .and_then(|value| value.to_str())\n .unwrap_or(output_format)\n .to_ascii_lowercase();\n match ext.as_str() {\n \"jpg\" | \"jpeg\" => \"image/jpeg\",\n \"webp\" => \"image/webp\",\n \"gif\" => \"image/gif\",\n \"bmp\" => \"image/bmp\",\n \"ico\" => \"image/x-icon\",\n _ => \"image/png\",\n }\n}\n\n#[cfg(test)]\nmod tests;\n"}, "33ce91d7c4e085ae072e42413bef1391e65e700a": {"t": 1785757203.5682838, "n": 9956, "h": "436a3623019b6bed", "c": "use crate::bus::{BackgroundTaskCompleted, BackgroundTaskProgressEvent, BackgroundTaskStatus};\nuse base64::Engine as _;\nuse regex::Regex;\nuse std::collections::HashSet;\nuse std::path::Path;\nuse std::sync::OnceLock;\n\npub use jcode_message_types::{\n CacheControl, ConnectionPhase, ContentBlock, InputShellResult, Message, Role, StreamEvent,\n TOOL_OUTPUT_MISSING_TEXT, ToolCall, ToolDefinition, ends_with_fresh_user_turn,\n extend_stable_hash, messages_with_dynamic_system_context, sanitize_tool_id,\n stable_message_hash,\n};\n\nmod notifications;\n\npub use notifications::{\n ParsedBackgroundTaskNotification, ParsedBackgroundTaskProgressNotification,\n background_task_display_label, background_task_status_notice,\n format_background_task_notification_markdown, format_background_task_progress_markdown,\n format_input_shell_result_markdown, input_shell_status_notice,\n parse_background_task_notification_markdown,\n parse_background_task_progress_notification_markdown,\n};\n\nfn compile_static_regex(pattern: &str) -> Option<Regex> {\n match Regex::new(pattern) {\n Ok(regex) => Some(regex),\n Err(err) => {\n eprintln!(\"jcode: failed to compile static regex: {err}\");\n None\n }\n }\n}\n\nfn compile_static_regexes(patterns: &[&str]) -> Vec<Regex> {\n patterns\n .iter()\n .filter_map(|pattern| compile_static_regex(pattern))\n .collect()\n}\n\n/// Redact likely secrets from persisted tool output.\n///\n/// This is a best-effort safeguard for local session history files. It targets\n/// high-confidence token/key patterns and common `KEY=VALUE` assignments used by\n/// auth flows.\npub fn redact_secrets(text: &str) -> String {\n // Fast path to avoid regex work for most tool outputs.\n let lower = text.to_ascii_lowercase();\n\n if !text.contains(\"sk-\")\n && !text.contains(\"ghp_\")\n && !text.contains(\"github_pat_\")\n && !text.contains(\"AIza\")\n && !text.contains(\"ya29.\")\n && !text.contains(\"xox\")\n && !lower.contains(\"api_key\")\n && !lower.contains(\"token\")\n {\n return text.to_string();\n }\n\n static DIRECT_PATTERNS: OnceLock<Vec<Regex>> = OnceLock::new();\n static ASSIGNMENT_PATTERNS: OnceLock<Vec<Regex>> = OnceLock::new();\n\n let direct_patterns = DIRECT_PATTERNS.get_or_init(|| {\n compile_static_regexes(&[\n r\"sk-ant-(?:oat|ort)01-[A-Za-z0-9_-]{20,}\",\n r\"sk-or-v1-[A-Za-z0-9_-]{20,}\",\n r\"ghp_[A-Za-z0-9]{20,}\",\n r\"github_pat_[A-Za-z0-9_]{20,}\",\n r\"ya29\\.[A-Za-z0-9._-]{20,}\",\n r\"AIza[0-9A-Za-z_-]{20,}\",\n r\"xox[baprs]-[A-Za-z0-9-]{10,}\",\n ])\n });\n\n let assignment_patterns = ASSIGNMENT_PATTERNS.get_or_init(|| {\n compile_static_regexes(&[\n r\"(?m)^\\s*(OPENROUTER_API_KEY\\s*=\\s*)[^\\r\\n]+\",\n r\"(?m)^\\s*(OPENCODE_API_KEY\\s*=\\s*)[^\\r\\n]+\",\n r\"(?m)^\\s*(OPENCODE_GO_API_KEY\\s*=\\s*)[^\\r\\n]+\",\n r\"(?m)^\\s*(ZHIPU_API_KEY\\s*=\\s*)[^\\r\\n]+\",\n r\"(?m)^\\s*(ZAI_API_KEY\\s*=\\s*)[^\\r\\n]+\",\n r\"(?m)^\\s*(302AI_API_KEY\\s*=\\s*)[^\\r\\n]+\",\n r\"(?m)^\\s*(BASETEN_API_KEY\\s*=\\s*)[^\\r\\n]+\",\n r\"(?m)^\\s*(CORTECS_API_KEY\\s*=\\s*)[^\\r\\n]+\",\n r\"(?m)^\\s*(DEEPSEEK_API_KEY\\s*=\\s*)[^\\r\\n]+\",\n r\"(?m)^\\s*(FIRMWARE_API_KEY\\s*=\\s*)[^\\r\\n]+\",\n r\"(?m)^\\s*(HF_TOKEN\\s*=\\s*)[^\\r\\n]+\",\n r\"(?m)^\\s*(MOONSHOT_API_KEY\\s*=\\s*)[^\\r\\n]+\",\n r\"(?m)^\\s*(NEBIUS_API_KEY\\s*=\\s*)[^\\r\\n]+\",\n r\"(?m)^\\s*(SCALEWAY_API_KEY\\s*=\\s*)[^\\r\\n]+\",\n r\"(?m)^\\s*(STACKIT_API_KEY\\s*=\\s*)[^\\r\\n]+\",\n r\"(?m)^\\s*(GROQ_API_KEY\\s*=\\s*)[^\\r\\n]+\",\n r\"(?m)^\\s*(MISTRAL_API_KEY\\s*=\\s*)[^\\r\\n]+\",\n r\"(?m)^\\s*(PERPLEXITY_API_KEY\\s*=\\s*)[^\\r\\n]+\",\n r\"(?m)^\\s*(TOGETHER_API_KEY\\s*=\\s*)[^\\r\\n]+\",\n r\"(?m)^\\s*(DEEPINFRA_API_KEY\\s*=\\s*)[^\\r\\n]+\",\n r\"(?m)^\\s*(XAI_API_KEY\\s*=\\s*)[^\\r\\n]+\",\n r\"(?m)^\\s*(LMSTUDIO_API_KEY\\s*=\\s*)[^\\r\\n]+\",\n r\"(?m)^\\s*(OLLAMA_API_KEY\\s*=\\s*)[^\\r\\n]+\",\n r\"(?m)^\\s*(CHUTES_API_KEY\\s*=\\s*)[^\\r\\n]+\",\n r\"(?m)^\\s*(CEREBRAS_API_KEY\\s*=\\s*)[^\\r\\n]+\",\n r\"(?m)^\\s*(OPENAI_COMPAT_API_KEY\\s*=\\s*)[^\\r\\n]+\",\n r\"(?m)^\\s*(ANTHROPIC_API_KEY\\s*=\\s*)[^\\r\\n]+\",\n r\"(?m)^\\s*(OPENAI_API_KEY\\s*=\\s*)[^\\r\\n]+\",\n r\"(?m)^\\s*(AZURE_OPENAI_API_KEY\\s*=\\s*)[^\\r\\n]+\",\n r\"(?m)^\\s*(CURSOR_API_KEY\\s*=\\s*)[^\\r\\n]+\",\n r\"(?m)^\\s*(GITHUB_TOKEN\\s*=\\s*)[^\\r\\n]+\",\n ])\n });\n\n let mut redacted = text.to_string();\n let mut redacted_keys: HashSet<String> = [\n \"OPENROUTER_API_KEY\",\n \"OPENCODE_API_KEY\",\n \"OPENCODE_GO_API_KEY\",\n \"ZHIPU_API_KEY\",\n \"ZAI_API_KEY\",\n \"302AI_API_KEY\",\n \"BASETEN_API_KEY\",\n \"CORTECS_API_KEY\",\n \"DEEPSEEK_API_KEY\",\n \"FIRMWARE_API_KEY\",\n \"HF_TOKEN\",\n \"MOONSHOT_API_KEY\",\n \"NEBIUS_API_KEY\",\n \"SCALEWAY_API_KEY\",\n \"STACKIT_API_KEY\",\n \"GROQ_API_KEY\",\n \"MISTRAL_API_KEY\",\n \"PERPLEXITY_API_KEY\",\n \"TOGETHER_API_KEY\",\n \"DEEPINFRA_API_KEY\",\n \"XAI_API_KEY\",\n \"LMSTUDIO_API_KEY\",\n \"OLLAMA_API_KEY\",\n \"CHUTES_API_KEY\",\n \"CEREBRAS_API_KEY\",\n \"OPENAI_COMPAT_API_KEY\",\n \"ANTHROPIC_API_KEY\",\n \"OPENAI_API_KEY\",\n \"AZURE_OPENAI_API_KEY\",\n \"CURSOR_API_KEY\",\n \"GITHUB_TOKEN\",\n ]\n .iter()\n .map(|k| (*k).to_string())\n .collect();\n\n for re in direct_patterns {\n redacted = re.replace_all(&redacted, \"[REDACTED_SECRET]\").into_owned();\n }\n\n for re in assignment_patterns {\n redacted = re\n .replace_all(&redacted, \"${1}[REDACTED_SECRET]\")\n .into_owned();\n }\n\n // Also redact custom API key variable names configured at runtime.\n for source in [\n \"JCODE_OPENROUTER_API_KEY_NAME\",\n \"JCODE_OPENAI_COMPAT_API_KEY_NAME\",\n ] {\n let Some(key_name) = std::env::var(source)\n .ok()\n .map(|v| v.trim().to_string())\n .filter(|v| !v.is_empty())\n else {\n continue;\n };\n\n if !key_name\n .chars()\n .all(|c| c.is_ascii_uppercase() || c.is_ascii_digit() || c == '_')\n {\n continue;\n }\n if !redacted_keys.insert(key_name.clone()) {\n continue;\n }\n\n let pattern = format!(r\"(?m)^\\s*({}\\s*=\\s*)[^\\r\\n]+\", regex::escape(&key_name));\n if let Ok(re) = Regex::new(&pattern) {\n redacted = re\n .replace_all(&redacted, \"${1}[REDACTED_SECRET]\")\n .into_owned();\n }\n }\n\n redacted\n}\n\npub const GENERATED_IMAGE_TOOL_NAME: &str = \"image_generation\";\npub const GENERATED_IMAGE_MAX_AUTO_VISION_BYTES: u64 = 20 * 1024 * 1024;\n\npub fn generated_image_tool_input(\n path: &str,\n metadata_path: Option<&str>,\n output_format: &str,\n revised_prompt: Option<&str>,\n) -> serde_json::Value {\n serde_json::json!({\n \"path\": path,\n \"metadata_path\": metadata_path,\n \"output_format\": output_format,\n \"revised_prompt\": revised_prompt,\n })\n}\n\npub fn generated_image_summary(\n path: &str,\n metadata_path: Option<&str>,\n output_format: &str,\n revised_prompt: Option<&str>,\n) -> String {\n let mut summary = format!(\"Generated image ({}) saved to `{}`.\", output_format, path);\n if let Some(metadata_path) = metadata_path {\n summary.push_str(&format!(\"\\nMetadata saved to `{}`.\", metadata_path));\n }\n if let Some(revised_prompt) = revised_prompt.filter(|prompt| !prompt.trim().is_empty()) {\n summary.push_str(\"\\n\\nRevised prompt:\\n\");\n summary.push_str(revised_prompt.trim());\n }\n summary\n}\n\npub fn generated_image_visual_context_blocks(\n path: &str,\n metadata_path: Option<&str>,\n output_format: &str,\n revised_prompt: Option<&str>,\n) -> Option<Vec<ContentBlock>> {\n let path_ref = Path::new(path);\n let metadata = std::fs::metadata(path_ref).ok()?;\n if !metadata.is_file() || metadata.len() > GENERATED_IMAGE_MAX_AUTO_VISION_BYTES {\n return None;\n }\n\n let data = std::fs::read(path_ref).ok()?;\n let media_type = generated_image_media_type(path_ref, output_format).to_string();\n let data_b64 = base64::engine::general_purpose::STANDARD.encode(data);\n let mut reminder = format!(\n \"<system-reminder>\\nA provider-native image generation call created `{}`. Jcode attached the image pixels as visual context for future turns because the active provider supports image input and the file is under the safe {} MB limit.\\nFormat: {}\",\n path,\n GENERATED_IMAGE_MAX_AUTO_VISION_BYTES / 1024 / 1024,\n output_format,\n );\n if let Some(metadata_path) = metadata_path.filter(|value| !value.trim().is_empty()) {\n reminder.push_str(&format!(\"\\nMetadata: {}\", metadata_path));\n }\n if let Some(revised_prompt) = revised_prompt.filter(|value| !value.trim().is_empty()) {\n reminder.push_str(\"\\nRevised prompt:\\n\");\n reminder.push_str(revised_prompt.trim());\n }\n reminder.push_str(\"\\n</system-reminder>\");\n\n Some(vec![\n ContentBlock::Text {\n text: reminder,\n cache_control: None,\n },\n ContentBlock::Image {\n media_type,\n data: data_b64,\n },\n ])\n}\n\nfn generated_image_media_type(path: &Path, output_format: &str) -> &'static str {\n let ext = path\n .extension()\n .and_then(|value| value.to_str())\n .unwrap_or(output_format)\n .to_ascii_lowercase();\n match ext.as_str() {\n \"jpg\" | \"jpeg\" => \"image/jpeg\",\n \"webp\" => \"image/webp\",\n \"gif\" => \"image/gif\",\n \"bmp\" => \"image/bmp\",\n \"ico\" => \"image/x-icon\",\n _ => \"image/png\",\n }\n}\n\n#[cfg(test)]\nmod tests;\n"}, "5bfd31d4db4d6baed5bfd5d426f125781817f385": {"t": 1785757204.9157453, "n": 9914, "h": "21109100f248eb0c", "c": "use crate::bus::{BackgroundTaskCompleted, BackgroundTaskProgressEvent, BackgroundTaskStatus};\nuse base64::Engine as _;\nuse regex::Regex;\nuse std::collections::HashSet;\nuse std::path::Path;\nuse std::sync::OnceLock;\n\npub use jcode_message_types::{\n CacheControl, ConnectionPhase, ContentBlock, InputShellResult, Message, Role, StreamEvent,\n TOOL_OUTPUT_MISSING_TEXT, ToolCall, ToolDefinition, ends_with_fresh_user_turn,\n extend_stable_hash, sanitize_tool_id, stable_message_hash,\n};\n\nmod notifications;\n\npub use notifications::{\n ParsedBackgroundTaskNotification, ParsedBackgroundTaskProgressNotification,\n background_task_display_label, background_task_status_notice,\n format_background_task_notification_markdown, format_background_task_progress_markdown,\n format_input_shell_result_markdown, input_shell_status_notice,\n parse_background_task_notification_markdown,\n parse_background_task_progress_notification_markdown,\n};\n\nfn compile_static_regex(pattern: &str) -> Option<Regex> {\n match Regex::new(pattern) {\n Ok(regex) => Some(regex),\n Err(err) => {\n eprintln!(\"jcode: failed to compile static regex: {err}\");\n None\n }\n }\n}\n\nfn compile_static_regexes(patterns: &[&str]) -> Vec<Regex> {\n patterns\n .iter()\n .filter_map(|pattern| compile_static_regex(pattern))\n .collect()\n}\n\n/// Redact likely secrets from persisted tool output.\n///\n/// This is a best-effort safeguard for local session history files. It targets\n/// high-confidence token/key patterns and common `KEY=VALUE` assignments used by\n/// auth flows.\npub fn redact_secrets(text: &str) -> String {\n // Fast path to avoid regex work for most tool outputs.\n let lower = text.to_ascii_lowercase();\n\n if !text.contains(\"sk-\")\n && !text.contains(\"ghp_\")\n && !text.contains(\"github_pat_\")\n && !text.contains(\"AIza\")\n && !text.contains(\"ya29.\")\n && !text.contains(\"xox\")\n && !lower.contains(\"api_key\")\n && !lower.contains(\"token\")\n {\n return text.to_string();\n }\n\n static DIRECT_PATTERNS: OnceLock<Vec<Regex>> = OnceLock::new();\n static ASSIGNMENT_PATTERNS: OnceLock<Vec<Regex>> = OnceLock::new();\n\n let direct_patterns = DIRECT_PATTERNS.get_or_init(|| {\n compile_static_regexes(&[\n r\"sk-ant-(?:oat|ort)01-[A-Za-z0-9_-]{20,}\",\n r\"sk-or-v1-[A-Za-z0-9_-]{20,}\",\n r\"ghp_[A-Za-z0-9]{20,}\",\n r\"github_pat_[A-Za-z0-9_]{20,}\",\n r\"ya29\\.[A-Za-z0-9._-]{20,}\",\n r\"AIza[0-9A-Za-z_-]{20,}\",\n r\"xox[baprs]-[A-Za-z0-9-]{10,}\",\n ])\n });\n\n let assignment_patterns = ASSIGNMENT_PATTERNS.get_or_init(|| {\n compile_static_regexes(&[\n r\"(?m)^\\s*(OPENROUTER_API_KEY\\s*=\\s*)[^\\r\\n]+\",\n r\"(?m)^\\s*(OPENCODE_API_KEY\\s*=\\s*)[^\\r\\n]+\",\n r\"(?m)^\\s*(OPENCODE_GO_API_KEY\\s*=\\s*)[^\\r\\n]+\",\n r\"(?m)^\\s*(ZHIPU_API_KEY\\s*=\\s*)[^\\r\\n]+\",\n r\"(?m)^\\s*(ZAI_API_KEY\\s*=\\s*)[^\\r\\n]+\",\n r\"(?m)^\\s*(302AI_API_KEY\\s*=\\s*)[^\\r\\n]+\",\n r\"(?m)^\\s*(BASETEN_API_KEY\\s*=\\s*)[^\\r\\n]+\",\n r\"(?m)^\\s*(CORTECS_API_KEY\\s*=\\s*)[^\\r\\n]+\",\n r\"(?m)^\\s*(DEEPSEEK_API_KEY\\s*=\\s*)[^\\r\\n]+\",\n r\"(?m)^\\s*(FIRMWARE_API_KEY\\s*=\\s*)[^\\r\\n]+\",\n r\"(?m)^\\s*(HF_TOKEN\\s*=\\s*)[^\\r\\n]+\",\n r\"(?m)^\\s*(MOONSHOT_API_KEY\\s*=\\s*)[^\\r\\n]+\",\n r\"(?m)^\\s*(NEBIUS_API_KEY\\s*=\\s*)[^\\r\\n]+\",\n r\"(?m)^\\s*(SCALEWAY_API_KEY\\s*=\\s*)[^\\r\\n]+\",\n r\"(?m)^\\s*(STACKIT_API_KEY\\s*=\\s*)[^\\r\\n]+\",\n r\"(?m)^\\s*(GROQ_API_KEY\\s*=\\s*)[^\\r\\n]+\",\n r\"(?m)^\\s*(MISTRAL_API_KEY\\s*=\\s*)[^\\r\\n]+\",\n r\"(?m)^\\s*(PERPLEXITY_API_KEY\\s*=\\s*)[^\\r\\n]+\",\n r\"(?m)^\\s*(TOGETHER_API_KEY\\s*=\\s*)[^\\r\\n]+\",\n r\"(?m)^\\s*(DEEPINFRA_API_KEY\\s*=\\s*)[^\\r\\n]+\",\n r\"(?m)^\\s*(XAI_API_KEY\\s*=\\s*)[^\\r\\n]+\",\n r\"(?m)^\\s*(LMSTUDIO_API_KEY\\s*=\\s*)[^\\r\\n]+\",\n r\"(?m)^\\s*(OLLAMA_API_KEY\\s*=\\s*)[^\\r\\n]+\",\n r\"(?m)^\\s*(CHUTES_API_KEY\\s*=\\s*)[^\\r\\n]+\",\n r\"(?m)^\\s*(CEREBRAS_API_KEY\\s*=\\s*)[^\\r\\n]+\",\n r\"(?m)^\\s*(OPENAI_COMPAT_API_KEY\\s*=\\s*)[^\\r\\n]+\",\n r\"(?m)^\\s*(ANTHROPIC_API_KEY\\s*=\\s*)[^\\r\\n]+\",\n r\"(?m)^\\s*(OPENAI_API_KEY\\s*=\\s*)[^\\r\\n]+\",\n r\"(?m)^\\s*(AZURE_OPENAI_API_KEY\\s*=\\s*)[^\\r\\n]+\",\n r\"(?m)^\\s*(CURSOR_API_KEY\\s*=\\s*)[^\\r\\n]+\",\n r\"(?m)^\\s*(GITHUB_TOKEN\\s*=\\s*)[^\\r\\n]+\",\n ])\n });\n\n let mut redacted = text.to_string();\n let mut redacted_keys: HashSet<String> = [\n \"OPENROUTER_API_KEY\",\n \"OPENCODE_API_KEY\",\n \"OPENCODE_GO_API_KEY\",\n \"ZHIPU_API_KEY\",\n \"ZAI_API_KEY\",\n \"302AI_API_KEY\",\n \"BASETEN_API_KEY\",\n \"CORTECS_API_KEY\",\n \"DEEPSEEK_API_KEY\",\n \"FIRMWARE_API_KEY\",\n \"HF_TOKEN\",\n \"MOONSHOT_API_KEY\",\n \"NEBIUS_API_KEY\",\n \"SCALEWAY_API_KEY\",\n \"STACKIT_API_KEY\",\n \"GROQ_API_KEY\",\n \"MISTRAL_API_KEY\",\n \"PERPLEXITY_API_KEY\",\n \"TOGETHER_API_KEY\",\n \"DEEPINFRA_API_KEY\",\n \"XAI_API_KEY\",\n \"LMSTUDIO_API_KEY\",\n \"OLLAMA_API_KEY\",\n \"CHUTES_API_KEY\",\n \"CEREBRAS_API_KEY\",\n \"OPENAI_COMPAT_API_KEY\",\n \"ANTHROPIC_API_KEY\",\n \"OPENAI_API_KEY\",\n \"AZURE_OPENAI_API_KEY\",\n \"CURSOR_API_KEY\",\n \"GITHUB_TOKEN\",\n ]\n .iter()\n .map(|k| (*k).to_string())\n .collect();\n\n for re in direct_patterns {\n redacted = re.replace_all(&redacted, \"[REDACTED_SECRET]\").into_owned();\n }\n\n for re in assignment_patterns {\n redacted = re\n .replace_all(&redacted, \"${1}[REDACTED_SECRET]\")\n .into_owned();\n }\n\n // Also redact custom API key variable names configured at runtime.\n for source in [\n \"JCODE_OPENROUTER_API_KEY_NAME\",\n \"JCODE_OPENAI_COMPAT_API_KEY_NAME\",\n ] {\n let Some(key_name) = std::env::var(source)\n .ok()\n .map(|v| v.trim().to_string())\n .filter(|v| !v.is_empty())\n else {\n continue;\n };\n\n if !key_name\n .chars()\n .all(|c| c.is_ascii_uppercase() || c.is_ascii_digit() || c == '_')\n {\n continue;\n }\n if !redacted_keys.insert(key_name.clone()) {\n continue;\n }\n\n let pattern = format!(r\"(?m)^\\s*({}\\s*=\\s*)[^\\r\\n]+\", regex::escape(&key_name));\n if let Ok(re) = Regex::new(&pattern) {\n redacted = re\n .replace_all(&redacted, \"${1}[REDACTED_SECRET]\")\n .into_owned();\n }\n }\n\n redacted\n}\n\npub const GENERATED_IMAGE_TOOL_NAME: &str = \"image_generation\";\npub const GENERATED_IMAGE_MAX_AUTO_VISION_BYTES: u64 = 20 * 1024 * 1024;\n\npub fn generated_image_tool_input(\n path: &str,\n metadata_path: Option<&str>,\n output_format: &str,\n revised_prompt: Option<&str>,\n) -> serde_json::Value {\n serde_json::json!({\n \"path\": path,\n \"metadata_path\": metadata_path,\n \"output_format\": output_format,\n \"revised_prompt\": revised_prompt,\n })\n}\n\npub fn generated_image_summary(\n path: &str,\n metadata_path: Option<&str>,\n output_format: &str,\n revised_prompt: Option<&str>,\n) -> String {\n let mut summary = format!(\"Generated image ({}) saved to `{}`.\", output_format, path);\n if let Some(metadata_path) = metadata_path {\n summary.push_str(&format!(\"\\nMetadata saved to `{}`.\", metadata_path));\n }\n if let Some(revised_prompt) = revised_prompt.filter(|prompt| !prompt.trim().is_empty()) {\n summary.push_str(\"\\n\\nRevised prompt:\\n\");\n summary.push_str(revised_prompt.trim());\n }\n summary\n}\n\npub fn generated_image_visual_context_blocks(\n path: &str,\n metadata_path: Option<&str>,\n output_format: &str,\n revised_prompt: Option<&str>,\n) -> Option<Vec<ContentBlock>> {\n let path_ref = Path::new(path);\n let metadata = std::fs::metadata(path_ref).ok()?;\n if !metadata.is_file() || metadata.len() > GENERATED_IMAGE_MAX_AUTO_VISION_BYTES {\n return None;\n }\n\n let data = std::fs::read(path_ref).ok()?;\n let media_type = generated_image_media_type(path_ref, output_format).to_string();\n let data_b64 = base64::engine::general_purpose::STANDARD.encode(data);\n let mut reminder = format!(\n \"<system-reminder>\\nA provider-native image generation call created `{}`. Jcode attached the image pixels as visual context for future turns because the active provider supports image input and the file is under the safe {} MB limit.\\nFormat: {}\",\n path,\n GENERATED_IMAGE_MAX_AUTO_VISION_BYTES / 1024 / 1024,\n output_format,\n );\n if let Some(metadata_path) = metadata_path.filter(|value| !value.trim().is_empty()) {\n reminder.push_str(&format!(\"\\nMetadata: {}\", metadata_path));\n }\n if let Some(revised_prompt) = revised_prompt.filter(|value| !value.trim().is_empty()) {\n reminder.push_str(\"\\nRevised prompt:\\n\");\n reminder.push_str(revised_prompt.trim());\n }\n reminder.push_str(\"\\n</system-reminder>\");\n\n Some(vec![\n ContentBlock::Text {\n text: reminder,\n cache_control: None,\n },\n ContentBlock::Image {\n media_type,\n data: data_b64,\n },\n ])\n}\n\nfn generated_image_media_type(path: &Path, output_format: &str) -> &'static str {\n let ext = path\n .extension()\n .and_then(|value| value.to_str())\n .unwrap_or(output_format)\n .to_ascii_lowercase();\n match ext.as_str() {\n \"jpg\" | \"jpeg\" => \"image/jpeg\",\n \"webp\" => \"image/webp\",\n \"gif\" => \"image/gif\",\n \"bmp\" => \"image/bmp\",\n \"ico\" => \"image/x-icon\",\n _ => \"image/png\",\n }\n}\n\n#[cfg(test)]\nmod tests;\n"}, "80b7ffd60d799b27cf40cde3392b2af6d4312a89": {"t": 1785757207.590232, "n": 12524, "h": "cb056f268aa63288", "c": "use crate::bus::{BackgroundTaskCompleted, BackgroundTaskProgressEvent, BackgroundTaskStatus};\nuse base64::Engine as _;\nuse regex::Regex;\nuse std::collections::HashSet;\nuse std::path::Path;\nuse std::sync::OnceLock;\n\npub use jcode_message_types::{\n CacheControl, ConnectionPhase, ContentBlock, InputShellResult, Message, Role, StreamEvent,\n ToolCall, ToolDefinition,\n};\n\nmod notifications;\n\npub use notifications::{\n ParsedBackgroundTaskNotification, ParsedBackgroundTaskProgressNotification,\n background_task_display_label, background_task_status_notice,\n format_background_task_notification_markdown, format_background_task_progress_markdown,\n format_input_shell_result_markdown, input_shell_status_notice,\n parse_background_task_notification_markdown,\n parse_background_task_progress_notification_markdown,\n};\n\nfn compile_static_regex(pattern: &str) -> Option<Regex> {\n match Regex::new(pattern) {\n Ok(regex) => Some(regex),\n Err(err) => {\n eprintln!(\"jcode: failed to compile static regex: {err}\");\n None\n }\n }\n}\n\nfn compile_static_regexes(patterns: &[&str]) -> Vec<Regex> {\n patterns\n .iter()\n .filter_map(|pattern| compile_static_regex(pattern))\n .collect()\n}\n\npub const TOOL_OUTPUT_MISSING_TEXT: &str =\n \"Tool output missing (session interrupted before tool execution completed)\";\n\nconst STABLE_HASH_SEED: u64 = 0xcbf29ce484222325;\nconst STABLE_HASH_PRIME: u64 = 0x100000001b3;\n\nfn stable_hash_bytes(bytes: &[u8]) -> u64 {\n let mut hash = STABLE_HASH_SEED;\n for byte in bytes {\n hash ^= *byte as u64;\n hash = hash.wrapping_mul(STABLE_HASH_PRIME);\n }\n hash\n}\n\npub(crate) fn extend_stable_hash(acc: u64, next: u64) -> u64 {\n stable_hash_bytes(&[acc.to_le_bytes().as_slice(), next.to_le_bytes().as_slice()].concat())\n}\n\npub(crate) fn stable_message_hash(message: &Message) -> u64 {\n match serde_json::to_vec(message) {\n Ok(bytes) => stable_hash_bytes(&bytes),\n Err(_) => stable_hash_bytes(format!(\"{:?}\", message).as_bytes()),\n }\n}\n\npub fn ends_with_fresh_user_turn(messages: &[Message]) -> bool {\n for msg in messages.iter().rev() {\n if msg.role != Role::User {\n return false;\n }\n\n if msg\n .content\n .iter()\n .any(|block| matches!(block, ContentBlock::ToolResult { .. }))\n {\n return false;\n }\n\n if msg.content.is_empty() {\n return false;\n }\n\n let mut saw_user_text = false;\n for block in &msg.content {\n match block {\n ContentBlock::Text { text, .. } => {\n let trimmed = text.trim();\n if !trimmed.is_empty() && !trimmed.starts_with(\"<system-reminder>\") {\n saw_user_text = true;\n }\n }\n _ => return false,\n }\n }\n\n if saw_user_text {\n return true;\n }\n\n if msg.is_internal_system_reminder() {\n continue;\n }\n\n return false;\n }\n\n false\n}\n\n/// Sanitize a tool ID so it matches the pattern `^[a-zA-Z0-9_-]+$`.\n///\n/// Different providers generate tool IDs in different formats. When switching\n/// from one provider to another mid-conversation, the historical tool IDs may\n/// contain characters that the new provider rejects (e.g., dots in Copilot IDs\n/// sent to Anthropic). This function replaces any invalid characters with\n/// underscores.\npub fn sanitize_tool_id(id: &str) -> String {\n if id.is_empty() {\n return \"unknown\".to_string();\n }\n let sanitized: String = id\n .chars()\n .map(|c| {\n if c.is_ascii_alphanumeric() || c == '_' || c == '-' {\n c\n } else {\n '_'\n }\n })\n .collect();\n if sanitized.is_empty() {\n \"unknown\".to_string()\n } else {\n sanitized\n }\n}\n\n/// Redact likely secrets from persisted tool output.\n///\n/// This is a best-effort safeguard for local session history files. It targets\n/// high-confidence token/key patterns and common `KEY=VALUE` assignments used by\n/// auth flows.\npub fn redact_secrets(text: &str) -> String {\n // Fast path to avoid regex work for most tool outputs.\n let lower = text.to_ascii_lowercase();\n\n if !text.contains(\"sk-\")\n && !text.contains(\"ghp_\")\n && !text.contains(\"github_pat_\")\n && !text.contains(\"AIza\")\n && !text.contains(\"ya29.\")\n && !text.contains(\"xox\")\n && !lower.contains(\"api_key\")\n && !lower.contains(\"token\")\n {\n return text.to_string();\n }\n\n static DIRECT_PATTERNS: OnceLock<Vec<Regex>> = OnceLock::new();\n static ASSIGNMENT_PATTERNS: OnceLock<Vec<Regex>> = OnceLock::new();\n\n let direct_patterns = DIRECT_PATTERNS.get_or_init(|| {\n compile_static_regexes(&[\n r\"sk-ant-(?:oat|ort)01-[A-Za-z0-9_-]{20,}\",\n r\"sk-or-v1-[A-Za-z0-9_-]{20,}\",\n r\"ghp_[A-Za-z0-9]{20,}\",\n r\"github_pat_[A-Za-z0-9_]{20,}\",\n r\"ya29\\.[A-Za-z0-9._-]{20,}\",\n r\"AIza[0-9A-Za-z_-]{20,}\",\n r\"xox[baprs]-[A-Za-z0-9-]{10,}\",\n ])\n });\n\n let assignment_patterns = ASSIGNMENT_PATTERNS.get_or_init(|| {\n compile_static_regexes(&[\n r\"(?m)^\\s*(OPENROUTER_API_KEY\\s*=\\s*)[^\\r\\n]+\",\n r\"(?m)^\\s*(OPENCODE_API_KEY\\s*=\\s*)[^\\r\\n]+\",\n r\"(?m)^\\s*(OPENCODE_GO_API_KEY\\s*=\\s*)[^\\r\\n]+\",\n r\"(?m)^\\s*(ZHIPU_API_KEY\\s*=\\s*)[^\\r\\n]+\",\n r\"(?m)^\\s*(ZAI_API_KEY\\s*=\\s*)[^\\r\\n]+\",\n r\"(?m)^\\s*(302AI_API_KEY\\s*=\\s*)[^\\r\\n]+\",\n r\"(?m)^\\s*(BASETEN_API_KEY\\s*=\\s*)[^\\r\\n]+\",\n r\"(?m)^\\s*(CORTECS_API_KEY\\s*=\\s*)[^\\r\\n]+\",\n r\"(?m)^\\s*(DEEPSEEK_API_KEY\\s*=\\s*)[^\\r\\n]+\",\n r\"(?m)^\\s*(FIRMWARE_API_KEY\\s*=\\s*)[^\\r\\n]+\",\n r\"(?m)^\\s*(HF_TOKEN\\s*=\\s*)[^\\r\\n]+\",\n r\"(?m)^\\s*(MOONSHOT_API_KEY\\s*=\\s*)[^\\r\\n]+\",\n r\"(?m)^\\s*(NEBIUS_API_KEY\\s*=\\s*)[^\\r\\n]+\",\n r\"(?m)^\\s*(SCALEWAY_API_KEY\\s*=\\s*)[^\\r\\n]+\",\n r\"(?m)^\\s*(STACKIT_API_KEY\\s*=\\s*)[^\\r\\n]+\",\n r\"(?m)^\\s*(GROQ_API_KEY\\s*=\\s*)[^\\r\\n]+\",\n r\"(?m)^\\s*(MISTRAL_API_KEY\\s*=\\s*)[^\\r\\n]+\",\n r\"(?m)^\\s*(PERPLEXITY_API_KEY\\s*=\\s*)[^\\r\\n]+\",\n r\"(?m)^\\s*(TOGETHER_API_KEY\\s*=\\s*)[^\\r\\n]+\",\n r\"(?m)^\\s*(DEEPINFRA_API_KEY\\s*=\\s*)[^\\r\\n]+\",\n r\"(?m)^\\s*(XAI_API_KEY\\s*=\\s*)[^\\r\\n]+\",\n r\"(?m)^\\s*(LMSTUDIO_API_KEY\\s*=\\s*)[^\\r\\n]+\",\n r\"(?m)^\\s*(OLLAMA_API_KEY\\s*=\\s*)[^\\r\\n]+\",\n r\"(?m)^\\s*(CHUTES_API_KEY\\s*=\\s*)[^\\r\\n]+\",\n r\"(?m)^\\s*(CEREBRAS_API_KEY\\s*=\\s*)[^\\r\\n]+\",\n r\"(?m)^\\s*(OPENAI_COMPAT_API_KEY\\s*=\\s*)[^\\r\\n]+\",\n r\"(?m)^\\s*(ANTHROPIC_API_KEY\\s*=\\s*)[^\\r\\n]+\",\n r\"(?m)^\\s*(OPENAI_API_KEY\\s*=\\s*)[^\\r\\n]+\",\n r\"(?m)^\\s*(AZURE_OPENAI_API_KEY\\s*=\\s*)[^\\r\\n]+\",\n r\"(?m)^\\s*(CURSOR_API_KEY\\s*=\\s*)[^\\r\\n]+\",\n r\"(?m)^\\s*(GITHUB_TOKEN\\s*=\\s*)[^\\r\\n]+\",\n ])\n });\n\n let mut redacted = text.to_string();\n let mut redacted_keys: HashSet<String> = [\n \"OPENROUTER_API_KEY\",\n \"OPENCODE_API_KEY\",\n \"OPENCODE_GO_API_KEY\",\n \"ZHIPU_API_KEY\",\n \"ZAI_API_KEY\",\n \"302AI_API_KEY\",\n \"BASETEN_API_KEY\",\n \"CORTECS_API_KEY\",\n \"DEEPSEEK_API_KEY\",\n \"FIRMWARE_API_KEY\",\n \"HF_TOKEN\",\n \"MOONSHOT_API_KEY\",\n \"NEBIUS_API_KEY\",\n \"SCALEWAY_API_KEY\",\n \"STACKIT_API_KEY\",\n \"GROQ_API_KEY\",\n \"MISTRAL_API_KEY\",\n \"PERPLEXITY_API_KEY\",\n \"TOGETHER_API_KEY\",\n \"DEEPINFRA_API_KEY\",\n \"XAI_API_KEY\",\n \"LMSTUDIO_API_KEY\",\n \"OLLAMA_API_KEY\",\n \"CHUTES_API_KEY\",\n \"CEREBRAS_API_KEY\",\n \"OPENAI_COMPAT_API_KEY\",\n \"ANTHROPIC_API_KEY\",\n \"OPENAI_API_KEY\",\n \"AZURE_OPENAI_API_KEY\",\n \"CURSOR_API_KEY\",\n \"GITHUB_TOKEN\",\n ]\n .iter()\n .map(|k| (*k).to_string())\n .collect();\n\n for re in direct_patterns {\n redacted = re.replace_all(&redacted, \"[REDACTED_SECRET]\").into_owned();\n }\n\n for re in assignment_patterns {\n redacted = re\n .replace_all(&redacted, \"${1}[REDACTED_SECRET]\")\n .into_owned();\n }\n\n // Also redact custom API key variable names configured at runtime.\n for source in [\n \"JCODE_OPENROUTER_API_KEY_NAME\",\n \"JCODE_OPENAI_COMPAT_API_KEY_NAME\",\n ] {\n let Some(key_name) = std::env::var(source)\n .ok()\n .map(|v| v.trim().to_string())\n .filter(|v| !v.is_empty())\n else {\n continue;\n };\n\n if !key_name\n .chars()\n .all(|c| c.is_ascii_uppercase() || c.is_ascii_digit() || c == '_')\n {\n continue;\n }\n if !redacted_keys.insert(key_name.clone()) {\n continue;\n }\n\n let pattern = format!(r\"(?m)^\\s*({}\\s*=\\s*)[^\\r\\n]+\", regex::escape(&key_name));\n if let Ok(re) = Regex::new(&pattern) {\n redacted = re\n .replace_all(&redacted, \"${1}[REDACTED_SECRET]\")\n .into_owned();\n }\n }\n\n redacted\n}\n\npub const GENERATED_IMAGE_TOOL_NAME: &str = \"image_generation\";\npub const GENERATED_IMAGE_MAX_AUTO_VISION_BYTES: u64 = 20 * 1024 * 1024;\n\npub fn generated_image_tool_input(\n path: &str,\n metadata_path: Option<&str>,\n output_format: &str,\n revised_prompt: Option<&str>,\n) -> serde_json::Value {\n serde_json::json!({\n \"path\": path,\n \"metadata_path\": metadata_path,\n \"output_format\": output_format,\n \"revised_prompt\": revised_prompt,\n })\n}\n\npub fn generated_image_summary(\n path: &str,\n metadata_path: Option<&str>,\n output_format: &str,\n revised_prompt: Option<&str>,\n) -> String {\n let mut summary = format!(\"Generated image ({}) saved to `{}`.\", output_format, path);\n if let Some(metadata_path) = metadata_path {\n summary.push_str(&format!(\"\\nMetadata saved to `{}`.\", metadata_path));\n }\n if let Some(revised_prompt) = revised_prompt.filter(|prompt| !prompt.trim().is_empty()) {\n summary.push_str(\"\\n\\nRevised prompt:\\n\");\n summary.push_str(revised_prompt.trim());\n }\n summary\n}\n\npub fn generated_image_visual_context_blocks(\n path: &str,\n metadata_path: Option<&str>,\n output_format: &str,\n revised_prompt: Option<&str>,\n) -> Option<Vec<ContentBlock>> {\n let path_ref = Path::new(path);\n let metadata = std::fs::metadata(path_ref).ok()?;\n if !metadata.is_file() || metadata.len() > GENERATED_IMAGE_MAX_AUTO_VISION_BYTES {\n return None;\n }\n\n let data = std::fs::read(path_ref).ok()?;\n let media_type = generated_image_media_type(path_ref, output_format).to_string();\n let data_b64 = base64::engine::general_purpose::STANDARD.encode(data);\n let mut reminder = format!(\n \"<system-reminder>\\nA provider-native image generation call created `{}`. Jcode attached the image pixels as visual context for future turns because the active provider supports image input and the file is under the safe {} MB limit.\\nFormat: {}\",\n path,\n GENERATED_IMAGE_MAX_AUTO_VISION_BYTES / 1024 / 1024,\n output_format,\n );\n if let Some(metadata_path) = metadata_path.filter(|value| !value.trim().is_empty()) {\n reminder.push_str(&format!(\"\\nMetadata: {}\", metadata_path));\n }\n if let Some(revised_prompt) = revised_prompt.filter(|value| !value.trim().is_empty()) {\n reminder.push_str(\"\\nRevised prompt:\\n\");\n reminder.push_str(revised_prompt.trim());\n }\n reminder.push_str(\"\\n</system-reminder>\");\n\n Some(vec![\n ContentBlock::Text {\n text: reminder,\n cache_control: None,\n },\n ContentBlock::Image {\n media_type,\n data: data_b64,\n },\n ])\n}\n\nfn generated_image_media_type(path: &Path, output_format: &str) -> &'static str {\n let ext = path\n .extension()\n .and_then(|value| value.to_str())\n .unwrap_or(output_format)\n .to_ascii_lowercase();\n match ext.as_str() {\n \"jpg\" | \"jpeg\" => \"image/jpeg\",\n \"webp\" => \"image/webp\",\n \"gif\" => \"image/gif\",\n \"bmp\" => \"image/bmp\",\n \"ico\" => \"image/x-icon\",\n _ => \"image/png\",\n }\n}\n\n#[cfg(test)]\nmod tests;\n"}, "05d85c488ae3a9ae58bc61133e529b13b56e2dcf": {"t": 1785757209.6292987, "n": 20082, "h": "d089d091f3c0bf5b", "c": "use crate::bus::{BackgroundTaskCompleted, BackgroundTaskProgressEvent, BackgroundTaskStatus};\nuse base64::Engine as _;\nuse chrono::Utc;\nuse regex::Regex;\nuse serde::{Deserialize, Serialize};\nuse std::collections::HashSet;\nuse std::path::Path;\nuse std::sync::OnceLock;\n\npub use jcode_message_types::{\n ConnectionPhase, InputShellResult, StreamEvent, ToolCall, ToolDefinition,\n};\n\nmod notifications;\n\npub use notifications::{\n ParsedBackgroundTaskNotification, ParsedBackgroundTaskProgressNotification,\n background_task_display_label, background_task_status_notice,\n format_background_task_notification_markdown, format_background_task_progress_markdown,\n format_input_shell_result_markdown, input_shell_status_notice,\n parse_background_task_notification_markdown,\n parse_background_task_progress_notification_markdown,\n};\n\nfn compile_static_regex(pattern: &str) -> Option<Regex> {\n match Regex::new(pattern) {\n Ok(regex) => Some(regex),\n Err(err) => {\n eprintln!(\"jcode: failed to compile static regex: {err}\");\n None\n }\n }\n}\n\nfn compile_static_regexes(patterns: &[&str]) -> Vec<Regex> {\n patterns\n .iter()\n .filter_map(|pattern| compile_static_regex(pattern))\n .collect()\n}\n\n/// Role in conversation\n#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]\n#[serde(rename_all = \"lowercase\")]\npub enum Role {\n User,\n Assistant,\n}\n\n/// Plain-text tool output placeholder when execution was interrupted.\npub const TOOL_OUTPUT_MISSING_TEXT: &str =\n \"Tool output missing (session interrupted before tool execution completed)\";\n\n/// A message in the conversation\n#[derive(Debug, Clone, Serialize, Deserialize)]\npub struct Message {\n pub role: Role,\n pub content: Vec<ContentBlock>,\n #[serde(default, skip_serializing_if = \"Option::is_none\")]\n pub timestamp: Option<chrono::DateTime<Utc>>,\n #[serde(default, skip_serializing_if = \"Option::is_none\")]\n pub tool_duration_ms: Option<u64>,\n}\n\n/// Cache control metadata for prompt caching\n#[derive(Debug, Clone, Serialize, Deserialize)]\npub struct CacheControl {\n #[serde(rename = \"type\")]\n pub kind: String,\n #[serde(skip_serializing_if = \"Option::is_none\")]\n pub ttl: Option<String>,\n}\n\nimpl CacheControl {\n pub fn ephemeral(ttl: Option<String>) -> Self {\n Self {\n kind: \"ephemeral\".to_string(),\n ttl,\n }\n }\n}\n\n/// Content block within a message\n#[derive(Debug, Clone, Serialize, Deserialize)]\n#[serde(tag = \"type\", rename_all = \"snake_case\")]\npub enum ContentBlock {\n Text {\n text: String,\n #[serde(skip_serializing_if = \"Option::is_none\")]\n cache_control: Option<CacheControl>,\n },\n /// Hidden reasoning content used for providers that require it (not displayed)\n Reasoning {\n text: String,\n },\n ToolUse {\n id: String,\n name: String,\n input: serde_json::Value,\n },\n ToolResult {\n tool_use_id: String,\n content: String,\n #[serde(skip_serializing_if = \"Option::is_none\")]\n is_error: Option<bool>,\n },\n Image {\n media_type: String,\n data: String,\n },\n /// Hidden OpenAI Responses compaction item used to preserve native\n /// compaction state across turns/saves when jcode explicitly triggers it.\n OpenAICompaction {\n encrypted_content: String,\n },\n}\n\nimpl Message {\n pub fn user(text: &str) -> Self {\n Self {\n role: Role::User,\n content: vec![ContentBlock::Text {\n text: text.to_string(),\n cache_control: None,\n }],\n timestamp: Some(Utc::now()),\n tool_duration_ms: None,\n }\n }\n\n pub fn user_with_images(text: &str, images: Vec<(String, String)>) -> Self {\n let mut content: Vec<ContentBlock> = images\n .into_iter()\n .map(|(media_type, data)| ContentBlock::Image { media_type, data })\n .collect();\n content.push(ContentBlock::Text {\n text: text.to_string(),\n cache_control: None,\n });\n Self {\n role: Role::User,\n content,\n timestamp: Some(Utc::now()),\n tool_duration_ms: None,\n }\n }\n\n pub fn assistant_text(text: &str) -> Self {\n Self {\n role: Role::Assistant,\n content: vec![ContentBlock::Text {\n text: text.to_string(),\n cache_control: None,\n }],\n timestamp: Some(Utc::now()),\n tool_duration_ms: None,\n }\n }\n\n pub fn tool_result(tool_use_id: &str, content: &str, is_error: bool) -> Self {\n Self::tool_result_with_duration(tool_use_id, content, is_error, None)\n }\n\n pub fn tool_result_with_duration(\n tool_use_id: &str,\n content: &str,\n is_error: bool,\n tool_duration_ms: Option<u64>,\n ) -> Self {\n Self {\n role: Role::User,\n content: vec![ContentBlock::ToolResult {\n tool_use_id: tool_use_id.to_string(),\n content: content.to_string(),\n is_error: if is_error { Some(true) } else { None },\n }],\n timestamp: Some(Utc::now()),\n tool_duration_ms,\n }\n }\n\n /// Format a timestamp deterministically in UTC for injection into model-visible content.\n pub fn format_timestamp(ts: &chrono::DateTime<Utc>) -> String {\n ts.to_rfc3339_opts(chrono::SecondsFormat::Millis, true)\n }\n\n pub fn format_duration(duration_ms: u64) -> String {\n match duration_ms {\n 0..=999 => format!(\"{}ms\", duration_ms),\n 1_000..=9_999 => format!(\"{:.1}s\", duration_ms as f64 / 1000.0),\n 10_000..=59_999 => format!(\"{}s\", duration_ms / 1000),\n _ => {\n let total_seconds = duration_ms / 1000;\n let minutes = total_seconds / 60;\n let seconds = total_seconds % 60;\n if seconds == 0 {\n format!(\"{}m\", minutes)\n } else {\n format!(\"{}m {}s\", minutes, seconds)\n }\n }\n }\n }\n\n pub fn is_internal_system_reminder(&self) -> bool {\n self.content\n .iter()\n .find_map(|block| match block {\n ContentBlock::Text { text, .. } => Some(text.trim_start()),\n _ => None,\n })\n .is_some_and(|text| text.starts_with(\"<system-reminder>\"))\n }\n\n fn should_skip_timestamp_injection(&self) -> bool {\n self.is_internal_system_reminder()\n }\n\n fn tool_result_tag(&self, ts: &chrono::DateTime<Utc>) -> String {\n match self.tool_duration_ms {\n Some(duration_ms) => {\n let duration_ms_i64 = i64::try_from(duration_ms).unwrap_or(i64::MAX);\n let start_ts = ts\n .checked_sub_signed(chrono::Duration::milliseconds(duration_ms_i64))\n .unwrap_or(*ts);\n format!(\n \"[tool timing: start={} finish={} duration={}]\",\n Self::format_timestamp(&start_ts),\n Self::format_timestamp(ts),\n Self::format_duration(duration_ms)\n )\n }\n None => format!(\"[{}]\", Self::format_timestamp(ts)),\n }\n }\n\n /// Return a copy of messages with timestamps injected into user-role text content.\n /// Tool results get a stable UTC timing header prepended to content.\n /// User text messages get a stable UTC timestamp prepended to the first text block.\n pub fn with_timestamps(messages: &[Message]) -> Vec<Message> {\n messages\n .iter()\n .map(|msg| {\n let Some(ts) = msg.timestamp else {\n return msg.clone();\n };\n if msg.role != Role::User || msg.should_skip_timestamp_injection() {\n return msg.clone();\n }\n let text_tag = format!(\"[{}]\", Self::format_timestamp(&ts));\n let tool_result_tag = msg.tool_result_tag(&ts);\n let mut msg = msg.clone();\n let mut tagged = false;\n for block in &mut msg.content {\n match block {\n ContentBlock::Text { text, .. } if !tagged => {\n *text = format!(\"{} {}\", text_tag, text);\n tagged = true;\n }\n ContentBlock::ToolResult { content, .. } if !tagged => {\n *content = format!(\"{} {}\", tool_result_tag, content);\n tagged = true;\n }\n _ => {}\n }\n }\n msg\n })\n .collect()\n }\n}\n\nconst STABLE_HASH_SEED: u64 = 0xcbf29ce484222325;\nconst STABLE_HASH_PRIME: u64 = 0x100000001b3;\n\nfn stable_hash_bytes(bytes: &[u8]) -> u64 {\n let mut hash = STABLE_HASH_SEED;\n for byte in bytes {\n hash ^= *byte as u64;\n hash = hash.wrapping_mul(STABLE_HASH_PRIME);\n }\n hash\n}\n\npub(crate) fn extend_stable_hash(acc: u64, next: u64) -> u64 {\n stable_hash_bytes(&[acc.to_le_bytes().as_slice(), next.to_le_bytes().as_slice()].concat())\n}\n\npub(crate) fn stable_message_hash(message: &Message) -> u64 {\n match serde_json::to_vec(message) {\n Ok(bytes) => stable_hash_bytes(&bytes),\n Err(_) => stable_hash_bytes(format!(\"{:?}\", message).as_bytes()),\n }\n}\n\npub fn ends_with_fresh_user_turn(messages: &[Message]) -> bool {\n for msg in messages.iter().rev() {\n if msg.role != Role::User {\n return false;\n }\n\n if msg\n .content\n .iter()\n .any(|block| matches!(block, ContentBlock::ToolResult { .. }))\n {\n return false;\n }\n\n if msg.content.is_empty() {\n return false;\n }\n\n let mut saw_user_text = false;\n for block in &msg.content {\n match block {\n ContentBlock::Text { text, .. } => {\n let trimmed = text.trim();\n if !trimmed.is_empty() && !trimmed.starts_with(\"<system-reminder>\") {\n saw_user_text = true;\n }\n }\n _ => return false,\n }\n }\n\n if saw_user_text {\n return true;\n }\n\n if msg.is_internal_system_reminder() {\n continue;\n }\n\n return false;\n }\n\n false\n}\n\n/// Sanitize a tool ID so it matches the pattern `^[a-zA-Z0-9_-]+$`.\n///\n/// Different providers generate tool IDs in different formats. When switching\n/// from one provider to another mid-conversation, the historical tool IDs may\n/// contain characters that the new provider rejects (e.g., dots in Copilot IDs\n/// sent to Anthropic). This function replaces any invalid characters with\n/// underscores.\npub fn sanitize_tool_id(id: &str) -> String {\n if id.is_empty() {\n return \"unknown\".to_string();\n }\n let sanitized: String = id\n .chars()\n .map(|c| {\n if c.is_ascii_alphanumeric() || c == '_' || c == '-' {\n c\n } else {\n '_'\n }\n })\n .collect();\n if sanitized.is_empty() {\n \"unknown\".to_string()\n } else {\n sanitized\n }\n}\n\n/// Redact likely secrets from persisted tool output.\n///\n/// This is a best-effort safeguard for local session history files. It targets\n/// high-confidence token/key patterns and common `KEY=VALUE` assignments used by\n/// auth flows.\npub fn redact_secrets(text: &str) -> String {\n // Fast path to avoid regex work for most tool outputs.\n let lower = text.to_ascii_lowercase();\n\n if !text.contains(\"sk-\")\n && !text.contains(\"ghp_\")\n && !text.contains(\"github_pat_\")\n && !text.contains(\"AIza\")\n && !text.contains(\"ya29.\")\n && !text.contains(\"xox\")\n && !lower.contains(\"api_key\")\n && !lower.contains(\"token\")\n {\n return text.to_string();\n }\n\n static DIRECT_PATTERNS: OnceLock<Vec<Regex>> = OnceLock::new();\n static ASSIGNMENT_PATTERNS: OnceLock<Vec<Regex>> = OnceLock::new();\n\n let direct_patterns = DIRECT_PATTERNS.get_or_init(|| {\n compile_static_regexes(&[\n r\"sk-ant-(?:oat|ort)01-[A-Za-z0-9_-]{20,}\",\n r\"sk-or-v1-[A-Za-z0-9_-]{20,}\",\n r\"ghp_[A-Za-z0-9]{20,}\",\n r\"github_pat_[A-Za-z0-9_]{20,}\",\n r\"ya29\\.[A-Za-z0-9._-]{20,}\",\n r\"AIza[0-9A-Za-z_-]{20,}\",\n r\"xox[baprs]-[A-Za-z0-9-]{10,}\",\n ])\n });\n\n let assignment_patterns = ASSIGNMENT_PATTERNS.get_or_init(|| {\n compile_static_regexes(&[\n r\"(?m)^\\s*(OPENROUTER_API_KEY\\s*=\\s*)[^\\r\\n]+\",\n r\"(?m)^\\s*(OPENCODE_API_KEY\\s*=\\s*)[^\\r\\n]+\",\n r\"(?m)^\\s*(OPENCODE_GO_API_KEY\\s*=\\s*)[^\\r\\n]+\",\n r\"(?m)^\\s*(ZHIPU_API_KEY\\s*=\\s*)[^\\r\\n]+\",\n r\"(?m)^\\s*(ZAI_API_KEY\\s*=\\s*)[^\\r\\n]+\",\n r\"(?m)^\\s*(302AI_API_KEY\\s*=\\s*)[^\\r\\n]+\",\n r\"(?m)^\\s*(BASETEN_API_KEY\\s*=\\s*)[^\\r\\n]+\",\n r\"(?m)^\\s*(CORTECS_API_KEY\\s*=\\s*)[^\\r\\n]+\",\n r\"(?m)^\\s*(DEEPSEEK_API_KEY\\s*=\\s*)[^\\r\\n]+\",\n r\"(?m)^\\s*(FIRMWARE_API_KEY\\s*=\\s*)[^\\r\\n]+\",\n r\"(?m)^\\s*(HF_TOKEN\\s*=\\s*)[^\\r\\n]+\",\n r\"(?m)^\\s*(MOONSHOT_API_KEY\\s*=\\s*)[^\\r\\n]+\",\n r\"(?m)^\\s*(NEBIUS_API_KEY\\s*=\\s*)[^\\r\\n]+\",\n r\"(?m)^\\s*(SCALEWAY_API_KEY\\s*=\\s*)[^\\r\\n]+\",\n r\"(?m)^\\s*(STACKIT_API_KEY\\s*=\\s*)[^\\r\\n]+\",\n r\"(?m)^\\s*(GROQ_API_KEY\\s*=\\s*)[^\\r\\n]+\",\n r\"(?m)^\\s*(MISTRAL_API_KEY\\s*=\\s*)[^\\r\\n]+\",\n r\"(?m)^\\s*(PERPLEXITY_API_KEY\\s*=\\s*)[^\\r\\n]+\",\n r\"(?m)^\\s*(TOGETHER_API_KEY\\s*=\\s*)[^\\r\\n]+\",\n r\"(?m)^\\s*(DEEPINFRA_API_KEY\\s*=\\s*)[^\\r\\n]+\",\n r\"(?m)^\\s*(XAI_API_KEY\\s*=\\s*)[^\\r\\n]+\",\n r\"(?m)^\\s*(LMSTUDIO_API_KEY\\s*=\\s*)[^\\r\\n]+\",\n r\"(?m)^\\s*(OLLAMA_API_KEY\\s*=\\s*)[^\\r\\n]+\",\n r\"(?m)^\\s*(CHUTES_API_KEY\\s*=\\s*)[^\\r\\n]+\",\n r\"(?m)^\\s*(CEREBRAS_API_KEY\\s*=\\s*)[^\\r\\n]+\",\n r\"(?m)^\\s*(OPENAI_COMPAT_API_KEY\\s*=\\s*)[^\\r\\n]+\",\n r\"(?m)^\\s*(ANTHROPIC_API_KEY\\s*=\\s*)[^\\r\\n]+\",\n r\"(?m)^\\s*(OPENAI_API_KEY\\s*=\\s*)[^\\r\\n]+\",\n r\"(?m)^\\s*(AZURE_OPENAI_API_KEY\\s*=\\s*)[^\\r\\n]+\",\n r\"(?m)^\\s*(CURSOR_API_KEY\\s*=\\s*)[^\\r\\n]+\",\n r\"(?m)^\\s*(GITHUB_TOKEN\\s*=\\s*)[^\\r\\n]+\",\n ])\n });\n\n let mut redacted = text.to_string();\n let mut redacted_keys: HashSet<String> = [\n \"OPENROUTER_API_KEY\",\n \"OPENCODE_API_KEY\",\n \"OPENCODE_GO_API_KEY\",\n \"ZHIPU_API_KEY\",\n \"ZAI_API_KEY\",\n \"302AI_API_KEY\",\n \"BASETEN_API_KEY\",\n \"CORTECS_API_KEY\",\n \"DEEPSEEK_API_KEY\",\n \"FIRMWARE_API_KEY\",\n \"HF_TOKEN\",\n \"MOONSHOT_API_KEY\",\n \"NEBIUS_API_KEY\",\n \"SCALEWAY_API_KEY\",\n \"STACKIT_API_KEY\",\n \"GROQ_API_KEY\",\n \"MISTRAL_API_KEY\",\n \"PERPLEXITY_API_KEY\",\n \"TOGETHER_API_KEY\",\n \"DEEPINFRA_API_KEY\",\n \"XAI_API_KEY\",\n \"LMSTUDIO_API_KEY\",\n \"OLLAMA_API_KEY\",\n \"CHUTES_API_KEY\",\n \"CEREBRAS_API_KEY\",\n \"OPENAI_COMPAT_API_KEY\",\n \"ANTHROPIC_API_KEY\",\n \"OPENAI_API_KEY\",\n \"AZURE_OPENAI_API_KEY\",\n \"CURSOR_API_KEY\",\n \"GITHUB_TOKEN\",\n ]\n .iter()\n .map(|k| (*k).to_string())\n .collect();\n\n for re in direct_patterns {\n redacted = re.replace_all(&redacted, \"[REDACTED_SECRET]\").into_owned();\n }\n\n for re in assignment_patterns {\n redacted = re\n .replace_all(&redacted, \"${1}[REDACTED_SECRET]\")\n .into_owned();\n }\n\n // Also redact custom API key variable names configured at runtime.\n for source in [\n \"JCODE_OPENROUTER_API_KEY_NAME\",\n \"JCODE_OPENAI_COMPAT_API_KEY_NAME\",\n ] {\n let Some(key_name) = std::env::var(source)\n .ok()\n .map(|v| v.trim().to_string())\n .filter(|v| !v.is_empty())\n else {\n continue;\n };\n\n if !key_name\n .chars()\n .all(|c| c.is_ascii_uppercase() || c.is_ascii_digit() || c == '_')\n {\n continue;\n }\n if !redacted_keys.insert(key_name.clone()) {\n continue;\n }\n\n let pattern = format!(r\"(?m)^\\s*({}\\s*=\\s*)[^\\r\\n]+\", regex::escape(&key_name));\n if let Ok(re) = Regex::new(&pattern) {\n redacted = re\n .replace_all(&redacted, \"${1}[REDACTED_SECRET]\")\n .into_owned();\n }\n }\n\n redacted\n}\n\npub const GENERATED_IMAGE_TOOL_NAME: &str = \"image_generation\";\npub const GENERATED_IMAGE_MAX_AUTO_VISION_BYTES: u64 = 20 * 1024 * 1024;\n\npub fn generated_image_tool_input(\n path: &str,\n metadata_path: Option<&str>,\n output_format: &str,\n revised_prompt: Option<&str>,\n) -> serde_json::Value {\n serde_json::json!({\n \"path\": path,\n \"metadata_path\": metadata_path,\n \"output_format\": output_format,\n \"revised_prompt\": revised_prompt,\n })\n}\n\npub fn generated_image_summary(\n path: &str,\n metadata_path: Option<&str>,\n output_format: &str,\n revised_prompt: Option<&str>,\n) -> String {\n let mut summary = format!(\"Generated image ({}) saved to `{}`.\", output_format, path);\n if let Some(metadata_path) = metadata_path {\n summary.push_str(&format!(\"\\nMetadata saved to `{}`.\", metadata_path));\n }\n if let Some(revised_prompt) = revised_prompt.filter(|prompt| !prompt.trim().is_empty()) {\n summary.push_str(\"\\n\\nRevised prompt:\\n\");\n summary.push_str(revised_prompt.trim());\n }\n summary\n}\n\npub fn generated_image_visual_context_blocks(\n path: &str,\n metadata_path: Option<&str>,\n output_format: &str,\n revised_prompt: Option<&str>,\n) -> Option<Vec<ContentBlock>> {\n let path_ref = Path::new(path);\n let metadata = std::fs::metadata(path_ref).ok()?;\n if !metadata.is_file() || metadata.len() > GENERATED_IMAGE_MAX_AUTO_VISION_BYTES {\n return None;\n }\n\n let data = std::fs::read(path_ref).ok()?;\n let media_type = generated_image_media_type(path_ref, output_format).to_string();\n let data_b64 = base64::engine::general_purpose::STANDARD.encode(data);\n let mut reminder = format!(\n \"<system-reminder>\\nA provider-native image generation call created `{}`. Jcode attached the image pixels as visual context for future turns because the active provider supports image input and the file is under the safe {} MB limit.\\nFormat: {}\",\n path,\n GENERATED_IMAGE_MAX_AUTO_VISION_BYTES / 1024 / 1024,\n output_format,\n );\n if let Some(metadata_path) = metadata_path.filter(|value| !value.trim().is_empty()) {\n reminder.push_str(&format!(\"\\nMetadata: {}\", metadata_path));\n }\n if let Some(revised_prompt) = revised_prompt.filter(|value| !value.trim().is_empty()) {\n reminder.push_str(\"\\nRevised prompt:\\n\");\n reminder.push_str(revised_prompt.trim());\n }\n reminder.push_str(\"\\n</system-reminder>\");\n\n Some(vec![\n ContentBlock::Text {\n text: reminder,\n cache_control: None,\n },\n ContentBlock::Image {\n media_type,\n data: data_b64,\n },\n ])\n}\n\nfn generated_image_media_type(path: &Path, output_format: &str) -> &'static str {\n let ext = path\n .extension()\n .and_then(|value| value.to_str())\n .unwrap_or(output_format)\n .to_ascii_lowercase();\n match ext.as_str() {\n \"jpg\" | \"jpeg\" => \"image/jpeg\",\n \"webp\" => \"image/webp\",\n \"gif\" => \"image/gif\",\n \"bmp\" => \"image/bmp\",\n \"ico\" => \"image/x-icon\",\n _ => \"image/png\",\n }\n}\n\n#[cfg(test)]\nmod tests;\n"}, "8f41424da3a09cf7247881f6fdc686dd516dcc4e": {"t": 1785757211.1714127, "n": 23551, "h": "7441875eb413a1f2", "c": "use crate::bus::{BackgroundTaskCompleted, BackgroundTaskProgressEvent, BackgroundTaskStatus};\nuse base64::Engine as _;\nuse chrono::Utc;\nuse regex::Regex;\nuse serde::{Deserialize, Serialize};\nuse std::collections::HashSet;\nuse std::path::Path;\nuse std::sync::OnceLock;\n\npub use jcode_message_types::{InputShellResult, ToolCall, ToolDefinition};\n\nmod notifications;\n\npub use notifications::{\n ParsedBackgroundTaskNotification, ParsedBackgroundTaskProgressNotification,\n background_task_display_label, background_task_status_notice,\n format_background_task_notification_markdown, format_background_task_progress_markdown,\n format_input_shell_result_markdown, input_shell_status_notice,\n parse_background_task_notification_markdown,\n parse_background_task_progress_notification_markdown,\n};\n\nfn compile_static_regex(pattern: &str) -> Option<Regex> {\n match Regex::new(pattern) {\n Ok(regex) => Some(regex),\n Err(err) => {\n eprintln!(\"jcode: failed to compile static regex: {err}\");\n None\n }\n }\n}\n\nfn compile_static_regexes(patterns: &[&str]) -> Vec<Regex> {\n patterns\n .iter()\n .filter_map(|pattern| compile_static_regex(pattern))\n .collect()\n}\n\n/// Role in conversation\n#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]\n#[serde(rename_all = \"lowercase\")]\npub enum Role {\n User,\n Assistant,\n}\n\n/// Plain-text tool output placeholder when execution was interrupted.\npub const TOOL_OUTPUT_MISSING_TEXT: &str =\n \"Tool output missing (session interrupted before tool execution completed)\";\n\n/// A message in the conversation\n#[derive(Debug, Clone, Serialize, Deserialize)]\npub struct Message {\n pub role: Role,\n pub content: Vec<ContentBlock>,\n #[serde(default, skip_serializing_if = \"Option::is_none\")]\n pub timestamp: Option<chrono::DateTime<Utc>>,\n #[serde(default, skip_serializing_if = \"Option::is_none\")]\n pub tool_duration_ms: Option<u64>,\n}\n\n/// Cache control metadata for prompt caching\n#[derive(Debug, Clone, Serialize, Deserialize)]\npub struct CacheControl {\n #[serde(rename = \"type\")]\n pub kind: String,\n #[serde(skip_serializing_if = \"Option::is_none\")]\n pub ttl: Option<String>,\n}\n\nimpl CacheControl {\n pub fn ephemeral(ttl: Option<String>) -> Self {\n Self {\n kind: \"ephemeral\".to_string(),\n ttl,\n }\n }\n}\n\n/// Content block within a message\n#[derive(Debug, Clone, Serialize, Deserialize)]\n#[serde(tag = \"type\", rename_all = \"snake_case\")]\npub enum ContentBlock {\n Text {\n text: String,\n #[serde(skip_serializing_if = \"Option::is_none\")]\n cache_control: Option<CacheControl>,\n },\n /// Hidden reasoning content used for providers that require it (not displayed)\n Reasoning {\n text: String,\n },\n ToolUse {\n id: String,\n name: String,\n input: serde_json::Value,\n },\n ToolResult {\n tool_use_id: String,\n content: String,\n #[serde(skip_serializing_if = \"Option::is_none\")]\n is_error: Option<bool>,\n },\n Image {\n media_type: String,\n data: String,\n },\n /// Hidden OpenAI Responses compaction item used to preserve native\n /// compaction state across turns/saves when jcode explicitly triggers it.\n OpenAICompaction {\n encrypted_content: String,\n },\n}\n\nimpl Message {\n pub fn user(text: &str) -> Self {\n Self {\n role: Role::User,\n content: vec![ContentBlock::Text {\n text: text.to_string(),\n cache_control: None,\n }],\n timestamp: Some(Utc::now()),\n tool_duration_ms: None,\n }\n }\n\n pub fn user_with_images(text: &str, images: Vec<(String, String)>) -> Self {\n let mut content: Vec<ContentBlock> = images\n .into_iter()\n .map(|(media_type, data)| ContentBlock::Image { media_type, data })\n .collect();\n content.push(ContentBlock::Text {\n text: text.to_string(),\n cache_control: None,\n });\n Self {\n role: Role::User,\n content,\n timestamp: Some(Utc::now()),\n tool_duration_ms: None,\n }\n }\n\n pub fn assistant_text(text: &str) -> Self {\n Self {\n role: Role::Assistant,\n content: vec![ContentBlock::Text {\n text: text.to_string(),\n cache_control: None,\n }],\n timestamp: Some(Utc::now()),\n tool_duration_ms: None,\n }\n }\n\n pub fn tool_result(tool_use_id: &str, content: &str, is_error: bool) -> Self {\n Self::tool_result_with_duration(tool_use_id, content, is_error, None)\n }\n\n pub fn tool_result_with_duration(\n tool_use_id: &str,\n content: &str,\n is_error: bool,\n tool_duration_ms: Option<u64>,\n ) -> Self {\n Self {\n role: Role::User,\n content: vec![ContentBlock::ToolResult {\n tool_use_id: tool_use_id.to_string(),\n content: content.to_string(),\n is_error: if is_error { Some(true) } else { None },\n }],\n timestamp: Some(Utc::now()),\n tool_duration_ms,\n }\n }\n\n /// Format a timestamp deterministically in UTC for injection into model-visible content.\n pub fn format_timestamp(ts: &chrono::DateTime<Utc>) -> String {\n ts.to_rfc3339_opts(chrono::SecondsFormat::Millis, true)\n }\n\n pub fn format_duration(duration_ms: u64) -> String {\n match duration_ms {\n 0..=999 => format!(\"{}ms\", duration_ms),\n 1_000..=9_999 => format!(\"{:.1}s\", duration_ms as f64 / 1000.0),\n 10_000..=59_999 => format!(\"{}s\", duration_ms / 1000),\n _ => {\n let total_seconds = duration_ms / 1000;\n let minutes = total_seconds / 60;\n let seconds = total_seconds % 60;\n if seconds == 0 {\n format!(\"{}m\", minutes)\n } else {\n format!(\"{}m {}s\", minutes, seconds)\n }\n }\n }\n }\n\n pub fn is_internal_system_reminder(&self) -> bool {\n self.content\n .iter()\n .find_map(|block| match block {\n ContentBlock::Text { text, .. } => Some(text.trim_start()),\n _ => None,\n })\n .is_some_and(|text| text.starts_with(\"<system-reminder>\"))\n }\n\n fn should_skip_timestamp_injection(&self) -> bool {\n self.is_internal_system_reminder()\n }\n\n fn tool_result_tag(&self, ts: &chrono::DateTime<Utc>) -> String {\n match self.tool_duration_ms {\n Some(duration_ms) => {\n let duration_ms_i64 = i64::try_from(duration_ms).unwrap_or(i64::MAX);\n let start_ts = ts\n .checked_sub_signed(chrono::Duration::milliseconds(duration_ms_i64))\n .unwrap_or(*ts);\n format!(\n \"[tool timing: start={} finish={} duration={}]\",\n Self::format_timestamp(&start_ts),\n Self::format_timestamp(ts),\n Self::format_duration(duration_ms)\n )\n }\n None => format!(\"[{}]\", Self::format_timestamp(ts)),\n }\n }\n\n /// Return a copy of messages with timestamps injected into user-role text content.\n /// Tool results get a stable UTC timing header prepended to content.\n /// User text messages get a stable UTC timestamp prepended to the first text block.\n pub fn with_timestamps(messages: &[Message]) -> Vec<Message> {\n messages\n .iter()\n .map(|msg| {\n let Some(ts) = msg.timestamp else {\n return msg.clone();\n };\n if msg.role != Role::User || msg.should_skip_timestamp_injection() {\n return msg.clone();\n }\n let text_tag = format!(\"[{}]\", Self::format_timestamp(&ts));\n let tool_result_tag = msg.tool_result_tag(&ts);\n let mut msg = msg.clone();\n let mut tagged = false;\n for block in &mut msg.content {\n match block {\n ContentBlock::Text { text, .. } if !tagged => {\n *text = format!(\"{} {}\", text_tag, text);\n tagged = true;\n }\n ContentBlock::ToolResult { content, .. } if !tagged => {\n *content = format!(\"{} {}\", tool_result_tag, content);\n tagged = true;\n }\n _ => {}\n }\n }\n msg\n })\n .collect()\n }\n}\n\nconst STABLE_HASH_SEED: u64 = 0xcbf29ce484222325;\nconst STABLE_HASH_PRIME: u64 = 0x100000001b3;\n\nfn stable_hash_bytes(bytes: &[u8]) -> u64 {\n let mut hash = STABLE_HASH_SEED;\n for byte in bytes {\n hash ^= *byte as u64;\n hash = hash.wrapping_mul(STABLE_HASH_PRIME);\n }\n hash\n}\n\npub(crate) fn extend_stable_hash(acc: u64, next: u64) -> u64 {\n stable_hash_bytes(&[acc.to_le_bytes().as_slice(), next.to_le_bytes().as_slice()].concat())\n}\n\npub(crate) fn stable_message_hash(message: &Message) -> u64 {\n match serde_json::to_vec(message) {\n Ok(bytes) => stable_hash_bytes(&bytes),\n Err(_) => stable_hash_bytes(format!(\"{:?}\", message).as_bytes()),\n }\n}\n\npub fn ends_with_fresh_user_turn(messages: &[Message]) -> bool {\n for msg in messages.iter().rev() {\n if msg.role != Role::User {\n return false;\n }\n\n if msg\n .content\n .iter()\n .any(|block| matches!(block, ContentBlock::ToolResult { .. }))\n {\n return false;\n }\n\n if msg.content.is_empty() {\n return false;\n }\n\n let mut saw_user_text = false;\n for block in &msg.content {\n match block {\n ContentBlock::Text { text, .. } => {\n let trimmed = text.trim();\n if !trimmed.is_empty() && !trimmed.starts_with(\"<system-reminder>\") {\n saw_user_text = true;\n }\n }\n _ => return false,\n }\n }\n\n if saw_user_text {\n return true;\n }\n\n if msg.is_internal_system_reminder() {\n continue;\n }\n\n return false;\n }\n\n false\n}\n\n/// Sanitize a tool ID so it matches the pattern `^[a-zA-Z0-9_-]+$`.\n///\n/// Different providers generate tool IDs in different formats. When switching\n/// from one provider to another mid-conversation, the historical tool IDs may\n/// contain characters that the new provider rejects (e.g., dots in Copilot IDs\n/// sent to Anthropic). This function replaces any invalid characters with\n/// underscores.\npub fn sanitize_tool_id(id: &str) -> String {\n if id.is_empty() {\n return \"unknown\".to_string();\n }\n let sanitized: String = id\n .chars()\n .map(|c| {\n if c.is_ascii_alphanumeric() || c == '_' || c == '-' {\n c\n } else {\n '_'\n }\n })\n .collect();\n if sanitized.is_empty() {\n \"unknown\".to_string()\n } else {\n sanitized\n }\n}\n\n/// Redact likely secrets from persisted tool output.\n///\n/// This is a best-effort safeguard for local session history files. It targets\n/// high-confidence token/key patterns and common `KEY=VALUE` assignments used by\n/// auth flows.\npub fn redact_secrets(text: &str) -> String {\n // Fast path to avoid regex work for most tool outputs.\n let lower = text.to_ascii_lowercase();\n\n if !text.contains(\"sk-\")\n && !text.contains(\"ghp_\")\n && !text.contains(\"github_pat_\")\n && !text.contains(\"AIza\")\n && !text.contains(\"ya29.\")\n && !text.contains(\"xox\")\n && !lower.contains(\"api_key\")\n && !lower.contains(\"token\")\n {\n return text.to_string();\n }\n\n static DIRECT_PATTERNS: OnceLock<Vec<Regex>> = OnceLock::new();\n static ASSIGNMENT_PATTERNS: OnceLock<Vec<Regex>> = OnceLock::new();\n\n let direct_patterns = DIRECT_PATTERNS.get_or_init(|| {\n compile_static_regexes(&[\n r\"sk-ant-(?:oat|ort)01-[A-Za-z0-9_-]{20,}\",\n r\"sk-or-v1-[A-Za-z0-9_-]{20,}\",\n r\"ghp_[A-Za-z0-9]{20,}\",\n r\"github_pat_[A-Za-z0-9_]{20,}\",\n r\"ya29\\.[A-Za-z0-9._-]{20,}\",\n r\"AIza[0-9A-Za-z_-]{20,}\",\n r\"xox[baprs]-[A-Za-z0-9-]{10,}\",\n ])\n });\n\n let assignment_patterns = ASSIGNMENT_PATTERNS.get_or_init(|| {\n compile_static_regexes(&[\n r\"(?m)^\\s*(OPENROUTER_API_KEY\\s*=\\s*)[^\\r\\n]+\",\n r\"(?m)^\\s*(OPENCODE_API_KEY\\s*=\\s*)[^\\r\\n]+\",\n r\"(?m)^\\s*(OPENCODE_GO_API_KEY\\s*=\\s*)[^\\r\\n]+\",\n r\"(?m)^\\s*(ZHIPU_API_KEY\\s*=\\s*)[^\\r\\n]+\",\n r\"(?m)^\\s*(ZAI_API_KEY\\s*=\\s*)[^\\r\\n]+\",\n r\"(?m)^\\s*(302AI_API_KEY\\s*=\\s*)[^\\r\\n]+\",\n r\"(?m)^\\s*(BASETEN_API_KEY\\s*=\\s*)[^\\r\\n]+\",\n r\"(?m)^\\s*(CORTECS_API_KEY\\s*=\\s*)[^\\r\\n]+\",\n r\"(?m)^\\s*(DEEPSEEK_API_KEY\\s*=\\s*)[^\\r\\n]+\",\n r\"(?m)^\\s*(FIRMWARE_API_KEY\\s*=\\s*)[^\\r\\n]+\",\n r\"(?m)^\\s*(HF_TOKEN\\s*=\\s*)[^\\r\\n]+\",\n r\"(?m)^\\s*(MOONSHOT_API_KEY\\s*=\\s*)[^\\r\\n]+\",\n r\"(?m)^\\s*(NEBIUS_API_KEY\\s*=\\s*)[^\\r\\n]+\",\n r\"(?m)^\\s*(SCALEWAY_API_KEY\\s*=\\s*)[^\\r\\n]+\",\n r\"(?m)^\\s*(STACKIT_API_KEY\\s*=\\s*)[^\\r\\n]+\",\n r\"(?m)^\\s*(GROQ_API_KEY\\s*=\\s*)[^\\r\\n]+\",\n r\"(?m)^\\s*(MISTRAL_API_KEY\\s*=\\s*)[^\\r\\n]+\",\n r\"(?m)^\\s*(PERPLEXITY_API_KEY\\s*=\\s*)[^\\r\\n]+\",\n r\"(?m)^\\s*(TOGETHER_API_KEY\\s*=\\s*)[^\\r\\n]+\",\n r\"(?m)^\\s*(DEEPINFRA_API_KEY\\s*=\\s*)[^\\r\\n]+\",\n r\"(?m)^\\s*(XAI_API_KEY\\s*=\\s*)[^\\r\\n]+\",\n r\"(?m)^\\s*(LMSTUDIO_API_KEY\\s*=\\s*)[^\\r\\n]+\",\n r\"(?m)^\\s*(OLLAMA_API_KEY\\s*=\\s*)[^\\r\\n]+\",\n r\"(?m)^\\s*(CHUTES_API_KEY\\s*=\\s*)[^\\r\\n]+\",\n r\"(?m)^\\s*(CEREBRAS_API_KEY\\s*=\\s*)[^\\r\\n]+\",\n r\"(?m)^\\s*(OPENAI_COMPAT_API_KEY\\s*=\\s*)[^\\r\\n]+\",\n r\"(?m)^\\s*(ANTHROPIC_API_KEY\\s*=\\s*)[^\\r\\n]+\",\n r\"(?m)^\\s*(OPENAI_API_KEY\\s*=\\s*)[^\\r\\n]+\",\n r\"(?m)^\\s*(AZURE_OPENAI_API_KEY\\s*=\\s*)[^\\r\\n]+\",\n r\"(?m)^\\s*(CURSOR_API_KEY\\s*=\\s*)[^\\r\\n]+\",\n r\"(?m)^\\s*(GITHUB_TOKEN\\s*=\\s*)[^\\r\\n]+\",\n ])\n });\n\n let mut redacted = text.to_string();\n let mut redacted_keys: HashSet<String> = [\n \"OPENROUTER_API_KEY\",\n \"OPENCODE_API_KEY\",\n \"OPENCODE_GO_API_KEY\",\n \"ZHIPU_API_KEY\",\n \"ZAI_API_KEY\",\n \"302AI_API_KEY\",\n \"BASETEN_API_KEY\",\n \"CORTECS_API_KEY\",\n \"DEEPSEEK_API_KEY\",\n \"FIRMWARE_API_KEY\",\n \"HF_TOKEN\",\n \"MOONSHOT_API_KEY\",\n \"NEBIUS_API_KEY\",\n \"SCALEWAY_API_KEY\",\n \"STACKIT_API_KEY\",\n \"GROQ_API_KEY\",\n \"MISTRAL_API_KEY\",\n \"PERPLEXITY_API_KEY\",\n \"TOGETHER_API_KEY\",\n \"DEEPINFRA_API_KEY\",\n \"XAI_API_KEY\",\n \"LMSTUDIO_API_KEY\",\n \"OLLAMA_API_KEY\",\n \"CHUTES_API_KEY\",\n \"CEREBRAS_API_KEY\",\n \"OPENAI_COMPAT_API_KEY\",\n \"ANTHROPIC_API_KEY\",\n \"OPENAI_API_KEY\",\n \"AZURE_OPENAI_API_KEY\",\n \"CURSOR_API_KEY\",\n \"GITHUB_TOKEN\",\n ]\n .iter()\n .map(|k| (*k).to_string())\n .collect();\n\n for re in direct_patterns {\n redacted = re.replace_all(&redacted, \"[REDACTED_SECRET]\").into_owned();\n }\n\n for re in assignment_patterns {\n redacted = re\n .replace_all(&redacted, \"${1}[REDACTED_SECRET]\")\n .into_owned();\n }\n\n // Also redact custom API key variable names configured at runtime.\n for source in [\n \"JCODE_OPENROUTER_API_KEY_NAME\",\n \"JCODE_OPENAI_COMPAT_API_KEY_NAME\",\n ] {\n let Some(key_name) = std::env::var(source)\n .ok()\n .map(|v| v.trim().to_string())\n .filter(|v| !v.is_empty())\n else {\n continue;\n };\n\n if !key_name\n .chars()\n .all(|c| c.is_ascii_uppercase() || c.is_ascii_digit() || c == '_')\n {\n continue;\n }\n if !redacted_keys.insert(key_name.clone()) {\n continue;\n }\n\n let pattern = format!(r\"(?m)^\\s*({}\\s*=\\s*)[^\\r\\n]+\", regex::escape(&key_name));\n if let Ok(re) = Regex::new(&pattern) {\n redacted = re\n .replace_all(&redacted, \"${1}[REDACTED_SECRET]\")\n .into_owned();\n }\n }\n\n redacted\n}\n\npub const GENERATED_IMAGE_TOOL_NAME: &str = \"image_generation\";\npub const GENERATED_IMAGE_MAX_AUTO_VISION_BYTES: u64 = 20 * 1024 * 1024;\n\npub fn generated_image_tool_input(\n path: &str,\n metadata_path: Option<&str>,\n output_format: &str,\n revised_prompt: Option<&str>,\n) -> serde_json::Value {\n serde_json::json!({\n \"path\": path,\n \"metadata_path\": metadata_path,\n \"output_format\": output_format,\n \"revised_prompt\": revised_prompt,\n })\n}\n\npub fn generated_image_summary(\n path: &str,\n metadata_path: Option<&str>,\n output_format: &str,\n revised_prompt: Option<&str>,\n) -> String {\n let mut summary = format!(\"Generated image ({}) saved to `{}`.\", output_format, path);\n if let Some(metadata_path) = metadata_path {\n summary.push_str(&format!(\"\\nMetadata saved to `{}`.\", metadata_path));\n }\n if let Some(revised_prompt) = revised_prompt.filter(|prompt| !prompt.trim().is_empty()) {\n summary.push_str(\"\\n\\nRevised prompt:\\n\");\n summary.push_str(revised_prompt.trim());\n }\n summary\n}\n\npub fn generated_image_visual_context_blocks(\n path: &str,\n metadata_path: Option<&str>,\n output_format: &str,\n revised_prompt: Option<&str>,\n) -> Option<Vec<ContentBlock>> {\n let path_ref = Path::new(path);\n let metadata = std::fs::metadata(path_ref).ok()?;\n if !metadata.is_file() || metadata.len() > GENERATED_IMAGE_MAX_AUTO_VISION_BYTES {\n return None;\n }\n\n let data = std::fs::read(path_ref).ok()?;\n let media_type = generated_image_media_type(path_ref, output_format).to_string();\n let data_b64 = base64::engine::general_purpose::STANDARD.encode(data);\n let mut reminder = format!(\n \"<system-reminder>\\nA provider-native image generation call created `{}`. Jcode attached the image pixels as visual context for future turns because the active provider supports image input and the file is under the safe {} MB limit.\\nFormat: {}\",\n path,\n GENERATED_IMAGE_MAX_AUTO_VISION_BYTES / 1024 / 1024,\n output_format,\n );\n if let Some(metadata_path) = metadata_path.filter(|value| !value.trim().is_empty()) {\n reminder.push_str(&format!(\"\\nMetadata: {}\", metadata_path));\n }\n if let Some(revised_prompt) = revised_prompt.filter(|value| !value.trim().is_empty()) {\n reminder.push_str(\"\\nRevised prompt:\\n\");\n reminder.push_str(revised_prompt.trim());\n }\n reminder.push_str(\"\\n</system-reminder>\");\n\n Some(vec![\n ContentBlock::Text {\n text: reminder,\n cache_control: None,\n },\n ContentBlock::Image {\n media_type,\n data: data_b64,\n },\n ])\n}\n\nfn generated_image_media_type(path: &Path, output_format: &str) -> &'static str {\n let ext = path\n .extension()\n .and_then(|value| value.to_str())\n .unwrap_or(output_format)\n .to_ascii_lowercase();\n match ext.as_str() {\n \"jpg\" | \"jpeg\" => \"image/jpeg\",\n \"webp\" => \"image/webp\",\n \"gif\" => \"image/gif\",\n \"bmp\" => \"image/bmp\",\n \"ico\" => \"image/x-icon\",\n _ => \"image/png\",\n }\n}\n\n/// Connection phase for status bar transparency\n#[derive(Debug, Clone, PartialEq)]\npub enum ConnectionPhase {\n /// Refreshing OAuth token\n Authenticating,\n /// TCP + TLS connection to API\n Connecting,\n /// HTTP request sent, waiting for first response byte\n WaitingForResponse,\n /// First byte received, stream is active\n Streaming,\n /// Retrying after a transient error\n Retrying { attempt: u32, max: u32 },\n}\n\nimpl std::fmt::Display for ConnectionPhase {\n fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {\n match self {\n ConnectionPhase::Authenticating => write!(f, \"authenticating\"),\n ConnectionPhase::Connecting => write!(f, \"connecting\"),\n ConnectionPhase::WaitingForResponse => write!(f, \"waiting for response\"),\n ConnectionPhase::Streaming => write!(f, \"streaming\"),\n ConnectionPhase::Retrying { attempt, max } => {\n write!(f, \"retrying ({}/{})\", attempt, max)\n }\n }\n }\n}\n\n/// Streaming event from provider\n#[derive(Debug, Clone)]\npub enum StreamEvent {\n /// Text content delta\n TextDelta(String),\n /// Tool use started\n ToolUseStart { id: String, name: String },\n /// Tool input delta (JSON fragment)\n ToolInputDelta(String),\n /// Tool use complete\n ToolUseEnd,\n /// Tool result from provider (provider already executed the tool)\n ToolResult {\n tool_use_id: String,\n content: String,\n is_error: bool,\n },\n /// Image generated by a provider-native image generation tool.\n GeneratedImage {\n id: String,\n path: String,\n metadata_path: Option<String>,\n output_format: String,\n revised_prompt: Option<String>,\n },\n /// Extended thinking started\n ThinkingStart,\n /// Extended thinking delta (reasoning content)\n ThinkingDelta(String),\n /// Extended thinking ended\n ThinkingEnd,\n /// Extended thinking completed with duration\n ThinkingDone { duration_secs: f64 },\n /// Message complete (may have stop reason)\n MessageEnd { stop_reason: Option<String> },\n /// Token usage update\n TokenUsage {\n input_tokens: Option<u64>,\n output_tokens: Option<u64>,\n cache_read_input_tokens: Option<u64>,\n cache_creation_input_tokens: Option<u64>,\n },\n /// Active transport/connection type for this stream\n ConnectionType { connection: String },\n /// Connection phase update (for status bar transparency)\n ConnectionPhase { phase: ConnectionPhase },\n /// Provider-supplied human-readable transport detail for the status line\n StatusDetail { detail: String },\n /// Error occurred\n Error {\n message: String,\n /// Seconds until rate limit resets (if this is a rate limit error)\n retry_after_secs: Option<u64>,\n },\n /// Provider session ID (for conversation resume)\n SessionId(String),\n /// Compaction occurred (context was summarized)\n Compaction {\n trigger: String,\n pre_tokens: Option<u64>,\n /// Provider-native compaction artifact, if one was emitted.\n openai_encrypted_content: Option<String>,\n },\n /// Upstream provider info (e.g., which provider OpenRouter routed to)\n UpstreamProvider { provider: String },\n /// Native tool call from a provider bridge that needs execution by jcode\n NativeToolCall {\n request_id: String,\n tool_name: String,\n input: serde_json::Value,\n },\n}\n\n#[cfg(test)]\nmod tests;\n"}, "06334579ec59b64da7d34da1171a127d240146e3": {"t": 1785757212.620544, "n": 25262, "h": "6e9ab6dede151e6b", "c": "use crate::bus::{BackgroundTaskCompleted, BackgroundTaskProgressEvent, BackgroundTaskStatus};\nuse base64::Engine as _;\nuse chrono::Utc;\nuse regex::Regex;\nuse serde::{Deserialize, Serialize};\nuse std::collections::HashSet;\nuse std::path::Path;\nuse std::sync::OnceLock;\n\npub use jcode_message_types::{InputShellResult, ToolCall};\n\nmod notifications;\n\npub use notifications::{\n ParsedBackgroundTaskNotification, ParsedBackgroundTaskProgressNotification,\n background_task_display_label, background_task_status_notice,\n format_background_task_notification_markdown, format_background_task_progress_markdown,\n format_input_shell_result_markdown, input_shell_status_notice,\n parse_background_task_notification_markdown,\n parse_background_task_progress_notification_markdown,\n};\n\nfn compile_static_regex(pattern: &str) -> Option<Regex> {\n match Regex::new(pattern) {\n Ok(regex) => Some(regex),\n Err(err) => {\n eprintln!(\"jcode: failed to compile static regex: {err}\");\n None\n }\n }\n}\n\nfn compile_static_regexes(patterns: &[&str]) -> Vec<Regex> {\n patterns\n .iter()\n .filter_map(|pattern| compile_static_regex(pattern))\n .collect()\n}\n\n/// Role in conversation\n#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]\n#[serde(rename_all = \"lowercase\")]\npub enum Role {\n User,\n Assistant,\n}\n\n/// Plain-text tool output placeholder when execution was interrupted.\npub const TOOL_OUTPUT_MISSING_TEXT: &str =\n \"Tool output missing (session interrupted before tool execution completed)\";\n\n/// A message in the conversation\n#[derive(Debug, Clone, Serialize, Deserialize)]\npub struct Message {\n pub role: Role,\n pub content: Vec<ContentBlock>,\n #[serde(default, skip_serializing_if = \"Option::is_none\")]\n pub timestamp: Option<chrono::DateTime<Utc>>,\n #[serde(default, skip_serializing_if = \"Option::is_none\")]\n pub tool_duration_ms: Option<u64>,\n}\n\n/// Cache control metadata for prompt caching\n#[derive(Debug, Clone, Serialize, Deserialize)]\npub struct CacheControl {\n #[serde(rename = \"type\")]\n pub kind: String,\n #[serde(skip_serializing_if = \"Option::is_none\")]\n pub ttl: Option<String>,\n}\n\nimpl CacheControl {\n pub fn ephemeral(ttl: Option<String>) -> Self {\n Self {\n kind: \"ephemeral\".to_string(),\n ttl,\n }\n }\n}\n\n/// Content block within a message\n#[derive(Debug, Clone, Serialize, Deserialize)]\n#[serde(tag = \"type\", rename_all = \"snake_case\")]\npub enum ContentBlock {\n Text {\n text: String,\n #[serde(skip_serializing_if = \"Option::is_none\")]\n cache_control: Option<CacheControl>,\n },\n /// Hidden reasoning content used for providers that require it (not displayed)\n Reasoning {\n text: String,\n },\n ToolUse {\n id: String,\n name: String,\n input: serde_json::Value,\n },\n ToolResult {\n tool_use_id: String,\n content: String,\n #[serde(skip_serializing_if = \"Option::is_none\")]\n is_error: Option<bool>,\n },\n Image {\n media_type: String,\n data: String,\n },\n /// Hidden OpenAI Responses compaction item used to preserve native\n /// compaction state across turns/saves when jcode explicitly triggers it.\n OpenAICompaction {\n encrypted_content: String,\n },\n}\n\nimpl Message {\n pub fn user(text: &str) -> Self {\n Self {\n role: Role::User,\n content: vec![ContentBlock::Text {\n text: text.to_string(),\n cache_control: None,\n }],\n timestamp: Some(Utc::now()),\n tool_duration_ms: None,\n }\n }\n\n pub fn user_with_images(text: &str, images: Vec<(String, String)>) -> Self {\n let mut content: Vec<ContentBlock> = images\n .into_iter()\n .map(|(media_type, data)| ContentBlock::Image { media_type, data })\n .collect();\n content.push(ContentBlock::Text {\n text: text.to_string(),\n cache_control: None,\n });\n Self {\n role: Role::User,\n content,\n timestamp: Some(Utc::now()),\n tool_duration_ms: None,\n }\n }\n\n pub fn assistant_text(text: &str) -> Self {\n Self {\n role: Role::Assistant,\n content: vec![ContentBlock::Text {\n text: text.to_string(),\n cache_control: None,\n }],\n timestamp: Some(Utc::now()),\n tool_duration_ms: None,\n }\n }\n\n pub fn tool_result(tool_use_id: &str, content: &str, is_error: bool) -> Self {\n Self::tool_result_with_duration(tool_use_id, content, is_error, None)\n }\n\n pub fn tool_result_with_duration(\n tool_use_id: &str,\n content: &str,\n is_error: bool,\n tool_duration_ms: Option<u64>,\n ) -> Self {\n Self {\n role: Role::User,\n content: vec![ContentBlock::ToolResult {\n tool_use_id: tool_use_id.to_string(),\n content: content.to_string(),\n is_error: if is_error { Some(true) } else { None },\n }],\n timestamp: Some(Utc::now()),\n tool_duration_ms,\n }\n }\n\n /// Format a timestamp deterministically in UTC for injection into model-visible content.\n pub fn format_timestamp(ts: &chrono::DateTime<Utc>) -> String {\n ts.to_rfc3339_opts(chrono::SecondsFormat::Millis, true)\n }\n\n pub fn format_duration(duration_ms: u64) -> String {\n match duration_ms {\n 0..=999 => format!(\"{}ms\", duration_ms),\n 1_000..=9_999 => format!(\"{:.1}s\", duration_ms as f64 / 1000.0),\n 10_000..=59_999 => format!(\"{}s\", duration_ms / 1000),\n _ => {\n let total_seconds = duration_ms / 1000;\n let minutes = total_seconds / 60;\n let seconds = total_seconds % 60;\n if seconds == 0 {\n format!(\"{}m\", minutes)\n } else {\n format!(\"{}m {}s\", minutes, seconds)\n }\n }\n }\n }\n\n pub fn is_internal_system_reminder(&self) -> bool {\n self.content\n .iter()\n .find_map(|block| match block {\n ContentBlock::Text { text, .. } => Some(text.trim_start()),\n _ => None,\n })\n .is_some_and(|text| text.starts_with(\"<system-reminder>\"))\n }\n\n fn should_skip_timestamp_injection(&self) -> bool {\n self.is_internal_system_reminder()\n }\n\n fn tool_result_tag(&self, ts: &chrono::DateTime<Utc>) -> String {\n match self.tool_duration_ms {\n Some(duration_ms) => {\n let duration_ms_i64 = i64::try_from(duration_ms).unwrap_or(i64::MAX);\n let start_ts = ts\n .checked_sub_signed(chrono::Duration::milliseconds(duration_ms_i64))\n .unwrap_or(*ts);\n format!(\n \"[tool timing: start={} finish={} duration={}]\",\n Self::format_timestamp(&start_ts),\n Self::format_timestamp(ts),\n Self::format_duration(duration_ms)\n )\n }\n None => format!(\"[{}]\", Self::format_timestamp(ts)),\n }\n }\n\n /// Return a copy of messages with timestamps injected into user-role text content.\n /// Tool results get a stable UTC timing header prepended to content.\n /// User text messages get a stable UTC timestamp prepended to the first text block.\n pub fn with_timestamps(messages: &[Message]) -> Vec<Message> {\n messages\n .iter()\n .map(|msg| {\n let Some(ts) = msg.timestamp else {\n return msg.clone();\n };\n if msg.role != Role::User || msg.should_skip_timestamp_injection() {\n return msg.clone();\n }\n let text_tag = format!(\"[{}]\", Self::format_timestamp(&ts));\n let tool_result_tag = msg.tool_result_tag(&ts);\n let mut msg = msg.clone();\n let mut tagged = false;\n for block in &mut msg.content {\n match block {\n ContentBlock::Text { text, .. } if !tagged => {\n *text = format!(\"{} {}\", text_tag, text);\n tagged = true;\n }\n ContentBlock::ToolResult { content, .. } if !tagged => {\n *content = format!(\"{} {}\", tool_result_tag, content);\n tagged = true;\n }\n _ => {}\n }\n }\n msg\n })\n .collect()\n }\n}\n\nconst STABLE_HASH_SEED: u64 = 0xcbf29ce484222325;\nconst STABLE_HASH_PRIME: u64 = 0x100000001b3;\n\nfn stable_hash_bytes(bytes: &[u8]) -> u64 {\n let mut hash = STABLE_HASH_SEED;\n for byte in bytes {\n hash ^= *byte as u64;\n hash = hash.wrapping_mul(STABLE_HASH_PRIME);\n }\n hash\n}\n\npub(crate) fn extend_stable_hash(acc: u64, next: u64) -> u64 {\n stable_hash_bytes(&[acc.to_le_bytes().as_slice(), next.to_le_bytes().as_slice()].concat())\n}\n\npub(crate) fn stable_message_hash(message: &Message) -> u64 {\n match serde_json::to_vec(message) {\n Ok(bytes) => stable_hash_bytes(&bytes),\n Err(_) => stable_hash_bytes(format!(\"{:?}\", message).as_bytes()),\n }\n}\n\npub fn ends_with_fresh_user_turn(messages: &[Message]) -> bool {\n for msg in messages.iter().rev() {\n if msg.role != Role::User {\n return false;\n }\n\n if msg\n .content\n .iter()\n .any(|block| matches!(block, ContentBlock::ToolResult { .. }))\n {\n return false;\n }\n\n if msg.content.is_empty() {\n return false;\n }\n\n let mut saw_user_text = false;\n for block in &msg.content {\n match block {\n ContentBlock::Text { text, .. } => {\n let trimmed = text.trim();\n if !trimmed.is_empty() && !trimmed.starts_with(\"<system-reminder>\") {\n saw_user_text = true;\n }\n }\n _ => return false,\n }\n }\n\n if saw_user_text {\n return true;\n }\n\n if msg.is_internal_system_reminder() {\n continue;\n }\n\n return false;\n }\n\n false\n}\n\n/// Sanitize a tool ID so it matches the pattern `^[a-zA-Z0-9_-]+$`.\n///\n/// Different providers generate tool IDs in different formats. When switching\n/// from one provider to another mid-conversation, the historical tool IDs may\n/// contain characters that the new provider rejects (e.g., dots in Copilot IDs\n/// sent to Anthropic). This function replaces any invalid characters with\n/// underscores.\npub fn sanitize_tool_id(id: &str) -> String {\n if id.is_empty() {\n return \"unknown\".to_string();\n }\n let sanitized: String = id\n .chars()\n .map(|c| {\n if c.is_ascii_alphanumeric() || c == '_' || c == '-' {\n c\n } else {\n '_'\n }\n })\n .collect();\n if sanitized.is_empty() {\n \"unknown\".to_string()\n } else {\n sanitized\n }\n}\n\n/// Redact likely secrets from persisted tool output.\n///\n/// This is a best-effort safeguard for local session history files. It targets\n/// high-confidence token/key patterns and common `KEY=VALUE` assignments used by\n/// auth flows.\npub fn redact_secrets(text: &str) -> String {\n // Fast path to avoid regex work for most tool outputs.\n let lower = text.to_ascii_lowercase();\n\n if !text.contains(\"sk-\")\n && !text.contains(\"ghp_\")\n && !text.contains(\"github_pat_\")\n && !text.contains(\"AIza\")\n && !text.contains(\"ya29.\")\n && !text.contains(\"xox\")\n && !lower.contains(\"api_key\")\n && !lower.contains(\"token\")\n {\n return text.to_string();\n }\n\n static DIRECT_PATTERNS: OnceLock<Vec<Regex>> = OnceLock::new();\n static ASSIGNMENT_PATTERNS: OnceLock<Vec<Regex>> = OnceLock::new();\n\n let direct_patterns = DIRECT_PATTERNS.get_or_init(|| {\n compile_static_regexes(&[\n r\"sk-ant-(?:oat|ort)01-[A-Za-z0-9_-]{20,}\",\n r\"sk-or-v1-[A-Za-z0-9_-]{20,}\",\n r\"ghp_[A-Za-z0-9]{20,}\",\n r\"github_pat_[A-Za-z0-9_]{20,}\",\n r\"ya29\\.[A-Za-z0-9._-]{20,}\",\n r\"AIza[0-9A-Za-z_-]{20,}\",\n r\"xox[baprs]-[A-Za-z0-9-]{10,}\",\n ])\n });\n\n let assignment_patterns = ASSIGNMENT_PATTERNS.get_or_init(|| {\n compile_static_regexes(&[\n r\"(?m)^\\s*(OPENROUTER_API_KEY\\s*=\\s*)[^\\r\\n]+\",\n r\"(?m)^\\s*(OPENCODE_API_KEY\\s*=\\s*)[^\\r\\n]+\",\n r\"(?m)^\\s*(OPENCODE_GO_API_KEY\\s*=\\s*)[^\\r\\n]+\",\n r\"(?m)^\\s*(ZHIPU_API_KEY\\s*=\\s*)[^\\r\\n]+\",\n r\"(?m)^\\s*(ZAI_API_KEY\\s*=\\s*)[^\\r\\n]+\",\n r\"(?m)^\\s*(302AI_API_KEY\\s*=\\s*)[^\\r\\n]+\",\n r\"(?m)^\\s*(BASETEN_API_KEY\\s*=\\s*)[^\\r\\n]+\",\n r\"(?m)^\\s*(CORTECS_API_KEY\\s*=\\s*)[^\\r\\n]+\",\n r\"(?m)^\\s*(DEEPSEEK_API_KEY\\s*=\\s*)[^\\r\\n]+\",\n r\"(?m)^\\s*(FIRMWARE_API_KEY\\s*=\\s*)[^\\r\\n]+\",\n r\"(?m)^\\s*(HF_TOKEN\\s*=\\s*)[^\\r\\n]+\",\n r\"(?m)^\\s*(MOONSHOT_API_KEY\\s*=\\s*)[^\\r\\n]+\",\n r\"(?m)^\\s*(NEBIUS_API_KEY\\s*=\\s*)[^\\r\\n]+\",\n r\"(?m)^\\s*(SCALEWAY_API_KEY\\s*=\\s*)[^\\r\\n]+\",\n r\"(?m)^\\s*(STACKIT_API_KEY\\s*=\\s*)[^\\r\\n]+\",\n r\"(?m)^\\s*(GROQ_API_KEY\\s*=\\s*)[^\\r\\n]+\",\n r\"(?m)^\\s*(MISTRAL_API_KEY\\s*=\\s*)[^\\r\\n]+\",\n r\"(?m)^\\s*(PERPLEXITY_API_KEY\\s*=\\s*)[^\\r\\n]+\",\n r\"(?m)^\\s*(TOGETHER_API_KEY\\s*=\\s*)[^\\r\\n]+\",\n r\"(?m)^\\s*(DEEPINFRA_API_KEY\\s*=\\s*)[^\\r\\n]+\",\n r\"(?m)^\\s*(XAI_API_KEY\\s*=\\s*)[^\\r\\n]+\",\n r\"(?m)^\\s*(LMSTUDIO_API_KEY\\s*=\\s*)[^\\r\\n]+\",\n r\"(?m)^\\s*(OLLAMA_API_KEY\\s*=\\s*)[^\\r\\n]+\",\n r\"(?m)^\\s*(CHUTES_API_KEY\\s*=\\s*)[^\\r\\n]+\",\n r\"(?m)^\\s*(CEREBRAS_API_KEY\\s*=\\s*)[^\\r\\n]+\",\n r\"(?m)^\\s*(OPENAI_COMPAT_API_KEY\\s*=\\s*)[^\\r\\n]+\",\n r\"(?m)^\\s*(ANTHROPIC_API_KEY\\s*=\\s*)[^\\r\\n]+\",\n r\"(?m)^\\s*(OPENAI_API_KEY\\s*=\\s*)[^\\r\\n]+\",\n r\"(?m)^\\s*(AZURE_OPENAI_API_KEY\\s*=\\s*)[^\\r\\n]+\",\n r\"(?m)^\\s*(CURSOR_API_KEY\\s*=\\s*)[^\\r\\n]+\",\n r\"(?m)^\\s*(GITHUB_TOKEN\\s*=\\s*)[^\\r\\n]+\",\n ])\n });\n\n let mut redacted = text.to_string();\n let mut redacted_keys: HashSet<String> = [\n \"OPENROUTER_API_KEY\",\n \"OPENCODE_API_KEY\",\n \"OPENCODE_GO_API_KEY\",\n \"ZHIPU_API_KEY\",\n \"ZAI_API_KEY\",\n \"302AI_API_KEY\",\n \"BASETEN_API_KEY\",\n \"CORTECS_API_KEY\",\n \"DEEPSEEK_API_KEY\",\n \"FIRMWARE_API_KEY\",\n \"HF_TOKEN\",\n \"MOONSHOT_API_KEY\",\n \"NEBIUS_API_KEY\",\n \"SCALEWAY_API_KEY\",\n \"STACKIT_API_KEY\",\n \"GROQ_API_KEY\",\n \"MISTRAL_API_KEY\",\n \"PERPLEXITY_API_KEY\",\n \"TOGETHER_API_KEY\",\n \"DEEPINFRA_API_KEY\",\n \"XAI_API_KEY\",\n \"LMSTUDIO_API_KEY\",\n \"OLLAMA_API_KEY\",\n \"CHUTES_API_KEY\",\n \"CEREBRAS_API_KEY\",\n \"OPENAI_COMPAT_API_KEY\",\n \"ANTHROPIC_API_KEY\",\n \"OPENAI_API_KEY\",\n \"AZURE_OPENAI_API_KEY\",\n \"CURSOR_API_KEY\",\n \"GITHUB_TOKEN\",\n ]\n .iter()\n .map(|k| (*k).to_string())\n .collect();\n\n for re in direct_patterns {\n redacted = re.replace_all(&redacted, \"[REDACTED_SECRET]\").into_owned();\n }\n\n for re in assignment_patterns {\n redacted = re\n .replace_all(&redacted, \"${1}[REDACTED_SECRET]\")\n .into_owned();\n }\n\n // Also redact custom API key variable names configured at runtime.\n for source in [\n \"JCODE_OPENROUTER_API_KEY_NAME\",\n \"JCODE_OPENAI_COMPAT_API_KEY_NAME\",\n ] {\n let Some(key_name) = std::env::var(source)\n .ok()\n .map(|v| v.trim().to_string())\n .filter(|v| !v.is_empty())\n else {\n continue;\n };\n\n if !key_name\n .chars()\n .all(|c| c.is_ascii_uppercase() || c.is_ascii_digit() || c == '_')\n {\n continue;\n }\n if !redacted_keys.insert(key_name.clone()) {\n continue;\n }\n\n let pattern = format!(r\"(?m)^\\s*({}\\s*=\\s*)[^\\r\\n]+\", regex::escape(&key_name));\n if let Ok(re) = Regex::new(&pattern) {\n redacted = re\n .replace_all(&redacted, \"${1}[REDACTED_SECRET]\")\n .into_owned();\n }\n }\n\n redacted\n}\n\n/// Tool definition for the API\n#[derive(Debug, Clone, Serialize)]\npub struct ToolDefinition {\n pub name: String,\n // Prompt-visible text sent to the model by provider adapters.\n // Approximate prompt cost: description.len() / 4. Use\n // ToolDefinition::description_token_estimate() when reviewing tool bloat.\n pub description: String,\n pub input_schema: serde_json::Value,\n}\n\nimpl ToolDefinition {\n /// Serialized size of the full tool definition payload sent to providers.\n pub fn prompt_chars(&self) -> usize {\n serde_json::json!({\n \"name\": self.name,\n \"description\": self.description,\n \"input_schema\": self.input_schema,\n })\n .to_string()\n .len()\n }\n\n /// Approximate prompt-token cost of this tool's top-level description.\n ///\n /// This uses jcode's standard chars/4 heuristic, matching other token\n /// budget estimates in the codebase.\n pub fn description_token_estimate(&self) -> usize {\n crate::util::estimate_tokens(&self.description)\n }\n\n /// Approximate prompt-token cost of the full tool definition payload.\n pub fn prompt_token_estimate(&self) -> usize {\n crate::util::estimate_tokens(\n &serde_json::json!({\n \"name\": self.name,\n \"description\": self.description,\n \"input_schema\": self.input_schema,\n })\n .to_string(),\n )\n }\n\n pub fn aggregate_prompt_chars(defs: &[ToolDefinition]) -> usize {\n defs.iter().map(Self::prompt_chars).sum()\n }\n\n pub fn aggregate_prompt_token_estimate(defs: &[ToolDefinition]) -> usize {\n defs.iter().map(Self::prompt_token_estimate).sum()\n }\n}\n\npub const GENERATED_IMAGE_TOOL_NAME: &str = \"image_generation\";\npub const GENERATED_IMAGE_MAX_AUTO_VISION_BYTES: u64 = 20 * 1024 * 1024;\n\npub fn generated_image_tool_input(\n path: &str,\n metadata_path: Option<&str>,\n output_format: &str,\n revised_prompt: Option<&str>,\n) -> serde_json::Value {\n serde_json::json!({\n \"path\": path,\n \"metadata_path\": metadata_path,\n \"output_format\": output_format,\n \"revised_prompt\": revised_prompt,\n })\n}\n\npub fn generated_image_summary(\n path: &str,\n metadata_path: Option<&str>,\n output_format: &str,\n revised_prompt: Option<&str>,\n) -> String {\n let mut summary = format!(\"Generated image ({}) saved to `{}`.\", output_format, path);\n if let Some(metadata_path) = metadata_path {\n summary.push_str(&format!(\"\\nMetadata saved to `{}`.\", metadata_path));\n }\n if let Some(revised_prompt) = revised_prompt.filter(|prompt| !prompt.trim().is_empty()) {\n summary.push_str(\"\\n\\nRevised prompt:\\n\");\n summary.push_str(revised_prompt.trim());\n }\n summary\n}\n\npub fn generated_image_visual_context_blocks(\n path: &str,\n metadata_path: Option<&str>,\n output_format: &str,\n revised_prompt: Option<&str>,\n) -> Option<Vec<ContentBlock>> {\n let path_ref = Path::new(path);\n let metadata = std::fs::metadata(path_ref).ok()?;\n if !metadata.is_file() || metadata.len() > GENERATED_IMAGE_MAX_AUTO_VISION_BYTES {\n return None;\n }\n\n let data = std::fs::read(path_ref).ok()?;\n let media_type = generated_image_media_type(path_ref, output_format).to_string();\n let data_b64 = base64::engine::general_purpose::STANDARD.encode(data);\n let mut reminder = format!(\n \"<system-reminder>\\nA provider-native image generation call created `{}`. Jcode attached the image pixels as visual context for future turns because the active provider supports image input and the file is under the safe {} MB limit.\\nFormat: {}\",\n path,\n GENERATED_IMAGE_MAX_AUTO_VISION_BYTES / 1024 / 1024,\n output_format,\n );\n if let Some(metadata_path) = metadata_path.filter(|value| !value.trim().is_empty()) {\n reminder.push_str(&format!(\"\\nMetadata: {}\", metadata_path));\n }\n if let Some(revised_prompt) = revised_prompt.filter(|value| !value.trim().is_empty()) {\n reminder.push_str(\"\\nRevised prompt:\\n\");\n reminder.push_str(revised_prompt.trim());\n }\n reminder.push_str(\"\\n</system-reminder>\");\n\n Some(vec![\n ContentBlock::Text {\n text: reminder,\n cache_control: None,\n },\n ContentBlock::Image {\n media_type,\n data: data_b64,\n },\n ])\n}\n\nfn generated_image_media_type(path: &Path, output_format: &str) -> &'static str {\n let ext = path\n .extension()\n .and_then(|value| value.to_str())\n .unwrap_or(output_format)\n .to_ascii_lowercase();\n match ext.as_str() {\n \"jpg\" | \"jpeg\" => \"image/jpeg\",\n \"webp\" => \"image/webp\",\n \"gif\" => \"image/gif\",\n \"bmp\" => \"image/bmp\",\n \"ico\" => \"image/x-icon\",\n _ => \"image/png\",\n }\n}\n\n/// Connection phase for status bar transparency\n#[derive(Debug, Clone, PartialEq)]\npub enum ConnectionPhase {\n /// Refreshing OAuth token\n Authenticating,\n /// TCP + TLS connection to API\n Connecting,\n /// HTTP request sent, waiting for first response byte\n WaitingForResponse,\n /// First byte received, stream is active\n Streaming,\n /// Retrying after a transient error\n Retrying { attempt: u32, max: u32 },\n}\n\nimpl std::fmt::Display for ConnectionPhase {\n fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {\n match self {\n ConnectionPhase::Authenticating => write!(f, \"authenticating\"),\n ConnectionPhase::Connecting => write!(f, \"connecting\"),\n ConnectionPhase::WaitingForResponse => write!(f, \"waiting for response\"),\n ConnectionPhase::Streaming => write!(f, \"streaming\"),\n ConnectionPhase::Retrying { attempt, max } => {\n write!(f, \"retrying ({}/{})\", attempt, max)\n }\n }\n }\n}\n\n/// Streaming event from provider\n#[derive(Debug, Clone)]\npub enum StreamEvent {\n /// Text content delta\n TextDelta(String),\n /// Tool use started\n ToolUseStart { id: String, name: String },\n /// Tool input delta (JSON fragment)\n ToolInputDelta(String),\n /// Tool use complete\n ToolUseEnd,\n /// Tool result from provider (provider already executed the tool)\n ToolResult {\n tool_use_id: String,\n content: String,\n is_error: bool,\n },\n /// Image generated by a provider-native image generation tool.\n GeneratedImage {\n id: String,\n path: String,\n metadata_path: Option<String>,\n output_format: String,\n revised_prompt: Option<String>,\n },\n /// Extended thinking started\n ThinkingStart,\n /// Extended thinking delta (reasoning content)\n ThinkingDelta(String),\n /// Extended thinking ended\n ThinkingEnd,\n /// Extended thinking completed with duration\n ThinkingDone { duration_secs: f64 },\n /// Message complete (may have stop reason)\n MessageEnd { stop_reason: Option<String> },\n /// Token usage update\n TokenUsage {\n input_tokens: Option<u64>,\n output_tokens: Option<u64>,\n cache_read_input_tokens: Option<u64>,\n cache_creation_input_tokens: Option<u64>,\n },\n /// Active transport/connection type for this stream\n ConnectionType { connection: String },\n /// Connection phase update (for status bar transparency)\n ConnectionPhase { phase: ConnectionPhase },\n /// Provider-supplied human-readable transport detail for the status line\n StatusDetail { detail: String },\n /// Error occurred\n Error {\n message: String,\n /// Seconds until rate limit resets (if this is a rate limit error)\n retry_after_secs: Option<u64>,\n },\n /// Provider session ID (for conversation resume)\n SessionId(String),\n /// Compaction occurred (context was summarized)\n Compaction {\n trigger: String,\n pre_tokens: Option<u64>,\n /// Provider-native compaction artifact, if one was emitted.\n openai_encrypted_content: Option<String>,\n },\n /// Upstream provider info (e.g., which provider OpenRouter routed to)\n UpstreamProvider { provider: String },\n /// Native tool call from a provider bridge that needs execution by jcode\n NativeToolCall {\n request_id: String,\n tool_name: String,\n input: serde_json::Value,\n },\n}\n\n#[cfg(test)]\nmod tests;\n"}, "5d46c4932a49f1d7bb7d92c46f1a4558309424d0": {"t": 1785757214.6780546, "n": 23066, "h": "67623215dd405494", "c": "use crate::bus::{BackgroundTaskCompleted, BackgroundTaskProgressEvent, BackgroundTaskStatus};\nuse chrono::Utc;\nuse regex::Regex;\nuse serde::{Deserialize, Serialize};\nuse std::collections::HashSet;\nuse std::sync::OnceLock;\n\npub use jcode_message_types::{InputShellResult, ToolCall};\n\nmod notifications;\n\npub use notifications::{\n ParsedBackgroundTaskNotification, ParsedBackgroundTaskProgressNotification,\n background_task_display_label, background_task_status_notice,\n format_background_task_notification_markdown, format_background_task_progress_markdown,\n format_input_shell_result_markdown, input_shell_status_notice,\n parse_background_task_notification_markdown,\n parse_background_task_progress_notification_markdown,\n};\n\nfn compile_static_regex(pattern: &str) -> Option<Regex> {\n match Regex::new(pattern) {\n Ok(regex) => Some(regex),\n Err(err) => {\n eprintln!(\"jcode: failed to compile static regex: {err}\");\n None\n }\n }\n}\n\nfn compile_static_regexes(patterns: &[&str]) -> Vec<Regex> {\n patterns\n .iter()\n .filter_map(|pattern| compile_static_regex(pattern))\n .collect()\n}\n\n/// Role in conversation\n#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]\n#[serde(rename_all = \"lowercase\")]\npub enum Role {\n User,\n Assistant,\n}\n\n/// Plain-text tool output placeholder when execution was interrupted.\npub const TOOL_OUTPUT_MISSING_TEXT: &str =\n \"Tool output missing (session interrupted before tool execution completed)\";\n\n/// A message in the conversation\n#[derive(Debug, Clone, Serialize, Deserialize)]\npub struct Message {\n pub role: Role,\n pub content: Vec<ContentBlock>,\n #[serde(default, skip_serializing_if = \"Option::is_none\")]\n pub timestamp: Option<chrono::DateTime<Utc>>,\n #[serde(default, skip_serializing_if = \"Option::is_none\")]\n pub tool_duration_ms: Option<u64>,\n}\n\n/// Cache control metadata for prompt caching\n#[derive(Debug, Clone, Serialize, Deserialize)]\npub struct CacheControl {\n #[serde(rename = \"type\")]\n pub kind: String,\n #[serde(skip_serializing_if = \"Option::is_none\")]\n pub ttl: Option<String>,\n}\n\nimpl CacheControl {\n pub fn ephemeral(ttl: Option<String>) -> Self {\n Self {\n kind: \"ephemeral\".to_string(),\n ttl,\n }\n }\n}\n\n/// Content block within a message\n#[derive(Debug, Clone, Serialize, Deserialize)]\n#[serde(tag = \"type\", rename_all = \"snake_case\")]\npub enum ContentBlock {\n Text {\n text: String,\n #[serde(skip_serializing_if = \"Option::is_none\")]\n cache_control: Option<CacheControl>,\n },\n /// Hidden reasoning content used for providers that require it (not displayed)\n Reasoning {\n text: String,\n },\n ToolUse {\n id: String,\n name: String,\n input: serde_json::Value,\n },\n ToolResult {\n tool_use_id: String,\n content: String,\n #[serde(skip_serializing_if = \"Option::is_none\")]\n is_error: Option<bool>,\n },\n Image {\n media_type: String,\n data: String,\n },\n /// Hidden OpenAI Responses compaction item used to preserve native\n /// compaction state across turns/saves when jcode explicitly triggers it.\n OpenAICompaction {\n encrypted_content: String,\n },\n}\n\nimpl Message {\n pub fn user(text: &str) -> Self {\n Self {\n role: Role::User,\n content: vec![ContentBlock::Text {\n text: text.to_string(),\n cache_control: None,\n }],\n timestamp: Some(Utc::now()),\n tool_duration_ms: None,\n }\n }\n\n pub fn user_with_images(text: &str, images: Vec<(String, String)>) -> Self {\n let mut content: Vec<ContentBlock> = images\n .into_iter()\n .map(|(media_type, data)| ContentBlock::Image { media_type, data })\n .collect();\n content.push(ContentBlock::Text {\n text: text.to_string(),\n cache_control: None,\n });\n Self {\n role: Role::User,\n content,\n timestamp: Some(Utc::now()),\n tool_duration_ms: None,\n }\n }\n\n pub fn assistant_text(text: &str) -> Self {\n Self {\n role: Role::Assistant,\n content: vec![ContentBlock::Text {\n text: text.to_string(),\n cache_control: None,\n }],\n timestamp: Some(Utc::now()),\n tool_duration_ms: None,\n }\n }\n\n pub fn tool_result(tool_use_id: &str, content: &str, is_error: bool) -> Self {\n Self::tool_result_with_duration(tool_use_id, content, is_error, None)\n }\n\n pub fn tool_result_with_duration(\n tool_use_id: &str,\n content: &str,\n is_error: bool,\n tool_duration_ms: Option<u64>,\n ) -> Self {\n Self {\n role: Role::User,\n content: vec![ContentBlock::ToolResult {\n tool_use_id: tool_use_id.to_string(),\n content: content.to_string(),\n is_error: if is_error { Some(true) } else { None },\n }],\n timestamp: Some(Utc::now()),\n tool_duration_ms,\n }\n }\n\n /// Format a timestamp deterministically in UTC for injection into model-visible content.\n pub fn format_timestamp(ts: &chrono::DateTime<Utc>) -> String {\n ts.to_rfc3339_opts(chrono::SecondsFormat::Millis, true)\n }\n\n pub fn format_duration(duration_ms: u64) -> String {\n match duration_ms {\n 0..=999 => format!(\"{}ms\", duration_ms),\n 1_000..=9_999 => format!(\"{:.1}s\", duration_ms as f64 / 1000.0),\n 10_000..=59_999 => format!(\"{}s\", duration_ms / 1000),\n _ => {\n let total_seconds = duration_ms / 1000;\n let minutes = total_seconds / 60;\n let seconds = total_seconds % 60;\n if seconds == 0 {\n format!(\"{}m\", minutes)\n } else {\n format!(\"{}m {}s\", minutes, seconds)\n }\n }\n }\n }\n\n pub fn is_internal_system_reminder(&self) -> bool {\n self.content\n .iter()\n .find_map(|block| match block {\n ContentBlock::Text { text, .. } => Some(text.trim_start()),\n _ => None,\n })\n .is_some_and(|text| text.starts_with(\"<system-reminder>\"))\n }\n\n fn should_skip_timestamp_injection(&self) -> bool {\n self.is_internal_system_reminder()\n }\n\n fn tool_result_tag(&self, ts: &chrono::DateTime<Utc>) -> String {\n match self.tool_duration_ms {\n Some(duration_ms) => {\n let duration_ms_i64 = i64::try_from(duration_ms).unwrap_or(i64::MAX);\n let start_ts = ts\n .checked_sub_signed(chrono::Duration::milliseconds(duration_ms_i64))\n .unwrap_or(*ts);\n format!(\n \"[tool timing: start={} finish={} duration={}]\",\n Self::format_timestamp(&start_ts),\n Self::format_timestamp(ts),\n Self::format_duration(duration_ms)\n )\n }\n None => format!(\"[{}]\", Self::format_timestamp(ts)),\n }\n }\n\n /// Return a copy of messages with timestamps injected into user-role text content.\n /// Tool results get a stable UTC timing header prepended to content.\n /// User text messages get a stable UTC timestamp prepended to the first text block.\n pub fn with_timestamps(messages: &[Message]) -> Vec<Message> {\n messages\n .iter()\n .map(|msg| {\n let Some(ts) = msg.timestamp else {\n return msg.clone();\n };\n if msg.role != Role::User || msg.should_skip_timestamp_injection() {\n return msg.clone();\n }\n let text_tag = format!(\"[{}]\", Self::format_timestamp(&ts));\n let tool_result_tag = msg.tool_result_tag(&ts);\n let mut msg = msg.clone();\n let mut tagged = false;\n for block in &mut msg.content {\n match block {\n ContentBlock::Text { text, .. } if !tagged => {\n *text = format!(\"{} {}\", text_tag, text);\n tagged = true;\n }\n ContentBlock::ToolResult { content, .. } if !tagged => {\n *content = format!(\"{} {}\", tool_result_tag, content);\n tagged = true;\n }\n _ => {}\n }\n }\n msg\n })\n .collect()\n }\n}\n\nconst STABLE_HASH_SEED: u64 = 0xcbf29ce484222325;\nconst STABLE_HASH_PRIME: u64 = 0x100000001b3;\n\nfn stable_hash_bytes(bytes: &[u8]) -> u64 {\n let mut hash = STABLE_HASH_SEED;\n for byte in bytes {\n hash ^= *byte as u64;\n hash = hash.wrapping_mul(STABLE_HASH_PRIME);\n }\n hash\n}\n\npub(crate) fn extend_stable_hash(acc: u64, next: u64) -> u64 {\n stable_hash_bytes(&[acc.to_le_bytes().as_slice(), next.to_le_bytes().as_slice()].concat())\n}\n\npub(crate) fn stable_message_hash(message: &Message) -> u64 {\n match serde_json::to_vec(message) {\n Ok(bytes) => stable_hash_bytes(&bytes),\n Err(_) => stable_hash_bytes(format!(\"{:?}\", message).as_bytes()),\n }\n}\n\npub fn ends_with_fresh_user_turn(messages: &[Message]) -> bool {\n for msg in messages.iter().rev() {\n if msg.role != Role::User {\n return false;\n }\n\n if msg\n .content\n .iter()\n .any(|block| matches!(block, ContentBlock::ToolResult { .. }))\n {\n return false;\n }\n\n if msg.content.is_empty() {\n return false;\n }\n\n let mut saw_user_text = false;\n for block in &msg.content {\n match block {\n ContentBlock::Text { text, .. } => {\n let trimmed = text.trim();\n if !trimmed.is_empty() && !trimmed.starts_with(\"<system-reminder>\") {\n saw_user_text = true;\n }\n }\n _ => return false,\n }\n }\n\n if saw_user_text {\n return true;\n }\n\n if msg.is_internal_system_reminder() {\n continue;\n }\n\n return false;\n }\n\n false\n}\n\n/// Sanitize a tool ID so it matches the pattern `^[a-zA-Z0-9_-]+$`.\n///\n/// Different providers generate tool IDs in different formats. When switching\n/// from one provider to another mid-conversation, the historical tool IDs may\n/// contain characters that the new provider rejects (e.g., dots in Copilot IDs\n/// sent to Anthropic). This function replaces any invalid characters with\n/// underscores.\npub fn sanitize_tool_id(id: &str) -> String {\n if id.is_empty() {\n return \"unknown\".to_string();\n }\n let sanitized: String = id\n .chars()\n .map(|c| {\n if c.is_ascii_alphanumeric() || c == '_' || c == '-' {\n c\n } else {\n '_'\n }\n })\n .collect();\n if sanitized.is_empty() {\n \"unknown\".to_string()\n } else {\n sanitized\n }\n}\n\n/// Redact likely secrets from persisted tool output.\n///\n/// This is a best-effort safeguard for local session history files. It targets\n/// high-confidence token/key patterns and common `KEY=VALUE` assignments used by\n/// auth flows.\npub fn redact_secrets(text: &str) -> String {\n // Fast path to avoid regex work for most tool outputs.\n let lower = text.to_ascii_lowercase();\n\n if !text.contains(\"sk-\")\n && !text.contains(\"ghp_\")\n && !text.contains(\"github_pat_\")\n && !text.contains(\"AIza\")\n && !text.contains(\"ya29.\")\n && !text.contains(\"xox\")\n && !lower.contains(\"api_key\")\n && !lower.contains(\"token\")\n {\n return text.to_string();\n }\n\n static DIRECT_PATTERNS: OnceLock<Vec<Regex>> = OnceLock::new();\n static ASSIGNMENT_PATTERNS: OnceLock<Vec<Regex>> = OnceLock::new();\n\n let direct_patterns = DIRECT_PATTERNS.get_or_init(|| {\n compile_static_regexes(&[\n r\"sk-ant-(?:oat|ort)01-[A-Za-z0-9_-]{20,}\",\n r\"sk-or-v1-[A-Za-z0-9_-]{20,}\",\n r\"ghp_[A-Za-z0-9]{20,}\",\n r\"github_pat_[A-Za-z0-9_]{20,}\",\n r\"ya29\\.[A-Za-z0-9._-]{20,}\",\n r\"AIza[0-9A-Za-z_-]{20,}\",\n r\"xox[baprs]-[A-Za-z0-9-]{10,}\",\n ])\n });\n\n let assignment_patterns = ASSIGNMENT_PATTERNS.get_or_init(|| {\n compile_static_regexes(&[\n r\"(?m)^\\s*(OPENROUTER_API_KEY\\s*=\\s*)[^\\r\\n]+\",\n r\"(?m)^\\s*(OPENCODE_API_KEY\\s*=\\s*)[^\\r\\n]+\",\n r\"(?m)^\\s*(OPENCODE_GO_API_KEY\\s*=\\s*)[^\\r\\n]+\",\n r\"(?m)^\\s*(ZHIPU_API_KEY\\s*=\\s*)[^\\r\\n]+\",\n r\"(?m)^\\s*(ZAI_API_KEY\\s*=\\s*)[^\\r\\n]+\",\n r\"(?m)^\\s*(302AI_API_KEY\\s*=\\s*)[^\\r\\n]+\",\n r\"(?m)^\\s*(BASETEN_API_KEY\\s*=\\s*)[^\\r\\n]+\",\n r\"(?m)^\\s*(CORTECS_API_KEY\\s*=\\s*)[^\\r\\n]+\",\n r\"(?m)^\\s*(DEEPSEEK_API_KEY\\s*=\\s*)[^\\r\\n]+\",\n r\"(?m)^\\s*(FIRMWARE_API_KEY\\s*=\\s*)[^\\r\\n]+\",\n r\"(?m)^\\s*(HF_TOKEN\\s*=\\s*)[^\\r\\n]+\",\n r\"(?m)^\\s*(MOONSHOT_API_KEY\\s*=\\s*)[^\\r\\n]+\",\n r\"(?m)^\\s*(NEBIUS_API_KEY\\s*=\\s*)[^\\r\\n]+\",\n r\"(?m)^\\s*(SCALEWAY_API_KEY\\s*=\\s*)[^\\r\\n]+\",\n r\"(?m)^\\s*(STACKIT_API_KEY\\s*=\\s*)[^\\r\\n]+\",\n r\"(?m)^\\s*(GROQ_API_KEY\\s*=\\s*)[^\\r\\n]+\",\n r\"(?m)^\\s*(MISTRAL_API_KEY\\s*=\\s*)[^\\r\\n]+\",\n r\"(?m)^\\s*(PERPLEXITY_API_KEY\\s*=\\s*)[^\\r\\n]+\",\n r\"(?m)^\\s*(TOGETHER_API_KEY\\s*=\\s*)[^\\r\\n]+\",\n r\"(?m)^\\s*(DEEPINFRA_API_KEY\\s*=\\s*)[^\\r\\n]+\",\n r\"(?m)^\\s*(XAI_API_KEY\\s*=\\s*)[^\\r\\n]+\",\n r\"(?m)^\\s*(LMSTUDIO_API_KEY\\s*=\\s*)[^\\r\\n]+\",\n r\"(?m)^\\s*(OLLAMA_API_KEY\\s*=\\s*)[^\\r\\n]+\",\n r\"(?m)^\\s*(CHUTES_API_KEY\\s*=\\s*)[^\\r\\n]+\",\n r\"(?m)^\\s*(CEREBRAS_API_KEY\\s*=\\s*)[^\\r\\n]+\",\n r\"(?m)^\\s*(OPENAI_COMPAT_API_KEY\\s*=\\s*)[^\\r\\n]+\",\n r\"(?m)^\\s*(ANTHROPIC_API_KEY\\s*=\\s*)[^\\r\\n]+\",\n r\"(?m)^\\s*(OPENAI_API_KEY\\s*=\\s*)[^\\r\\n]+\",\n r\"(?m)^\\s*(AZURE_OPENAI_API_KEY\\s*=\\s*)[^\\r\\n]+\",\n r\"(?m)^\\s*(CURSOR_API_KEY\\s*=\\s*)[^\\r\\n]+\",\n r\"(?m)^\\s*(GITHUB_TOKEN\\s*=\\s*)[^\\r\\n]+\",\n ])\n });\n\n let mut redacted = text.to_string();\n let mut redacted_keys: HashSet<String> = [\n \"OPENROUTER_API_KEY\",\n \"OPENCODE_API_KEY\",\n \"OPENCODE_GO_API_KEY\",\n \"ZHIPU_API_KEY\",\n \"ZAI_API_KEY\",\n \"302AI_API_KEY\",\n \"BASETEN_API_KEY\",\n \"CORTECS_API_KEY\",\n \"DEEPSEEK_API_KEY\",\n \"FIRMWARE_API_KEY\",\n \"HF_TOKEN\",\n \"MOONSHOT_API_KEY\",\n \"NEBIUS_API_KEY\",\n \"SCALEWAY_API_KEY\",\n \"STACKIT_API_KEY\",\n \"GROQ_API_KEY\",\n \"MISTRAL_API_KEY\",\n \"PERPLEXITY_API_KEY\",\n \"TOGETHER_API_KEY\",\n \"DEEPINFRA_API_KEY\",\n \"XAI_API_KEY\",\n \"LMSTUDIO_API_KEY\",\n \"OLLAMA_API_KEY\",\n \"CHUTES_API_KEY\",\n \"CEREBRAS_API_KEY\",\n \"OPENAI_COMPAT_API_KEY\",\n \"ANTHROPIC_API_KEY\",\n \"OPENAI_API_KEY\",\n \"AZURE_OPENAI_API_KEY\",\n \"CURSOR_API_KEY\",\n \"GITHUB_TOKEN\",\n ]\n .iter()\n .map(|k| (*k).to_string())\n .collect();\n\n for re in direct_patterns {\n redacted = re.replace_all(&redacted, \"[REDACTED_SECRET]\").into_owned();\n }\n\n for re in assignment_patterns {\n redacted = re\n .replace_all(&redacted, \"${1}[REDACTED_SECRET]\")\n .into_owned();\n }\n\n // Also redact custom API key variable names configured at runtime.\n for source in [\n \"JCODE_OPENROUTER_API_KEY_NAME\",\n \"JCODE_OPENAI_COMPAT_API_KEY_NAME\",\n ] {\n let Some(key_name) = std::env::var(source)\n .ok()\n .map(|v| v.trim().to_string())\n .filter(|v| !v.is_empty())\n else {\n continue;\n };\n\n if !key_name\n .chars()\n .all(|c| c.is_ascii_uppercase() || c.is_ascii_digit() || c == '_')\n {\n continue;\n }\n if !redacted_keys.insert(key_name.clone()) {\n continue;\n }\n\n let pattern = format!(r\"(?m)^\\s*({}\\s*=\\s*)[^\\r\\n]+\", regex::escape(&key_name));\n if let Ok(re) = Regex::new(&pattern) {\n redacted = re\n .replace_all(&redacted, \"${1}[REDACTED_SECRET]\")\n .into_owned();\n }\n }\n\n redacted\n}\n\n/// Tool definition for the API\n#[derive(Debug, Clone, Serialize)]\npub struct ToolDefinition {\n pub name: String,\n // Prompt-visible text sent to the model by provider adapters.\n // Approximate prompt cost: description.len() / 4. Use\n // ToolDefinition::description_token_estimate() when reviewing tool bloat.\n pub description: String,\n pub input_schema: serde_json::Value,\n}\n\nimpl ToolDefinition {\n /// Serialized size of the full tool definition payload sent to providers.\n pub fn prompt_chars(&self) -> usize {\n serde_json::json!({\n \"name\": self.name,\n \"description\": self.description,\n \"input_schema\": self.input_schema,\n })\n .to_string()\n .len()\n }\n\n /// Approximate prompt-token cost of this tool's top-level description.\n ///\n /// This uses jcode's standard chars/4 heuristic, matching other token\n /// budget estimates in the codebase.\n pub fn description_token_estimate(&self) -> usize {\n crate::util::estimate_tokens(&self.description)\n }\n\n /// Approximate prompt-token cost of the full tool definition payload.\n pub fn prompt_token_estimate(&self) -> usize {\n crate::util::estimate_tokens(\n &serde_json::json!({\n \"name\": self.name,\n \"description\": self.description,\n \"input_schema\": self.input_schema,\n })\n .to_string(),\n )\n }\n\n pub fn aggregate_prompt_chars(defs: &[ToolDefinition]) -> usize {\n defs.iter().map(Self::prompt_chars).sum()\n }\n\n pub fn aggregate_prompt_token_estimate(defs: &[ToolDefinition]) -> usize {\n defs.iter().map(Self::prompt_token_estimate).sum()\n }\n}\n\npub const GENERATED_IMAGE_TOOL_NAME: &str = \"image_generation\";\n\npub fn generated_image_tool_input(\n path: &str,\n metadata_path: Option<&str>,\n output_format: &str,\n revised_prompt: Option<&str>,\n) -> serde_json::Value {\n serde_json::json!({\n \"path\": path,\n \"metadata_path\": metadata_path,\n \"output_format\": output_format,\n \"revised_prompt\": revised_prompt,\n })\n}\n\npub fn generated_image_summary(\n path: &str,\n metadata_path: Option<&str>,\n output_format: &str,\n revised_prompt: Option<&str>,\n) -> String {\n let mut summary = format!(\"Generated image ({}) saved to `{}`.\", output_format, path);\n if let Some(metadata_path) = metadata_path {\n summary.push_str(&format!(\"\\nMetadata saved to `{}`.\", metadata_path));\n }\n if let Some(revised_prompt) = revised_prompt.filter(|prompt| !prompt.trim().is_empty()) {\n summary.push_str(\"\\n\\nRevised prompt:\\n\");\n summary.push_str(revised_prompt.trim());\n }\n summary\n}\n\n/// Connection phase for status bar transparency\n#[derive(Debug, Clone, PartialEq)]\npub enum ConnectionPhase {\n /// Refreshing OAuth token\n Authenticating,\n /// TCP + TLS connection to API\n Connecting,\n /// HTTP request sent, waiting for first response byte\n WaitingForResponse,\n /// First byte received, stream is active\n Streaming,\n /// Retrying after a transient error\n Retrying { attempt: u32, max: u32 },\n}\n\nimpl std::fmt::Display for ConnectionPhase {\n fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {\n match self {\n ConnectionPhase::Authenticating => write!(f, \"authenticating\"),\n ConnectionPhase::Connecting => write!(f, \"connecting\"),\n ConnectionPhase::WaitingForResponse => write!(f, \"waiting for response\"),\n ConnectionPhase::Streaming => write!(f, \"streaming\"),\n ConnectionPhase::Retrying { attempt, max } => {\n write!(f, \"retrying ({}/{})\", attempt, max)\n }\n }\n }\n}\n\n/// Streaming event from provider\n#[derive(Debug, Clone)]\npub enum StreamEvent {\n /// Text content delta\n TextDelta(String),\n /// Tool use started\n ToolUseStart { id: String, name: String },\n /// Tool input delta (JSON fragment)\n ToolInputDelta(String),\n /// Tool use complete\n ToolUseEnd,\n /// Tool result from provider (provider already executed the tool)\n ToolResult {\n tool_use_id: String,\n content: String,\n is_error: bool,\n },\n /// Image generated by a provider-native image generation tool.\n GeneratedImage {\n id: String,\n path: String,\n metadata_path: Option<String>,\n output_format: String,\n revised_prompt: Option<String>,\n },\n /// Extended thinking started\n ThinkingStart,\n /// Extended thinking delta (reasoning content)\n ThinkingDelta(String),\n /// Extended thinking ended\n ThinkingEnd,\n /// Extended thinking completed with duration\n ThinkingDone { duration_secs: f64 },\n /// Message complete (may have stop reason)\n MessageEnd { stop_reason: Option<String> },\n /// Token usage update\n TokenUsage {\n input_tokens: Option<u64>,\n output_tokens: Option<u64>,\n cache_read_input_tokens: Option<u64>,\n cache_creation_input_tokens: Option<u64>,\n },\n /// Active transport/connection type for this stream\n ConnectionType { connection: String },\n /// Connection phase update (for status bar transparency)\n ConnectionPhase { phase: ConnectionPhase },\n /// Provider-supplied human-readable transport detail for the status line\n StatusDetail { detail: String },\n /// Error occurred\n Error {\n message: String,\n /// Seconds until rate limit resets (if this is a rate limit error)\n retry_after_secs: Option<u64>,\n },\n /// Provider session ID (for conversation resume)\n SessionId(String),\n /// Compaction occurred (context was summarized)\n Compaction {\n trigger: String,\n pre_tokens: Option<u64>,\n /// Provider-native compaction artifact, if one was emitted.\n openai_encrypted_content: Option<String>,\n },\n /// Upstream provider info (e.g., which provider OpenRouter routed to)\n UpstreamProvider { provider: String },\n /// Native tool call from a provider bridge that needs execution by jcode\n NativeToolCall {\n request_id: String,\n tool_name: String,\n input: serde_json::Value,\n },\n}\n\n#[cfg(test)]\nmod tests;\n"}, "e8b7dd675819b8307015e37894b01b94a673d507": {"t": 1785757216.8948607, "n": 23823, "h": "fe7c627bf4cb84bb", "c": "use crate::bus::{BackgroundTaskCompleted, BackgroundTaskProgressEvent, BackgroundTaskStatus};\nuse chrono::Utc;\nuse regex::Regex;\nuse serde::{Deserialize, Serialize};\nuse std::collections::HashSet;\nuse std::sync::OnceLock;\n\nmod notifications;\n\npub use notifications::{\n InputShellResult, ParsedBackgroundTaskNotification, ParsedBackgroundTaskProgressNotification,\n background_task_display_label, background_task_status_notice,\n format_background_task_notification_markdown, format_background_task_progress_markdown,\n format_input_shell_result_markdown, input_shell_status_notice,\n parse_background_task_notification_markdown,\n parse_background_task_progress_notification_markdown,\n};\n\nfn compile_static_regex(pattern: &str) -> Option<Regex> {\n match Regex::new(pattern) {\n Ok(regex) => Some(regex),\n Err(err) => {\n eprintln!(\"jcode: failed to compile static regex: {err}\");\n None\n }\n }\n}\n\nfn compile_static_regexes(patterns: &[&str]) -> Vec<Regex> {\n patterns\n .iter()\n .filter_map(|pattern| compile_static_regex(pattern))\n .collect()\n}\n\n/// Role in conversation\n#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]\n#[serde(rename_all = \"lowercase\")]\npub enum Role {\n User,\n Assistant,\n}\n\n/// Plain-text tool output placeholder when execution was interrupted.\npub const TOOL_OUTPUT_MISSING_TEXT: &str =\n \"Tool output missing (session interrupted before tool execution completed)\";\n\n/// A message in the conversation\n#[derive(Debug, Clone, Serialize, Deserialize)]\npub struct Message {\n pub role: Role,\n pub content: Vec<ContentBlock>,\n #[serde(default, skip_serializing_if = \"Option::is_none\")]\n pub timestamp: Option<chrono::DateTime<Utc>>,\n #[serde(default, skip_serializing_if = \"Option::is_none\")]\n pub tool_duration_ms: Option<u64>,\n}\n\n/// Cache control metadata for prompt caching\n#[derive(Debug, Clone, Serialize, Deserialize)]\npub struct CacheControl {\n #[serde(rename = \"type\")]\n pub kind: String,\n #[serde(skip_serializing_if = \"Option::is_none\")]\n pub ttl: Option<String>,\n}\n\nimpl CacheControl {\n pub fn ephemeral(ttl: Option<String>) -> Self {\n Self {\n kind: \"ephemeral\".to_string(),\n ttl,\n }\n }\n}\n\n/// Content block within a message\n#[derive(Debug, Clone, Serialize, Deserialize)]\n#[serde(tag = \"type\", rename_all = \"snake_case\")]\npub enum ContentBlock {\n Text {\n text: String,\n #[serde(skip_serializing_if = \"Option::is_none\")]\n cache_control: Option<CacheControl>,\n },\n /// Hidden reasoning content used for providers that require it (not displayed)\n Reasoning {\n text: String,\n },\n ToolUse {\n id: String,\n name: String,\n input: serde_json::Value,\n },\n ToolResult {\n tool_use_id: String,\n content: String,\n #[serde(skip_serializing_if = \"Option::is_none\")]\n is_error: Option<bool>,\n },\n Image {\n media_type: String,\n data: String,\n },\n /// Hidden OpenAI Responses compaction item used to preserve native\n /// compaction state across turns/saves when jcode explicitly triggers it.\n OpenAICompaction {\n encrypted_content: String,\n },\n}\n\nimpl Message {\n pub fn user(text: &str) -> Self {\n Self {\n role: Role::User,\n content: vec![ContentBlock::Text {\n text: text.to_string(),\n cache_control: None,\n }],\n timestamp: Some(Utc::now()),\n tool_duration_ms: None,\n }\n }\n\n pub fn user_with_images(text: &str, images: Vec<(String, String)>) -> Self {\n let mut content: Vec<ContentBlock> = images\n .into_iter()\n .map(|(media_type, data)| ContentBlock::Image { media_type, data })\n .collect();\n content.push(ContentBlock::Text {\n text: text.to_string(),\n cache_control: None,\n });\n Self {\n role: Role::User,\n content,\n timestamp: Some(Utc::now()),\n tool_duration_ms: None,\n }\n }\n\n pub fn assistant_text(text: &str) -> Self {\n Self {\n role: Role::Assistant,\n content: vec![ContentBlock::Text {\n text: text.to_string(),\n cache_control: None,\n }],\n timestamp: Some(Utc::now()),\n tool_duration_ms: None,\n }\n }\n\n pub fn tool_result(tool_use_id: &str, content: &str, is_error: bool) -> Self {\n Self::tool_result_with_duration(tool_use_id, content, is_error, None)\n }\n\n pub fn tool_result_with_duration(\n tool_use_id: &str,\n content: &str,\n is_error: bool,\n tool_duration_ms: Option<u64>,\n ) -> Self {\n Self {\n role: Role::User,\n content: vec![ContentBlock::ToolResult {\n tool_use_id: tool_use_id.to_string(),\n content: content.to_string(),\n is_error: if is_error { Some(true) } else { None },\n }],\n timestamp: Some(Utc::now()),\n tool_duration_ms,\n }\n }\n\n /// Format a timestamp deterministically in UTC for injection into model-visible content.\n pub fn format_timestamp(ts: &chrono::DateTime<Utc>) -> String {\n ts.to_rfc3339_opts(chrono::SecondsFormat::Millis, true)\n }\n\n pub fn format_duration(duration_ms: u64) -> String {\n match duration_ms {\n 0..=999 => format!(\"{}ms\", duration_ms),\n 1_000..=9_999 => format!(\"{:.1}s\", duration_ms as f64 / 1000.0),\n 10_000..=59_999 => format!(\"{}s\", duration_ms / 1000),\n _ => {\n let total_seconds = duration_ms / 1000;\n let minutes = total_seconds / 60;\n let seconds = total_seconds % 60;\n if seconds == 0 {\n format!(\"{}m\", minutes)\n } else {\n format!(\"{}m {}s\", minutes, seconds)\n }\n }\n }\n }\n\n pub fn is_internal_system_reminder(&self) -> bool {\n self.content\n .iter()\n .find_map(|block| match block {\n ContentBlock::Text { text, .. } => Some(text.trim_start()),\n _ => None,\n })\n .is_some_and(|text| text.starts_with(\"<system-reminder>\"))\n }\n\n fn should_skip_timestamp_injection(&self) -> bool {\n self.is_internal_system_reminder()\n }\n\n fn tool_result_tag(&self, ts: &chrono::DateTime<Utc>) -> String {\n match self.tool_duration_ms {\n Some(duration_ms) => {\n let duration_ms_i64 = i64::try_from(duration_ms).unwrap_or(i64::MAX);\n let start_ts = ts\n .checked_sub_signed(chrono::Duration::milliseconds(duration_ms_i64))\n .unwrap_or(*ts);\n format!(\n \"[tool timing: start={} finish={} duration={}]\",\n Self::format_timestamp(&start_ts),\n Self::format_timestamp(ts),\n Self::format_duration(duration_ms)\n )\n }\n None => format!(\"[{}]\", Self::format_timestamp(ts)),\n }\n }\n\n /// Return a copy of messages with timestamps injected into user-role text content.\n /// Tool results get a stable UTC timing header prepended to content.\n /// User text messages get a stable UTC timestamp prepended to the first text block.\n pub fn with_timestamps(messages: &[Message]) -> Vec<Message> {\n messages\n .iter()\n .map(|msg| {\n let Some(ts) = msg.timestamp else {\n return msg.clone();\n };\n if msg.role != Role::User || msg.should_skip_timestamp_injection() {\n return msg.clone();\n }\n let text_tag = format!(\"[{}]\", Self::format_timestamp(&ts));\n let tool_result_tag = msg.tool_result_tag(&ts);\n let mut msg = msg.clone();\n let mut tagged = false;\n for block in &mut msg.content {\n match block {\n ContentBlock::Text { text, .. } if !tagged => {\n *text = format!(\"{} {}\", text_tag, text);\n tagged = true;\n }\n ContentBlock::ToolResult { content, .. } if !tagged => {\n *content = format!(\"{} {}\", tool_result_tag, content);\n tagged = true;\n }\n _ => {}\n }\n }\n msg\n })\n .collect()\n }\n}\n\nconst STABLE_HASH_SEED: u64 = 0xcbf29ce484222325;\nconst STABLE_HASH_PRIME: u64 = 0x100000001b3;\n\nfn stable_hash_bytes(bytes: &[u8]) -> u64 {\n let mut hash = STABLE_HASH_SEED;\n for byte in bytes {\n hash ^= *byte as u64;\n hash = hash.wrapping_mul(STABLE_HASH_PRIME);\n }\n hash\n}\n\npub(crate) fn extend_stable_hash(acc: u64, next: u64) -> u64 {\n stable_hash_bytes(&[acc.to_le_bytes().as_slice(), next.to_le_bytes().as_slice()].concat())\n}\n\npub(crate) fn stable_message_hash(message: &Message) -> u64 {\n match serde_json::to_vec(message) {\n Ok(bytes) => stable_hash_bytes(&bytes),\n Err(_) => stable_hash_bytes(format!(\"{:?}\", message).as_bytes()),\n }\n}\n\npub fn ends_with_fresh_user_turn(messages: &[Message]) -> bool {\n for msg in messages.iter().rev() {\n if msg.role != Role::User {\n return false;\n }\n\n if msg\n .content\n .iter()\n .any(|block| matches!(block, ContentBlock::ToolResult { .. }))\n {\n return false;\n }\n\n if msg.content.is_empty() {\n return false;\n }\n\n let mut saw_user_text = false;\n for block in &msg.content {\n match block {\n ContentBlock::Text { text, .. } => {\n let trimmed = text.trim();\n if !trimmed.is_empty() && !trimmed.starts_with(\"<system-reminder>\") {\n saw_user_text = true;\n }\n }\n _ => return false,\n }\n }\n\n if saw_user_text {\n return true;\n }\n\n if msg.is_internal_system_reminder() {\n continue;\n }\n\n return false;\n }\n\n false\n}\n\n/// Sanitize a tool ID so it matches the pattern `^[a-zA-Z0-9_-]+$`.\n///\n/// Different providers generate tool IDs in different formats. When switching\n/// from one provider to another mid-conversation, the historical tool IDs may\n/// contain characters that the new provider rejects (e.g., dots in Copilot IDs\n/// sent to Anthropic). This function replaces any invalid characters with\n/// underscores.\npub fn sanitize_tool_id(id: &str) -> String {\n if id.is_empty() {\n return \"unknown\".to_string();\n }\n let sanitized: String = id\n .chars()\n .map(|c| {\n if c.is_ascii_alphanumeric() || c == '_' || c == '-' {\n c\n } else {\n '_'\n }\n })\n .collect();\n if sanitized.is_empty() {\n \"unknown\".to_string()\n } else {\n sanitized\n }\n}\n\n/// Redact likely secrets from persisted tool output.\n///\n/// This is a best-effort safeguard for local session history files. It targets\n/// high-confidence token/key patterns and common `KEY=VALUE` assignments used by\n/// auth flows.\npub fn redact_secrets(text: &str) -> String {\n // Fast path to avoid regex work for most tool outputs.\n let lower = text.to_ascii_lowercase();\n\n if !text.contains(\"sk-\")\n && !text.contains(\"ghp_\")\n && !text.contains(\"github_pat_\")\n && !text.contains(\"AIza\")\n && !text.contains(\"ya29.\")\n && !text.contains(\"xox\")\n && !lower.contains(\"api_key\")\n && !lower.contains(\"token\")\n {\n return text.to_string();\n }\n\n static DIRECT_PATTERNS: OnceLock<Vec<Regex>> = OnceLock::new();\n static ASSIGNMENT_PATTERNS: OnceLock<Vec<Regex>> = OnceLock::new();\n\n let direct_patterns = DIRECT_PATTERNS.get_or_init(|| {\n compile_static_regexes(&[\n r\"sk-ant-(?:oat|ort)01-[A-Za-z0-9_-]{20,}\",\n r\"sk-or-v1-[A-Za-z0-9_-]{20,}\",\n r\"ghp_[A-Za-z0-9]{20,}\",\n r\"github_pat_[A-Za-z0-9_]{20,}\",\n r\"ya29\\.[A-Za-z0-9._-]{20,}\",\n r\"AIza[0-9A-Za-z_-]{20,}\",\n r\"xox[baprs]-[A-Za-z0-9-]{10,}\",\n ])\n });\n\n let assignment_patterns = ASSIGNMENT_PATTERNS.get_or_init(|| {\n compile_static_regexes(&[\n r\"(?m)^\\s*(OPENROUTER_API_KEY\\s*=\\s*)[^\\r\\n]+\",\n r\"(?m)^\\s*(OPENCODE_API_KEY\\s*=\\s*)[^\\r\\n]+\",\n r\"(?m)^\\s*(OPENCODE_GO_API_KEY\\s*=\\s*)[^\\r\\n]+\",\n r\"(?m)^\\s*(ZHIPU_API_KEY\\s*=\\s*)[^\\r\\n]+\",\n r\"(?m)^\\s*(ZAI_API_KEY\\s*=\\s*)[^\\r\\n]+\",\n r\"(?m)^\\s*(302AI_API_KEY\\s*=\\s*)[^\\r\\n]+\",\n r\"(?m)^\\s*(BASETEN_API_KEY\\s*=\\s*)[^\\r\\n]+\",\n r\"(?m)^\\s*(CORTECS_API_KEY\\s*=\\s*)[^\\r\\n]+\",\n r\"(?m)^\\s*(DEEPSEEK_API_KEY\\s*=\\s*)[^\\r\\n]+\",\n r\"(?m)^\\s*(FIRMWARE_API_KEY\\s*=\\s*)[^\\r\\n]+\",\n r\"(?m)^\\s*(HF_TOKEN\\s*=\\s*)[^\\r\\n]+\",\n r\"(?m)^\\s*(MOONSHOT_API_KEY\\s*=\\s*)[^\\r\\n]+\",\n r\"(?m)^\\s*(NEBIUS_API_KEY\\s*=\\s*)[^\\r\\n]+\",\n r\"(?m)^\\s*(SCALEWAY_API_KEY\\s*=\\s*)[^\\r\\n]+\",\n r\"(?m)^\\s*(STACKIT_API_KEY\\s*=\\s*)[^\\r\\n]+\",\n r\"(?m)^\\s*(GROQ_API_KEY\\s*=\\s*)[^\\r\\n]+\",\n r\"(?m)^\\s*(MISTRAL_API_KEY\\s*=\\s*)[^\\r\\n]+\",\n r\"(?m)^\\s*(PERPLEXITY_API_KEY\\s*=\\s*)[^\\r\\n]+\",\n r\"(?m)^\\s*(TOGETHER_API_KEY\\s*=\\s*)[^\\r\\n]+\",\n r\"(?m)^\\s*(DEEPINFRA_API_KEY\\s*=\\s*)[^\\r\\n]+\",\n r\"(?m)^\\s*(XAI_API_KEY\\s*=\\s*)[^\\r\\n]+\",\n r\"(?m)^\\s*(LMSTUDIO_API_KEY\\s*=\\s*)[^\\r\\n]+\",\n r\"(?m)^\\s*(OLLAMA_API_KEY\\s*=\\s*)[^\\r\\n]+\",\n r\"(?m)^\\s*(CHUTES_API_KEY\\s*=\\s*)[^\\r\\n]+\",\n r\"(?m)^\\s*(CEREBRAS_API_KEY\\s*=\\s*)[^\\r\\n]+\",\n r\"(?m)^\\s*(OPENAI_COMPAT_API_KEY\\s*=\\s*)[^\\r\\n]+\",\n r\"(?m)^\\s*(ANTHROPIC_API_KEY\\s*=\\s*)[^\\r\\n]+\",\n r\"(?m)^\\s*(OPENAI_API_KEY\\s*=\\s*)[^\\r\\n]+\",\n r\"(?m)^\\s*(AZURE_OPENAI_API_KEY\\s*=\\s*)[^\\r\\n]+\",\n r\"(?m)^\\s*(CURSOR_API_KEY\\s*=\\s*)[^\\r\\n]+\",\n r\"(?m)^\\s*(GITHUB_TOKEN\\s*=\\s*)[^\\r\\n]+\",\n ])\n });\n\n let mut redacted = text.to_string();\n let mut redacted_keys: HashSet<String> = [\n \"OPENROUTER_API_KEY\",\n \"OPENCODE_API_KEY\",\n \"OPENCODE_GO_API_KEY\",\n \"ZHIPU_API_KEY\",\n \"ZAI_API_KEY\",\n \"302AI_API_KEY\",\n \"BASETEN_API_KEY\",\n \"CORTECS_API_KEY\",\n \"DEEPSEEK_API_KEY\",\n \"FIRMWARE_API_KEY\",\n \"HF_TOKEN\",\n \"MOONSHOT_API_KEY\",\n \"NEBIUS_API_KEY\",\n \"SCALEWAY_API_KEY\",\n \"STACKIT_API_KEY\",\n \"GROQ_API_KEY\",\n \"MISTRAL_API_KEY\",\n \"PERPLEXITY_API_KEY\",\n \"TOGETHER_API_KEY\",\n \"DEEPINFRA_API_KEY\",\n \"XAI_API_KEY\",\n \"LMSTUDIO_API_KEY\",\n \"OLLAMA_API_KEY\",\n \"CHUTES_API_KEY\",\n \"CEREBRAS_API_KEY\",\n \"OPENAI_COMPAT_API_KEY\",\n \"ANTHROPIC_API_KEY\",\n \"OPENAI_API_KEY\",\n \"AZURE_OPENAI_API_KEY\",\n \"CURSOR_API_KEY\",\n \"GITHUB_TOKEN\",\n ]\n .iter()\n .map(|k| (*k).to_string())\n .collect();\n\n for re in direct_patterns {\n redacted = re.replace_all(&redacted, \"[REDACTED_SECRET]\").into_owned();\n }\n\n for re in assignment_patterns {\n redacted = re\n .replace_all(&redacted, \"${1}[REDACTED_SECRET]\")\n .into_owned();\n }\n\n // Also redact custom API key variable names configured at runtime.\n for source in [\n \"JCODE_OPENROUTER_API_KEY_NAME\",\n \"JCODE_OPENAI_COMPAT_API_KEY_NAME\",\n ] {\n let Some(key_name) = std::env::var(source)\n .ok()\n .map(|v| v.trim().to_string())\n .filter(|v| !v.is_empty())\n else {\n continue;\n };\n\n if !key_name\n .chars()\n .all(|c| c.is_ascii_uppercase() || c.is_ascii_digit() || c == '_')\n {\n continue;\n }\n if !redacted_keys.insert(key_name.clone()) {\n continue;\n }\n\n let pattern = format!(r\"(?m)^\\s*({}\\s*=\\s*)[^\\r\\n]+\", regex::escape(&key_name));\n if let Ok(re) = Regex::new(&pattern) {\n redacted = re\n .replace_all(&redacted, \"${1}[REDACTED_SECRET]\")\n .into_owned();\n }\n }\n\n redacted\n}\n\n/// Tool definition for the API\n#[derive(Debug, Clone, Serialize)]\npub struct ToolDefinition {\n pub name: String,\n // Prompt-visible text sent to the model by provider adapters.\n // Approximate prompt cost: description.len() / 4. Use\n // ToolDefinition::description_token_estimate() when reviewing tool bloat.\n pub description: String,\n pub input_schema: serde_json::Value,\n}\n\nimpl ToolDefinition {\n /// Serialized size of the full tool definition payload sent to providers.\n pub fn prompt_chars(&self) -> usize {\n serde_json::json!({\n \"name\": self.name,\n \"description\": self.description,\n \"input_schema\": self.input_schema,\n })\n .to_string()\n .len()\n }\n\n /// Approximate prompt-token cost of this tool's top-level description.\n ///\n /// This uses jcode's standard chars/4 heuristic, matching other token\n /// budget estimates in the codebase.\n pub fn description_token_estimate(&self) -> usize {\n crate::util::estimate_tokens(&self.description)\n }\n\n /// Approximate prompt-token cost of the full tool definition payload.\n pub fn prompt_token_estimate(&self) -> usize {\n crate::util::estimate_tokens(\n &serde_json::json!({\n \"name\": self.name,\n \"description\": self.description,\n \"input_schema\": self.input_schema,\n })\n .to_string(),\n )\n }\n\n pub fn aggregate_prompt_chars(defs: &[ToolDefinition]) -> usize {\n defs.iter().map(Self::prompt_chars).sum()\n }\n\n pub fn aggregate_prompt_token_estimate(defs: &[ToolDefinition]) -> usize {\n defs.iter().map(Self::prompt_token_estimate).sum()\n }\n}\n\n/// A tool call from the model\n#[derive(Debug, Clone, Default, PartialEq, Eq, serde::Serialize, serde::Deserialize)]\npub struct ToolCall {\n #[serde(default)]\n pub id: String,\n #[serde(default)]\n pub name: String,\n #[serde(default)]\n pub input: serde_json::Value,\n #[serde(default, skip_serializing_if = \"Option::is_none\")]\n pub intent: Option<String>,\n}\n\nimpl ToolCall {\n pub fn intent_from_input(input: &serde_json::Value) -> Option<String> {\n input\n .get(\"intent\")\n .and_then(|value| value.as_str())\n .map(str::trim)\n .filter(|intent| !intent.is_empty())\n .map(ToString::to_string)\n }\n\n pub fn refresh_intent_from_input(&mut self) {\n self.intent = Self::intent_from_input(&self.input);\n }\n}\n\npub const GENERATED_IMAGE_TOOL_NAME: &str = \"image_generation\";\n\npub fn generated_image_tool_input(\n path: &str,\n metadata_path: Option<&str>,\n output_format: &str,\n revised_prompt: Option<&str>,\n) -> serde_json::Value {\n serde_json::json!({\n \"path\": path,\n \"metadata_path\": metadata_path,\n \"output_format\": output_format,\n \"revised_prompt\": revised_prompt,\n })\n}\n\npub fn generated_image_summary(\n path: &str,\n metadata_path: Option<&str>,\n output_format: &str,\n revised_prompt: Option<&str>,\n) -> String {\n let mut summary = format!(\"Generated image ({}) saved to `{}`.\", output_format, path);\n if let Some(metadata_path) = metadata_path {\n summary.push_str(&format!(\"\\nMetadata saved to `{}`.\", metadata_path));\n }\n if let Some(revised_prompt) = revised_prompt.filter(|prompt| !prompt.trim().is_empty()) {\n summary.push_str(\"\\n\\nRevised prompt:\\n\");\n summary.push_str(revised_prompt.trim());\n }\n summary\n}\n\n/// Connection phase for status bar transparency\n#[derive(Debug, Clone, PartialEq)]\npub enum ConnectionPhase {\n /// Refreshing OAuth token\n Authenticating,\n /// TCP + TLS connection to API\n Connecting,\n /// HTTP request sent, waiting for first response byte\n WaitingForResponse,\n /// First byte received, stream is active\n Streaming,\n /// Retrying after a transient error\n Retrying { attempt: u32, max: u32 },\n}\n\nimpl std::fmt::Display for ConnectionPhase {\n fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {\n match self {\n ConnectionPhase::Authenticating => write!(f, \"authenticating\"),\n ConnectionPhase::Connecting => write!(f, \"connecting\"),\n ConnectionPhase::WaitingForResponse => write!(f, \"waiting for response\"),\n ConnectionPhase::Streaming => write!(f, \"streaming\"),\n ConnectionPhase::Retrying { attempt, max } => {\n write!(f, \"retrying ({}/{})\", attempt, max)\n }\n }\n }\n}\n\n/// Streaming event from provider\n#[derive(Debug, Clone)]\npub enum StreamEvent {\n /// Text content delta\n TextDelta(String),\n /// Tool use started\n ToolUseStart { id: String, name: String },\n /// Tool input delta (JSON fragment)\n ToolInputDelta(String),\n /// Tool use complete\n ToolUseEnd,\n /// Tool result from provider (provider already executed the tool)\n ToolResult {\n tool_use_id: String,\n content: String,\n is_error: bool,\n },\n /// Image generated by a provider-native image generation tool.\n GeneratedImage {\n id: String,\n path: String,\n metadata_path: Option<String>,\n output_format: String,\n revised_prompt: Option<String>,\n },\n /// Extended thinking started\n ThinkingStart,\n /// Extended thinking delta (reasoning content)\n ThinkingDelta(String),\n /// Extended thinking ended\n ThinkingEnd,\n /// Extended thinking completed with duration\n ThinkingDone { duration_secs: f64 },\n /// Message complete (may have stop reason)\n MessageEnd { stop_reason: Option<String> },\n /// Token usage update\n TokenUsage {\n input_tokens: Option<u64>,\n output_tokens: Option<u64>,\n cache_read_input_tokens: Option<u64>,\n cache_creation_input_tokens: Option<u64>,\n },\n /// Active transport/connection type for this stream\n ConnectionType { connection: String },\n /// Connection phase update (for status bar transparency)\n ConnectionPhase { phase: ConnectionPhase },\n /// Provider-supplied human-readable transport detail for the status line\n StatusDetail { detail: String },\n /// Error occurred\n Error {\n message: String,\n /// Seconds until rate limit resets (if this is a rate limit error)\n retry_after_secs: Option<u64>,\n },\n /// Provider session ID (for conversation resume)\n SessionId(String),\n /// Compaction occurred (context was summarized)\n Compaction {\n trigger: String,\n pre_tokens: Option<u64>,\n /// Provider-native compaction artifact, if one was emitted.\n openai_encrypted_content: Option<String>,\n },\n /// Upstream provider info (e.g., which provider OpenRouter routed to)\n UpstreamProvider { provider: String },\n /// Native tool call from a provider bridge that needs execution by jcode\n NativeToolCall {\n request_id: String,\n tool_name: String,\n input: serde_json::Value,\n },\n}\n\n#[cfg(test)]\nmod tests;\n"}, "589539757ac4819a0f7e88ebce70d4e897e2db2f": {"t": 1785757218.71688, "n": 22569, "h": "66e591cd2aeb6721", "c": "use crate::bus::{BackgroundTaskCompleted, BackgroundTaskProgressEvent, BackgroundTaskStatus};\nuse chrono::Utc;\nuse regex::Regex;\nuse serde::{Deserialize, Serialize};\nuse std::collections::HashSet;\nuse std::sync::OnceLock;\n\nmod notifications;\n\npub use notifications::{\n InputShellResult, ParsedBackgroundTaskNotification, ParsedBackgroundTaskProgressNotification,\n background_task_display_label, background_task_status_notice,\n format_background_task_notification_markdown, format_background_task_progress_markdown,\n format_input_shell_result_markdown, input_shell_status_notice,\n parse_background_task_notification_markdown,\n parse_background_task_progress_notification_markdown,\n};\n\nfn compile_static_regex(pattern: &str) -> Option<Regex> {\n match Regex::new(pattern) {\n Ok(regex) => Some(regex),\n Err(err) => {\n eprintln!(\"jcode: failed to compile static regex: {err}\");\n None\n }\n }\n}\n\nfn compile_static_regexes(patterns: &[&str]) -> Vec<Regex> {\n patterns\n .iter()\n .filter_map(|pattern| compile_static_regex(pattern))\n .collect()\n}\n\n/// Role in conversation\n#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]\n#[serde(rename_all = \"lowercase\")]\npub enum Role {\n User,\n Assistant,\n}\n\n/// Plain-text tool output placeholder when execution was interrupted.\npub const TOOL_OUTPUT_MISSING_TEXT: &str =\n \"Tool output missing (session interrupted before tool execution completed)\";\n\n/// A message in the conversation\n#[derive(Debug, Clone, Serialize, Deserialize)]\npub struct Message {\n pub role: Role,\n pub content: Vec<ContentBlock>,\n #[serde(default, skip_serializing_if = \"Option::is_none\")]\n pub timestamp: Option<chrono::DateTime<Utc>>,\n #[serde(default, skip_serializing_if = \"Option::is_none\")]\n pub tool_duration_ms: Option<u64>,\n}\n\n/// Cache control metadata for prompt caching\n#[derive(Debug, Clone, Serialize, Deserialize)]\npub struct CacheControl {\n #[serde(rename = \"type\")]\n pub kind: String,\n #[serde(skip_serializing_if = \"Option::is_none\")]\n pub ttl: Option<String>,\n}\n\nimpl CacheControl {\n pub fn ephemeral(ttl: Option<String>) -> Self {\n Self {\n kind: \"ephemeral\".to_string(),\n ttl,\n }\n }\n}\n\n/// Content block within a message\n#[derive(Debug, Clone, Serialize, Deserialize)]\n#[serde(tag = \"type\", rename_all = \"snake_case\")]\npub enum ContentBlock {\n Text {\n text: String,\n #[serde(skip_serializing_if = \"Option::is_none\")]\n cache_control: Option<CacheControl>,\n },\n /// Hidden reasoning content used for providers that require it (not displayed)\n Reasoning {\n text: String,\n },\n ToolUse {\n id: String,\n name: String,\n input: serde_json::Value,\n },\n ToolResult {\n tool_use_id: String,\n content: String,\n #[serde(skip_serializing_if = \"Option::is_none\")]\n is_error: Option<bool>,\n },\n Image {\n media_type: String,\n data: String,\n },\n /// Hidden OpenAI Responses compaction item used to preserve native\n /// compaction state across turns/saves when jcode explicitly triggers it.\n OpenAICompaction {\n encrypted_content: String,\n },\n}\n\nimpl Message {\n pub fn user(text: &str) -> Self {\n Self {\n role: Role::User,\n content: vec![ContentBlock::Text {\n text: text.to_string(),\n cache_control: None,\n }],\n timestamp: Some(Utc::now()),\n tool_duration_ms: None,\n }\n }\n\n pub fn user_with_images(text: &str, images: Vec<(String, String)>) -> Self {\n let mut content: Vec<ContentBlock> = images\n .into_iter()\n .map(|(media_type, data)| ContentBlock::Image { media_type, data })\n .collect();\n content.push(ContentBlock::Text {\n text: text.to_string(),\n cache_control: None,\n });\n Self {\n role: Role::User,\n content,\n timestamp: Some(Utc::now()),\n tool_duration_ms: None,\n }\n }\n\n pub fn assistant_text(text: &str) -> Self {\n Self {\n role: Role::Assistant,\n content: vec![ContentBlock::Text {\n text: text.to_string(),\n cache_control: None,\n }],\n timestamp: Some(Utc::now()),\n tool_duration_ms: None,\n }\n }\n\n pub fn tool_result(tool_use_id: &str, content: &str, is_error: bool) -> Self {\n Self::tool_result_with_duration(tool_use_id, content, is_error, None)\n }\n\n pub fn tool_result_with_duration(\n tool_use_id: &str,\n content: &str,\n is_error: bool,\n tool_duration_ms: Option<u64>,\n ) -> Self {\n Self {\n role: Role::User,\n content: vec![ContentBlock::ToolResult {\n tool_use_id: tool_use_id.to_string(),\n content: content.to_string(),\n is_error: if is_error { Some(true) } else { None },\n }],\n timestamp: Some(Utc::now()),\n tool_duration_ms,\n }\n }\n\n /// Format a timestamp deterministically in UTC for injection into model-visible content.\n pub fn format_timestamp(ts: &chrono::DateTime<Utc>) -> String {\n ts.to_rfc3339_opts(chrono::SecondsFormat::Millis, true)\n }\n\n pub fn format_duration(duration_ms: u64) -> String {\n match duration_ms {\n 0..=999 => format!(\"{}ms\", duration_ms),\n 1_000..=9_999 => format!(\"{:.1}s\", duration_ms as f64 / 1000.0),\n 10_000..=59_999 => format!(\"{}s\", duration_ms / 1000),\n _ => {\n let total_seconds = duration_ms / 1000;\n let minutes = total_seconds / 60;\n let seconds = total_seconds % 60;\n if seconds == 0 {\n format!(\"{}m\", minutes)\n } else {\n format!(\"{}m {}s\", minutes, seconds)\n }\n }\n }\n }\n\n pub fn is_internal_system_reminder(&self) -> bool {\n self.content\n .iter()\n .find_map(|block| match block {\n ContentBlock::Text { text, .. } => Some(text.trim_start()),\n _ => None,\n })\n .is_some_and(|text| text.starts_with(\"<system-reminder>\"))\n }\n\n fn should_skip_timestamp_injection(&self) -> bool {\n self.is_internal_system_reminder()\n }\n\n fn tool_result_tag(&self, ts: &chrono::DateTime<Utc>) -> String {\n match self.tool_duration_ms {\n Some(duration_ms) => {\n let duration_ms_i64 = i64::try_from(duration_ms).unwrap_or(i64::MAX);\n let start_ts = ts\n .checked_sub_signed(chrono::Duration::milliseconds(duration_ms_i64))\n .unwrap_or(*ts);\n format!(\n \"[tool timing: start={} finish={} duration={}]\",\n Self::format_timestamp(&start_ts),\n Self::format_timestamp(ts),\n Self::format_duration(duration_ms)\n )\n }\n None => format!(\"[{}]\", Self::format_timestamp(ts)),\n }\n }\n\n /// Return a copy of messages with timestamps injected into user-role text content.\n /// Tool results get a stable UTC timing header prepended to content.\n /// User text messages get a stable UTC timestamp prepended to the first text block.\n pub fn with_timestamps(messages: &[Message]) -> Vec<Message> {\n messages\n .iter()\n .map(|msg| {\n let Some(ts) = msg.timestamp else {\n return msg.clone();\n };\n if msg.role != Role::User || msg.should_skip_timestamp_injection() {\n return msg.clone();\n }\n let text_tag = format!(\"[{}]\", Self::format_timestamp(&ts));\n let tool_result_tag = msg.tool_result_tag(&ts);\n let mut msg = msg.clone();\n let mut tagged = false;\n for block in &mut msg.content {\n match block {\n ContentBlock::Text { text, .. } if !tagged => {\n *text = format!(\"{} {}\", text_tag, text);\n tagged = true;\n }\n ContentBlock::ToolResult { content, .. } if !tagged => {\n *content = format!(\"{} {}\", tool_result_tag, content);\n tagged = true;\n }\n _ => {}\n }\n }\n msg\n })\n .collect()\n }\n}\n\nconst STABLE_HASH_SEED: u64 = 0xcbf29ce484222325;\nconst STABLE_HASH_PRIME: u64 = 0x100000001b3;\n\nfn stable_hash_bytes(bytes: &[u8]) -> u64 {\n let mut hash = STABLE_HASH_SEED;\n for byte in bytes {\n hash ^= *byte as u64;\n hash = hash.wrapping_mul(STABLE_HASH_PRIME);\n }\n hash\n}\n\npub(crate) fn extend_stable_hash(acc: u64, next: u64) -> u64 {\n stable_hash_bytes(&[acc.to_le_bytes().as_slice(), next.to_le_bytes().as_slice()].concat())\n}\n\npub(crate) fn stable_message_hash(message: &Message) -> u64 {\n match serde_json::to_vec(message) {\n Ok(bytes) => stable_hash_bytes(&bytes),\n Err(_) => stable_hash_bytes(format!(\"{:?}\", message).as_bytes()),\n }\n}\n\npub fn ends_with_fresh_user_turn(messages: &[Message]) -> bool {\n for msg in messages.iter().rev() {\n if msg.role != Role::User {\n return false;\n }\n\n if msg\n .content\n .iter()\n .any(|block| matches!(block, ContentBlock::ToolResult { .. }))\n {\n return false;\n }\n\n if msg.content.is_empty() {\n return false;\n }\n\n let mut saw_user_text = false;\n for block in &msg.content {\n match block {\n ContentBlock::Text { text, .. } => {\n let trimmed = text.trim();\n if !trimmed.is_empty() && !trimmed.starts_with(\"<system-reminder>\") {\n saw_user_text = true;\n }\n }\n _ => return false,\n }\n }\n\n if saw_user_text {\n return true;\n }\n\n if msg.is_internal_system_reminder() {\n continue;\n }\n\n return false;\n }\n\n false\n}\n\n/// Sanitize a tool ID so it matches the pattern `^[a-zA-Z0-9_-]+$`.\n///\n/// Different providers generate tool IDs in different formats. When switching\n/// from one provider to another mid-conversation, the historical tool IDs may\n/// contain characters that the new provider rejects (e.g., dots in Copilot IDs\n/// sent to Anthropic). This function replaces any invalid characters with\n/// underscores.\npub fn sanitize_tool_id(id: &str) -> String {\n if id.is_empty() {\n return \"unknown\".to_string();\n }\n let sanitized: String = id\n .chars()\n .map(|c| {\n if c.is_ascii_alphanumeric() || c == '_' || c == '-' {\n c\n } else {\n '_'\n }\n })\n .collect();\n if sanitized.is_empty() {\n \"unknown\".to_string()\n } else {\n sanitized\n }\n}\n\n/// Redact likely secrets from persisted tool output.\n///\n/// This is a best-effort safeguard for local session history files. It targets\n/// high-confidence token/key patterns and common `KEY=VALUE` assignments used by\n/// auth flows.\npub fn redact_secrets(text: &str) -> String {\n // Fast path to avoid regex work for most tool outputs.\n let lower = text.to_ascii_lowercase();\n\n if !text.contains(\"sk-\")\n && !text.contains(\"ghp_\")\n && !text.contains(\"github_pat_\")\n && !text.contains(\"AIza\")\n && !text.contains(\"ya29.\")\n && !text.contains(\"xox\")\n && !lower.contains(\"api_key\")\n && !lower.contains(\"token\")\n {\n return text.to_string();\n }\n\n static DIRECT_PATTERNS: OnceLock<Vec<Regex>> = OnceLock::new();\n static ASSIGNMENT_PATTERNS: OnceLock<Vec<Regex>> = OnceLock::new();\n\n let direct_patterns = DIRECT_PATTERNS.get_or_init(|| {\n compile_static_regexes(&[\n r\"sk-ant-(?:oat|ort)01-[A-Za-z0-9_-]{20,}\",\n r\"sk-or-v1-[A-Za-z0-9_-]{20,}\",\n r\"ghp_[A-Za-z0-9]{20,}\",\n r\"github_pat_[A-Za-z0-9_]{20,}\",\n r\"ya29\\.[A-Za-z0-9._-]{20,}\",\n r\"AIza[0-9A-Za-z_-]{20,}\",\n r\"xox[baprs]-[A-Za-z0-9-]{10,}\",\n ])\n });\n\n let assignment_patterns = ASSIGNMENT_PATTERNS.get_or_init(|| {\n compile_static_regexes(&[\n r\"(?m)^\\s*(OPENROUTER_API_KEY\\s*=\\s*)[^\\r\\n]+\",\n r\"(?m)^\\s*(OPENCODE_API_KEY\\s*=\\s*)[^\\r\\n]+\",\n r\"(?m)^\\s*(OPENCODE_GO_API_KEY\\s*=\\s*)[^\\r\\n]+\",\n r\"(?m)^\\s*(ZHIPU_API_KEY\\s*=\\s*)[^\\r\\n]+\",\n r\"(?m)^\\s*(ZAI_API_KEY\\s*=\\s*)[^\\r\\n]+\",\n r\"(?m)^\\s*(302AI_API_KEY\\s*=\\s*)[^\\r\\n]+\",\n r\"(?m)^\\s*(BASETEN_API_KEY\\s*=\\s*)[^\\r\\n]+\",\n r\"(?m)^\\s*(CORTECS_API_KEY\\s*=\\s*)[^\\r\\n]+\",\n r\"(?m)^\\s*(DEEPSEEK_API_KEY\\s*=\\s*)[^\\r\\n]+\",\n r\"(?m)^\\s*(FIRMWARE_API_KEY\\s*=\\s*)[^\\r\\n]+\",\n r\"(?m)^\\s*(HF_TOKEN\\s*=\\s*)[^\\r\\n]+\",\n r\"(?m)^\\s*(MOONSHOT_API_KEY\\s*=\\s*)[^\\r\\n]+\",\n r\"(?m)^\\s*(NEBIUS_API_KEY\\s*=\\s*)[^\\r\\n]+\",\n r\"(?m)^\\s*(SCALEWAY_API_KEY\\s*=\\s*)[^\\r\\n]+\",\n r\"(?m)^\\s*(STACKIT_API_KEY\\s*=\\s*)[^\\r\\n]+\",\n r\"(?m)^\\s*(GROQ_API_KEY\\s*=\\s*)[^\\r\\n]+\",\n r\"(?m)^\\s*(MISTRAL_API_KEY\\s*=\\s*)[^\\r\\n]+\",\n r\"(?m)^\\s*(PERPLEXITY_API_KEY\\s*=\\s*)[^\\r\\n]+\",\n r\"(?m)^\\s*(TOGETHER_API_KEY\\s*=\\s*)[^\\r\\n]+\",\n r\"(?m)^\\s*(DEEPINFRA_API_KEY\\s*=\\s*)[^\\r\\n]+\",\n r\"(?m)^\\s*(XAI_API_KEY\\s*=\\s*)[^\\r\\n]+\",\n r\"(?m)^\\s*(LMSTUDIO_API_KEY\\s*=\\s*)[^\\r\\n]+\",\n r\"(?m)^\\s*(OLLAMA_API_KEY\\s*=\\s*)[^\\r\\n]+\",\n r\"(?m)^\\s*(CHUTES_API_KEY\\s*=\\s*)[^\\r\\n]+\",\n r\"(?m)^\\s*(CEREBRAS_API_KEY\\s*=\\s*)[^\\r\\n]+\",\n r\"(?m)^\\s*(OPENAI_COMPAT_API_KEY\\s*=\\s*)[^\\r\\n]+\",\n r\"(?m)^\\s*(ANTHROPIC_API_KEY\\s*=\\s*)[^\\r\\n]+\",\n r\"(?m)^\\s*(OPENAI_API_KEY\\s*=\\s*)[^\\r\\n]+\",\n r\"(?m)^\\s*(AZURE_OPENAI_API_KEY\\s*=\\s*)[^\\r\\n]+\",\n r\"(?m)^\\s*(CURSOR_API_KEY\\s*=\\s*)[^\\r\\n]+\",\n r\"(?m)^\\s*(GITHUB_TOKEN\\s*=\\s*)[^\\r\\n]+\",\n ])\n });\n\n let mut redacted = text.to_string();\n let mut redacted_keys: HashSet<String> = [\n \"OPENROUTER_API_KEY\",\n \"OPENCODE_API_KEY\",\n \"OPENCODE_GO_API_KEY\",\n \"ZHIPU_API_KEY\",\n \"ZAI_API_KEY\",\n \"302AI_API_KEY\",\n \"BASETEN_API_KEY\",\n \"CORTECS_API_KEY\",\n \"DEEPSEEK_API_KEY\",\n \"FIRMWARE_API_KEY\",\n \"HF_TOKEN\",\n \"MOONSHOT_API_KEY\",\n \"NEBIUS_API_KEY\",\n \"SCALEWAY_API_KEY\",\n \"STACKIT_API_KEY\",\n \"GROQ_API_KEY\",\n \"MISTRAL_API_KEY\",\n \"PERPLEXITY_API_KEY\",\n \"TOGETHER_API_KEY\",\n \"DEEPINFRA_API_KEY\",\n \"XAI_API_KEY\",\n \"LMSTUDIO_API_KEY\",\n \"OLLAMA_API_KEY\",\n \"CHUTES_API_KEY\",\n \"CEREBRAS_API_KEY\",\n \"OPENAI_COMPAT_API_KEY\",\n \"ANTHROPIC_API_KEY\",\n \"OPENAI_API_KEY\",\n \"AZURE_OPENAI_API_KEY\",\n \"CURSOR_API_KEY\",\n \"GITHUB_TOKEN\",\n ]\n .iter()\n .map(|k| (*k).to_string())\n .collect();\n\n for re in direct_patterns {\n redacted = re.replace_all(&redacted, \"[REDACTED_SECRET]\").into_owned();\n }\n\n for re in assignment_patterns {\n redacted = re\n .replace_all(&redacted, \"${1}[REDACTED_SECRET]\")\n .into_owned();\n }\n\n // Also redact custom API key variable names configured at runtime.\n for source in [\n \"JCODE_OPENROUTER_API_KEY_NAME\",\n \"JCODE_OPENAI_COMPAT_API_KEY_NAME\",\n ] {\n let Some(key_name) = std::env::var(source)\n .ok()\n .map(|v| v.trim().to_string())\n .filter(|v| !v.is_empty())\n else {\n continue;\n };\n\n if !key_name\n .chars()\n .all(|c| c.is_ascii_uppercase() || c.is_ascii_digit() || c == '_')\n {\n continue;\n }\n if !redacted_keys.insert(key_name.clone()) {\n continue;\n }\n\n let pattern = format!(r\"(?m)^\\s*({}\\s*=\\s*)[^\\r\\n]+\", regex::escape(&key_name));\n if let Ok(re) = Regex::new(&pattern) {\n redacted = re\n .replace_all(&redacted, \"${1}[REDACTED_SECRET]\")\n .into_owned();\n }\n }\n\n redacted\n}\n\n/// Tool definition for the API\n#[derive(Debug, Clone, Serialize)]\npub struct ToolDefinition {\n pub name: String,\n // Prompt-visible text sent to the model by provider adapters.\n // Approximate prompt cost: description.len() / 4. Use\n // ToolDefinition::description_token_estimate() when reviewing tool bloat.\n pub description: String,\n pub input_schema: serde_json::Value,\n}\n\nimpl ToolDefinition {\n /// Serialized size of the full tool definition payload sent to providers.\n pub fn prompt_chars(&self) -> usize {\n serde_json::json!({\n \"name\": self.name,\n \"description\": self.description,\n \"input_schema\": self.input_schema,\n })\n .to_string()\n .len()\n }\n\n /// Approximate prompt-token cost of this tool's top-level description.\n ///\n /// This uses jcode's standard chars/4 heuristic, matching other token\n /// budget estimates in the codebase.\n pub fn description_token_estimate(&self) -> usize {\n crate::util::estimate_tokens(&self.description)\n }\n\n /// Approximate prompt-token cost of the full tool definition payload.\n pub fn prompt_token_estimate(&self) -> usize {\n crate::util::estimate_tokens(\n &serde_json::json!({\n \"name\": self.name,\n \"description\": self.description,\n \"input_schema\": self.input_schema,\n })\n .to_string(),\n )\n }\n\n pub fn aggregate_prompt_chars(defs: &[ToolDefinition]) -> usize {\n defs.iter().map(Self::prompt_chars).sum()\n }\n\n pub fn aggregate_prompt_token_estimate(defs: &[ToolDefinition]) -> usize {\n defs.iter().map(Self::prompt_token_estimate).sum()\n }\n}\n\n/// A tool call from the model\n#[derive(Debug, Clone, Default, PartialEq, Eq, serde::Serialize, serde::Deserialize)]\npub struct ToolCall {\n #[serde(default)]\n pub id: String,\n #[serde(default)]\n pub name: String,\n #[serde(default)]\n pub input: serde_json::Value,\n #[serde(default, skip_serializing_if = \"Option::is_none\")]\n pub intent: Option<String>,\n}\n\nimpl ToolCall {\n pub fn intent_from_input(input: &serde_json::Value) -> Option<String> {\n input\n .get(\"intent\")\n .and_then(|value| value.as_str())\n .map(str::trim)\n .filter(|intent| !intent.is_empty())\n .map(ToString::to_string)\n }\n\n pub fn refresh_intent_from_input(&mut self) {\n self.intent = Self::intent_from_input(&self.input);\n }\n}\n\n/// Connection phase for status bar transparency\n#[derive(Debug, Clone, PartialEq)]\npub enum ConnectionPhase {\n /// Refreshing OAuth token\n Authenticating,\n /// TCP + TLS connection to API\n Connecting,\n /// HTTP request sent, waiting for first response byte\n WaitingForResponse,\n /// First byte received, stream is active\n Streaming,\n /// Retrying after a transient error\n Retrying { attempt: u32, max: u32 },\n}\n\nimpl std::fmt::Display for ConnectionPhase {\n fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {\n match self {\n ConnectionPhase::Authenticating => write!(f, \"authenticating\"),\n ConnectionPhase::Connecting => write!(f, \"connecting\"),\n ConnectionPhase::WaitingForResponse => write!(f, \"waiting for response\"),\n ConnectionPhase::Streaming => write!(f, \"streaming\"),\n ConnectionPhase::Retrying { attempt, max } => {\n write!(f, \"retrying ({}/{})\", attempt, max)\n }\n }\n }\n}\n\n/// Streaming event from provider\n#[derive(Debug, Clone)]\npub enum StreamEvent {\n /// Text content delta\n TextDelta(String),\n /// Tool use started\n ToolUseStart { id: String, name: String },\n /// Tool input delta (JSON fragment)\n ToolInputDelta(String),\n /// Tool use complete\n ToolUseEnd,\n /// Tool result from provider (provider already executed the tool)\n ToolResult {\n tool_use_id: String,\n content: String,\n is_error: bool,\n },\n /// Extended thinking started\n ThinkingStart,\n /// Extended thinking delta (reasoning content)\n ThinkingDelta(String),\n /// Extended thinking ended\n ThinkingEnd,\n /// Extended thinking completed with duration\n ThinkingDone { duration_secs: f64 },\n /// Message complete (may have stop reason)\n MessageEnd { stop_reason: Option<String> },\n /// Token usage update\n TokenUsage {\n input_tokens: Option<u64>,\n output_tokens: Option<u64>,\n cache_read_input_tokens: Option<u64>,\n cache_creation_input_tokens: Option<u64>,\n },\n /// Active transport/connection type for this stream\n ConnectionType { connection: String },\n /// Connection phase update (for status bar transparency)\n ConnectionPhase { phase: ConnectionPhase },\n /// Provider-supplied human-readable transport detail for the status line\n StatusDetail { detail: String },\n /// Error occurred\n Error {\n message: String,\n /// Seconds until rate limit resets (if this is a rate limit error)\n retry_after_secs: Option<u64>,\n },\n /// Provider session ID (for conversation resume)\n SessionId(String),\n /// Compaction occurred (context was summarized)\n Compaction {\n trigger: String,\n pre_tokens: Option<u64>,\n /// Provider-native compaction artifact, if one was emitted.\n openai_encrypted_content: Option<String>,\n },\n /// Upstream provider info (e.g., which provider OpenRouter routed to)\n UpstreamProvider { provider: String },\n /// Native tool call from a provider bridge that needs execution by jcode\n NativeToolCall {\n request_id: String,\n tool_name: String,\n input: serde_json::Value,\n },\n}\n\n#[cfg(test)]\nmod tests;\n"}, "e120b048d740785f78483a648ddf5e3b083dd081": {"t": 1785757221.4628694, "n": 43523, "h": "a5e812582b1d0c8a", "c": "use crate::bus::{BackgroundTaskCompleted, BackgroundTaskProgressEvent, BackgroundTaskStatus};\nuse chrono::Utc;\nuse regex::Regex;\nuse serde::{Deserialize, Serialize};\nuse std::collections::HashSet;\nuse std::sync::OnceLock;\n\nmod notifications;\n\npub use notifications::{\n InputShellResult, ParsedBackgroundTaskNotification, ParsedBackgroundTaskProgressNotification,\n background_task_display_label, background_task_status_notice,\n format_background_task_notification_markdown, format_background_task_progress_markdown,\n format_input_shell_result_markdown, input_shell_status_notice,\n parse_background_task_notification_markdown,\n parse_background_task_progress_notification_markdown,\n};\n\nfn compile_static_regex(pattern: &str) -> Option<Regex> {\n match Regex::new(pattern) {\n Ok(regex) => Some(regex),\n Err(err) => {\n eprintln!(\"jcode: failed to compile static regex: {err}\");\n None\n }\n }\n}\n\nfn compile_static_regexes(patterns: &[&str]) -> Vec<Regex> {\n patterns\n .iter()\n .filter_map(|pattern| compile_static_regex(pattern))\n .collect()\n}\n\n/// Role in conversation\n#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]\n#[serde(rename_all = \"lowercase\")]\npub enum Role {\n User,\n Assistant,\n}\n\n/// Plain-text tool output placeholder when execution was interrupted.\npub const TOOL_OUTPUT_MISSING_TEXT: &str =\n \"Tool output missing (session interrupted before tool execution completed)\";\n\n/// A message in the conversation\n#[derive(Debug, Clone, Serialize, Deserialize)]\npub struct Message {\n pub role: Role,\n pub content: Vec<ContentBlock>,\n #[serde(default, skip_serializing_if = \"Option::is_none\")]\n pub timestamp: Option<chrono::DateTime<Utc>>,\n #[serde(default, skip_serializing_if = \"Option::is_none\")]\n pub tool_duration_ms: Option<u64>,\n}\n\n/// Cache control metadata for prompt caching\n#[derive(Debug, Clone, Serialize, Deserialize)]\npub struct CacheControl {\n #[serde(rename = \"type\")]\n pub kind: String,\n #[serde(skip_serializing_if = \"Option::is_none\")]\n pub ttl: Option<String>,\n}\n\nimpl CacheControl {\n pub fn ephemeral(ttl: Option<String>) -> Self {\n Self {\n kind: \"ephemeral\".to_string(),\n ttl,\n }\n }\n}\n\n/// Content block within a message\n#[derive(Debug, Clone, Serialize, Deserialize)]\n#[serde(tag = \"type\", rename_all = \"snake_case\")]\npub enum ContentBlock {\n Text {\n text: String,\n #[serde(skip_serializing_if = \"Option::is_none\")]\n cache_control: Option<CacheControl>,\n },\n /// Hidden reasoning content used for providers that require it (not displayed)\n Reasoning {\n text: String,\n },\n ToolUse {\n id: String,\n name: String,\n input: serde_json::Value,\n },\n ToolResult {\n tool_use_id: String,\n content: String,\n #[serde(skip_serializing_if = \"Option::is_none\")]\n is_error: Option<bool>,\n },\n Image {\n media_type: String,\n data: String,\n },\n /// Hidden OpenAI Responses compaction item used to preserve native\n /// compaction state across turns/saves when jcode explicitly triggers it.\n OpenAICompaction {\n encrypted_content: String,\n },\n}\n\nimpl Message {\n pub fn user(text: &str) -> Self {\n Self {\n role: Role::User,\n content: vec![ContentBlock::Text {\n text: text.to_string(),\n cache_control: None,\n }],\n timestamp: Some(Utc::now()),\n tool_duration_ms: None,\n }\n }\n\n pub fn user_with_images(text: &str, images: Vec<(String, String)>) -> Self {\n let mut content: Vec<ContentBlock> = images\n .into_iter()\n .map(|(media_type, data)| ContentBlock::Image { media_type, data })\n .collect();\n content.push(ContentBlock::Text {\n text: text.to_string(),\n cache_control: None,\n });\n Self {\n role: Role::User,\n content,\n timestamp: Some(Utc::now()),\n tool_duration_ms: None,\n }\n }\n\n pub fn assistant_text(text: &str) -> Self {\n Self {\n role: Role::Assistant,\n content: vec![ContentBlock::Text {\n text: text.to_string(),\n cache_control: None,\n }],\n timestamp: Some(Utc::now()),\n tool_duration_ms: None,\n }\n }\n\n pub fn tool_result(tool_use_id: &str, content: &str, is_error: bool) -> Self {\n Self::tool_result_with_duration(tool_use_id, content, is_error, None)\n }\n\n pub fn tool_result_with_duration(\n tool_use_id: &str,\n content: &str,\n is_error: bool,\n tool_duration_ms: Option<u64>,\n ) -> Self {\n Self {\n role: Role::User,\n content: vec![ContentBlock::ToolResult {\n tool_use_id: tool_use_id.to_string(),\n content: content.to_string(),\n is_error: if is_error { Some(true) } else { None },\n }],\n timestamp: Some(Utc::now()),\n tool_duration_ms,\n }\n }\n\n /// Format a timestamp deterministically in UTC for injection into model-visible content.\n pub fn format_timestamp(ts: &chrono::DateTime<Utc>) -> String {\n ts.to_rfc3339_opts(chrono::SecondsFormat::Millis, true)\n }\n\n pub fn format_duration(duration_ms: u64) -> String {\n match duration_ms {\n 0..=999 => format!(\"{}ms\", duration_ms),\n 1_000..=9_999 => format!(\"{:.1}s\", duration_ms as f64 / 1000.0),\n 10_000..=59_999 => format!(\"{}s\", duration_ms / 1000),\n _ => {\n let total_seconds = duration_ms / 1000;\n let minutes = total_seconds / 60;\n let seconds = total_seconds % 60;\n if seconds == 0 {\n format!(\"{}m\", minutes)\n } else {\n format!(\"{}m {}s\", minutes, seconds)\n }\n }\n }\n }\n\n pub fn is_internal_system_reminder(&self) -> bool {\n self.content\n .iter()\n .find_map(|block| match block {\n ContentBlock::Text { text, .. } => Some(text.trim_start()),\n _ => None,\n })\n .is_some_and(|text| text.starts_with(\"<system-reminder>\"))\n }\n\n fn should_skip_timestamp_injection(&self) -> bool {\n self.is_internal_system_reminder()\n }\n\n fn tool_result_tag(&self, ts: &chrono::DateTime<Utc>) -> String {\n match self.tool_duration_ms {\n Some(duration_ms) => {\n let duration_ms_i64 = i64::try_from(duration_ms).unwrap_or(i64::MAX);\n let start_ts = ts\n .checked_sub_signed(chrono::Duration::milliseconds(duration_ms_i64))\n .unwrap_or(*ts);\n format!(\n \"[tool timing: start={} finish={} duration={}]\",\n Self::format_timestamp(&start_ts),\n Self::format_timestamp(ts),\n Self::format_duration(duration_ms)\n )\n }\n None => format!(\"[{}]\", Self::format_timestamp(ts)),\n }\n }\n\n /// Return a copy of messages with timestamps injected into user-role text content.\n /// Tool results get a stable UTC timing header prepended to content.\n /// User text messages get a stable UTC timestamp prepended to the first text block.\n pub fn with_timestamps(messages: &[Message]) -> Vec<Message> {\n messages\n .iter()\n .map(|msg| {\n let Some(ts) = msg.timestamp else {\n return msg.clone();\n };\n if msg.role != Role::User || msg.should_skip_timestamp_injection() {\n return msg.clone();\n }\n let text_tag = format!(\"[{}]\", Self::format_timestamp(&ts));\n let tool_result_tag = msg.tool_result_tag(&ts);\n let mut msg = msg.clone();\n let mut tagged = false;\n for block in &mut msg.content {\n match block {\n ContentBlock::Text { text, .. } if !tagged => {\n *text = format!(\"{} {}\", text_tag, text);\n tagged = true;\n }\n ContentBlock::ToolResult { content, .. } if !tagged => {\n *content = format!(\"{} {}\", tool_result_tag, content);\n tagged = true;\n }\n _ => {}\n }\n }\n msg\n })\n .collect()\n }\n}\n\nconst STABLE_HASH_SEED: u64 = 0xcbf29ce484222325;\nconst STABLE_HASH_PRIME: u64 = 0x100000001b3;\n\nfn stable_hash_bytes(bytes: &[u8]) -> u64 {\n let mut hash = STABLE_HASH_SEED;\n for byte in bytes {\n hash ^= *byte as u64;\n hash = hash.wrapping_mul(STABLE_HASH_PRIME);\n }\n hash\n}\n\npub(crate) fn extend_stable_hash(acc: u64, next: u64) -> u64 {\n stable_hash_bytes(&[acc.to_le_bytes().as_slice(), next.to_le_bytes().as_slice()].concat())\n}\n\npub(crate) fn stable_message_hash(message: &Message) -> u64 {\n match serde_json::to_vec(message) {\n Ok(bytes) => stable_hash_bytes(&bytes),\n Err(_) => stable_hash_bytes(format!(\"{:?}\", message).as_bytes()),\n }\n}\n\npub fn ends_with_fresh_user_turn(messages: &[Message]) -> bool {\n for msg in messages.iter().rev() {\n if msg.role != Role::User {\n return false;\n }\n\n if msg\n .content\n .iter()\n .any(|block| matches!(block, ContentBlock::ToolResult { .. }))\n {\n return false;\n }\n\n if msg.content.is_empty() {\n return false;\n }\n\n let mut saw_user_text = false;\n for block in &msg.content {\n match block {\n ContentBlock::Text { text, .. } => {\n let trimmed = text.trim();\n if !trimmed.is_empty() && !trimmed.starts_with(\"<system-reminder>\") {\n saw_user_text = true;\n }\n }\n _ => return false,\n }\n }\n\n if saw_user_text {\n return true;\n }\n\n if msg.is_internal_system_reminder() {\n continue;\n }\n\n return false;\n }\n\n false\n}\n\n/// Sanitize a tool ID so it matches the pattern `^[a-zA-Z0-9_-]+$`.\n///\n/// Different providers generate tool IDs in different formats. When switching\n/// from one provider to another mid-conversation, the historical tool IDs may\n/// contain characters that the new provider rejects (e.g., dots in Copilot IDs\n/// sent to Anthropic). This function replaces any invalid characters with\n/// underscores.\npub fn sanitize_tool_id(id: &str) -> String {\n if id.is_empty() {\n return \"unknown\".to_string();\n }\n let sanitized: String = id\n .chars()\n .map(|c| {\n if c.is_ascii_alphanumeric() || c == '_' || c == '-' {\n c\n } else {\n '_'\n }\n })\n .collect();\n if sanitized.is_empty() {\n \"unknown\".to_string()\n } else {\n sanitized\n }\n}\n\n/// Redact likely secrets from persisted tool output.\n///\n/// This is a best-effort safeguard for local session history files. It targets\n/// high-confidence token/key patterns and common `KEY=VALUE` assignments used by\n/// auth flows.\npub fn redact_secrets(text: &str) -> String {\n // Fast path to avoid regex work for most tool outputs.\n let lower = text.to_ascii_lowercase();\n\n if !text.contains(\"sk-\")\n && !text.contains(\"ghp_\")\n && !text.contains(\"github_pat_\")\n && !text.contains(\"AIza\")\n && !text.contains(\"ya29.\")\n && !text.contains(\"xox\")\n && !lower.contains(\"api_key\")\n && !lower.contains(\"token\")\n {\n return text.to_string();\n }\n\n static DIRECT_PATTERNS: OnceLock<Vec<Regex>> = OnceLock::new();\n static ASSIGNMENT_PATTERNS: OnceLock<Vec<Regex>> = OnceLock::new();\n\n let direct_patterns = DIRECT_PATTERNS.get_or_init(|| {\n compile_static_regexes(&[\n r\"sk-ant-(?:oat|ort)01-[A-Za-z0-9_-]{20,}\",\n r\"sk-or-v1-[A-Za-z0-9_-]{20,}\",\n r\"ghp_[A-Za-z0-9]{20,}\",\n r\"github_pat_[A-Za-z0-9_]{20,}\",\n r\"ya29\\.[A-Za-z0-9._-]{20,}\",\n r\"AIza[0-9A-Za-z_-]{20,}\",\n r\"xox[baprs]-[A-Za-z0-9-]{10,}\",\n ])\n });\n\n let assignment_patterns = ASSIGNMENT_PATTERNS.get_or_init(|| {\n compile_static_regexes(&[\n r\"(?m)^\\s*(OPENROUTER_API_KEY\\s*=\\s*)[^\\r\\n]+\",\n r\"(?m)^\\s*(OPENCODE_API_KEY\\s*=\\s*)[^\\r\\n]+\",\n r\"(?m)^\\s*(OPENCODE_GO_API_KEY\\s*=\\s*)[^\\r\\n]+\",\n r\"(?m)^\\s*(ZHIPU_API_KEY\\s*=\\s*)[^\\r\\n]+\",\n r\"(?m)^\\s*(ZAI_API_KEY\\s*=\\s*)[^\\r\\n]+\",\n r\"(?m)^\\s*(302AI_API_KEY\\s*=\\s*)[^\\r\\n]+\",\n r\"(?m)^\\s*(BASETEN_API_KEY\\s*=\\s*)[^\\r\\n]+\",\n r\"(?m)^\\s*(CORTECS_API_KEY\\s*=\\s*)[^\\r\\n]+\",\n r\"(?m)^\\s*(DEEPSEEK_API_KEY\\s*=\\s*)[^\\r\\n]+\",\n r\"(?m)^\\s*(FIRMWARE_API_KEY\\s*=\\s*)[^\\r\\n]+\",\n r\"(?m)^\\s*(HF_TOKEN\\s*=\\s*)[^\\r\\n]+\",\n r\"(?m)^\\s*(MOONSHOT_API_KEY\\s*=\\s*)[^\\r\\n]+\",\n r\"(?m)^\\s*(NEBIUS_API_KEY\\s*=\\s*)[^\\r\\n]+\",\n r\"(?m)^\\s*(SCALEWAY_API_KEY\\s*=\\s*)[^\\r\\n]+\",\n r\"(?m)^\\s*(STACKIT_API_KEY\\s*=\\s*)[^\\r\\n]+\",\n r\"(?m)^\\s*(GROQ_API_KEY\\s*=\\s*)[^\\r\\n]+\",\n r\"(?m)^\\s*(MISTRAL_API_KEY\\s*=\\s*)[^\\r\\n]+\",\n r\"(?m)^\\s*(PERPLEXITY_API_KEY\\s*=\\s*)[^\\r\\n]+\",\n r\"(?m)^\\s*(TOGETHER_API_KEY\\s*=\\s*)[^\\r\\n]+\",\n r\"(?m)^\\s*(DEEPINFRA_API_KEY\\s*=\\s*)[^\\r\\n]+\",\n r\"(?m)^\\s*(XAI_API_KEY\\s*=\\s*)[^\\r\\n]+\",\n r\"(?m)^\\s*(LMSTUDIO_API_KEY\\s*=\\s*)[^\\r\\n]+\",\n r\"(?m)^\\s*(OLLAMA_API_KEY\\s*=\\s*)[^\\r\\n]+\",\n r\"(?m)^\\s*(CHUTES_API_KEY\\s*=\\s*)[^\\r\\n]+\",\n r\"(?m)^\\s*(CEREBRAS_API_KEY\\s*=\\s*)[^\\r\\n]+\",\n r\"(?m)^\\s*(OPENAI_COMPAT_API_KEY\\s*=\\s*)[^\\r\\n]+\",\n r\"(?m)^\\s*(ANTHROPIC_API_KEY\\s*=\\s*)[^\\r\\n]+\",\n r\"(?m)^\\s*(OPENAI_API_KEY\\s*=\\s*)[^\\r\\n]+\",\n r\"(?m)^\\s*(AZURE_OPENAI_API_KEY\\s*=\\s*)[^\\r\\n]+\",\n r\"(?m)^\\s*(CURSOR_API_KEY\\s*=\\s*)[^\\r\\n]+\",\n r\"(?m)^\\s*(GITHUB_TOKEN\\s*=\\s*)[^\\r\\n]+\",\n ])\n });\n\n let mut redacted = text.to_string();\n let mut redacted_keys: HashSet<String> = [\n \"OPENROUTER_API_KEY\",\n \"OPENCODE_API_KEY\",\n \"OPENCODE_GO_API_KEY\",\n \"ZHIPU_API_KEY\",\n \"ZAI_API_KEY\",\n \"302AI_API_KEY\",\n \"BASETEN_API_KEY\",\n \"CORTECS_API_KEY\",\n \"DEEPSEEK_API_KEY\",\n \"FIRMWARE_API_KEY\",\n \"HF_TOKEN\",\n \"MOONSHOT_API_KEY\",\n \"NEBIUS_API_KEY\",\n \"SCALEWAY_API_KEY\",\n \"STACKIT_API_KEY\",\n \"GROQ_API_KEY\",\n \"MISTRAL_API_KEY\",\n \"PERPLEXITY_API_KEY\",\n \"TOGETHER_API_KEY\",\n \"DEEPINFRA_API_KEY\",\n \"XAI_API_KEY\",\n \"LMSTUDIO_API_KEY\",\n \"OLLAMA_API_KEY\",\n \"CHUTES_API_KEY\",\n \"CEREBRAS_API_KEY\",\n \"OPENAI_COMPAT_API_KEY\",\n \"ANTHROPIC_API_KEY\",\n \"OPENAI_API_KEY\",\n \"AZURE_OPENAI_API_KEY\",\n \"CURSOR_API_KEY\",\n \"GITHUB_TOKEN\",\n ]\n .iter()\n .map(|k| (*k).to_string())\n .collect();\n\n for re in direct_patterns {\n redacted = re.replace_all(&redacted, \"[REDACTED_SECRET]\").into_owned();\n }\n\n for re in assignment_patterns {\n redacted = re\n .replace_all(&redacted, \"${1}[REDACTED_SECRET]\")\n .into_owned();\n }\n\n // Also redact custom API key variable names configured at runtime.\n for source in [\n \"JCODE_OPENROUTER_API_KEY_NAME\",\n \"JCODE_OPENAI_COMPAT_API_KEY_NAME\",\n ] {\n let Some(key_name) = std::env::var(source)\n .ok()\n .map(|v| v.trim().to_string())\n .filter(|v| !v.is_empty())\n else {\n continue;\n };\n\n if !key_name\n .chars()\n .all(|c| c.is_ascii_uppercase() || c.is_ascii_digit() || c == '_')\n {\n continue;\n }\n if !redacted_keys.insert(key_name.clone()) {\n continue;\n }\n\n let pattern = format!(r\"(?m)^\\s*({}\\s*=\\s*)[^\\r\\n]+\", regex::escape(&key_name));\n if let Ok(re) = Regex::new(&pattern) {\n redacted = re\n .replace_all(&redacted, \"${1}[REDACTED_SECRET]\")\n .into_owned();\n }\n }\n\n redacted\n}\n\n/// Tool definition for the API\n#[derive(Debug, Clone, Serialize)]\npub struct ToolDefinition {\n pub name: String,\n // Prompt-visible text sent to the model by provider adapters.\n // Approximate prompt cost: description.len() / 4. Use\n // ToolDefinition::description_token_estimate() when reviewing tool bloat.\n pub description: String,\n pub input_schema: serde_json::Value,\n}\n\nimpl ToolDefinition {\n /// Serialized size of the full tool definition payload sent to providers.\n pub fn prompt_chars(&self) -> usize {\n serde_json::json!({\n \"name\": self.name,\n \"description\": self.description,\n \"input_schema\": self.input_schema,\n })\n .to_string()\n .len()\n }\n\n /// Approximate prompt-token cost of this tool's top-level description.\n ///\n /// This uses jcode's standard chars/4 heuristic, matching other token\n /// budget estimates in the codebase.\n pub fn description_token_estimate(&self) -> usize {\n crate::util::estimate_tokens(&self.description)\n }\n\n /// Approximate prompt-token cost of the full tool definition payload.\n pub fn prompt_token_estimate(&self) -> usize {\n crate::util::estimate_tokens(\n &serde_json::json!({\n \"name\": self.name,\n \"description\": self.description,\n \"input_schema\": self.input_schema,\n })\n .to_string(),\n )\n }\n\n pub fn aggregate_prompt_chars(defs: &[ToolDefinition]) -> usize {\n defs.iter().map(Self::prompt_chars).sum()\n }\n\n pub fn aggregate_prompt_token_estimate(defs: &[ToolDefinition]) -> usize {\n defs.iter().map(Self::prompt_token_estimate).sum()\n }\n}\n\n/// A tool call from the model\n#[derive(Debug, Clone, Default, PartialEq, Eq, serde::Serialize, serde::Deserialize)]\npub struct ToolCall {\n #[serde(default)]\n pub id: String,\n #[serde(default)]\n pub name: String,\n #[serde(default)]\n pub input: serde_json::Value,\n #[serde(default, skip_serializing_if = \"Option::is_none\")]\n pub intent: Option<String>,\n}\n\nimpl ToolCall {\n pub fn intent_from_input(input: &serde_json::Value) -> Option<String> {\n input\n .get(\"intent\")\n .and_then(|value| value.as_str())\n .map(str::trim)\n .filter(|intent| !intent.is_empty())\n .map(ToString::to_string)\n }\n\n pub fn refresh_intent_from_input(&mut self) {\n self.intent = Self::intent_from_input(&self.input);\n }\n}\n\n/// Connection phase for status bar transparency\n#[derive(Debug, Clone, PartialEq)]\npub enum ConnectionPhase {\n /// Refreshing OAuth token\n Authenticating,\n /// TCP + TLS connection to API\n Connecting,\n /// HTTP request sent, waiting for first response byte\n WaitingForResponse,\n /// First byte received, stream is active\n Streaming,\n /// Retrying after a transient error\n Retrying { attempt: u32, max: u32 },\n}\n\nimpl std::fmt::Display for ConnectionPhase {\n fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {\n match self {\n ConnectionPhase::Authenticating => write!(f, \"authenticating\"),\n ConnectionPhase::Connecting => write!(f, \"connecting\"),\n ConnectionPhase::WaitingForResponse => write!(f, \"waiting for response\"),\n ConnectionPhase::Streaming => write!(f, \"streaming\"),\n ConnectionPhase::Retrying { attempt, max } => {\n write!(f, \"retrying ({}/{})\", attempt, max)\n }\n }\n }\n}\n\n/// Streaming event from provider\n#[derive(Debug, Clone)]\npub enum StreamEvent {\n /// Text content delta\n TextDelta(String),\n /// Tool use started\n ToolUseStart { id: String, name: String },\n /// Tool input delta (JSON fragment)\n ToolInputDelta(String),\n /// Tool use complete\n ToolUseEnd,\n /// Tool result from provider (provider already executed the tool)\n ToolResult {\n tool_use_id: String,\n content: String,\n is_error: bool,\n },\n /// Extended thinking started\n ThinkingStart,\n /// Extended thinking delta (reasoning content)\n ThinkingDelta(String),\n /// Extended thinking ended\n ThinkingEnd,\n /// Extended thinking completed with duration\n ThinkingDone { duration_secs: f64 },\n /// Message complete (may have stop reason)\n MessageEnd { stop_reason: Option<String> },\n /// Token usage update\n TokenUsage {\n input_tokens: Option<u64>,\n output_tokens: Option<u64>,\n cache_read_input_tokens: Option<u64>,\n cache_creation_input_tokens: Option<u64>,\n },\n /// Active transport/connection type for this stream\n ConnectionType { connection: String },\n /// Connection phase update (for status bar transparency)\n ConnectionPhase { phase: ConnectionPhase },\n /// Provider-supplied human-readable transport detail for the status line\n StatusDetail { detail: String },\n /// Error occurred\n Error {\n message: String,\n /// Seconds until rate limit resets (if this is a rate limit error)\n retry_after_secs: Option<u64>,\n },\n /// Provider session ID (for conversation resume)\n SessionId(String),\n /// Compaction occurred (context was summarized)\n Compaction {\n trigger: String,\n pre_tokens: Option<u64>,\n /// Provider-native compaction artifact, if one was emitted.\n openai_encrypted_content: Option<String>,\n },\n /// Upstream provider info (e.g., which provider OpenRouter routed to)\n UpstreamProvider { provider: String },\n /// Native tool call from a provider bridge that needs execution by jcode\n NativeToolCall {\n request_id: String,\n tool_name: String,\n input: serde_json::Value,\n },\n}\n\n#[cfg(test)]\nmod tests {\n use super::*;\n use anyhow::{Result, anyhow};\n\n #[test]\n fn sanitize_tool_id_alphanumeric_passthrough() {\n assert_eq!(\n sanitize_tool_id(\"toolu_01XFDUDYJgAACzvnptvVer6u\"),\n \"toolu_01XFDUDYJgAACzvnptvVer6u\"\n );\n assert_eq!(sanitize_tool_id(\"call_abc123\"), \"call_abc123\");\n assert_eq!(\n sanitize_tool_id(\"call_1234567890_9876543210\"),\n \"call_1234567890_9876543210\"\n );\n }\n\n #[test]\n fn tool_call_intent_from_input_trims_optional_intent() {\n let input = serde_json::json!({\n \"intent\": \" Verify compact rendering \",\n \"command\": \"cargo test\"\n });\n assert_eq!(\n ToolCall::intent_from_input(&input).as_deref(),\n Some(\"Verify compact rendering\")\n );\n assert_eq!(\n ToolCall::intent_from_input(&serde_json::json!({\"intent\": \" \"})),\n None\n );\n }\n\n #[test]\n fn sanitize_tool_id_hyphens_passthrough() {\n assert_eq!(sanitize_tool_id(\"call-abc-123\"), \"call-abc-123\");\n assert_eq!(\n sanitize_tool_id(\"tool_use-id_with-mixed\"),\n \"tool_use-id_with-mixed\"\n );\n }\n\n #[test]\n fn sanitize_tool_id_replaces_dots() {\n assert_eq!(\n sanitize_tool_id(\"chatcmpl-abc.def.ghi\"),\n \"chatcmpl-abc_def_ghi\"\n );\n assert_eq!(sanitize_tool_id(\"call.123\"), \"call_123\");\n }\n\n #[test]\n fn sanitize_tool_id_replaces_colons() {\n assert_eq!(sanitize_tool_id(\"call:123:456\"), \"call_123_456\");\n }\n\n #[test]\n fn sanitize_tool_id_replaces_special_chars() {\n assert_eq!(\n sanitize_tool_id(\"id@with#special$chars\"),\n \"id_with_special_chars\"\n );\n assert_eq!(sanitize_tool_id(\"id with spaces\"), \"id_with_spaces\");\n }\n\n #[test]\n fn sanitize_tool_id_empty_returns_unknown() {\n assert_eq!(sanitize_tool_id(\"\"), \"unknown\");\n }\n\n #[test]\n fn sanitize_tool_id_copilot_to_anthropic() {\n assert_eq!(\n sanitize_tool_id(\"chatcmpl-BF2xX.tool_call.0\"),\n \"chatcmpl-BF2xX_tool_call_0\"\n );\n }\n\n #[test]\n fn sanitize_tool_id_already_valid_unchanged() {\n let valid_ids = [\n \"toolu_01XFDUDYJgAACzvnptvVer6u\",\n \"call_abc123\",\n \"fallback_text_call_call_1234567890_9876543210\",\n \"tool_123\",\n \"a\",\n \"A\",\n \"0\",\n \"_\",\n \"-\",\n \"a-b_c\",\n ];\n for id in valid_ids {\n assert_eq!(sanitize_tool_id(id), id, \"ID '{}' should be unchanged\", id);\n }\n }\n\n #[test]\n fn redact_secrets_redacts_known_direct_token_formats() {\n let input = \"access=sk-ant-oat01-ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789\\nopenrouter=sk-or-v1-abcdefghijklmnopqrstuvwxyz0123456789\\ngithub=ghp_ABCDEFGHIJKLMNOPQRSTUVWXYZ0123\\n\";\n let out = redact_secrets(input);\n assert!(!out.contains(\"sk-ant-oat01-\"));\n assert!(!out.contains(\"sk-or-v1-\"));\n assert!(!out.contains(\"ghp_ABCDEFGHIJKLMNOPQRSTUVWXYZ0123\"));\n assert!(out.matches(\"[REDACTED_SECRET]\").count() >= 3);\n }\n\n #[test]\n fn redact_secrets_redacts_env_style_assignments() {\n let input = \"OPENROUTER_API_KEY=sk-or-v1-abc123abc123abc123abc123\\nOPENCODE_API_KEY=oc_test_secret\\nOPENCODE_GO_API_KEY=ocgo_test_secret\\nZAI_API_KEY=zai_secret\\nCHUTES_API_KEY=chutes_secret\\nCEREBRAS_API_KEY=cerebras_secret\\nOPENAI_COMPAT_API_KEY=compat_secret\\nCURSOR_API_KEY='my_cursor_secret_value'\\nOPENAI_API_KEY=sk-test-openai-example\\nAZURE_OPENAI_API_KEY=azure-openai-secret\\n\";\n let out = redact_secrets(input);\n assert!(out.contains(\"OPENROUTER_API_KEY=[REDACTED_SECRET]\"));\n assert!(out.contains(\"OPENCODE_API_KEY=[REDACTED_SECRET]\"));\n assert!(out.contains(\"OPENCODE_GO_API_KEY=[REDACTED_SECRET]\"));\n assert!(out.contains(\"ZAI_API_KEY=[REDACTED_SECRET]\"));\n assert!(out.contains(\"CHUTES_API_KEY=[REDACTED_SECRET]\"));\n assert!(out.contains(\"CEREBRAS_API_KEY=[REDACTED_SECRET]\"));\n assert!(out.contains(\"OPENAI_COMPAT_API_KEY=[REDACTED_SECRET]\"));\n assert!(out.contains(\"CURSOR_API_KEY=[REDACTED_SECRET]\"));\n assert!(out.contains(\"OPENAI_API_KEY=[REDACTED_SECRET]\"));\n assert!(out.contains(\"AZURE_OPENAI_API_KEY=[REDACTED_SECRET]\"));\n assert!(!out.contains(\"my_cursor_secret_value\"));\n }\n\n #[test]\n fn redact_secrets_redacts_runtime_key_assignment() {\n let key_var = \"JCODE_OPENAI_COMPAT_API_KEY_NAME\";\n let prev = std::env::var(key_var).ok();\n crate::env::set_var(key_var, \"GROQ_API_KEY\");\n\n let input = \"GROQ_API_KEY=my_secret_token_value\";\n let out = redact_secrets(input);\n assert_eq!(out, \"GROQ_API_KEY=[REDACTED_SECRET]\");\n\n if let Some(v) = prev {\n crate::env::set_var(key_var, v);\n } else {\n crate::env::remove_var(key_var);\n }\n }\n\n #[test]\n fn redact_secrets_redacts_mixed_case_token_assignments() {\n let input = \"my_token=ya29.ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789\";\n let out = redact_secrets(input);\n assert!(out.contains(\"[REDACTED_SECRET]\"));\n assert!(!out.contains(\"ya29.ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789\"));\n }\n\n #[test]\n fn redact_secrets_leaves_normal_output_unchanged() {\n let input = \"Found 5 files\\nNo auth errors\\nDone.\";\n assert_eq!(redact_secrets(input), input);\n }\n\n #[test]\n fn format_timestamp_is_stable_utc_rfc3339() -> Result<()> {\n let ts =\n chrono::DateTime::parse_from_rfc3339(\"2025-03-15T02:24:13.250Z\")?.with_timezone(&Utc);\n assert_eq!(Message::format_timestamp(&ts), \"2025-03-15T02:24:13.250Z\");\n Ok(())\n }\n\n #[test]\n fn with_timestamps_prepends_utc_prefix_to_user_text() -> Result<()> {\n let ts = chrono::DateTime::parse_from_rfc3339(\"2025-03-15T02:24:03Z\")?.with_timezone(&Utc);\n let stamped = Message::with_timestamps(&[Message {\n role: Role::User,\n content: vec![ContentBlock::Text {\n text: \"hello\".to_string(),\n cache_control: None,\n }],\n timestamp: Some(ts),\n tool_duration_ms: None,\n }]);\n let ContentBlock::Text { text, .. } = &stamped[0].content[0] else {\n return Err(anyhow!(\n \"expected text block, got {:?}\",\n stamped[0].content[0]\n ));\n };\n assert_eq!(text, \"[2025-03-15T02:24:03.000Z] hello\");\n Ok(())\n }\n\n #[test]\n fn with_timestamps_adds_tool_timing_header_with_duration() -> Result<()> {\n let ts = chrono::DateTime::parse_from_rfc3339(\"2025-03-15T02:24:13Z\")?.with_timezone(&Utc);\n let stamped = Message::with_timestamps(&[Message {\n role: Role::User,\n content: vec![ContentBlock::ToolResult {\n tool_use_id: \"call_1\".to_string(),\n content: \"ok\".to_string(),\n is_error: None,\n }],\n timestamp: Some(ts),\n tool_duration_ms: Some(3_200),\n }]);\n let ContentBlock::ToolResult { content, .. } = &stamped[0].content[0] else {\n return Err(anyhow!(\n \"expected tool result block, got {:?}\",\n stamped[0].content[0]\n ));\n };\n assert_eq!(\n content,\n \"[tool timing: start=2025-03-15T02:24:09.800Z finish=2025-03-15T02:24:13.000Z duration=3.2s] ok\"\n );\n Ok(())\n }\n\n #[test]\n fn with_timestamps_skips_internal_system_reminders() -> Result<()> {\n let ts = chrono::DateTime::parse_from_rfc3339(\"2025-03-15T02:24:13Z\")?.with_timezone(&Utc);\n let original = Message {\n role: Role::User,\n content: vec![ContentBlock::Text {\n text: \"<system-reminder>\\ninternal\\n</system-reminder>\".to_string(),\n cache_control: None,\n }],\n timestamp: Some(ts),\n tool_duration_ms: None,\n };\n let stamped = Message::with_timestamps(std::slice::from_ref(&original));\n let ContentBlock::Text { text, .. } = &stamped[0].content[0] else {\n return Err(anyhow!(\n \"expected text block, got {:?}\",\n stamped[0].content[0]\n ));\n };\n assert_eq!(text, \"<system-reminder>\\ninternal\\n</system-reminder>\");\n Ok(())\n }\n\n #[test]\n fn ends_with_fresh_user_turn_accepts_plain_user_text() {\n let messages = vec![Message::user(\"hello\")];\n assert!(ends_with_fresh_user_turn(&messages));\n }\n\n #[test]\n fn ends_with_fresh_user_turn_rejects_trailing_tool_result() {\n let messages = vec![\n Message::user(\"hello\"),\n Message {\n role: Role::Assistant,\n content: vec![ContentBlock::ToolUse {\n id: \"call_1\".to_string(),\n name: \"bash\".to_string(),\n input: serde_json::json!({}),\n }],\n timestamp: Some(Utc::now()),\n tool_duration_ms: None,\n },\n Message::tool_result(\"call_1\", \"ok\", false),\n ];\n assert!(!ends_with_fresh_user_turn(&messages));\n }\n\n #[test]\n fn ends_with_fresh_user_turn_skips_internal_system_reminders() {\n let messages = vec![\n Message::user(\"hello\"),\n Message::user(\"<system-reminder>\\ninternal\\n</system-reminder>\"),\n ];\n assert!(ends_with_fresh_user_turn(&messages));\n }\n\n #[test]\n fn ends_with_fresh_user_turn_rejects_assistant_tail() {\n let messages = vec![\n Message::user(\"hello\"),\n Message::assistant_text(\"working on it\"),\n ];\n assert!(!ends_with_fresh_user_turn(&messages));\n }\n\n #[test]\n fn format_background_task_notification_markdown_uses_code_block_preview() {\n let rendered = format_background_task_notification_markdown(&BackgroundTaskCompleted {\n task_id: \"abc123\".to_string(),\n tool_name: \"bash\".to_string(),\n display_name: None,\n session_id: \"session\".to_string(),\n status: BackgroundTaskStatus::Completed,\n exit_code: Some(0),\n output_preview: \"[stderr] first line\\n[stdout] second line\\n\".to_string(),\n output_file: std::path::PathBuf::from(\"/tmp/output.log\"),\n duration_secs: 7.1,\n notify: true,\n wake: false,\n });\n\n assert!(\n rendered\n .contains(\"**Background task** `abc123` · `bash` · ✓ completed · 7.1s · exit 0\")\n );\n assert!(rendered.contains(\"```text\\n[stderr] first line\\n[stdout] second line\\n```\"));\n assert!(rendered.contains(\"_Full output:_ `bg action=\\\"output\\\" task_id=\\\"abc123\\\"`\"));\n }\n\n #[test]\n fn format_background_task_notification_markdown_handles_empty_preview() {\n let rendered = format_background_task_notification_markdown(&BackgroundTaskCompleted {\n task_id: \"abc123\".to_string(),\n tool_name: \"bash\".to_string(),\n display_name: None,\n session_id: \"session\".to_string(),\n status: BackgroundTaskStatus::Failed,\n exit_code: Some(9),\n output_preview: \"\\n\\n\".to_string(),\n output_file: std::path::PathBuf::from(\"/tmp/output.log\"),\n duration_secs: 1.0,\n notify: true,\n wake: false,\n });\n\n assert!(rendered.contains(\"✗ failed\"));\n assert!(rendered.contains(\"_No output captured._\"));\n }\n\n #[test]\n fn format_background_task_notification_markdown_highlights_failure_reason() -> Result<()> {\n let rendered = format_background_task_notification_markdown(&BackgroundTaskCompleted {\n task_id: \"build123\".to_string(),\n tool_name: \"selfdev-build\".to_string(),\n display_name: Some(\"Build jcode\".to_string()),\n session_id: \"session\".to_string(),\n status: BackgroundTaskStatus::Failed,\n exit_code: Some(101),\n output_preview: \"[stderr] Compiling jcode\\nsccache: Compile terminated by signal 15\\n[stderr] error: could not compile `jcode` (lib)\".to_string(),\n output_file: std::path::PathBuf::from(\"/tmp/output.log\"),\n duration_secs: 62.5,\n notify: true,\n wake: false,\n });\n\n assert!(rendered.contains(\"_Failure:_ sccache: Compile terminated by signal 15\"));\n let parsed = parse_background_task_notification_markdown(&rendered)\n .ok_or_else(|| anyhow!(\"failure notification should parse\"))?;\n assert_eq!(\n parsed.failure_summary.as_deref(),\n Some(\"sccache: Compile terminated by signal 15\")\n );\n Ok(())\n }\n\n #[test]\n fn format_background_task_notification_markdown_renders_superseded_status() {\n let rendered = format_background_task_notification_markdown(&BackgroundTaskCompleted {\n task_id: \"abc123\".to_string(),\n tool_name: \"selfdev-build\".to_string(),\n display_name: None,\n session_id: \"session\".to_string(),\n status: BackgroundTaskStatus::Superseded,\n exit_code: Some(0),\n output_preview: \"Build completed, but source changed before activation\".to_string(),\n output_file: std::path::PathBuf::from(\"/tmp/output.log\"),\n duration_secs: 5.0,\n notify: true,\n wake: false,\n });\n\n assert!(rendered.contains(\"↻ superseded\"));\n assert!(rendered.contains(\"exit 0\"));\n assert!(rendered.contains(\"source changed before activation\"));\n }\n\n #[test]\n fn format_background_task_progress_markdown_uses_compact_multiline_layout() {\n let rendered = format_background_task_progress_markdown(&BackgroundTaskProgressEvent {\n task_id: \"bgprogress\".to_string(),\n tool_name: \"bash\".to_string(),\n display_name: None,\n session_id: \"session\".to_string(),\n progress: crate::bus::BackgroundTaskProgress {\n kind: crate::bus::BackgroundTaskProgressKind::Determinate,\n percent: Some(42.0),\n message: Some(\"Running tests\".to_string()),\n current: Some(21),\n total: Some(50),\n unit: Some(\"tests\".to_string()),\n eta_seconds: None,\n updated_at: Utc::now().to_rfc3339(),\n source: crate::bus::BackgroundTaskProgressSource::Reported,\n },\n });\n\n assert!(rendered.starts_with(\"**Background task progress** `bgprogress` · `bash`\\n\\n\"));\n assert!(rendered.contains(\"42% · Running tests\"));\n assert!(rendered.contains(\"(reported)\"));\n }\n\n #[test]\n fn background_task_notifications_include_display_name_when_available() -> Result<()> {\n let rendered = format_background_task_notification_markdown(&BackgroundTaskCompleted {\n task_id: \"abc123\".to_string(),\n tool_name: \"bash\".to_string(),\n display_name: Some(\"Run integration tests\".to_string()),\n session_id: \"session\".to_string(),\n status: BackgroundTaskStatus::Completed,\n exit_code: Some(0),\n output_preview: \"done\".to_string(),\n output_file: std::path::PathBuf::from(\"/tmp/output.log\"),\n duration_secs: 7.1,\n notify: true,\n wake: false,\n });\n\n assert!(rendered.contains(\n \"**Background task** `abc123` · `Run integration tests` (`bash`) · ✓ completed\"\n ));\n let parsed = parse_background_task_notification_markdown(&rendered)\n .ok_or_else(|| anyhow!(\"named background task notification should parse\"))?;\n assert_eq!(parsed.tool_name, \"bash\");\n assert_eq!(\n parsed.display_name.as_deref(),\n Some(\"Run integration tests\")\n );\n Ok(())\n }\n\n #[test]\n fn background_task_progress_notifications_include_display_name_when_available() -> Result<()> {\n let rendered = format_background_task_progress_markdown(&BackgroundTaskProgressEvent {\n task_id: \"bgprogress\".to_string(),\n tool_name: \"bash\".to_string(),\n display_name: Some(\"Run integration tests\".to_string()),\n session_id: \"session\".to_string(),\n progress: crate::bus::BackgroundTaskProgress {\n kind: crate::bus::BackgroundTaskProgressKind::Determinate,\n percent: Some(42.0),\n message: Some(\"Running tests\".to_string()),\n current: Some(21),\n total: Some(50),\n unit: Some(\"tests\".to_string()),\n eta_seconds: None,\n updated_at: Utc::now().to_rfc3339(),\n source: crate::bus::BackgroundTaskProgressSource::Reported,\n },\n });\n\n assert!(rendered.starts_with(\n \"**Background task progress** `bgprogress` · `Run integration tests` (`bash`)\\n\\n\"\n ));\n let parsed = parse_background_task_progress_notification_markdown(&rendered)\n .ok_or_else(|| anyhow!(\"named progress notification should parse\"))?;\n assert_eq!(parsed.tool_name, \"bash\");\n assert_eq!(\n parsed.display_name.as_deref(),\n Some(\"Run integration tests\")\n );\n assert_eq!(parsed.summary, \"42% · Running tests\");\n Ok(())\n }\n\n #[test]\n fn parse_background_task_progress_notification_extracts_card_fields() -> Result<()> {\n let parsed = parse_background_task_progress_notification_markdown(\n \"**Background task progress** `bgprogress` · `bash`\\n\\n[#####-------] 42% · Running tests (reported)\",\n )\n .ok_or_else(|| anyhow!(\"progress notification should parse\"))?;\n\n assert_eq!(parsed.task_id, \"bgprogress\");\n assert_eq!(parsed.tool_name, \"bash\");\n assert_eq!(parsed.display_name, None);\n assert_eq!(parsed.summary, \"42% · Running tests\");\n assert_eq!(parsed.source.as_deref(), Some(\"reported\"));\n assert_eq!(parsed.percent, Some(42.0));\n Ok(())\n }\n\n #[test]\n fn parse_background_task_progress_notification_supports_legacy_inline_layout() -> Result<()> {\n let parsed = parse_background_task_progress_notification_markdown(\n \"**Background task progress** `bgprogress` · `bash` · Release run in_progress: - 7/8 jobs completed (reported)\",\n )\n .ok_or_else(|| anyhow!(\"legacy progress notification should parse\"))?;\n\n assert_eq!(parsed.task_id, \"bgprogress\");\n assert_eq!(parsed.tool_name, \"bash\");\n assert_eq!(parsed.display_name, None);\n assert_eq!(\n parsed.summary,\n \"Release run in_progress: - 7/8 jobs completed\"\n );\n assert_eq!(parsed.source.as_deref(), Some(\"reported\"));\n assert_eq!(parsed.percent, None);\n Ok(())\n }\n\n #[test]\n fn description_token_estimate_uses_chars_per_token_heuristic() {\n let def = ToolDefinition {\n name: \"read\".to_string(),\n description: \"abcdwxyz\".to_string(),\n input_schema: serde_json::json!({\"type\": \"object\"}),\n };\n\n assert_eq!(def.description_token_estimate(), 2);\n }\n\n #[test]\n fn parse_background_task_notification_markdown_extracts_fields() -> Result<()> {\n let rendered = format_background_task_notification_markdown(&BackgroundTaskCompleted {\n task_id: \"abc123\".to_string(),\n tool_name: \"bash\".to_string(),\n display_name: None,\n session_id: \"session\".to_string(),\n status: BackgroundTaskStatus::Completed,\n exit_code: Some(0),\n output_preview: \"[stderr] first line\\n[stdout] second line\\n\".to_string(),\n output_file: std::path::PathBuf::from(\"/tmp/output.log\"),\n duration_secs: 7.1,\n notify: true,\n wake: false,\n });\n\n let parsed = parse_background_task_notification_markdown(&rendered)\n .ok_or_else(|| anyhow!(\"background task notification should parse\"))?;\n assert_eq!(parsed.task_id, \"abc123\");\n assert_eq!(parsed.tool_name, \"bash\");\n assert_eq!(parsed.display_name, None);\n assert_eq!(parsed.status, \"✓ completed\");\n assert_eq!(parsed.duration, \"7.1s\");\n assert_eq!(parsed.exit_label, \"exit 0\");\n assert_eq!(parsed.failure_summary, None);\n assert_eq!(\n parsed.preview.as_deref(),\n Some(\"[stderr] first line\\n[stdout] second line\")\n );\n assert_eq!(\n parsed.full_output_command,\n \"bg action=\\\"output\\\" task_id=\\\"abc123\\\"\"\n );\n Ok(())\n }\n}\n"}, "cf0a77ffa2eeb3ffe934e0f03da7ae85c72a4b65": {"t": 1785757223.1892676, "n": 43140, "h": "66623389ed9f71d8", "c": "use crate::bus::{BackgroundTaskCompleted, BackgroundTaskProgressEvent, BackgroundTaskStatus};\nuse chrono::Utc;\nuse regex::Regex;\nuse serde::{Deserialize, Serialize};\nuse std::collections::HashSet;\nuse std::sync::OnceLock;\n\nmod notifications;\n\npub use notifications::{\n InputShellResult, ParsedBackgroundTaskNotification, ParsedBackgroundTaskProgressNotification,\n background_task_display_label, background_task_status_notice,\n format_background_task_notification_markdown, format_background_task_progress_markdown,\n format_input_shell_result_markdown, input_shell_status_notice,\n parse_background_task_notification_markdown,\n parse_background_task_progress_notification_markdown,\n};\n\nfn compile_static_regex(pattern: &str) -> Option<Regex> {\n match Regex::new(pattern) {\n Ok(regex) => Some(regex),\n Err(err) => {\n eprintln!(\"jcode: failed to compile static regex: {err}\");\n None\n }\n }\n}\n\nfn compile_static_regexes(patterns: &[&str]) -> Vec<Regex> {\n patterns\n .iter()\n .filter_map(|pattern| compile_static_regex(pattern))\n .collect()\n}\n\n/// Role in conversation\n#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]\n#[serde(rename_all = \"lowercase\")]\npub enum Role {\n User,\n Assistant,\n}\n\n/// Plain-text tool output placeholder when execution was interrupted.\npub const TOOL_OUTPUT_MISSING_TEXT: &str =\n \"Tool output missing (session interrupted before tool execution completed)\";\n\n/// A message in the conversation\n#[derive(Debug, Clone, Serialize, Deserialize)]\npub struct Message {\n pub role: Role,\n pub content: Vec<ContentBlock>,\n #[serde(default, skip_serializing_if = \"Option::is_none\")]\n pub timestamp: Option<chrono::DateTime<Utc>>,\n #[serde(default, skip_serializing_if = \"Option::is_none\")]\n pub tool_duration_ms: Option<u64>,\n}\n\n/// Cache control metadata for prompt caching\n#[derive(Debug, Clone, Serialize, Deserialize)]\npub struct CacheControl {\n #[serde(rename = \"type\")]\n pub kind: String,\n #[serde(skip_serializing_if = \"Option::is_none\")]\n pub ttl: Option<String>,\n}\n\nimpl CacheControl {\n pub fn ephemeral(ttl: Option<String>) -> Self {\n Self {\n kind: \"ephemeral\".to_string(),\n ttl,\n }\n }\n}\n\n/// Content block within a message\n#[derive(Debug, Clone, Serialize, Deserialize)]\n#[serde(tag = \"type\", rename_all = \"snake_case\")]\npub enum ContentBlock {\n Text {\n text: String,\n #[serde(skip_serializing_if = \"Option::is_none\")]\n cache_control: Option<CacheControl>,\n },\n /// Hidden reasoning content used for providers that require it (not displayed)\n Reasoning {\n text: String,\n },\n ToolUse {\n id: String,\n name: String,\n input: serde_json::Value,\n },\n ToolResult {\n tool_use_id: String,\n content: String,\n #[serde(skip_serializing_if = \"Option::is_none\")]\n is_error: Option<bool>,\n },\n Image {\n media_type: String,\n data: String,\n },\n /// Hidden OpenAI Responses compaction item used to preserve native\n /// compaction state across turns/saves when jcode explicitly triggers it.\n OpenAICompaction {\n encrypted_content: String,\n },\n}\n\nimpl Message {\n pub fn user(text: &str) -> Self {\n Self {\n role: Role::User,\n content: vec![ContentBlock::Text {\n text: text.to_string(),\n cache_control: None,\n }],\n timestamp: Some(Utc::now()),\n tool_duration_ms: None,\n }\n }\n\n pub fn user_with_images(text: &str, images: Vec<(String, String)>) -> Self {\n let mut content: Vec<ContentBlock> = images\n .into_iter()\n .map(|(media_type, data)| ContentBlock::Image { media_type, data })\n .collect();\n content.push(ContentBlock::Text {\n text: text.to_string(),\n cache_control: None,\n });\n Self {\n role: Role::User,\n content,\n timestamp: Some(Utc::now()),\n tool_duration_ms: None,\n }\n }\n\n pub fn assistant_text(text: &str) -> Self {\n Self {\n role: Role::Assistant,\n content: vec![ContentBlock::Text {\n text: text.to_string(),\n cache_control: None,\n }],\n timestamp: Some(Utc::now()),\n tool_duration_ms: None,\n }\n }\n\n pub fn tool_result(tool_use_id: &str, content: &str, is_error: bool) -> Self {\n Self::tool_result_with_duration(tool_use_id, content, is_error, None)\n }\n\n pub fn tool_result_with_duration(\n tool_use_id: &str,\n content: &str,\n is_error: bool,\n tool_duration_ms: Option<u64>,\n ) -> Self {\n Self {\n role: Role::User,\n content: vec![ContentBlock::ToolResult {\n tool_use_id: tool_use_id.to_string(),\n content: content.to_string(),\n is_error: if is_error { Some(true) } else { None },\n }],\n timestamp: Some(Utc::now()),\n tool_duration_ms,\n }\n }\n\n /// Format a timestamp deterministically in UTC for injection into model-visible content.\n pub fn format_timestamp(ts: &chrono::DateTime<Utc>) -> String {\n ts.to_rfc3339_opts(chrono::SecondsFormat::Millis, true)\n }\n\n pub fn format_duration(duration_ms: u64) -> String {\n match duration_ms {\n 0..=999 => format!(\"{}ms\", duration_ms),\n 1_000..=9_999 => format!(\"{:.1}s\", duration_ms as f64 / 1000.0),\n 10_000..=59_999 => format!(\"{}s\", duration_ms / 1000),\n _ => {\n let total_seconds = duration_ms / 1000;\n let minutes = total_seconds / 60;\n let seconds = total_seconds % 60;\n if seconds == 0 {\n format!(\"{}m\", minutes)\n } else {\n format!(\"{}m {}s\", minutes, seconds)\n }\n }\n }\n }\n\n pub fn is_internal_system_reminder(&self) -> bool {\n self.content\n .iter()\n .find_map(|block| match block {\n ContentBlock::Text { text, .. } => Some(text.trim_start()),\n _ => None,\n })\n .is_some_and(|text| text.starts_with(\"<system-reminder>\"))\n }\n\n fn should_skip_timestamp_injection(&self) -> bool {\n self.is_internal_system_reminder()\n }\n\n fn tool_result_tag(&self, ts: &chrono::DateTime<Utc>) -> String {\n match self.tool_duration_ms {\n Some(duration_ms) => {\n let duration_ms_i64 = i64::try_from(duration_ms).unwrap_or(i64::MAX);\n let start_ts = ts\n .checked_sub_signed(chrono::Duration::milliseconds(duration_ms_i64))\n .unwrap_or(*ts);\n format!(\n \"[tool timing: start={} finish={} duration={}]\",\n Self::format_timestamp(&start_ts),\n Self::format_timestamp(ts),\n Self::format_duration(duration_ms)\n )\n }\n None => format!(\"[{}]\", Self::format_timestamp(ts)),\n }\n }\n\n /// Return a copy of messages with timestamps injected into user-role text content.\n /// Tool results get a stable UTC timing header prepended to content.\n /// User text messages get a stable UTC timestamp prepended to the first text block.\n pub fn with_timestamps(messages: &[Message]) -> Vec<Message> {\n messages\n .iter()\n .map(|msg| {\n let Some(ts) = msg.timestamp else {\n return msg.clone();\n };\n if msg.role != Role::User || msg.should_skip_timestamp_injection() {\n return msg.clone();\n }\n let text_tag = format!(\"[{}]\", Self::format_timestamp(&ts));\n let tool_result_tag = msg.tool_result_tag(&ts);\n let mut msg = msg.clone();\n let mut tagged = false;\n for block in &mut msg.content {\n match block {\n ContentBlock::Text { text, .. } if !tagged => {\n *text = format!(\"{} {}\", text_tag, text);\n tagged = true;\n }\n ContentBlock::ToolResult { content, .. } if !tagged => {\n *content = format!(\"{} {}\", tool_result_tag, content);\n tagged = true;\n }\n _ => {}\n }\n }\n msg\n })\n .collect()\n }\n}\n\nconst STABLE_HASH_SEED: u64 = 0xcbf29ce484222325;\nconst STABLE_HASH_PRIME: u64 = 0x100000001b3;\n\nfn stable_hash_bytes(bytes: &[u8]) -> u64 {\n let mut hash = STABLE_HASH_SEED;\n for byte in bytes {\n hash ^= *byte as u64;\n hash = hash.wrapping_mul(STABLE_HASH_PRIME);\n }\n hash\n}\n\npub(crate) fn extend_stable_hash(acc: u64, next: u64) -> u64 {\n stable_hash_bytes(&[acc.to_le_bytes().as_slice(), next.to_le_bytes().as_slice()].concat())\n}\n\npub(crate) fn stable_message_hash(message: &Message) -> u64 {\n match serde_json::to_vec(message) {\n Ok(bytes) => stable_hash_bytes(&bytes),\n Err(_) => stable_hash_bytes(format!(\"{:?}\", message).as_bytes()),\n }\n}\n\npub fn ends_with_fresh_user_turn(messages: &[Message]) -> bool {\n for msg in messages.iter().rev() {\n if msg.role != Role::User {\n return false;\n }\n\n if msg\n .content\n .iter()\n .any(|block| matches!(block, ContentBlock::ToolResult { .. }))\n {\n return false;\n }\n\n if msg.content.is_empty() {\n return false;\n }\n\n let mut saw_user_text = false;\n for block in &msg.content {\n match block {\n ContentBlock::Text { text, .. } => {\n let trimmed = text.trim();\n if !trimmed.is_empty() && !trimmed.starts_with(\"<system-reminder>\") {\n saw_user_text = true;\n }\n }\n _ => return false,\n }\n }\n\n if saw_user_text {\n return true;\n }\n\n if msg.is_internal_system_reminder() {\n continue;\n }\n\n return false;\n }\n\n false\n}\n\n/// Sanitize a tool ID so it matches the pattern `^[a-zA-Z0-9_-]+$`.\n///\n/// Different providers generate tool IDs in different formats. When switching\n/// from one provider to another mid-conversation, the historical tool IDs may\n/// contain characters that the new provider rejects (e.g., dots in Copilot IDs\n/// sent to Anthropic). This function replaces any invalid characters with\n/// underscores.\npub fn sanitize_tool_id(id: &str) -> String {\n if id.is_empty() {\n return \"unknown\".to_string();\n }\n let sanitized: String = id\n .chars()\n .map(|c| {\n if c.is_ascii_alphanumeric() || c == '_' || c == '-' {\n c\n } else {\n '_'\n }\n })\n .collect();\n if sanitized.is_empty() {\n \"unknown\".to_string()\n } else {\n sanitized\n }\n}\n\n/// Redact likely secrets from persisted tool output.\n///\n/// This is a best-effort safeguard for local session history files. It targets\n/// high-confidence token/key patterns and common `KEY=VALUE` assignments used by\n/// auth flows.\npub fn redact_secrets(text: &str) -> String {\n // Fast path to avoid regex work for most tool outputs.\n let lower = text.to_ascii_lowercase();\n\n if !text.contains(\"sk-\")\n && !text.contains(\"ghp_\")\n && !text.contains(\"github_pat_\")\n && !text.contains(\"AIza\")\n && !text.contains(\"ya29.\")\n && !text.contains(\"xox\")\n && !lower.contains(\"api_key\")\n && !lower.contains(\"token\")\n {\n return text.to_string();\n }\n\n static DIRECT_PATTERNS: OnceLock<Vec<Regex>> = OnceLock::new();\n static ASSIGNMENT_PATTERNS: OnceLock<Vec<Regex>> = OnceLock::new();\n\n let direct_patterns = DIRECT_PATTERNS.get_or_init(|| {\n compile_static_regexes(&[\n r\"sk-ant-(?:oat|ort)01-[A-Za-z0-9_-]{20,}\",\n r\"sk-or-v1-[A-Za-z0-9_-]{20,}\",\n r\"ghp_[A-Za-z0-9]{20,}\",\n r\"github_pat_[A-Za-z0-9_]{20,}\",\n r\"ya29\\.[A-Za-z0-9._-]{20,}\",\n r\"AIza[0-9A-Za-z_-]{20,}\",\n r\"xox[baprs]-[A-Za-z0-9-]{10,}\",\n ])\n });\n\n let assignment_patterns = ASSIGNMENT_PATTERNS.get_or_init(|| {\n compile_static_regexes(&[\n r\"(?m)^\\s*(OPENROUTER_API_KEY\\s*=\\s*)[^\\r\\n]+\",\n r\"(?m)^\\s*(OPENCODE_API_KEY\\s*=\\s*)[^\\r\\n]+\",\n r\"(?m)^\\s*(OPENCODE_GO_API_KEY\\s*=\\s*)[^\\r\\n]+\",\n r\"(?m)^\\s*(ZHIPU_API_KEY\\s*=\\s*)[^\\r\\n]+\",\n r\"(?m)^\\s*(ZAI_API_KEY\\s*=\\s*)[^\\r\\n]+\",\n r\"(?m)^\\s*(302AI_API_KEY\\s*=\\s*)[^\\r\\n]+\",\n r\"(?m)^\\s*(BASETEN_API_KEY\\s*=\\s*)[^\\r\\n]+\",\n r\"(?m)^\\s*(CORTECS_API_KEY\\s*=\\s*)[^\\r\\n]+\",\n r\"(?m)^\\s*(DEEPSEEK_API_KEY\\s*=\\s*)[^\\r\\n]+\",\n r\"(?m)^\\s*(FIRMWARE_API_KEY\\s*=\\s*)[^\\r\\n]+\",\n r\"(?m)^\\s*(HF_TOKEN\\s*=\\s*)[^\\r\\n]+\",\n r\"(?m)^\\s*(MOONSHOT_API_KEY\\s*=\\s*)[^\\r\\n]+\",\n r\"(?m)^\\s*(NEBIUS_API_KEY\\s*=\\s*)[^\\r\\n]+\",\n r\"(?m)^\\s*(SCALEWAY_API_KEY\\s*=\\s*)[^\\r\\n]+\",\n r\"(?m)^\\s*(STACKIT_API_KEY\\s*=\\s*)[^\\r\\n]+\",\n r\"(?m)^\\s*(GROQ_API_KEY\\s*=\\s*)[^\\r\\n]+\",\n r\"(?m)^\\s*(MISTRAL_API_KEY\\s*=\\s*)[^\\r\\n]+\",\n r\"(?m)^\\s*(PERPLEXITY_API_KEY\\s*=\\s*)[^\\r\\n]+\",\n r\"(?m)^\\s*(TOGETHER_API_KEY\\s*=\\s*)[^\\r\\n]+\",\n r\"(?m)^\\s*(DEEPINFRA_API_KEY\\s*=\\s*)[^\\r\\n]+\",\n r\"(?m)^\\s*(XAI_API_KEY\\s*=\\s*)[^\\r\\n]+\",\n r\"(?m)^\\s*(LMSTUDIO_API_KEY\\s*=\\s*)[^\\r\\n]+\",\n r\"(?m)^\\s*(OLLAMA_API_KEY\\s*=\\s*)[^\\r\\n]+\",\n r\"(?m)^\\s*(CHUTES_API_KEY\\s*=\\s*)[^\\r\\n]+\",\n r\"(?m)^\\s*(CEREBRAS_API_KEY\\s*=\\s*)[^\\r\\n]+\",\n r\"(?m)^\\s*(OPENAI_COMPAT_API_KEY\\s*=\\s*)[^\\r\\n]+\",\n r\"(?m)^\\s*(ANTHROPIC_API_KEY\\s*=\\s*)[^\\r\\n]+\",\n r\"(?m)^\\s*(OPENAI_API_KEY\\s*=\\s*)[^\\r\\n]+\",\n r\"(?m)^\\s*(AZURE_OPENAI_API_KEY\\s*=\\s*)[^\\r\\n]+\",\n r\"(?m)^\\s*(CURSOR_API_KEY\\s*=\\s*)[^\\r\\n]+\",\n r\"(?m)^\\s*(GITHUB_TOKEN\\s*=\\s*)[^\\r\\n]+\",\n ])\n });\n\n let mut redacted = text.to_string();\n let mut redacted_keys: HashSet<String> = [\n \"OPENROUTER_API_KEY\",\n \"OPENCODE_API_KEY\",\n \"OPENCODE_GO_API_KEY\",\n \"ZHIPU_API_KEY\",\n \"ZAI_API_KEY\",\n \"302AI_API_KEY\",\n \"BASETEN_API_KEY\",\n \"CORTECS_API_KEY\",\n \"DEEPSEEK_API_KEY\",\n \"FIRMWARE_API_KEY\",\n \"HF_TOKEN\",\n \"MOONSHOT_API_KEY\",\n \"NEBIUS_API_KEY\",\n \"SCALEWAY_API_KEY\",\n \"STACKIT_API_KEY\",\n \"GROQ_API_KEY\",\n \"MISTRAL_API_KEY\",\n \"PERPLEXITY_API_KEY\",\n \"TOGETHER_API_KEY\",\n \"DEEPINFRA_API_KEY\",\n \"XAI_API_KEY\",\n \"LMSTUDIO_API_KEY\",\n \"OLLAMA_API_KEY\",\n \"CHUTES_API_KEY\",\n \"CEREBRAS_API_KEY\",\n \"OPENAI_COMPAT_API_KEY\",\n \"ANTHROPIC_API_KEY\",\n \"OPENAI_API_KEY\",\n \"AZURE_OPENAI_API_KEY\",\n \"CURSOR_API_KEY\",\n \"GITHUB_TOKEN\",\n ]\n .iter()\n .map(|k| (*k).to_string())\n .collect();\n\n for re in direct_patterns {\n redacted = re.replace_all(&redacted, \"[REDACTED_SECRET]\").into_owned();\n }\n\n for re in assignment_patterns {\n redacted = re\n .replace_all(&redacted, \"${1}[REDACTED_SECRET]\")\n .into_owned();\n }\n\n // Also redact custom API key variable names configured at runtime.\n for source in [\n \"JCODE_OPENROUTER_API_KEY_NAME\",\n \"JCODE_OPENAI_COMPAT_API_KEY_NAME\",\n ] {\n let Some(key_name) = std::env::var(source)\n .ok()\n .map(|v| v.trim().to_string())\n .filter(|v| !v.is_empty())\n else {\n continue;\n };\n\n if !key_name\n .chars()\n .all(|c| c.is_ascii_uppercase() || c.is_ascii_digit() || c == '_')\n {\n continue;\n }\n if !redacted_keys.insert(key_name.clone()) {\n continue;\n }\n\n let pattern = format!(r\"(?m)^\\s*({}\\s*=\\s*)[^\\r\\n]+\", regex::escape(&key_name));\n if let Ok(re) = Regex::new(&pattern) {\n redacted = re\n .replace_all(&redacted, \"${1}[REDACTED_SECRET]\")\n .into_owned();\n }\n }\n\n redacted\n}\n\n/// Tool definition for the API\n#[derive(Debug, Clone, Serialize)]\npub struct ToolDefinition {\n pub name: String,\n // Prompt-visible text sent to the model by provider adapters.\n // Approximate prompt cost: description.len() / 4. Use\n // ToolDefinition::description_token_estimate() when reviewing tool bloat.\n pub description: String,\n pub input_schema: serde_json::Value,\n}\n\nimpl ToolDefinition {\n /// Serialized size of the full tool definition payload sent to providers.\n pub fn prompt_chars(&self) -> usize {\n serde_json::json!({\n \"name\": self.name,\n \"description\": self.description,\n \"input_schema\": self.input_schema,\n })\n .to_string()\n .len()\n }\n\n /// Approximate prompt-token cost of this tool's top-level description.\n ///\n /// This uses jcode's standard chars/4 heuristic, matching other token\n /// budget estimates in the codebase.\n pub fn description_token_estimate(&self) -> usize {\n crate::util::estimate_tokens(&self.description)\n }\n\n /// Approximate prompt-token cost of the full tool definition payload.\n pub fn prompt_token_estimate(&self) -> usize {\n crate::util::estimate_tokens(\n &serde_json::json!({\n \"name\": self.name,\n \"description\": self.description,\n \"input_schema\": self.input_schema,\n })\n .to_string(),\n )\n }\n\n pub fn aggregate_prompt_chars(defs: &[ToolDefinition]) -> usize {\n defs.iter().map(Self::prompt_chars).sum()\n }\n\n pub fn aggregate_prompt_token_estimate(defs: &[ToolDefinition]) -> usize {\n defs.iter().map(Self::prompt_token_estimate).sum()\n }\n}\n\n/// A tool call from the model\n#[derive(Debug, Clone, Default, PartialEq, Eq, serde::Serialize, serde::Deserialize)]\npub struct ToolCall {\n #[serde(default)]\n pub id: String,\n #[serde(default)]\n pub name: String,\n #[serde(default)]\n pub input: serde_json::Value,\n #[serde(default, skip_serializing_if = \"Option::is_none\")]\n pub intent: Option<String>,\n}\n\nimpl ToolCall {\n pub fn intent_from_input(input: &serde_json::Value) -> Option<String> {\n input\n .get(\"intent\")\n .and_then(|value| value.as_str())\n .map(str::trim)\n .filter(|intent| !intent.is_empty())\n .map(ToString::to_string)\n }\n\n pub fn refresh_intent_from_input(&mut self) {\n self.intent = Self::intent_from_input(&self.input);\n }\n}\n\n/// Connection phase for status bar transparency\n#[derive(Debug, Clone, PartialEq)]\npub enum ConnectionPhase {\n /// Refreshing OAuth token\n Authenticating,\n /// TCP + TLS connection to API\n Connecting,\n /// HTTP request sent, waiting for first response byte\n WaitingForResponse,\n /// First byte received, stream is active\n Streaming,\n /// Retrying after a transient error\n Retrying { attempt: u32, max: u32 },\n}\n\nimpl std::fmt::Display for ConnectionPhase {\n fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {\n match self {\n ConnectionPhase::Authenticating => write!(f, \"authenticating\"),\n ConnectionPhase::Connecting => write!(f, \"connecting\"),\n ConnectionPhase::WaitingForResponse => write!(f, \"waiting for response\"),\n ConnectionPhase::Streaming => write!(f, \"streaming\"),\n ConnectionPhase::Retrying { attempt, max } => {\n write!(f, \"retrying ({}/{})\", attempt, max)\n }\n }\n }\n}\n\n/// Streaming event from provider\n#[derive(Debug, Clone)]\npub enum StreamEvent {\n /// Text content delta\n TextDelta(String),\n /// Tool use started\n ToolUseStart { id: String, name: String },\n /// Tool input delta (JSON fragment)\n ToolInputDelta(String),\n /// Tool use complete\n ToolUseEnd,\n /// Tool result from provider (provider already executed the tool)\n ToolResult {\n tool_use_id: String,\n content: String,\n is_error: bool,\n },\n /// Extended thinking started\n ThinkingStart,\n /// Extended thinking delta (reasoning content)\n ThinkingDelta(String),\n /// Extended thinking ended\n ThinkingEnd,\n /// Extended thinking completed with duration\n ThinkingDone { duration_secs: f64 },\n /// Message complete (may have stop reason)\n MessageEnd { stop_reason: Option<String> },\n /// Token usage update\n TokenUsage {\n input_tokens: Option<u64>,\n output_tokens: Option<u64>,\n cache_read_input_tokens: Option<u64>,\n cache_creation_input_tokens: Option<u64>,\n },\n /// Active transport/connection type for this stream\n ConnectionType { connection: String },\n /// Connection phase update (for status bar transparency)\n ConnectionPhase { phase: ConnectionPhase },\n /// Provider-supplied human-readable transport detail for the status line\n StatusDetail { detail: String },\n /// Error occurred\n Error {\n message: String,\n /// Seconds until rate limit resets (if this is a rate limit error)\n retry_after_secs: Option<u64>,\n },\n /// Provider session ID (for conversation resume)\n SessionId(String),\n /// Compaction occurred (context was summarized)\n Compaction {\n trigger: String,\n pre_tokens: Option<u64>,\n /// Provider-native compaction artifact, if one was emitted.\n openai_encrypted_content: Option<String>,\n },\n /// Upstream provider info (e.g., which provider OpenRouter routed to)\n UpstreamProvider { provider: String },\n /// Native tool call from a provider bridge that needs execution by jcode\n NativeToolCall {\n request_id: String,\n tool_name: String,\n input: serde_json::Value,\n },\n}\n\n#[cfg(test)]\nmod tests {\n use super::*;\n\n #[test]\n fn sanitize_tool_id_alphanumeric_passthrough() {\n assert_eq!(\n sanitize_tool_id(\"toolu_01XFDUDYJgAACzvnptvVer6u\"),\n \"toolu_01XFDUDYJgAACzvnptvVer6u\"\n );\n assert_eq!(sanitize_tool_id(\"call_abc123\"), \"call_abc123\");\n assert_eq!(\n sanitize_tool_id(\"call_1234567890_9876543210\"),\n \"call_1234567890_9876543210\"\n );\n }\n\n #[test]\n fn tool_call_intent_from_input_trims_optional_intent() {\n let input = serde_json::json!({\n \"intent\": \" Verify compact rendering \",\n \"command\": \"cargo test\"\n });\n assert_eq!(\n ToolCall::intent_from_input(&input).as_deref(),\n Some(\"Verify compact rendering\")\n );\n assert_eq!(\n ToolCall::intent_from_input(&serde_json::json!({\"intent\": \" \"})),\n None\n );\n }\n\n #[test]\n fn sanitize_tool_id_hyphens_passthrough() {\n assert_eq!(sanitize_tool_id(\"call-abc-123\"), \"call-abc-123\");\n assert_eq!(\n sanitize_tool_id(\"tool_use-id_with-mixed\"),\n \"tool_use-id_with-mixed\"\n );\n }\n\n #[test]\n fn sanitize_tool_id_replaces_dots() {\n assert_eq!(\n sanitize_tool_id(\"chatcmpl-abc.def.ghi\"),\n \"chatcmpl-abc_def_ghi\"\n );\n assert_eq!(sanitize_tool_id(\"call.123\"), \"call_123\");\n }\n\n #[test]\n fn sanitize_tool_id_replaces_colons() {\n assert_eq!(sanitize_tool_id(\"call:123:456\"), \"call_123_456\");\n }\n\n #[test]\n fn sanitize_tool_id_replaces_special_chars() {\n assert_eq!(\n sanitize_tool_id(\"id@with#special$chars\"),\n \"id_with_special_chars\"\n );\n assert_eq!(sanitize_tool_id(\"id with spaces\"), \"id_with_spaces\");\n }\n\n #[test]\n fn sanitize_tool_id_empty_returns_unknown() {\n assert_eq!(sanitize_tool_id(\"\"), \"unknown\");\n }\n\n #[test]\n fn sanitize_tool_id_copilot_to_anthropic() {\n assert_eq!(\n sanitize_tool_id(\"chatcmpl-BF2xX.tool_call.0\"),\n \"chatcmpl-BF2xX_tool_call_0\"\n );\n }\n\n #[test]\n fn sanitize_tool_id_already_valid_unchanged() {\n let valid_ids = [\n \"toolu_01XFDUDYJgAACzvnptvVer6u\",\n \"call_abc123\",\n \"fallback_text_call_call_1234567890_9876543210\",\n \"tool_123\",\n \"a\",\n \"A\",\n \"0\",\n \"_\",\n \"-\",\n \"a-b_c\",\n ];\n for id in valid_ids {\n assert_eq!(sanitize_tool_id(id), id, \"ID '{}' should be unchanged\", id);\n }\n }\n\n #[test]\n fn redact_secrets_redacts_known_direct_token_formats() {\n let input = \"access=sk-ant-oat01-ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789\\nopenrouter=sk-or-v1-abcdefghijklmnopqrstuvwxyz0123456789\\ngithub=ghp_ABCDEFGHIJKLMNOPQRSTUVWXYZ0123\\n\";\n let out = redact_secrets(input);\n assert!(!out.contains(\"sk-ant-oat01-\"));\n assert!(!out.contains(\"sk-or-v1-\"));\n assert!(!out.contains(\"ghp_ABCDEFGHIJKLMNOPQRSTUVWXYZ0123\"));\n assert!(out.matches(\"[REDACTED_SECRET]\").count() >= 3);\n }\n\n #[test]\n fn redact_secrets_redacts_env_style_assignments() {\n let input = \"OPENROUTER_API_KEY=sk-or-v1-abc123abc123abc123abc123\\nOPENCODE_API_KEY=oc_test_secret\\nOPENCODE_GO_API_KEY=ocgo_test_secret\\nZAI_API_KEY=zai_secret\\nCHUTES_API_KEY=chutes_secret\\nCEREBRAS_API_KEY=cerebras_secret\\nOPENAI_COMPAT_API_KEY=compat_secret\\nCURSOR_API_KEY='my_cursor_secret_value'\\nOPENAI_API_KEY=sk-test-openai-example\\nAZURE_OPENAI_API_KEY=azure-openai-secret\\n\";\n let out = redact_secrets(input);\n assert!(out.contains(\"OPENROUTER_API_KEY=[REDACTED_SECRET]\"));\n assert!(out.contains(\"OPENCODE_API_KEY=[REDACTED_SECRET]\"));\n assert!(out.contains(\"OPENCODE_GO_API_KEY=[REDACTED_SECRET]\"));\n assert!(out.contains(\"ZAI_API_KEY=[REDACTED_SECRET]\"));\n assert!(out.contains(\"CHUTES_API_KEY=[REDACTED_SECRET]\"));\n assert!(out.contains(\"CEREBRAS_API_KEY=[REDACTED_SECRET]\"));\n assert!(out.contains(\"OPENAI_COMPAT_API_KEY=[REDACTED_SECRET]\"));\n assert!(out.contains(\"CURSOR_API_KEY=[REDACTED_SECRET]\"));\n assert!(out.contains(\"OPENAI_API_KEY=[REDACTED_SECRET]\"));\n assert!(out.contains(\"AZURE_OPENAI_API_KEY=[REDACTED_SECRET]\"));\n assert!(!out.contains(\"my_cursor_secret_value\"));\n }\n\n #[test]\n fn redact_secrets_redacts_runtime_key_assignment() {\n let key_var = \"JCODE_OPENAI_COMPAT_API_KEY_NAME\";\n let prev = std::env::var(key_var).ok();\n crate::env::set_var(key_var, \"GROQ_API_KEY\");\n\n let input = \"GROQ_API_KEY=my_secret_token_value\";\n let out = redact_secrets(input);\n assert_eq!(out, \"GROQ_API_KEY=[REDACTED_SECRET]\");\n\n if let Some(v) = prev {\n crate::env::set_var(key_var, v);\n } else {\n crate::env::remove_var(key_var);\n }\n }\n\n #[test]\n fn redact_secrets_redacts_mixed_case_token_assignments() {\n let input = \"my_token=ya29.ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789\";\n let out = redact_secrets(input);\n assert!(out.contains(\"[REDACTED_SECRET]\"));\n assert!(!out.contains(\"ya29.ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789\"));\n }\n\n #[test]\n fn redact_secrets_leaves_normal_output_unchanged() {\n let input = \"Found 5 files\\nNo auth errors\\nDone.\";\n assert_eq!(redact_secrets(input), input);\n }\n\n #[test]\n fn format_timestamp_is_stable_utc_rfc3339() {\n let ts = chrono::DateTime::parse_from_rfc3339(\"2025-03-15T02:24:13.250Z\")\n .unwrap()\n .with_timezone(&Utc);\n assert_eq!(Message::format_timestamp(&ts), \"2025-03-15T02:24:13.250Z\");\n }\n\n #[test]\n fn with_timestamps_prepends_utc_prefix_to_user_text() {\n let ts = chrono::DateTime::parse_from_rfc3339(\"2025-03-15T02:24:03Z\")\n .unwrap()\n .with_timezone(&Utc);\n let stamped = Message::with_timestamps(&[Message {\n role: Role::User,\n content: vec![ContentBlock::Text {\n text: \"hello\".to_string(),\n cache_control: None,\n }],\n timestamp: Some(ts),\n tool_duration_ms: None,\n }]);\n match &stamped[0].content[0] {\n ContentBlock::Text { text, .. } => {\n assert_eq!(text, \"[2025-03-15T02:24:03.000Z] hello\");\n }\n other => panic!(\"expected text block, got {other:?}\"),\n }\n }\n\n #[test]\n fn with_timestamps_adds_tool_timing_header_with_duration() {\n let ts = chrono::DateTime::parse_from_rfc3339(\"2025-03-15T02:24:13Z\")\n .unwrap()\n .with_timezone(&Utc);\n let stamped = Message::with_timestamps(&[Message {\n role: Role::User,\n content: vec![ContentBlock::ToolResult {\n tool_use_id: \"call_1\".to_string(),\n content: \"ok\".to_string(),\n is_error: None,\n }],\n timestamp: Some(ts),\n tool_duration_ms: Some(3_200),\n }]);\n match &stamped[0].content[0] {\n ContentBlock::ToolResult { content, .. } => {\n assert_eq!(\n content,\n \"[tool timing: start=2025-03-15T02:24:09.800Z finish=2025-03-15T02:24:13.000Z duration=3.2s] ok\"\n );\n }\n other => panic!(\"expected tool result block, got {other:?}\"),\n }\n }\n\n #[test]\n fn with_timestamps_skips_internal_system_reminders() {\n let ts = chrono::DateTime::parse_from_rfc3339(\"2025-03-15T02:24:13Z\")\n .unwrap()\n .with_timezone(&Utc);\n let original = Message {\n role: Role::User,\n content: vec![ContentBlock::Text {\n text: \"<system-reminder>\\ninternal\\n</system-reminder>\".to_string(),\n cache_control: None,\n }],\n timestamp: Some(ts),\n tool_duration_ms: None,\n };\n let stamped = Message::with_timestamps(std::slice::from_ref(&original));\n match &stamped[0].content[0] {\n ContentBlock::Text { text, .. } => {\n assert_eq!(text, \"<system-reminder>\\ninternal\\n</system-reminder>\");\n }\n other => panic!(\"expected text block, got {other:?}\"),\n }\n }\n\n #[test]\n fn ends_with_fresh_user_turn_accepts_plain_user_text() {\n let messages = vec![Message::user(\"hello\")];\n assert!(ends_with_fresh_user_turn(&messages));\n }\n\n #[test]\n fn ends_with_fresh_user_turn_rejects_trailing_tool_result() {\n let messages = vec![\n Message::user(\"hello\"),\n Message {\n role: Role::Assistant,\n content: vec![ContentBlock::ToolUse {\n id: \"call_1\".to_string(),\n name: \"bash\".to_string(),\n input: serde_json::json!({}),\n }],\n timestamp: Some(Utc::now()),\n tool_duration_ms: None,\n },\n Message::tool_result(\"call_1\", \"ok\", false),\n ];\n assert!(!ends_with_fresh_user_turn(&messages));\n }\n\n #[test]\n fn ends_with_fresh_user_turn_skips_internal_system_reminders() {\n let messages = vec![\n Message::user(\"hello\"),\n Message::user(\"<system-reminder>\\ninternal\\n</system-reminder>\"),\n ];\n assert!(ends_with_fresh_user_turn(&messages));\n }\n\n #[test]\n fn ends_with_fresh_user_turn_rejects_assistant_tail() {\n let messages = vec![\n Message::user(\"hello\"),\n Message::assistant_text(\"working on it\"),\n ];\n assert!(!ends_with_fresh_user_turn(&messages));\n }\n\n #[test]\n fn format_background_task_notification_markdown_uses_code_block_preview() {\n let rendered = format_background_task_notification_markdown(&BackgroundTaskCompleted {\n task_id: \"abc123\".to_string(),\n tool_name: \"bash\".to_string(),\n display_name: None,\n session_id: \"session\".to_string(),\n status: BackgroundTaskStatus::Completed,\n exit_code: Some(0),\n output_preview: \"[stderr] first line\\n[stdout] second line\\n\".to_string(),\n output_file: std::path::PathBuf::from(\"/tmp/output.log\"),\n duration_secs: 7.1,\n notify: true,\n wake: false,\n });\n\n assert!(\n rendered\n .contains(\"**Background task** `abc123` · `bash` · ✓ completed · 7.1s · exit 0\")\n );\n assert!(rendered.contains(\"```text\\n[stderr] first line\\n[stdout] second line\\n```\"));\n assert!(rendered.contains(\"_Full output:_ `bg action=\\\"output\\\" task_id=\\\"abc123\\\"`\"));\n }\n\n #[test]\n fn format_background_task_notification_markdown_handles_empty_preview() {\n let rendered = format_background_task_notification_markdown(&BackgroundTaskCompleted {\n task_id: \"abc123\".to_string(),\n tool_name: \"bash\".to_string(),\n display_name: None,\n session_id: \"session\".to_string(),\n status: BackgroundTaskStatus::Failed,\n exit_code: Some(9),\n output_preview: \"\\n\\n\".to_string(),\n output_file: std::path::PathBuf::from(\"/tmp/output.log\"),\n duration_secs: 1.0,\n notify: true,\n wake: false,\n });\n\n assert!(rendered.contains(\"✗ failed\"));\n assert!(rendered.contains(\"_No output captured._\"));\n }\n\n #[test]\n fn format_background_task_notification_markdown_highlights_failure_reason() {\n let rendered = format_background_task_notification_markdown(&BackgroundTaskCompleted {\n task_id: \"build123\".to_string(),\n tool_name: \"selfdev-build\".to_string(),\n display_name: Some(\"Build jcode\".to_string()),\n session_id: \"session\".to_string(),\n status: BackgroundTaskStatus::Failed,\n exit_code: Some(101),\n output_preview: \"[stderr] Compiling jcode\\nsccache: Compile terminated by signal 15\\n[stderr] error: could not compile `jcode` (lib)\".to_string(),\n output_file: std::path::PathBuf::from(\"/tmp/output.log\"),\n duration_secs: 62.5,\n notify: true,\n wake: false,\n });\n\n assert!(rendered.contains(\"_Failure:_ sccache: Compile terminated by signal 15\"));\n let parsed = parse_background_task_notification_markdown(&rendered)\n .expect(\"failure notification should parse\");\n assert_eq!(\n parsed.failure_summary.as_deref(),\n Some(\"sccache: Compile terminated by signal 15\")\n );\n }\n\n #[test]\n fn format_background_task_notification_markdown_renders_superseded_status() {\n let rendered = format_background_task_notification_markdown(&BackgroundTaskCompleted {\n task_id: \"abc123\".to_string(),\n tool_name: \"selfdev-build\".to_string(),\n display_name: None,\n session_id: \"session\".to_string(),\n status: BackgroundTaskStatus::Superseded,\n exit_code: Some(0),\n output_preview: \"Build completed, but source changed before activation\".to_string(),\n output_file: std::path::PathBuf::from(\"/tmp/output.log\"),\n duration_secs: 5.0,\n notify: true,\n wake: false,\n });\n\n assert!(rendered.contains(\"↻ superseded\"));\n assert!(rendered.contains(\"exit 0\"));\n assert!(rendered.contains(\"source changed before activation\"));\n }\n\n #[test]\n fn format_background_task_progress_markdown_uses_compact_multiline_layout() {\n let rendered = format_background_task_progress_markdown(&BackgroundTaskProgressEvent {\n task_id: \"bgprogress\".to_string(),\n tool_name: \"bash\".to_string(),\n display_name: None,\n session_id: \"session\".to_string(),\n progress: crate::bus::BackgroundTaskProgress {\n kind: crate::bus::BackgroundTaskProgressKind::Determinate,\n percent: Some(42.0),\n message: Some(\"Running tests\".to_string()),\n current: Some(21),\n total: Some(50),\n unit: Some(\"tests\".to_string()),\n eta_seconds: None,\n updated_at: Utc::now().to_rfc3339(),\n source: crate::bus::BackgroundTaskProgressSource::Reported,\n },\n });\n\n assert!(rendered.starts_with(\"**Background task progress** `bgprogress` · `bash`\\n\\n\"));\n assert!(rendered.contains(\"42% · Running tests\"));\n assert!(rendered.contains(\"(reported)\"));\n }\n\n #[test]\n fn background_task_notifications_include_display_name_when_available() {\n let rendered = format_background_task_notification_markdown(&BackgroundTaskCompleted {\n task_id: \"abc123\".to_string(),\n tool_name: \"bash\".to_string(),\n display_name: Some(\"Run integration tests\".to_string()),\n session_id: \"session\".to_string(),\n status: BackgroundTaskStatus::Completed,\n exit_code: Some(0),\n output_preview: \"done\".to_string(),\n output_file: std::path::PathBuf::from(\"/tmp/output.log\"),\n duration_secs: 7.1,\n notify: true,\n wake: false,\n });\n\n assert!(rendered.contains(\n \"**Background task** `abc123` · `Run integration tests` (`bash`) · ✓ completed\"\n ));\n let parsed = parse_background_task_notification_markdown(&rendered)\n .expect(\"named background task notification should parse\");\n assert_eq!(parsed.tool_name, \"bash\");\n assert_eq!(\n parsed.display_name.as_deref(),\n Some(\"Run integration tests\")\n );\n }\n\n #[test]\n fn background_task_progress_notifications_include_display_name_when_available() {\n let rendered = format_background_task_progress_markdown(&BackgroundTaskProgressEvent {\n task_id: \"bgprogress\".to_string(),\n tool_name: \"bash\".to_string(),\n display_name: Some(\"Run integration tests\".to_string()),\n session_id: \"session\".to_string(),\n progress: crate::bus::BackgroundTaskProgress {\n kind: crate::bus::BackgroundTaskProgressKind::Determinate,\n percent: Some(42.0),\n message: Some(\"Running tests\".to_string()),\n current: Some(21),\n total: Some(50),\n unit: Some(\"tests\".to_string()),\n eta_seconds: None,\n updated_at: Utc::now().to_rfc3339(),\n source: crate::bus::BackgroundTaskProgressSource::Reported,\n },\n });\n\n assert!(rendered.starts_with(\n \"**Background task progress** `bgprogress` · `Run integration tests` (`bash`)\\n\\n\"\n ));\n let parsed = parse_background_task_progress_notification_markdown(&rendered)\n .expect(\"named progress notification should parse\");\n assert_eq!(parsed.tool_name, \"bash\");\n assert_eq!(\n parsed.display_name.as_deref(),\n Some(\"Run integration tests\")\n );\n assert_eq!(parsed.summary, \"42% · Running tests\");\n }\n\n #[test]\n fn parse_background_task_progress_notification_extracts_card_fields() {\n let parsed = parse_background_task_progress_notification_markdown(\n \"**Background task progress** `bgprogress` · `bash`\\n\\n[#####-------] 42% · Running tests (reported)\",\n )\n .expect(\"progress notification should parse\");\n\n assert_eq!(parsed.task_id, \"bgprogress\");\n assert_eq!(parsed.tool_name, \"bash\");\n assert_eq!(parsed.display_name, None);\n assert_eq!(parsed.summary, \"42% · Running tests\");\n assert_eq!(parsed.source.as_deref(), Some(\"reported\"));\n assert_eq!(parsed.percent, Some(42.0));\n }\n\n #[test]\n fn parse_background_task_progress_notification_supports_legacy_inline_layout() {\n let parsed = parse_background_task_progress_notification_markdown(\n \"**Background task progress** `bgprogress` · `bash` · Release run in_progress: - 7/8 jobs completed (reported)\",\n )\n .expect(\"legacy progress notification should parse\");\n\n assert_eq!(parsed.task_id, \"bgprogress\");\n assert_eq!(parsed.tool_name, \"bash\");\n assert_eq!(parsed.display_name, None);\n assert_eq!(\n parsed.summary,\n \"Release run in_progress: - 7/8 jobs completed\"\n );\n assert_eq!(parsed.source.as_deref(), Some(\"reported\"));\n assert_eq!(parsed.percent, None);\n }\n\n #[test]\n fn description_token_estimate_uses_chars_per_token_heuristic() {\n let def = ToolDefinition {\n name: \"read\".to_string(),\n description: \"abcdwxyz\".to_string(),\n input_schema: serde_json::json!({\"type\": \"object\"}),\n };\n\n assert_eq!(def.description_token_estimate(), 2);\n }\n\n #[test]\n fn parse_background_task_notification_markdown_extracts_fields() {\n let rendered = format_background_task_notification_markdown(&BackgroundTaskCompleted {\n task_id: \"abc123\".to_string(),\n tool_name: \"bash\".to_string(),\n display_name: None,\n session_id: \"session\".to_string(),\n status: BackgroundTaskStatus::Completed,\n exit_code: Some(0),\n output_preview: \"[stderr] first line\\n[stdout] second line\\n\".to_string(),\n output_file: std::path::PathBuf::from(\"/tmp/output.log\"),\n duration_secs: 7.1,\n notify: true,\n wake: false,\n });\n\n let parsed = parse_background_task_notification_markdown(&rendered)\n .expect(\"background task notification should parse\");\n assert_eq!(parsed.task_id, \"abc123\");\n assert_eq!(parsed.tool_name, \"bash\");\n assert_eq!(parsed.display_name, None);\n assert_eq!(parsed.status, \"✓ completed\");\n assert_eq!(parsed.duration, \"7.1s\");\n assert_eq!(parsed.exit_label, \"exit 0\");\n assert_eq!(parsed.failure_summary, None);\n assert_eq!(\n parsed.preview.as_deref(),\n Some(\"[stderr] first line\\n[stdout] second line\")\n );\n assert_eq!(\n parsed.full_output_command,\n \"bg action=\\\"output\\\" task_id=\\\"abc123\\\"\"\n );\n }\n}\n"}, "e49d41ace6208c3c537a232ba38190b72f04edd7": {"t": 1785757224.787785, "n": 41914, "h": "19b02320f0d1369e", "c": "use crate::bus::{BackgroundTaskCompleted, BackgroundTaskProgressEvent, BackgroundTaskStatus};\nuse chrono::Utc;\nuse regex::Regex;\nuse serde::{Deserialize, Serialize};\nuse std::collections::HashSet;\nuse std::sync::OnceLock;\n\nmod notifications;\n\npub use notifications::{\n InputShellResult, ParsedBackgroundTaskNotification, ParsedBackgroundTaskProgressNotification,\n background_task_display_label, background_task_status_notice,\n format_background_task_notification_markdown, format_background_task_progress_markdown,\n format_input_shell_result_markdown, input_shell_status_notice,\n parse_background_task_notification_markdown,\n parse_background_task_progress_notification_markdown,\n};\n\nfn compile_static_regex(pattern: &str) -> Option<Regex> {\n match Regex::new(pattern) {\n Ok(regex) => Some(regex),\n Err(err) => {\n eprintln!(\"jcode: failed to compile static regex: {err}\");\n None\n }\n }\n}\n\nfn compile_static_regexes(patterns: &[&str]) -> Vec<Regex> {\n patterns\n .iter()\n .filter_map(|pattern| compile_static_regex(pattern))\n .collect()\n}\n\n/// Role in conversation\n#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]\n#[serde(rename_all = \"lowercase\")]\npub enum Role {\n User,\n Assistant,\n}\n\n/// Plain-text tool output placeholder when execution was interrupted.\npub const TOOL_OUTPUT_MISSING_TEXT: &str =\n \"Tool output missing (session interrupted before tool execution completed)\";\n\n/// A message in the conversation\n#[derive(Debug, Clone, Serialize, Deserialize)]\npub struct Message {\n pub role: Role,\n pub content: Vec<ContentBlock>,\n #[serde(default, skip_serializing_if = \"Option::is_none\")]\n pub timestamp: Option<chrono::DateTime<Utc>>,\n #[serde(default, skip_serializing_if = \"Option::is_none\")]\n pub tool_duration_ms: Option<u64>,\n}\n\n/// Cache control metadata for prompt caching\n#[derive(Debug, Clone, Serialize, Deserialize)]\npub struct CacheControl {\n #[serde(rename = \"type\")]\n pub kind: String,\n #[serde(skip_serializing_if = \"Option::is_none\")]\n pub ttl: Option<String>,\n}\n\nimpl CacheControl {\n pub fn ephemeral(ttl: Option<String>) -> Self {\n Self {\n kind: \"ephemeral\".to_string(),\n ttl,\n }\n }\n}\n\n/// Content block within a message\n#[derive(Debug, Clone, Serialize, Deserialize)]\n#[serde(tag = \"type\", rename_all = \"snake_case\")]\npub enum ContentBlock {\n Text {\n text: String,\n #[serde(skip_serializing_if = \"Option::is_none\")]\n cache_control: Option<CacheControl>,\n },\n /// Hidden reasoning content used for providers that require it (not displayed)\n Reasoning {\n text: String,\n },\n ToolUse {\n id: String,\n name: String,\n input: serde_json::Value,\n },\n ToolResult {\n tool_use_id: String,\n content: String,\n #[serde(skip_serializing_if = \"Option::is_none\")]\n is_error: Option<bool>,\n },\n Image {\n media_type: String,\n data: String,\n },\n /// Hidden OpenAI Responses compaction item used to preserve native\n /// compaction state across turns/saves when jcode explicitly triggers it.\n OpenAICompaction {\n encrypted_content: String,\n },\n}\n\nimpl Message {\n pub fn user(text: &str) -> Self {\n Self {\n role: Role::User,\n content: vec![ContentBlock::Text {\n text: text.to_string(),\n cache_control: None,\n }],\n timestamp: Some(Utc::now()),\n tool_duration_ms: None,\n }\n }\n\n pub fn user_with_images(text: &str, images: Vec<(String, String)>) -> Self {\n let mut content: Vec<ContentBlock> = images\n .into_iter()\n .map(|(media_type, data)| ContentBlock::Image { media_type, data })\n .collect();\n content.push(ContentBlock::Text {\n text: text.to_string(),\n cache_control: None,\n });\n Self {\n role: Role::User,\n content,\n timestamp: Some(Utc::now()),\n tool_duration_ms: None,\n }\n }\n\n pub fn assistant_text(text: &str) -> Self {\n Self {\n role: Role::Assistant,\n content: vec![ContentBlock::Text {\n text: text.to_string(),\n cache_control: None,\n }],\n timestamp: Some(Utc::now()),\n tool_duration_ms: None,\n }\n }\n\n pub fn tool_result(tool_use_id: &str, content: &str, is_error: bool) -> Self {\n Self::tool_result_with_duration(tool_use_id, content, is_error, None)\n }\n\n pub fn tool_result_with_duration(\n tool_use_id: &str,\n content: &str,\n is_error: bool,\n tool_duration_ms: Option<u64>,\n ) -> Self {\n Self {\n role: Role::User,\n content: vec![ContentBlock::ToolResult {\n tool_use_id: tool_use_id.to_string(),\n content: content.to_string(),\n is_error: if is_error { Some(true) } else { None },\n }],\n timestamp: Some(Utc::now()),\n tool_duration_ms,\n }\n }\n\n /// Format a timestamp deterministically in UTC for injection into model-visible content.\n pub fn format_timestamp(ts: &chrono::DateTime<Utc>) -> String {\n ts.to_rfc3339_opts(chrono::SecondsFormat::Millis, true)\n }\n\n pub fn format_duration(duration_ms: u64) -> String {\n match duration_ms {\n 0..=999 => format!(\"{}ms\", duration_ms),\n 1_000..=9_999 => format!(\"{:.1}s\", duration_ms as f64 / 1000.0),\n 10_000..=59_999 => format!(\"{}s\", duration_ms / 1000),\n _ => {\n let total_seconds = duration_ms / 1000;\n let minutes = total_seconds / 60;\n let seconds = total_seconds % 60;\n if seconds == 0 {\n format!(\"{}m\", minutes)\n } else {\n format!(\"{}m {}s\", minutes, seconds)\n }\n }\n }\n }\n\n pub fn is_internal_system_reminder(&self) -> bool {\n self.content\n .iter()\n .find_map(|block| match block {\n ContentBlock::Text { text, .. } => Some(text.trim_start()),\n _ => None,\n })\n .is_some_and(|text| text.starts_with(\"<system-reminder>\"))\n }\n\n fn should_skip_timestamp_injection(&self) -> bool {\n self.is_internal_system_reminder()\n }\n\n fn tool_result_tag(&self, ts: &chrono::DateTime<Utc>) -> String {\n match self.tool_duration_ms {\n Some(duration_ms) => {\n let duration_ms_i64 = i64::try_from(duration_ms).unwrap_or(i64::MAX);\n let start_ts = ts\n .checked_sub_signed(chrono::Duration::milliseconds(duration_ms_i64))\n .unwrap_or(*ts);\n format!(\n \"[tool timing: start={} finish={} duration={}]\",\n Self::format_timestamp(&start_ts),\n Self::format_timestamp(ts),\n Self::format_duration(duration_ms)\n )\n }\n None => format!(\"[{}]\", Self::format_timestamp(ts)),\n }\n }\n\n /// Return a copy of messages with timestamps injected into user-role text content.\n /// Tool results get a stable UTC timing header prepended to content.\n /// User text messages get a stable UTC timestamp prepended to the first text block.\n pub fn with_timestamps(messages: &[Message]) -> Vec<Message> {\n messages\n .iter()\n .map(|msg| {\n let Some(ts) = msg.timestamp else {\n return msg.clone();\n };\n if msg.role != Role::User || msg.should_skip_timestamp_injection() {\n return msg.clone();\n }\n let text_tag = format!(\"[{}]\", Self::format_timestamp(&ts));\n let tool_result_tag = msg.tool_result_tag(&ts);\n let mut msg = msg.clone();\n let mut tagged = false;\n for block in &mut msg.content {\n match block {\n ContentBlock::Text { text, .. } if !tagged => {\n *text = format!(\"{} {}\", text_tag, text);\n tagged = true;\n }\n ContentBlock::ToolResult { content, .. } if !tagged => {\n *content = format!(\"{} {}\", tool_result_tag, content);\n tagged = true;\n }\n _ => {}\n }\n }\n msg\n })\n .collect()\n }\n}\n\nconst STABLE_HASH_SEED: u64 = 0xcbf29ce484222325;\nconst STABLE_HASH_PRIME: u64 = 0x100000001b3;\n\nfn stable_hash_bytes(bytes: &[u8]) -> u64 {\n let mut hash = STABLE_HASH_SEED;\n for byte in bytes {\n hash ^= *byte as u64;\n hash = hash.wrapping_mul(STABLE_HASH_PRIME);\n }\n hash\n}\n\npub(crate) fn extend_stable_hash(acc: u64, next: u64) -> u64 {\n stable_hash_bytes(&[acc.to_le_bytes().as_slice(), next.to_le_bytes().as_slice()].concat())\n}\n\npub(crate) fn stable_message_hash(message: &Message) -> u64 {\n match serde_json::to_vec(message) {\n Ok(bytes) => stable_hash_bytes(&bytes),\n Err(_) => stable_hash_bytes(format!(\"{:?}\", message).as_bytes()),\n }\n}\n\npub fn ends_with_fresh_user_turn(messages: &[Message]) -> bool {\n for msg in messages.iter().rev() {\n if msg.role != Role::User {\n return false;\n }\n\n if msg\n .content\n .iter()\n .any(|block| matches!(block, ContentBlock::ToolResult { .. }))\n {\n return false;\n }\n\n if msg.content.is_empty() {\n return false;\n }\n\n let mut saw_user_text = false;\n for block in &msg.content {\n match block {\n ContentBlock::Text { text, .. } => {\n let trimmed = text.trim();\n if !trimmed.is_empty() && !trimmed.starts_with(\"<system-reminder>\") {\n saw_user_text = true;\n }\n }\n _ => return false,\n }\n }\n\n if saw_user_text {\n return true;\n }\n\n if msg.is_internal_system_reminder() {\n continue;\n }\n\n return false;\n }\n\n false\n}\n\n/// Sanitize a tool ID so it matches the pattern `^[a-zA-Z0-9_-]+$`.\n///\n/// Different providers generate tool IDs in different formats. When switching\n/// from one provider to another mid-conversation, the historical tool IDs may\n/// contain characters that the new provider rejects (e.g., dots in Copilot IDs\n/// sent to Anthropic). This function replaces any invalid characters with\n/// underscores.\npub fn sanitize_tool_id(id: &str) -> String {\n if id.is_empty() {\n return \"unknown\".to_string();\n }\n let sanitized: String = id\n .chars()\n .map(|c| {\n if c.is_ascii_alphanumeric() || c == '_' || c == '-' {\n c\n } else {\n '_'\n }\n })\n .collect();\n if sanitized.is_empty() {\n \"unknown\".to_string()\n } else {\n sanitized\n }\n}\n\n/// Redact likely secrets from persisted tool output.\n///\n/// This is a best-effort safeguard for local session history files. It targets\n/// high-confidence token/key patterns and common `KEY=VALUE` assignments used by\n/// auth flows.\npub fn redact_secrets(text: &str) -> String {\n // Fast path to avoid regex work for most tool outputs.\n let lower = text.to_ascii_lowercase();\n\n if !text.contains(\"sk-\")\n && !text.contains(\"ghp_\")\n && !text.contains(\"github_pat_\")\n && !text.contains(\"AIza\")\n && !text.contains(\"ya29.\")\n && !text.contains(\"xox\")\n && !lower.contains(\"api_key\")\n && !lower.contains(\"token\")\n {\n return text.to_string();\n }\n\n static DIRECT_PATTERNS: OnceLock<Vec<Regex>> = OnceLock::new();\n static ASSIGNMENT_PATTERNS: OnceLock<Vec<Regex>> = OnceLock::new();\n\n let direct_patterns = DIRECT_PATTERNS.get_or_init(|| {\n compile_static_regexes(&[\n r\"sk-ant-(?:oat|ort)01-[A-Za-z0-9_-]{20,}\",\n r\"sk-or-v1-[A-Za-z0-9_-]{20,}\",\n r\"ghp_[A-Za-z0-9]{20,}\",\n r\"github_pat_[A-Za-z0-9_]{20,}\",\n r\"ya29\\.[A-Za-z0-9._-]{20,}\",\n r\"AIza[0-9A-Za-z_-]{20,}\",\n r\"xox[baprs]-[A-Za-z0-9-]{10,}\",\n ])\n });\n\n let assignment_patterns = ASSIGNMENT_PATTERNS.get_or_init(|| {\n compile_static_regexes(&[\n r\"(?m)^\\s*(OPENROUTER_API_KEY\\s*=\\s*)[^\\r\\n]+\",\n r\"(?m)^\\s*(OPENCODE_API_KEY\\s*=\\s*)[^\\r\\n]+\",\n r\"(?m)^\\s*(OPENCODE_GO_API_KEY\\s*=\\s*)[^\\r\\n]+\",\n r\"(?m)^\\s*(ZHIPU_API_KEY\\s*=\\s*)[^\\r\\n]+\",\n r\"(?m)^\\s*(ZAI_API_KEY\\s*=\\s*)[^\\r\\n]+\",\n r\"(?m)^\\s*(302AI_API_KEY\\s*=\\s*)[^\\r\\n]+\",\n r\"(?m)^\\s*(BASETEN_API_KEY\\s*=\\s*)[^\\r\\n]+\",\n r\"(?m)^\\s*(CORTECS_API_KEY\\s*=\\s*)[^\\r\\n]+\",\n r\"(?m)^\\s*(DEEPSEEK_API_KEY\\s*=\\s*)[^\\r\\n]+\",\n r\"(?m)^\\s*(FIRMWARE_API_KEY\\s*=\\s*)[^\\r\\n]+\",\n r\"(?m)^\\s*(HF_TOKEN\\s*=\\s*)[^\\r\\n]+\",\n r\"(?m)^\\s*(MOONSHOT_API_KEY\\s*=\\s*)[^\\r\\n]+\",\n r\"(?m)^\\s*(NEBIUS_API_KEY\\s*=\\s*)[^\\r\\n]+\",\n r\"(?m)^\\s*(SCALEWAY_API_KEY\\s*=\\s*)[^\\r\\n]+\",\n r\"(?m)^\\s*(STACKIT_API_KEY\\s*=\\s*)[^\\r\\n]+\",\n r\"(?m)^\\s*(GROQ_API_KEY\\s*=\\s*)[^\\r\\n]+\",\n r\"(?m)^\\s*(MISTRAL_API_KEY\\s*=\\s*)[^\\r\\n]+\",\n r\"(?m)^\\s*(PERPLEXITY_API_KEY\\s*=\\s*)[^\\r\\n]+\",\n r\"(?m)^\\s*(TOGETHER_API_KEY\\s*=\\s*)[^\\r\\n]+\",\n r\"(?m)^\\s*(DEEPINFRA_API_KEY\\s*=\\s*)[^\\r\\n]+\",\n r\"(?m)^\\s*(XAI_API_KEY\\s*=\\s*)[^\\r\\n]+\",\n r\"(?m)^\\s*(LMSTUDIO_API_KEY\\s*=\\s*)[^\\r\\n]+\",\n r\"(?m)^\\s*(OLLAMA_API_KEY\\s*=\\s*)[^\\r\\n]+\",\n r\"(?m)^\\s*(CHUTES_API_KEY\\s*=\\s*)[^\\r\\n]+\",\n r\"(?m)^\\s*(CEREBRAS_API_KEY\\s*=\\s*)[^\\r\\n]+\",\n r\"(?m)^\\s*(OPENAI_COMPAT_API_KEY\\s*=\\s*)[^\\r\\n]+\",\n r\"(?m)^\\s*(ANTHROPIC_API_KEY\\s*=\\s*)[^\\r\\n]+\",\n r\"(?m)^\\s*(OPENAI_API_KEY\\s*=\\s*)[^\\r\\n]+\",\n r\"(?m)^\\s*(AZURE_OPENAI_API_KEY\\s*=\\s*)[^\\r\\n]+\",\n r\"(?m)^\\s*(CURSOR_API_KEY\\s*=\\s*)[^\\r\\n]+\",\n r\"(?m)^\\s*(GITHUB_TOKEN\\s*=\\s*)[^\\r\\n]+\",\n ])\n });\n\n let mut redacted = text.to_string();\n let mut redacted_keys: HashSet<String> = [\n \"OPENROUTER_API_KEY\",\n \"OPENCODE_API_KEY\",\n \"OPENCODE_GO_API_KEY\",\n \"ZHIPU_API_KEY\",\n \"ZAI_API_KEY\",\n \"302AI_API_KEY\",\n \"BASETEN_API_KEY\",\n \"CORTECS_API_KEY\",\n \"DEEPSEEK_API_KEY\",\n \"FIRMWARE_API_KEY\",\n \"HF_TOKEN\",\n \"MOONSHOT_API_KEY\",\n \"NEBIUS_API_KEY\",\n \"SCALEWAY_API_KEY\",\n \"STACKIT_API_KEY\",\n \"GROQ_API_KEY\",\n \"MISTRAL_API_KEY\",\n \"PERPLEXITY_API_KEY\",\n \"TOGETHER_API_KEY\",\n \"DEEPINFRA_API_KEY\",\n \"XAI_API_KEY\",\n \"LMSTUDIO_API_KEY\",\n \"OLLAMA_API_KEY\",\n \"CHUTES_API_KEY\",\n \"CEREBRAS_API_KEY\",\n \"OPENAI_COMPAT_API_KEY\",\n \"ANTHROPIC_API_KEY\",\n \"OPENAI_API_KEY\",\n \"AZURE_OPENAI_API_KEY\",\n \"CURSOR_API_KEY\",\n \"GITHUB_TOKEN\",\n ]\n .iter()\n .map(|k| (*k).to_string())\n .collect();\n\n for re in direct_patterns {\n redacted = re.replace_all(&redacted, \"[REDACTED_SECRET]\").into_owned();\n }\n\n for re in assignment_patterns {\n redacted = re\n .replace_all(&redacted, \"${1}[REDACTED_SECRET]\")\n .into_owned();\n }\n\n // Also redact custom API key variable names configured at runtime.\n for source in [\n \"JCODE_OPENROUTER_API_KEY_NAME\",\n \"JCODE_OPENAI_COMPAT_API_KEY_NAME\",\n ] {\n let Some(key_name) = std::env::var(source)\n .ok()\n .map(|v| v.trim().to_string())\n .filter(|v| !v.is_empty())\n else {\n continue;\n };\n\n if !key_name\n .chars()\n .all(|c| c.is_ascii_uppercase() || c.is_ascii_digit() || c == '_')\n {\n continue;\n }\n if !redacted_keys.insert(key_name.clone()) {\n continue;\n }\n\n let pattern = format!(r\"(?m)^\\s*({}\\s*=\\s*)[^\\r\\n]+\", regex::escape(&key_name));\n if let Ok(re) = Regex::new(&pattern) {\n redacted = re\n .replace_all(&redacted, \"${1}[REDACTED_SECRET]\")\n .into_owned();\n }\n }\n\n redacted\n}\n\n/// Tool definition for the API\n#[derive(Debug, Clone, Serialize)]\npub struct ToolDefinition {\n pub name: String,\n // Prompt-visible text sent to the model by provider adapters.\n // Approximate prompt cost: description.len() / 4. Use\n // ToolDefinition::description_token_estimate() when reviewing tool bloat.\n pub description: String,\n pub input_schema: serde_json::Value,\n}\n\nimpl ToolDefinition {\n /// Serialized size of the full tool definition payload sent to providers.\n pub fn prompt_chars(&self) -> usize {\n serde_json::json!({\n \"name\": self.name,\n \"description\": self.description,\n \"input_schema\": self.input_schema,\n })\n .to_string()\n .len()\n }\n\n /// Approximate prompt-token cost of this tool's top-level description.\n ///\n /// This uses jcode's standard chars/4 heuristic, matching other token\n /// budget estimates in the codebase.\n pub fn description_token_estimate(&self) -> usize {\n crate::util::estimate_tokens(&self.description)\n }\n\n /// Approximate prompt-token cost of the full tool definition payload.\n pub fn prompt_token_estimate(&self) -> usize {\n crate::util::estimate_tokens(\n &serde_json::json!({\n \"name\": self.name,\n \"description\": self.description,\n \"input_schema\": self.input_schema,\n })\n .to_string(),\n )\n }\n\n pub fn aggregate_prompt_chars(defs: &[ToolDefinition]) -> usize {\n defs.iter().map(Self::prompt_chars).sum()\n }\n\n pub fn aggregate_prompt_token_estimate(defs: &[ToolDefinition]) -> usize {\n defs.iter().map(Self::prompt_token_estimate).sum()\n }\n}\n\n/// A tool call from the model\n#[derive(Debug, Clone, Default, PartialEq, Eq, serde::Serialize, serde::Deserialize)]\npub struct ToolCall {\n #[serde(default)]\n pub id: String,\n #[serde(default)]\n pub name: String,\n #[serde(default)]\n pub input: serde_json::Value,\n #[serde(default, skip_serializing_if = \"Option::is_none\")]\n pub intent: Option<String>,\n}\n\nimpl ToolCall {\n pub fn intent_from_input(input: &serde_json::Value) -> Option<String> {\n input\n .get(\"intent\")\n .and_then(|value| value.as_str())\n .map(str::trim)\n .filter(|intent| !intent.is_empty())\n .map(ToString::to_string)\n }\n\n pub fn refresh_intent_from_input(&mut self) {\n self.intent = Self::intent_from_input(&self.input);\n }\n}\n\n/// Connection phase for status bar transparency\n#[derive(Debug, Clone, PartialEq)]\npub enum ConnectionPhase {\n /// Refreshing OAuth token\n Authenticating,\n /// TCP + TLS connection to API\n Connecting,\n /// HTTP request sent, waiting for first response byte\n WaitingForResponse,\n /// First byte received, stream is active\n Streaming,\n /// Retrying after a transient error\n Retrying { attempt: u32, max: u32 },\n}\n\nimpl std::fmt::Display for ConnectionPhase {\n fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {\n match self {\n ConnectionPhase::Authenticating => write!(f, \"authenticating\"),\n ConnectionPhase::Connecting => write!(f, \"connecting\"),\n ConnectionPhase::WaitingForResponse => write!(f, \"waiting for response\"),\n ConnectionPhase::Streaming => write!(f, \"streaming\"),\n ConnectionPhase::Retrying { attempt, max } => {\n write!(f, \"retrying ({}/{})\", attempt, max)\n }\n }\n }\n}\n\n/// Streaming event from provider\n#[derive(Debug, Clone)]\npub enum StreamEvent {\n /// Text content delta\n TextDelta(String),\n /// Tool use started\n ToolUseStart { id: String, name: String },\n /// Tool input delta (JSON fragment)\n ToolInputDelta(String),\n /// Tool use complete\n ToolUseEnd,\n /// Tool result from provider (provider already executed the tool)\n ToolResult {\n tool_use_id: String,\n content: String,\n is_error: bool,\n },\n /// Extended thinking started\n ThinkingStart,\n /// Extended thinking delta (reasoning content)\n ThinkingDelta(String),\n /// Extended thinking ended\n ThinkingEnd,\n /// Extended thinking completed with duration\n ThinkingDone { duration_secs: f64 },\n /// Message complete (may have stop reason)\n MessageEnd { stop_reason: Option<String> },\n /// Token usage update\n TokenUsage {\n input_tokens: Option<u64>,\n output_tokens: Option<u64>,\n cache_read_input_tokens: Option<u64>,\n cache_creation_input_tokens: Option<u64>,\n },\n /// Active transport/connection type for this stream\n ConnectionType { connection: String },\n /// Connection phase update (for status bar transparency)\n ConnectionPhase { phase: ConnectionPhase },\n /// Provider-supplied human-readable transport detail for the status line\n StatusDetail { detail: String },\n /// Error occurred\n Error {\n message: String,\n /// Seconds until rate limit resets (if this is a rate limit error)\n retry_after_secs: Option<u64>,\n },\n /// Provider session ID (for conversation resume)\n SessionId(String),\n /// Compaction occurred (context was summarized)\n Compaction {\n trigger: String,\n pre_tokens: Option<u64>,\n /// Provider-native compaction artifact, if one was emitted.\n openai_encrypted_content: Option<String>,\n },\n /// Upstream provider info (e.g., which provider OpenRouter routed to)\n UpstreamProvider { provider: String },\n /// Native tool call from a provider bridge that needs execution by jcode\n NativeToolCall {\n request_id: String,\n tool_name: String,\n input: serde_json::Value,\n },\n}\n\n#[cfg(test)]\nmod tests {\n use super::*;\n\n #[test]\n fn sanitize_tool_id_alphanumeric_passthrough() {\n assert_eq!(\n sanitize_tool_id(\"toolu_01XFDUDYJgAACzvnptvVer6u\"),\n \"toolu_01XFDUDYJgAACzvnptvVer6u\"\n );\n assert_eq!(sanitize_tool_id(\"call_abc123\"), \"call_abc123\");\n assert_eq!(\n sanitize_tool_id(\"call_1234567890_9876543210\"),\n \"call_1234567890_9876543210\"\n );\n }\n\n #[test]\n fn tool_call_intent_from_input_trims_optional_intent() {\n let input = serde_json::json!({\n \"intent\": \" Verify compact rendering \",\n \"command\": \"cargo test\"\n });\n assert_eq!(\n ToolCall::intent_from_input(&input).as_deref(),\n Some(\"Verify compact rendering\")\n );\n assert_eq!(\n ToolCall::intent_from_input(&serde_json::json!({\"intent\": \" \"})),\n None\n );\n }\n\n #[test]\n fn sanitize_tool_id_hyphens_passthrough() {\n assert_eq!(sanitize_tool_id(\"call-abc-123\"), \"call-abc-123\");\n assert_eq!(\n sanitize_tool_id(\"tool_use-id_with-mixed\"),\n \"tool_use-id_with-mixed\"\n );\n }\n\n #[test]\n fn sanitize_tool_id_replaces_dots() {\n assert_eq!(\n sanitize_tool_id(\"chatcmpl-abc.def.ghi\"),\n \"chatcmpl-abc_def_ghi\"\n );\n assert_eq!(sanitize_tool_id(\"call.123\"), \"call_123\");\n }\n\n #[test]\n fn sanitize_tool_id_replaces_colons() {\n assert_eq!(sanitize_tool_id(\"call:123:456\"), \"call_123_456\");\n }\n\n #[test]\n fn sanitize_tool_id_replaces_special_chars() {\n assert_eq!(\n sanitize_tool_id(\"id@with#special$chars\"),\n \"id_with_special_chars\"\n );\n assert_eq!(sanitize_tool_id(\"id with spaces\"), \"id_with_spaces\");\n }\n\n #[test]\n fn sanitize_tool_id_empty_returns_unknown() {\n assert_eq!(sanitize_tool_id(\"\"), \"unknown\");\n }\n\n #[test]\n fn sanitize_tool_id_copilot_to_anthropic() {\n assert_eq!(\n sanitize_tool_id(\"chatcmpl-BF2xX.tool_call.0\"),\n \"chatcmpl-BF2xX_tool_call_0\"\n );\n }\n\n #[test]\n fn sanitize_tool_id_already_valid_unchanged() {\n let valid_ids = [\n \"toolu_01XFDUDYJgAACzvnptvVer6u\",\n \"call_abc123\",\n \"fallback_text_call_call_1234567890_9876543210\",\n \"tool_123\",\n \"a\",\n \"A\",\n \"0\",\n \"_\",\n \"-\",\n \"a-b_c\",\n ];\n for id in valid_ids {\n assert_eq!(sanitize_tool_id(id), id, \"ID '{}' should be unchanged\", id);\n }\n }\n\n #[test]\n fn redact_secrets_redacts_known_direct_token_formats() {\n let input = \"access=sk-ant-oat01-ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789\\nopenrouter=sk-or-v1-abcdefghijklmnopqrstuvwxyz0123456789\\ngithub=ghp_ABCDEFGHIJKLMNOPQRSTUVWXYZ0123\\n\";\n let out = redact_secrets(input);\n assert!(!out.contains(\"sk-ant-oat01-\"));\n assert!(!out.contains(\"sk-or-v1-\"));\n assert!(!out.contains(\"ghp_ABCDEFGHIJKLMNOPQRSTUVWXYZ0123\"));\n assert!(out.matches(\"[REDACTED_SECRET]\").count() >= 3);\n }\n\n #[test]\n fn redact_secrets_redacts_env_style_assignments() {\n let input = \"OPENROUTER_API_KEY=sk-or-v1-abc123abc123abc123abc123\\nOPENCODE_API_KEY=oc_test_secret\\nOPENCODE_GO_API_KEY=ocgo_test_secret\\nZAI_API_KEY=zai_secret\\nCHUTES_API_KEY=chutes_secret\\nCEREBRAS_API_KEY=cerebras_secret\\nOPENAI_COMPAT_API_KEY=compat_secret\\nCURSOR_API_KEY='my_cursor_secret_value'\\nOPENAI_API_KEY=sk-test-openai-example\\nAZURE_OPENAI_API_KEY=azure-openai-secret\\n\";\n let out = redact_secrets(input);\n assert!(out.contains(\"OPENROUTER_API_KEY=[REDACTED_SECRET]\"));\n assert!(out.contains(\"OPENCODE_API_KEY=[REDACTED_SECRET]\"));\n assert!(out.contains(\"OPENCODE_GO_API_KEY=[REDACTED_SECRET]\"));\n assert!(out.contains(\"ZAI_API_KEY=[REDACTED_SECRET]\"));\n assert!(out.contains(\"CHUTES_API_KEY=[REDACTED_SECRET]\"));\n assert!(out.contains(\"CEREBRAS_API_KEY=[REDACTED_SECRET]\"));\n assert!(out.contains(\"OPENAI_COMPAT_API_KEY=[REDACTED_SECRET]\"));\n assert!(out.contains(\"CURSOR_API_KEY=[REDACTED_SECRET]\"));\n assert!(out.contains(\"OPENAI_API_KEY=[REDACTED_SECRET]\"));\n assert!(out.contains(\"AZURE_OPENAI_API_KEY=[REDACTED_SECRET]\"));\n assert!(!out.contains(\"my_cursor_secret_value\"));\n }\n\n #[test]\n fn redact_secrets_redacts_runtime_key_assignment() {\n let key_var = \"JCODE_OPENAI_COMPAT_API_KEY_NAME\";\n let prev = std::env::var(key_var).ok();\n crate::env::set_var(key_var, \"GROQ_API_KEY\");\n\n let input = \"GROQ_API_KEY=my_secret_token_value\";\n let out = redact_secrets(input);\n assert_eq!(out, \"GROQ_API_KEY=[REDACTED_SECRET]\");\n\n if let Some(v) = prev {\n crate::env::set_var(key_var, v);\n } else {\n crate::env::remove_var(key_var);\n }\n }\n\n #[test]\n fn redact_secrets_redacts_mixed_case_token_assignments() {\n let input = \"my_token=ya29.ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789\";\n let out = redact_secrets(input);\n assert!(out.contains(\"[REDACTED_SECRET]\"));\n assert!(!out.contains(\"ya29.ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789\"));\n }\n\n #[test]\n fn redact_secrets_leaves_normal_output_unchanged() {\n let input = \"Found 5 files\\nNo auth errors\\nDone.\";\n assert_eq!(redact_secrets(input), input);\n }\n\n #[test]\n fn format_timestamp_is_stable_utc_rfc3339() {\n let ts = chrono::DateTime::parse_from_rfc3339(\"2025-03-15T02:24:13.250Z\")\n .unwrap()\n .with_timezone(&Utc);\n assert_eq!(Message::format_timestamp(&ts), \"2025-03-15T02:24:13.250Z\");\n }\n\n #[test]\n fn with_timestamps_prepends_utc_prefix_to_user_text() {\n let ts = chrono::DateTime::parse_from_rfc3339(\"2025-03-15T02:24:03Z\")\n .unwrap()\n .with_timezone(&Utc);\n let stamped = Message::with_timestamps(&[Message {\n role: Role::User,\n content: vec![ContentBlock::Text {\n text: \"hello\".to_string(),\n cache_control: None,\n }],\n timestamp: Some(ts),\n tool_duration_ms: None,\n }]);\n match &stamped[0].content[0] {\n ContentBlock::Text { text, .. } => {\n assert_eq!(text, \"[2025-03-15T02:24:03.000Z] hello\");\n }\n other => panic!(\"expected text block, got {other:?}\"),\n }\n }\n\n #[test]\n fn with_timestamps_adds_tool_timing_header_with_duration() {\n let ts = chrono::DateTime::parse_from_rfc3339(\"2025-03-15T02:24:13Z\")\n .unwrap()\n .with_timezone(&Utc);\n let stamped = Message::with_timestamps(&[Message {\n role: Role::User,\n content: vec![ContentBlock::ToolResult {\n tool_use_id: \"call_1\".to_string(),\n content: \"ok\".to_string(),\n is_error: None,\n }],\n timestamp: Some(ts),\n tool_duration_ms: Some(3_200),\n }]);\n match &stamped[0].content[0] {\n ContentBlock::ToolResult { content, .. } => {\n assert_eq!(\n content,\n \"[tool timing: start=2025-03-15T02:24:09.800Z finish=2025-03-15T02:24:13.000Z duration=3.2s] ok\"\n );\n }\n other => panic!(\"expected tool result block, got {other:?}\"),\n }\n }\n\n #[test]\n fn with_timestamps_skips_internal_system_reminders() {\n let ts = chrono::DateTime::parse_from_rfc3339(\"2025-03-15T02:24:13Z\")\n .unwrap()\n .with_timezone(&Utc);\n let original = Message {\n role: Role::User,\n content: vec![ContentBlock::Text {\n text: \"<system-reminder>\\ninternal\\n</system-reminder>\".to_string(),\n cache_control: None,\n }],\n timestamp: Some(ts),\n tool_duration_ms: None,\n };\n let stamped = Message::with_timestamps(std::slice::from_ref(&original));\n match &stamped[0].content[0] {\n ContentBlock::Text { text, .. } => {\n assert_eq!(text, \"<system-reminder>\\ninternal\\n</system-reminder>\");\n }\n other => panic!(\"expected text block, got {other:?}\"),\n }\n }\n\n #[test]\n fn ends_with_fresh_user_turn_accepts_plain_user_text() {\n let messages = vec![Message::user(\"hello\")];\n assert!(ends_with_fresh_user_turn(&messages));\n }\n\n #[test]\n fn ends_with_fresh_user_turn_rejects_trailing_tool_result() {\n let messages = vec![\n Message::user(\"hello\"),\n Message {\n role: Role::Assistant,\n content: vec![ContentBlock::ToolUse {\n id: \"call_1\".to_string(),\n name: \"bash\".to_string(),\n input: serde_json::json!({}),\n }],\n timestamp: Some(Utc::now()),\n tool_duration_ms: None,\n },\n Message::tool_result(\"call_1\", \"ok\", false),\n ];\n assert!(!ends_with_fresh_user_turn(&messages));\n }\n\n #[test]\n fn ends_with_fresh_user_turn_skips_internal_system_reminders() {\n let messages = vec![\n Message::user(\"hello\"),\n Message::user(\"<system-reminder>\\ninternal\\n</system-reminder>\"),\n ];\n assert!(ends_with_fresh_user_turn(&messages));\n }\n\n #[test]\n fn ends_with_fresh_user_turn_rejects_assistant_tail() {\n let messages = vec![\n Message::user(\"hello\"),\n Message::assistant_text(\"working on it\"),\n ];\n assert!(!ends_with_fresh_user_turn(&messages));\n }\n\n #[test]\n fn format_background_task_notification_markdown_uses_code_block_preview() {\n let rendered = format_background_task_notification_markdown(&BackgroundTaskCompleted {\n task_id: \"abc123\".to_string(),\n tool_name: \"bash\".to_string(),\n display_name: None,\n session_id: \"session\".to_string(),\n status: BackgroundTaskStatus::Completed,\n exit_code: Some(0),\n output_preview: \"[stderr] first line\\n[stdout] second line\\n\".to_string(),\n output_file: std::path::PathBuf::from(\"/tmp/output.log\"),\n duration_secs: 7.1,\n notify: true,\n wake: false,\n });\n\n assert!(\n rendered\n .contains(\"**Background task** `abc123` · `bash` · ✓ completed · 7.1s · exit 0\")\n );\n assert!(rendered.contains(\"```text\\n[stderr] first line\\n[stdout] second line\\n```\"));\n assert!(rendered.contains(\"_Full output:_ `bg action=\\\"output\\\" task_id=\\\"abc123\\\"`\"));\n }\n\n #[test]\n fn format_background_task_notification_markdown_handles_empty_preview() {\n let rendered = format_background_task_notification_markdown(&BackgroundTaskCompleted {\n task_id: \"abc123\".to_string(),\n tool_name: \"bash\".to_string(),\n display_name: None,\n session_id: \"session\".to_string(),\n status: BackgroundTaskStatus::Failed,\n exit_code: Some(9),\n output_preview: \"\\n\\n\".to_string(),\n output_file: std::path::PathBuf::from(\"/tmp/output.log\"),\n duration_secs: 1.0,\n notify: true,\n wake: false,\n });\n\n assert!(rendered.contains(\"✗ failed\"));\n assert!(rendered.contains(\"_No output captured._\"));\n }\n\n #[test]\n fn format_background_task_notification_markdown_renders_superseded_status() {\n let rendered = format_background_task_notification_markdown(&BackgroundTaskCompleted {\n task_id: \"abc123\".to_string(),\n tool_name: \"selfdev-build\".to_string(),\n display_name: None,\n session_id: \"session\".to_string(),\n status: BackgroundTaskStatus::Superseded,\n exit_code: Some(0),\n output_preview: \"Build completed, but source changed before activation\".to_string(),\n output_file: std::path::PathBuf::from(\"/tmp/output.log\"),\n duration_secs: 5.0,\n notify: true,\n wake: false,\n });\n\n assert!(rendered.contains(\"↻ superseded\"));\n assert!(rendered.contains(\"exit 0\"));\n assert!(rendered.contains(\"source changed before activation\"));\n }\n\n #[test]\n fn format_background_task_progress_markdown_uses_compact_multiline_layout() {\n let rendered = format_background_task_progress_markdown(&BackgroundTaskProgressEvent {\n task_id: \"bgprogress\".to_string(),\n tool_name: \"bash\".to_string(),\n display_name: None,\n session_id: \"session\".to_string(),\n progress: crate::bus::BackgroundTaskProgress {\n kind: crate::bus::BackgroundTaskProgressKind::Determinate,\n percent: Some(42.0),\n message: Some(\"Running tests\".to_string()),\n current: Some(21),\n total: Some(50),\n unit: Some(\"tests\".to_string()),\n eta_seconds: None,\n updated_at: Utc::now().to_rfc3339(),\n source: crate::bus::BackgroundTaskProgressSource::Reported,\n },\n });\n\n assert!(rendered.starts_with(\"**Background task progress** `bgprogress` · `bash`\\n\\n\"));\n assert!(rendered.contains(\"42% · Running tests\"));\n assert!(rendered.contains(\"(reported)\"));\n }\n\n #[test]\n fn background_task_notifications_include_display_name_when_available() {\n let rendered = format_background_task_notification_markdown(&BackgroundTaskCompleted {\n task_id: \"abc123\".to_string(),\n tool_name: \"bash\".to_string(),\n display_name: Some(\"Run integration tests\".to_string()),\n session_id: \"session\".to_string(),\n status: BackgroundTaskStatus::Completed,\n exit_code: Some(0),\n output_preview: \"done\".to_string(),\n output_file: std::path::PathBuf::from(\"/tmp/output.log\"),\n duration_secs: 7.1,\n notify: true,\n wake: false,\n });\n\n assert!(rendered.contains(\n \"**Background task** `abc123` · `Run integration tests` (`bash`) · ✓ completed\"\n ));\n let parsed = parse_background_task_notification_markdown(&rendered)\n .expect(\"named background task notification should parse\");\n assert_eq!(parsed.tool_name, \"bash\");\n assert_eq!(\n parsed.display_name.as_deref(),\n Some(\"Run integration tests\")\n );\n }\n\n #[test]\n fn background_task_progress_notifications_include_display_name_when_available() {\n let rendered = format_background_task_progress_markdown(&BackgroundTaskProgressEvent {\n task_id: \"bgprogress\".to_string(),\n tool_name: \"bash\".to_string(),\n display_name: Some(\"Run integration tests\".to_string()),\n session_id: \"session\".to_string(),\n progress: crate::bus::BackgroundTaskProgress {\n kind: crate::bus::BackgroundTaskProgressKind::Determinate,\n percent: Some(42.0),\n message: Some(\"Running tests\".to_string()),\n current: Some(21),\n total: Some(50),\n unit: Some(\"tests\".to_string()),\n eta_seconds: None,\n updated_at: Utc::now().to_rfc3339(),\n source: crate::bus::BackgroundTaskProgressSource::Reported,\n },\n });\n\n assert!(rendered.starts_with(\n \"**Background task progress** `bgprogress` · `Run integration tests` (`bash`)\\n\\n\"\n ));\n let parsed = parse_background_task_progress_notification_markdown(&rendered)\n .expect(\"named progress notification should parse\");\n assert_eq!(parsed.tool_name, \"bash\");\n assert_eq!(\n parsed.display_name.as_deref(),\n Some(\"Run integration tests\")\n );\n assert_eq!(parsed.summary, \"42% · Running tests\");\n }\n\n #[test]\n fn parse_background_task_progress_notification_extracts_card_fields() {\n let parsed = parse_background_task_progress_notification_markdown(\n \"**Background task progress** `bgprogress` · `bash`\\n\\n[#####-------] 42% · Running tests (reported)\",\n )\n .expect(\"progress notification should parse\");\n\n assert_eq!(parsed.task_id, \"bgprogress\");\n assert_eq!(parsed.tool_name, \"bash\");\n assert_eq!(parsed.display_name, None);\n assert_eq!(parsed.summary, \"42% · Running tests\");\n assert_eq!(parsed.source.as_deref(), Some(\"reported\"));\n assert_eq!(parsed.percent, Some(42.0));\n }\n\n #[test]\n fn parse_background_task_progress_notification_supports_legacy_inline_layout() {\n let parsed = parse_background_task_progress_notification_markdown(\n \"**Background task progress** `bgprogress` · `bash` · Release run in_progress: - 7/8 jobs completed (reported)\",\n )\n .expect(\"legacy progress notification should parse\");\n\n assert_eq!(parsed.task_id, \"bgprogress\");\n assert_eq!(parsed.tool_name, \"bash\");\n assert_eq!(parsed.display_name, None);\n assert_eq!(\n parsed.summary,\n \"Release run in_progress: - 7/8 jobs completed\"\n );\n assert_eq!(parsed.source.as_deref(), Some(\"reported\"));\n assert_eq!(parsed.percent, None);\n }\n\n #[test]\n fn description_token_estimate_uses_chars_per_token_heuristic() {\n let def = ToolDefinition {\n name: \"read\".to_string(),\n description: \"abcdwxyz\".to_string(),\n input_schema: serde_json::json!({\"type\": \"object\"}),\n };\n\n assert_eq!(def.description_token_estimate(), 2);\n }\n\n #[test]\n fn parse_background_task_notification_markdown_extracts_fields() {\n let rendered = format_background_task_notification_markdown(&BackgroundTaskCompleted {\n task_id: \"abc123\".to_string(),\n tool_name: \"bash\".to_string(),\n display_name: None,\n session_id: \"session\".to_string(),\n status: BackgroundTaskStatus::Completed,\n exit_code: Some(0),\n output_preview: \"[stderr] first line\\n[stdout] second line\\n\".to_string(),\n output_file: std::path::PathBuf::from(\"/tmp/output.log\"),\n duration_secs: 7.1,\n notify: true,\n wake: false,\n });\n\n let parsed = parse_background_task_notification_markdown(&rendered)\n .expect(\"background task notification should parse\");\n assert_eq!(parsed.task_id, \"abc123\");\n assert_eq!(parsed.tool_name, \"bash\");\n assert_eq!(parsed.display_name, None);\n assert_eq!(parsed.status, \"✓ completed\");\n assert_eq!(parsed.duration, \"7.1s\");\n assert_eq!(parsed.exit_label, \"exit 0\");\n assert_eq!(\n parsed.preview.as_deref(),\n Some(\"[stderr] first line\\n[stdout] second line\")\n );\n assert_eq!(\n parsed.full_output_command,\n \"bg action=\\\"output\\\" task_id=\\\"abc123\\\"\"\n );\n }\n}\n"}, "a38aca4598395cf3a7b2d83299a9de942b03ee85": {"t": 1785757226.6373034, "n": 38927, "h": "24adafe39a1eccb9", "c": "use crate::bus::{BackgroundTaskCompleted, BackgroundTaskProgressEvent, BackgroundTaskStatus};\nuse chrono::Utc;\nuse regex::Regex;\nuse serde::{Deserialize, Serialize};\nuse std::collections::HashSet;\nuse std::sync::OnceLock;\n\nmod notifications;\n\npub use notifications::{\n InputShellResult, ParsedBackgroundTaskNotification, ParsedBackgroundTaskProgressNotification,\n background_task_status_notice, format_background_task_notification_markdown,\n format_background_task_progress_markdown, format_input_shell_result_markdown,\n input_shell_status_notice, parse_background_task_notification_markdown,\n parse_background_task_progress_notification_markdown,\n};\n\nfn compile_static_regex(pattern: &str) -> Option<Regex> {\n match Regex::new(pattern) {\n Ok(regex) => Some(regex),\n Err(err) => {\n eprintln!(\"jcode: failed to compile static regex: {err}\");\n None\n }\n }\n}\n\nfn compile_static_regexes(patterns: &[&str]) -> Vec<Regex> {\n patterns\n .iter()\n .filter_map(|pattern| compile_static_regex(pattern))\n .collect()\n}\n\n/// Role in conversation\n#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]\n#[serde(rename_all = \"lowercase\")]\npub enum Role {\n User,\n Assistant,\n}\n\n/// Plain-text tool output placeholder when execution was interrupted.\npub const TOOL_OUTPUT_MISSING_TEXT: &str =\n \"Tool output missing (session interrupted before tool execution completed)\";\n\n/// A message in the conversation\n#[derive(Debug, Clone, Serialize, Deserialize)]\npub struct Message {\n pub role: Role,\n pub content: Vec<ContentBlock>,\n #[serde(default, skip_serializing_if = \"Option::is_none\")]\n pub timestamp: Option<chrono::DateTime<Utc>>,\n #[serde(default, skip_serializing_if = \"Option::is_none\")]\n pub tool_duration_ms: Option<u64>,\n}\n\n/// Cache control metadata for prompt caching\n#[derive(Debug, Clone, Serialize, Deserialize)]\npub struct CacheControl {\n #[serde(rename = \"type\")]\n pub kind: String,\n #[serde(skip_serializing_if = \"Option::is_none\")]\n pub ttl: Option<String>,\n}\n\nimpl CacheControl {\n pub fn ephemeral(ttl: Option<String>) -> Self {\n Self {\n kind: \"ephemeral\".to_string(),\n ttl,\n }\n }\n}\n\n/// Content block within a message\n#[derive(Debug, Clone, Serialize, Deserialize)]\n#[serde(tag = \"type\", rename_all = \"snake_case\")]\npub enum ContentBlock {\n Text {\n text: String,\n #[serde(skip_serializing_if = \"Option::is_none\")]\n cache_control: Option<CacheControl>,\n },\n /// Hidden reasoning content used for providers that require it (not displayed)\n Reasoning {\n text: String,\n },\n ToolUse {\n id: String,\n name: String,\n input: serde_json::Value,\n },\n ToolResult {\n tool_use_id: String,\n content: String,\n #[serde(skip_serializing_if = \"Option::is_none\")]\n is_error: Option<bool>,\n },\n Image {\n media_type: String,\n data: String,\n },\n /// Hidden OpenAI Responses compaction item used to preserve native\n /// compaction state across turns/saves when jcode explicitly triggers it.\n OpenAICompaction {\n encrypted_content: String,\n },\n}\n\nimpl Message {\n pub fn user(text: &str) -> Self {\n Self {\n role: Role::User,\n content: vec![ContentBlock::Text {\n text: text.to_string(),\n cache_control: None,\n }],\n timestamp: Some(Utc::now()),\n tool_duration_ms: None,\n }\n }\n\n pub fn user_with_images(text: &str, images: Vec<(String, String)>) -> Self {\n let mut content: Vec<ContentBlock> = images\n .into_iter()\n .map(|(media_type, data)| ContentBlock::Image { media_type, data })\n .collect();\n content.push(ContentBlock::Text {\n text: text.to_string(),\n cache_control: None,\n });\n Self {\n role: Role::User,\n content,\n timestamp: Some(Utc::now()),\n tool_duration_ms: None,\n }\n }\n\n pub fn assistant_text(text: &str) -> Self {\n Self {\n role: Role::Assistant,\n content: vec![ContentBlock::Text {\n text: text.to_string(),\n cache_control: None,\n }],\n timestamp: Some(Utc::now()),\n tool_duration_ms: None,\n }\n }\n\n pub fn tool_result(tool_use_id: &str, content: &str, is_error: bool) -> Self {\n Self::tool_result_with_duration(tool_use_id, content, is_error, None)\n }\n\n pub fn tool_result_with_duration(\n tool_use_id: &str,\n content: &str,\n is_error: bool,\n tool_duration_ms: Option<u64>,\n ) -> Self {\n Self {\n role: Role::User,\n content: vec![ContentBlock::ToolResult {\n tool_use_id: tool_use_id.to_string(),\n content: content.to_string(),\n is_error: if is_error { Some(true) } else { None },\n }],\n timestamp: Some(Utc::now()),\n tool_duration_ms,\n }\n }\n\n /// Format a timestamp deterministically in UTC for injection into model-visible content.\n pub fn format_timestamp(ts: &chrono::DateTime<Utc>) -> String {\n ts.to_rfc3339_opts(chrono::SecondsFormat::Millis, true)\n }\n\n pub fn format_duration(duration_ms: u64) -> String {\n match duration_ms {\n 0..=999 => format!(\"{}ms\", duration_ms),\n 1_000..=9_999 => format!(\"{:.1}s\", duration_ms as f64 / 1000.0),\n 10_000..=59_999 => format!(\"{}s\", duration_ms / 1000),\n _ => {\n let total_seconds = duration_ms / 1000;\n let minutes = total_seconds / 60;\n let seconds = total_seconds % 60;\n if seconds == 0 {\n format!(\"{}m\", minutes)\n } else {\n format!(\"{}m {}s\", minutes, seconds)\n }\n }\n }\n }\n\n pub fn is_internal_system_reminder(&self) -> bool {\n self.content\n .iter()\n .find_map(|block| match block {\n ContentBlock::Text { text, .. } => Some(text.trim_start()),\n _ => None,\n })\n .is_some_and(|text| text.starts_with(\"<system-reminder>\"))\n }\n\n fn should_skip_timestamp_injection(&self) -> bool {\n self.is_internal_system_reminder()\n }\n\n fn tool_result_tag(&self, ts: &chrono::DateTime<Utc>) -> String {\n match self.tool_duration_ms {\n Some(duration_ms) => {\n let duration_ms_i64 = i64::try_from(duration_ms).unwrap_or(i64::MAX);\n let start_ts = ts\n .checked_sub_signed(chrono::Duration::milliseconds(duration_ms_i64))\n .unwrap_or(*ts);\n format!(\n \"[tool timing: start={} finish={} duration={}]\",\n Self::format_timestamp(&start_ts),\n Self::format_timestamp(ts),\n Self::format_duration(duration_ms)\n )\n }\n None => format!(\"[{}]\", Self::format_timestamp(ts)),\n }\n }\n\n /// Return a copy of messages with timestamps injected into user-role text content.\n /// Tool results get a stable UTC timing header prepended to content.\n /// User text messages get a stable UTC timestamp prepended to the first text block.\n pub fn with_timestamps(messages: &[Message]) -> Vec<Message> {\n messages\n .iter()\n .map(|msg| {\n let Some(ts) = msg.timestamp else {\n return msg.clone();\n };\n if msg.role != Role::User || msg.should_skip_timestamp_injection() {\n return msg.clone();\n }\n let text_tag = format!(\"[{}]\", Self::format_timestamp(&ts));\n let tool_result_tag = msg.tool_result_tag(&ts);\n let mut msg = msg.clone();\n let mut tagged = false;\n for block in &mut msg.content {\n match block {\n ContentBlock::Text { text, .. } if !tagged => {\n *text = format!(\"{} {}\", text_tag, text);\n tagged = true;\n }\n ContentBlock::ToolResult { content, .. } if !tagged => {\n *content = format!(\"{} {}\", tool_result_tag, content);\n tagged = true;\n }\n _ => {}\n }\n }\n msg\n })\n .collect()\n }\n}\n\nconst STABLE_HASH_SEED: u64 = 0xcbf29ce484222325;\nconst STABLE_HASH_PRIME: u64 = 0x100000001b3;\n\nfn stable_hash_bytes(bytes: &[u8]) -> u64 {\n let mut hash = STABLE_HASH_SEED;\n for byte in bytes {\n hash ^= *byte as u64;\n hash = hash.wrapping_mul(STABLE_HASH_PRIME);\n }\n hash\n}\n\npub(crate) fn extend_stable_hash(acc: u64, next: u64) -> u64 {\n stable_hash_bytes(&[acc.to_le_bytes().as_slice(), next.to_le_bytes().as_slice()].concat())\n}\n\npub(crate) fn stable_message_hash(message: &Message) -> u64 {\n match serde_json::to_vec(message) {\n Ok(bytes) => stable_hash_bytes(&bytes),\n Err(_) => stable_hash_bytes(format!(\"{:?}\", message).as_bytes()),\n }\n}\n\npub fn ends_with_fresh_user_turn(messages: &[Message]) -> bool {\n for msg in messages.iter().rev() {\n if msg.role != Role::User {\n return false;\n }\n\n if msg\n .content\n .iter()\n .any(|block| matches!(block, ContentBlock::ToolResult { .. }))\n {\n return false;\n }\n\n if msg.content.is_empty() {\n return false;\n }\n\n let mut saw_user_text = false;\n for block in &msg.content {\n match block {\n ContentBlock::Text { text, .. } => {\n let trimmed = text.trim();\n if !trimmed.is_empty() && !trimmed.starts_with(\"<system-reminder>\") {\n saw_user_text = true;\n }\n }\n _ => return false,\n }\n }\n\n if saw_user_text {\n return true;\n }\n\n if msg.is_internal_system_reminder() {\n continue;\n }\n\n return false;\n }\n\n false\n}\n\n/// Sanitize a tool ID so it matches the pattern `^[a-zA-Z0-9_-]+$`.\n///\n/// Different providers generate tool IDs in different formats. When switching\n/// from one provider to another mid-conversation, the historical tool IDs may\n/// contain characters that the new provider rejects (e.g., dots in Copilot IDs\n/// sent to Anthropic). This function replaces any invalid characters with\n/// underscores.\npub fn sanitize_tool_id(id: &str) -> String {\n if id.is_empty() {\n return \"unknown\".to_string();\n }\n let sanitized: String = id\n .chars()\n .map(|c| {\n if c.is_ascii_alphanumeric() || c == '_' || c == '-' {\n c\n } else {\n '_'\n }\n })\n .collect();\n if sanitized.is_empty() {\n \"unknown\".to_string()\n } else {\n sanitized\n }\n}\n\n/// Redact likely secrets from persisted tool output.\n///\n/// This is a best-effort safeguard for local session history files. It targets\n/// high-confidence token/key patterns and common `KEY=VALUE` assignments used by\n/// auth flows.\npub fn redact_secrets(text: &str) -> String {\n // Fast path to avoid regex work for most tool outputs.\n let lower = text.to_ascii_lowercase();\n\n if !text.contains(\"sk-\")\n && !text.contains(\"ghp_\")\n && !text.contains(\"github_pat_\")\n && !text.contains(\"AIza\")\n && !text.contains(\"ya29.\")\n && !text.contains(\"xox\")\n && !lower.contains(\"api_key\")\n && !lower.contains(\"token\")\n {\n return text.to_string();\n }\n\n static DIRECT_PATTERNS: OnceLock<Vec<Regex>> = OnceLock::new();\n static ASSIGNMENT_PATTERNS: OnceLock<Vec<Regex>> = OnceLock::new();\n\n let direct_patterns = DIRECT_PATTERNS.get_or_init(|| {\n compile_static_regexes(&[\n r\"sk-ant-(?:oat|ort)01-[A-Za-z0-9_-]{20,}\",\n r\"sk-or-v1-[A-Za-z0-9_-]{20,}\",\n r\"ghp_[A-Za-z0-9]{20,}\",\n r\"github_pat_[A-Za-z0-9_]{20,}\",\n r\"ya29\\.[A-Za-z0-9._-]{20,}\",\n r\"AIza[0-9A-Za-z_-]{20,}\",\n r\"xox[baprs]-[A-Za-z0-9-]{10,}\",\n ])\n });\n\n let assignment_patterns = ASSIGNMENT_PATTERNS.get_or_init(|| {\n compile_static_regexes(&[\n r\"(?m)^\\s*(OPENROUTER_API_KEY\\s*=\\s*)[^\\r\\n]+\",\n r\"(?m)^\\s*(OPENCODE_API_KEY\\s*=\\s*)[^\\r\\n]+\",\n r\"(?m)^\\s*(OPENCODE_GO_API_KEY\\s*=\\s*)[^\\r\\n]+\",\n r\"(?m)^\\s*(ZHIPU_API_KEY\\s*=\\s*)[^\\r\\n]+\",\n r\"(?m)^\\s*(ZAI_API_KEY\\s*=\\s*)[^\\r\\n]+\",\n r\"(?m)^\\s*(302AI_API_KEY\\s*=\\s*)[^\\r\\n]+\",\n r\"(?m)^\\s*(BASETEN_API_KEY\\s*=\\s*)[^\\r\\n]+\",\n r\"(?m)^\\s*(CORTECS_API_KEY\\s*=\\s*)[^\\r\\n]+\",\n r\"(?m)^\\s*(DEEPSEEK_API_KEY\\s*=\\s*)[^\\r\\n]+\",\n r\"(?m)^\\s*(FIRMWARE_API_KEY\\s*=\\s*)[^\\r\\n]+\",\n r\"(?m)^\\s*(HF_TOKEN\\s*=\\s*)[^\\r\\n]+\",\n r\"(?m)^\\s*(MOONSHOT_API_KEY\\s*=\\s*)[^\\r\\n]+\",\n r\"(?m)^\\s*(NEBIUS_API_KEY\\s*=\\s*)[^\\r\\n]+\",\n r\"(?m)^\\s*(SCALEWAY_API_KEY\\s*=\\s*)[^\\r\\n]+\",\n r\"(?m)^\\s*(STACKIT_API_KEY\\s*=\\s*)[^\\r\\n]+\",\n r\"(?m)^\\s*(GROQ_API_KEY\\s*=\\s*)[^\\r\\n]+\",\n r\"(?m)^\\s*(MISTRAL_API_KEY\\s*=\\s*)[^\\r\\n]+\",\n r\"(?m)^\\s*(PERPLEXITY_API_KEY\\s*=\\s*)[^\\r\\n]+\",\n r\"(?m)^\\s*(TOGETHER_API_KEY\\s*=\\s*)[^\\r\\n]+\",\n r\"(?m)^\\s*(DEEPINFRA_API_KEY\\s*=\\s*)[^\\r\\n]+\",\n r\"(?m)^\\s*(XAI_API_KEY\\s*=\\s*)[^\\r\\n]+\",\n r\"(?m)^\\s*(LMSTUDIO_API_KEY\\s*=\\s*)[^\\r\\n]+\",\n r\"(?m)^\\s*(OLLAMA_API_KEY\\s*=\\s*)[^\\r\\n]+\",\n r\"(?m)^\\s*(CHUTES_API_KEY\\s*=\\s*)[^\\r\\n]+\",\n r\"(?m)^\\s*(CEREBRAS_API_KEY\\s*=\\s*)[^\\r\\n]+\",\n r\"(?m)^\\s*(OPENAI_COMPAT_API_KEY\\s*=\\s*)[^\\r\\n]+\",\n r\"(?m)^\\s*(ANTHROPIC_API_KEY\\s*=\\s*)[^\\r\\n]+\",\n r\"(?m)^\\s*(OPENAI_API_KEY\\s*=\\s*)[^\\r\\n]+\",\n r\"(?m)^\\s*(AZURE_OPENAI_API_KEY\\s*=\\s*)[^\\r\\n]+\",\n r\"(?m)^\\s*(CURSOR_API_KEY\\s*=\\s*)[^\\r\\n]+\",\n r\"(?m)^\\s*(GITHUB_TOKEN\\s*=\\s*)[^\\r\\n]+\",\n ])\n });\n\n let mut redacted = text.to_string();\n let mut redacted_keys: HashSet<String> = [\n \"OPENROUTER_API_KEY\",\n \"OPENCODE_API_KEY\",\n \"OPENCODE_GO_API_KEY\",\n \"ZHIPU_API_KEY\",\n \"ZAI_API_KEY\",\n \"302AI_API_KEY\",\n \"BASETEN_API_KEY\",\n \"CORTECS_API_KEY\",\n \"DEEPSEEK_API_KEY\",\n \"FIRMWARE_API_KEY\",\n \"HF_TOKEN\",\n \"MOONSHOT_API_KEY\",\n \"NEBIUS_API_KEY\",\n \"SCALEWAY_API_KEY\",\n \"STACKIT_API_KEY\",\n \"GROQ_API_KEY\",\n \"MISTRAL_API_KEY\",\n \"PERPLEXITY_API_KEY\",\n \"TOGETHER_API_KEY\",\n \"DEEPINFRA_API_KEY\",\n \"XAI_API_KEY\",\n \"LMSTUDIO_API_KEY\",\n \"OLLAMA_API_KEY\",\n \"CHUTES_API_KEY\",\n \"CEREBRAS_API_KEY\",\n \"OPENAI_COMPAT_API_KEY\",\n \"ANTHROPIC_API_KEY\",\n \"OPENAI_API_KEY\",\n \"AZURE_OPENAI_API_KEY\",\n \"CURSOR_API_KEY\",\n \"GITHUB_TOKEN\",\n ]\n .iter()\n .map(|k| (*k).to_string())\n .collect();\n\n for re in direct_patterns {\n redacted = re.replace_all(&redacted, \"[REDACTED_SECRET]\").into_owned();\n }\n\n for re in assignment_patterns {\n redacted = re\n .replace_all(&redacted, \"${1}[REDACTED_SECRET]\")\n .into_owned();\n }\n\n // Also redact custom API key variable names configured at runtime.\n for source in [\n \"JCODE_OPENROUTER_API_KEY_NAME\",\n \"JCODE_OPENAI_COMPAT_API_KEY_NAME\",\n ] {\n let Some(key_name) = std::env::var(source)\n .ok()\n .map(|v| v.trim().to_string())\n .filter(|v| !v.is_empty())\n else {\n continue;\n };\n\n if !key_name\n .chars()\n .all(|c| c.is_ascii_uppercase() || c.is_ascii_digit() || c == '_')\n {\n continue;\n }\n if !redacted_keys.insert(key_name.clone()) {\n continue;\n }\n\n let pattern = format!(r\"(?m)^\\s*({}\\s*=\\s*)[^\\r\\n]+\", regex::escape(&key_name));\n if let Ok(re) = Regex::new(&pattern) {\n redacted = re\n .replace_all(&redacted, \"${1}[REDACTED_SECRET]\")\n .into_owned();\n }\n }\n\n redacted\n}\n\n/// Tool definition for the API\n#[derive(Debug, Clone, Serialize)]\npub struct ToolDefinition {\n pub name: String,\n // Prompt-visible text sent to the model by provider adapters.\n // Approximate prompt cost: description.len() / 4. Use\n // ToolDefinition::description_token_estimate() when reviewing tool bloat.\n pub description: String,\n pub input_schema: serde_json::Value,\n}\n\nimpl ToolDefinition {\n /// Serialized size of the full tool definition payload sent to providers.\n pub fn prompt_chars(&self) -> usize {\n serde_json::json!({\n \"name\": self.name,\n \"description\": self.description,\n \"input_schema\": self.input_schema,\n })\n .to_string()\n .len()\n }\n\n /// Approximate prompt-token cost of this tool's top-level description.\n ///\n /// This uses jcode's standard chars/4 heuristic, matching other token\n /// budget estimates in the codebase.\n pub fn description_token_estimate(&self) -> usize {\n crate::util::estimate_tokens(&self.description)\n }\n\n /// Approximate prompt-token cost of the full tool definition payload.\n pub fn prompt_token_estimate(&self) -> usize {\n crate::util::estimate_tokens(\n &serde_json::json!({\n \"name\": self.name,\n \"description\": self.description,\n \"input_schema\": self.input_schema,\n })\n .to_string(),\n )\n }\n\n pub fn aggregate_prompt_chars(defs: &[ToolDefinition]) -> usize {\n defs.iter().map(Self::prompt_chars).sum()\n }\n\n pub fn aggregate_prompt_token_estimate(defs: &[ToolDefinition]) -> usize {\n defs.iter().map(Self::prompt_token_estimate).sum()\n }\n}\n\n/// A tool call from the model\n#[derive(Debug, Clone, Default, PartialEq, Eq, serde::Serialize, serde::Deserialize)]\npub struct ToolCall {\n #[serde(default)]\n pub id: String,\n #[serde(default)]\n pub name: String,\n #[serde(default)]\n pub input: serde_json::Value,\n #[serde(default, skip_serializing_if = \"Option::is_none\")]\n pub intent: Option<String>,\n}\n\nimpl ToolCall {\n pub fn intent_from_input(input: &serde_json::Value) -> Option<String> {\n input\n .get(\"intent\")\n .and_then(|value| value.as_str())\n .map(str::trim)\n .filter(|intent| !intent.is_empty())\n .map(ToString::to_string)\n }\n\n pub fn refresh_intent_from_input(&mut self) {\n self.intent = Self::intent_from_input(&self.input);\n }\n}\n\n/// Connection phase for status bar transparency\n#[derive(Debug, Clone, PartialEq)]\npub enum ConnectionPhase {\n /// Refreshing OAuth token\n Authenticating,\n /// TCP + TLS connection to API\n Connecting,\n /// HTTP request sent, waiting for first response byte\n WaitingForResponse,\n /// First byte received, stream is active\n Streaming,\n /// Retrying after a transient error\n Retrying { attempt: u32, max: u32 },\n}\n\nimpl std::fmt::Display for ConnectionPhase {\n fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {\n match self {\n ConnectionPhase::Authenticating => write!(f, \"authenticating\"),\n ConnectionPhase::Connecting => write!(f, \"connecting\"),\n ConnectionPhase::WaitingForResponse => write!(f, \"waiting for response\"),\n ConnectionPhase::Streaming => write!(f, \"streaming\"),\n ConnectionPhase::Retrying { attempt, max } => {\n write!(f, \"retrying ({}/{})\", attempt, max)\n }\n }\n }\n}\n\n/// Streaming event from provider\n#[derive(Debug, Clone)]\npub enum StreamEvent {\n /// Text content delta\n TextDelta(String),\n /// Tool use started\n ToolUseStart { id: String, name: String },\n /// Tool input delta (JSON fragment)\n ToolInputDelta(String),\n /// Tool use complete\n ToolUseEnd,\n /// Tool result from provider (provider already executed the tool)\n ToolResult {\n tool_use_id: String,\n content: String,\n is_error: bool,\n },\n /// Extended thinking started\n ThinkingStart,\n /// Extended thinking delta (reasoning content)\n ThinkingDelta(String),\n /// Extended thinking ended\n ThinkingEnd,\n /// Extended thinking completed with duration\n ThinkingDone { duration_secs: f64 },\n /// Message complete (may have stop reason)\n MessageEnd { stop_reason: Option<String> },\n /// Token usage update\n TokenUsage {\n input_tokens: Option<u64>,\n output_tokens: Option<u64>,\n cache_read_input_tokens: Option<u64>,\n cache_creation_input_tokens: Option<u64>,\n },\n /// Active transport/connection type for this stream\n ConnectionType { connection: String },\n /// Connection phase update (for status bar transparency)\n ConnectionPhase { phase: ConnectionPhase },\n /// Provider-supplied human-readable transport detail for the status line\n StatusDetail { detail: String },\n /// Error occurred\n Error {\n message: String,\n /// Seconds until rate limit resets (if this is a rate limit error)\n retry_after_secs: Option<u64>,\n },\n /// Provider session ID (for conversation resume)\n SessionId(String),\n /// Compaction occurred (context was summarized)\n Compaction {\n trigger: String,\n pre_tokens: Option<u64>,\n /// Provider-native compaction artifact, if one was emitted.\n openai_encrypted_content: Option<String>,\n },\n /// Upstream provider info (e.g., which provider OpenRouter routed to)\n UpstreamProvider { provider: String },\n /// Native tool call from a provider bridge that needs execution by jcode\n NativeToolCall {\n request_id: String,\n tool_name: String,\n input: serde_json::Value,\n },\n}\n\n#[cfg(test)]\nmod tests {\n use super::*;\n\n #[test]\n fn sanitize_tool_id_alphanumeric_passthrough() {\n assert_eq!(\n sanitize_tool_id(\"toolu_01XFDUDYJgAACzvnptvVer6u\"),\n \"toolu_01XFDUDYJgAACzvnptvVer6u\"\n );\n assert_eq!(sanitize_tool_id(\"call_abc123\"), \"call_abc123\");\n assert_eq!(\n sanitize_tool_id(\"call_1234567890_9876543210\"),\n \"call_1234567890_9876543210\"\n );\n }\n\n #[test]\n fn tool_call_intent_from_input_trims_optional_intent() {\n let input = serde_json::json!({\n \"intent\": \" Verify compact rendering \",\n \"command\": \"cargo test\"\n });\n assert_eq!(\n ToolCall::intent_from_input(&input).as_deref(),\n Some(\"Verify compact rendering\")\n );\n assert_eq!(\n ToolCall::intent_from_input(&serde_json::json!({\"intent\": \" \"})),\n None\n );\n }\n\n #[test]\n fn sanitize_tool_id_hyphens_passthrough() {\n assert_eq!(sanitize_tool_id(\"call-abc-123\"), \"call-abc-123\");\n assert_eq!(\n sanitize_tool_id(\"tool_use-id_with-mixed\"),\n \"tool_use-id_with-mixed\"\n );\n }\n\n #[test]\n fn sanitize_tool_id_replaces_dots() {\n assert_eq!(\n sanitize_tool_id(\"chatcmpl-abc.def.ghi\"),\n \"chatcmpl-abc_def_ghi\"\n );\n assert_eq!(sanitize_tool_id(\"call.123\"), \"call_123\");\n }\n\n #[test]\n fn sanitize_tool_id_replaces_colons() {\n assert_eq!(sanitize_tool_id(\"call:123:456\"), \"call_123_456\");\n }\n\n #[test]\n fn sanitize_tool_id_replaces_special_chars() {\n assert_eq!(\n sanitize_tool_id(\"id@with#special$chars\"),\n \"id_with_special_chars\"\n );\n assert_eq!(sanitize_tool_id(\"id with spaces\"), \"id_with_spaces\");\n }\n\n #[test]\n fn sanitize_tool_id_empty_returns_unknown() {\n assert_eq!(sanitize_tool_id(\"\"), \"unknown\");\n }\n\n #[test]\n fn sanitize_tool_id_copilot_to_anthropic() {\n assert_eq!(\n sanitize_tool_id(\"chatcmpl-BF2xX.tool_call.0\"),\n \"chatcmpl-BF2xX_tool_call_0\"\n );\n }\n\n #[test]\n fn sanitize_tool_id_already_valid_unchanged() {\n let valid_ids = [\n \"toolu_01XFDUDYJgAACzvnptvVer6u\",\n \"call_abc123\",\n \"fallback_text_call_call_1234567890_9876543210\",\n \"tool_123\",\n \"a\",\n \"A\",\n \"0\",\n \"_\",\n \"-\",\n \"a-b_c\",\n ];\n for id in valid_ids {\n assert_eq!(sanitize_tool_id(id), id, \"ID '{}' should be unchanged\", id);\n }\n }\n\n #[test]\n fn redact_secrets_redacts_known_direct_token_formats() {\n let input = \"access=sk-ant-oat01-ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789\\nopenrouter=sk-or-v1-abcdefghijklmnopqrstuvwxyz0123456789\\ngithub=ghp_ABCDEFGHIJKLMNOPQRSTUVWXYZ0123\\n\";\n let out = redact_secrets(input);\n assert!(!out.contains(\"sk-ant-oat01-\"));\n assert!(!out.contains(\"sk-or-v1-\"));\n assert!(!out.contains(\"ghp_ABCDEFGHIJKLMNOPQRSTUVWXYZ0123\"));\n assert!(out.matches(\"[REDACTED_SECRET]\").count() >= 3);\n }\n\n #[test]\n fn redact_secrets_redacts_env_style_assignments() {\n let input = \"OPENROUTER_API_KEY=sk-or-v1-abc123abc123abc123abc123\\nOPENCODE_API_KEY=oc_test_secret\\nOPENCODE_GO_API_KEY=ocgo_test_secret\\nZAI_API_KEY=zai_secret\\nCHUTES_API_KEY=chutes_secret\\nCEREBRAS_API_KEY=cerebras_secret\\nOPENAI_COMPAT_API_KEY=compat_secret\\nCURSOR_API_KEY='my_cursor_secret_value'\\nOPENAI_API_KEY=sk-test-openai-example\\nAZURE_OPENAI_API_KEY=azure-openai-secret\\n\";\n let out = redact_secrets(input);\n assert!(out.contains(\"OPENROUTER_API_KEY=[REDACTED_SECRET]\"));\n assert!(out.contains(\"OPENCODE_API_KEY=[REDACTED_SECRET]\"));\n assert!(out.contains(\"OPENCODE_GO_API_KEY=[REDACTED_SECRET]\"));\n assert!(out.contains(\"ZAI_API_KEY=[REDACTED_SECRET]\"));\n assert!(out.contains(\"CHUTES_API_KEY=[REDACTED_SECRET]\"));\n assert!(out.contains(\"CEREBRAS_API_KEY=[REDACTED_SECRET]\"));\n assert!(out.contains(\"OPENAI_COMPAT_API_KEY=[REDACTED_SECRET]\"));\n assert!(out.contains(\"CURSOR_API_KEY=[REDACTED_SECRET]\"));\n assert!(out.contains(\"OPENAI_API_KEY=[REDACTED_SECRET]\"));\n assert!(out.contains(\"AZURE_OPENAI_API_KEY=[REDACTED_SECRET]\"));\n assert!(!out.contains(\"my_cursor_secret_value\"));\n }\n\n #[test]\n fn redact_secrets_redacts_runtime_key_assignment() {\n let key_var = \"JCODE_OPENAI_COMPAT_API_KEY_NAME\";\n let prev = std::env::var(key_var).ok();\n crate::env::set_var(key_var, \"GROQ_API_KEY\");\n\n let input = \"GROQ_API_KEY=my_secret_token_value\";\n let out = redact_secrets(input);\n assert_eq!(out, \"GROQ_API_KEY=[REDACTED_SECRET]\");\n\n if let Some(v) = prev {\n crate::env::set_var(key_var, v);\n } else {\n crate::env::remove_var(key_var);\n }\n }\n\n #[test]\n fn redact_secrets_redacts_mixed_case_token_assignments() {\n let input = \"my_token=ya29.ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789\";\n let out = redact_secrets(input);\n assert!(out.contains(\"[REDACTED_SECRET]\"));\n assert!(!out.contains(\"ya29.ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789\"));\n }\n\n #[test]\n fn redact_secrets_leaves_normal_output_unchanged() {\n let input = \"Found 5 files\\nNo auth errors\\nDone.\";\n assert_eq!(redact_secrets(input), input);\n }\n\n #[test]\n fn format_timestamp_is_stable_utc_rfc3339() {\n let ts = chrono::DateTime::parse_from_rfc3339(\"2025-03-15T02:24:13.250Z\")\n .unwrap()\n .with_timezone(&Utc);\n assert_eq!(Message::format_timestamp(&ts), \"2025-03-15T02:24:13.250Z\");\n }\n\n #[test]\n fn with_timestamps_prepends_utc_prefix_to_user_text() {\n let ts = chrono::DateTime::parse_from_rfc3339(\"2025-03-15T02:24:03Z\")\n .unwrap()\n .with_timezone(&Utc);\n let stamped = Message::with_timestamps(&[Message {\n role: Role::User,\n content: vec![ContentBlock::Text {\n text: \"hello\".to_string(),\n cache_control: None,\n }],\n timestamp: Some(ts),\n tool_duration_ms: None,\n }]);\n match &stamped[0].content[0] {\n ContentBlock::Text { text, .. } => {\n assert_eq!(text, \"[2025-03-15T02:24:03.000Z] hello\");\n }\n other => panic!(\"expected text block, got {other:?}\"),\n }\n }\n\n #[test]\n fn with_timestamps_adds_tool_timing_header_with_duration() {\n let ts = chrono::DateTime::parse_from_rfc3339(\"2025-03-15T02:24:13Z\")\n .unwrap()\n .with_timezone(&Utc);\n let stamped = Message::with_timestamps(&[Message {\n role: Role::User,\n content: vec![ContentBlock::ToolResult {\n tool_use_id: \"call_1\".to_string(),\n content: \"ok\".to_string(),\n is_error: None,\n }],\n timestamp: Some(ts),\n tool_duration_ms: Some(3_200),\n }]);\n match &stamped[0].content[0] {\n ContentBlock::ToolResult { content, .. } => {\n assert_eq!(\n content,\n \"[tool timing: start=2025-03-15T02:24:09.800Z finish=2025-03-15T02:24:13.000Z duration=3.2s] ok\"\n );\n }\n other => panic!(\"expected tool result block, got {other:?}\"),\n }\n }\n\n #[test]\n fn with_timestamps_skips_internal_system_reminders() {\n let ts = chrono::DateTime::parse_from_rfc3339(\"2025-03-15T02:24:13Z\")\n .unwrap()\n .with_timezone(&Utc);\n let original = Message {\n role: Role::User,\n content: vec![ContentBlock::Text {\n text: \"<system-reminder>\\ninternal\\n</system-reminder>\".to_string(),\n cache_control: None,\n }],\n timestamp: Some(ts),\n tool_duration_ms: None,\n };\n let stamped = Message::with_timestamps(std::slice::from_ref(&original));\n match &stamped[0].content[0] {\n ContentBlock::Text { text, .. } => {\n assert_eq!(text, \"<system-reminder>\\ninternal\\n</system-reminder>\");\n }\n other => panic!(\"expected text block, got {other:?}\"),\n }\n }\n\n #[test]\n fn ends_with_fresh_user_turn_accepts_plain_user_text() {\n let messages = vec![Message::user(\"hello\")];\n assert!(ends_with_fresh_user_turn(&messages));\n }\n\n #[test]\n fn ends_with_fresh_user_turn_rejects_trailing_tool_result() {\n let messages = vec![\n Message::user(\"hello\"),\n Message {\n role: Role::Assistant,\n content: vec![ContentBlock::ToolUse {\n id: \"call_1\".to_string(),\n name: \"bash\".to_string(),\n input: serde_json::json!({}),\n }],\n timestamp: Some(Utc::now()),\n tool_duration_ms: None,\n },\n Message::tool_result(\"call_1\", \"ok\", false),\n ];\n assert!(!ends_with_fresh_user_turn(&messages));\n }\n\n #[test]\n fn ends_with_fresh_user_turn_skips_internal_system_reminders() {\n let messages = vec![\n Message::user(\"hello\"),\n Message::user(\"<system-reminder>\\ninternal\\n</system-reminder>\"),\n ];\n assert!(ends_with_fresh_user_turn(&messages));\n }\n\n #[test]\n fn ends_with_fresh_user_turn_rejects_assistant_tail() {\n let messages = vec![\n Message::user(\"hello\"),\n Message::assistant_text(\"working on it\"),\n ];\n assert!(!ends_with_fresh_user_turn(&messages));\n }\n\n #[test]\n fn format_background_task_notification_markdown_uses_code_block_preview() {\n let rendered = format_background_task_notification_markdown(&BackgroundTaskCompleted {\n task_id: \"abc123\".to_string(),\n tool_name: \"bash\".to_string(),\n session_id: \"session\".to_string(),\n status: BackgroundTaskStatus::Completed,\n exit_code: Some(0),\n output_preview: \"[stderr] first line\\n[stdout] second line\\n\".to_string(),\n output_file: std::path::PathBuf::from(\"/tmp/output.log\"),\n duration_secs: 7.1,\n notify: true,\n wake: false,\n });\n\n assert!(\n rendered\n .contains(\"**Background task** `abc123` · `bash` · ✓ completed · 7.1s · exit 0\")\n );\n assert!(rendered.contains(\"```text\\n[stderr] first line\\n[stdout] second line\\n```\"));\n assert!(rendered.contains(\"_Full output:_ `bg action=\\\"output\\\" task_id=\\\"abc123\\\"`\"));\n }\n\n #[test]\n fn format_background_task_notification_markdown_handles_empty_preview() {\n let rendered = format_background_task_notification_markdown(&BackgroundTaskCompleted {\n task_id: \"abc123\".to_string(),\n tool_name: \"bash\".to_string(),\n session_id: \"session\".to_string(),\n status: BackgroundTaskStatus::Failed,\n exit_code: Some(9),\n output_preview: \"\\n\\n\".to_string(),\n output_file: std::path::PathBuf::from(\"/tmp/output.log\"),\n duration_secs: 1.0,\n notify: true,\n wake: false,\n });\n\n assert!(rendered.contains(\"✗ failed\"));\n assert!(rendered.contains(\"_No output captured._\"));\n }\n\n #[test]\n fn format_background_task_notification_markdown_renders_superseded_status() {\n let rendered = format_background_task_notification_markdown(&BackgroundTaskCompleted {\n task_id: \"abc123\".to_string(),\n tool_name: \"selfdev-build\".to_string(),\n session_id: \"session\".to_string(),\n status: BackgroundTaskStatus::Superseded,\n exit_code: Some(0),\n output_preview: \"Build completed, but source changed before activation\".to_string(),\n output_file: std::path::PathBuf::from(\"/tmp/output.log\"),\n duration_secs: 5.0,\n notify: true,\n wake: false,\n });\n\n assert!(rendered.contains(\"↻ superseded\"));\n assert!(rendered.contains(\"exit 0\"));\n assert!(rendered.contains(\"source changed before activation\"));\n }\n\n #[test]\n fn format_background_task_progress_markdown_uses_compact_multiline_layout() {\n let rendered = format_background_task_progress_markdown(&BackgroundTaskProgressEvent {\n task_id: \"bgprogress\".to_string(),\n tool_name: \"bash\".to_string(),\n session_id: \"session\".to_string(),\n progress: crate::bus::BackgroundTaskProgress {\n kind: crate::bus::BackgroundTaskProgressKind::Determinate,\n percent: Some(42.0),\n message: Some(\"Running tests\".to_string()),\n current: Some(21),\n total: Some(50),\n unit: Some(\"tests\".to_string()),\n eta_seconds: None,\n updated_at: Utc::now().to_rfc3339(),\n source: crate::bus::BackgroundTaskProgressSource::Reported,\n },\n });\n\n assert!(rendered.starts_with(\"**Background task progress** `bgprogress` · `bash`\\n\\n\"));\n assert!(rendered.contains(\"42% · Running tests\"));\n assert!(rendered.contains(\"(reported)\"));\n }\n\n #[test]\n fn parse_background_task_progress_notification_extracts_card_fields() {\n let parsed = parse_background_task_progress_notification_markdown(\n \"**Background task progress** `bgprogress` · `bash`\\n\\n[#####-------] 42% · Running tests (reported)\",\n )\n .expect(\"progress notification should parse\");\n\n assert_eq!(parsed.task_id, \"bgprogress\");\n assert_eq!(parsed.tool_name, \"bash\");\n assert_eq!(parsed.summary, \"42% · Running tests\");\n assert_eq!(parsed.source.as_deref(), Some(\"reported\"));\n assert_eq!(parsed.percent, Some(42.0));\n }\n\n #[test]\n fn parse_background_task_progress_notification_supports_legacy_inline_layout() {\n let parsed = parse_background_task_progress_notification_markdown(\n \"**Background task progress** `bgprogress` · `bash` · Release run in_progress: - 7/8 jobs completed (reported)\",\n )\n .expect(\"legacy progress notification should parse\");\n\n assert_eq!(parsed.task_id, \"bgprogress\");\n assert_eq!(\n parsed.summary,\n \"Release run in_progress: - 7/8 jobs completed\"\n );\n assert_eq!(parsed.source.as_deref(), Some(\"reported\"));\n assert_eq!(parsed.percent, None);\n }\n\n #[test]\n fn description_token_estimate_uses_chars_per_token_heuristic() {\n let def = ToolDefinition {\n name: \"read\".to_string(),\n description: \"abcdwxyz\".to_string(),\n input_schema: serde_json::json!({\"type\": \"object\"}),\n };\n\n assert_eq!(def.description_token_estimate(), 2);\n }\n\n #[test]\n fn parse_background_task_notification_markdown_extracts_fields() {\n let rendered = format_background_task_notification_markdown(&BackgroundTaskCompleted {\n task_id: \"abc123\".to_string(),\n tool_name: \"bash\".to_string(),\n session_id: \"session\".to_string(),\n status: BackgroundTaskStatus::Completed,\n exit_code: Some(0),\n output_preview: \"[stderr] first line\\n[stdout] second line\\n\".to_string(),\n output_file: std::path::PathBuf::from(\"/tmp/output.log\"),\n duration_secs: 7.1,\n notify: true,\n wake: false,\n });\n\n let parsed = parse_background_task_notification_markdown(&rendered)\n .expect(\"background task notification should parse\");\n assert_eq!(parsed.task_id, \"abc123\");\n assert_eq!(parsed.tool_name, \"bash\");\n assert_eq!(parsed.status, \"✓ completed\");\n assert_eq!(parsed.duration, \"7.1s\");\n assert_eq!(parsed.exit_label, \"exit 0\");\n assert_eq!(\n parsed.preview.as_deref(),\n Some(\"[stderr] first line\\n[stdout] second line\")\n );\n assert_eq!(\n parsed.full_output_command,\n \"bg action=\\\"output\\\" task_id=\\\"abc123\\\"\"\n );\n }\n}\n"}, "5898de6041e7f5f2daed79af760fed40c0c062e0": {"t": 1785757228.4786892, "n": 38022, "h": "2b452f1dde14fff1", "c": "use crate::bus::{BackgroundTaskCompleted, BackgroundTaskProgressEvent, BackgroundTaskStatus};\nuse chrono::Utc;\nuse regex::Regex;\nuse serde::{Deserialize, Serialize};\nuse std::collections::HashSet;\nuse std::sync::OnceLock;\n\nmod notifications;\n\npub use notifications::{\n InputShellResult, ParsedBackgroundTaskNotification, ParsedBackgroundTaskProgressNotification,\n background_task_status_notice, format_background_task_notification_markdown,\n format_background_task_progress_markdown, format_input_shell_result_markdown,\n input_shell_status_notice, parse_background_task_notification_markdown,\n parse_background_task_progress_notification_markdown,\n};\n\nfn compile_static_regex(pattern: &str) -> Option<Regex> {\n match Regex::new(pattern) {\n Ok(regex) => Some(regex),\n Err(err) => {\n eprintln!(\"jcode: failed to compile static regex: {err}\");\n None\n }\n }\n}\n\nfn compile_static_regexes(patterns: &[&str]) -> Vec<Regex> {\n patterns\n .iter()\n .filter_map(|pattern| compile_static_regex(pattern))\n .collect()\n}\n\n/// Role in conversation\n#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]\n#[serde(rename_all = \"lowercase\")]\npub enum Role {\n User,\n Assistant,\n}\n\n/// Plain-text tool output placeholder when execution was interrupted.\npub const TOOL_OUTPUT_MISSING_TEXT: &str =\n \"Tool output missing (session interrupted before tool execution completed)\";\n\n/// A message in the conversation\n#[derive(Debug, Clone, Serialize, Deserialize)]\npub struct Message {\n pub role: Role,\n pub content: Vec<ContentBlock>,\n #[serde(default, skip_serializing_if = \"Option::is_none\")]\n pub timestamp: Option<chrono::DateTime<Utc>>,\n #[serde(default, skip_serializing_if = \"Option::is_none\")]\n pub tool_duration_ms: Option<u64>,\n}\n\n/// Cache control metadata for prompt caching\n#[derive(Debug, Clone, Serialize, Deserialize)]\npub struct CacheControl {\n #[serde(rename = \"type\")]\n pub kind: String,\n #[serde(skip_serializing_if = \"Option::is_none\")]\n pub ttl: Option<String>,\n}\n\nimpl CacheControl {\n pub fn ephemeral(ttl: Option<String>) -> Self {\n Self {\n kind: \"ephemeral\".to_string(),\n ttl,\n }\n }\n}\n\n/// Content block within a message\n#[derive(Debug, Clone, Serialize, Deserialize)]\n#[serde(tag = \"type\", rename_all = \"snake_case\")]\npub enum ContentBlock {\n Text {\n text: String,\n #[serde(skip_serializing_if = \"Option::is_none\")]\n cache_control: Option<CacheControl>,\n },\n /// Hidden reasoning content used for providers that require it (not displayed)\n Reasoning {\n text: String,\n },\n ToolUse {\n id: String,\n name: String,\n input: serde_json::Value,\n },\n ToolResult {\n tool_use_id: String,\n content: String,\n #[serde(skip_serializing_if = \"Option::is_none\")]\n is_error: Option<bool>,\n },\n Image {\n media_type: String,\n data: String,\n },\n /// Hidden OpenAI Responses compaction item used to preserve native\n /// compaction state across turns/saves when jcode explicitly triggers it.\n OpenAICompaction {\n encrypted_content: String,\n },\n}\n\nimpl Message {\n pub fn user(text: &str) -> Self {\n Self {\n role: Role::User,\n content: vec![ContentBlock::Text {\n text: text.to_string(),\n cache_control: None,\n }],\n timestamp: Some(Utc::now()),\n tool_duration_ms: None,\n }\n }\n\n pub fn user_with_images(text: &str, images: Vec<(String, String)>) -> Self {\n let mut content: Vec<ContentBlock> = images\n .into_iter()\n .map(|(media_type, data)| ContentBlock::Image { media_type, data })\n .collect();\n content.push(ContentBlock::Text {\n text: text.to_string(),\n cache_control: None,\n });\n Self {\n role: Role::User,\n content,\n timestamp: Some(Utc::now()),\n tool_duration_ms: None,\n }\n }\n\n pub fn assistant_text(text: &str) -> Self {\n Self {\n role: Role::Assistant,\n content: vec![ContentBlock::Text {\n text: text.to_string(),\n cache_control: None,\n }],\n timestamp: Some(Utc::now()),\n tool_duration_ms: None,\n }\n }\n\n pub fn tool_result(tool_use_id: &str, content: &str, is_error: bool) -> Self {\n Self::tool_result_with_duration(tool_use_id, content, is_error, None)\n }\n\n pub fn tool_result_with_duration(\n tool_use_id: &str,\n content: &str,\n is_error: bool,\n tool_duration_ms: Option<u64>,\n ) -> Self {\n Self {\n role: Role::User,\n content: vec![ContentBlock::ToolResult {\n tool_use_id: tool_use_id.to_string(),\n content: content.to_string(),\n is_error: if is_error { Some(true) } else { None },\n }],\n timestamp: Some(Utc::now()),\n tool_duration_ms,\n }\n }\n\n /// Format a timestamp deterministically in UTC for injection into model-visible content.\n pub fn format_timestamp(ts: &chrono::DateTime<Utc>) -> String {\n ts.to_rfc3339_opts(chrono::SecondsFormat::Millis, true)\n }\n\n pub fn format_duration(duration_ms: u64) -> String {\n match duration_ms {\n 0..=999 => format!(\"{}ms\", duration_ms),\n 1_000..=9_999 => format!(\"{:.1}s\", duration_ms as f64 / 1000.0),\n 10_000..=59_999 => format!(\"{}s\", duration_ms / 1000),\n _ => {\n let total_seconds = duration_ms / 1000;\n let minutes = total_seconds / 60;\n let seconds = total_seconds % 60;\n if seconds == 0 {\n format!(\"{}m\", minutes)\n } else {\n format!(\"{}m {}s\", minutes, seconds)\n }\n }\n }\n }\n\n pub fn is_internal_system_reminder(&self) -> bool {\n self.content\n .iter()\n .find_map(|block| match block {\n ContentBlock::Text { text, .. } => Some(text.trim_start()),\n _ => None,\n })\n .is_some_and(|text| text.starts_with(\"<system-reminder>\"))\n }\n\n fn should_skip_timestamp_injection(&self) -> bool {\n self.is_internal_system_reminder()\n }\n\n fn tool_result_tag(&self, ts: &chrono::DateTime<Utc>) -> String {\n match self.tool_duration_ms {\n Some(duration_ms) => {\n let duration_ms_i64 = i64::try_from(duration_ms).unwrap_or(i64::MAX);\n let start_ts = ts\n .checked_sub_signed(chrono::Duration::milliseconds(duration_ms_i64))\n .unwrap_or(*ts);\n format!(\n \"[tool timing: start={} finish={} duration={}]\",\n Self::format_timestamp(&start_ts),\n Self::format_timestamp(ts),\n Self::format_duration(duration_ms)\n )\n }\n None => format!(\"[{}]\", Self::format_timestamp(ts)),\n }\n }\n\n /// Return a copy of messages with timestamps injected into user-role text content.\n /// Tool results get a stable UTC timing header prepended to content.\n /// User text messages get a stable UTC timestamp prepended to the first text block.\n pub fn with_timestamps(messages: &[Message]) -> Vec<Message> {\n messages\n .iter()\n .map(|msg| {\n let Some(ts) = msg.timestamp else {\n return msg.clone();\n };\n if msg.role != Role::User || msg.should_skip_timestamp_injection() {\n return msg.clone();\n }\n let text_tag = format!(\"[{}]\", Self::format_timestamp(&ts));\n let tool_result_tag = msg.tool_result_tag(&ts);\n let mut msg = msg.clone();\n let mut tagged = false;\n for block in &mut msg.content {\n match block {\n ContentBlock::Text { text, .. } if !tagged => {\n *text = format!(\"{} {}\", text_tag, text);\n tagged = true;\n }\n ContentBlock::ToolResult { content, .. } if !tagged => {\n *content = format!(\"{} {}\", tool_result_tag, content);\n tagged = true;\n }\n _ => {}\n }\n }\n msg\n })\n .collect()\n }\n}\n\nconst STABLE_HASH_SEED: u64 = 0xcbf29ce484222325;\nconst STABLE_HASH_PRIME: u64 = 0x100000001b3;\n\nfn stable_hash_bytes(bytes: &[u8]) -> u64 {\n let mut hash = STABLE_HASH_SEED;\n for byte in bytes {\n hash ^= *byte as u64;\n hash = hash.wrapping_mul(STABLE_HASH_PRIME);\n }\n hash\n}\n\npub(crate) fn extend_stable_hash(acc: u64, next: u64) -> u64 {\n stable_hash_bytes(&[acc.to_le_bytes().as_slice(), next.to_le_bytes().as_slice()].concat())\n}\n\npub(crate) fn stable_message_hash(message: &Message) -> u64 {\n match serde_json::to_vec(message) {\n Ok(bytes) => stable_hash_bytes(&bytes),\n Err(_) => stable_hash_bytes(format!(\"{:?}\", message).as_bytes()),\n }\n}\n\npub fn ends_with_fresh_user_turn(messages: &[Message]) -> bool {\n for msg in messages.iter().rev() {\n if msg.role != Role::User {\n return false;\n }\n\n if msg\n .content\n .iter()\n .any(|block| matches!(block, ContentBlock::ToolResult { .. }))\n {\n return false;\n }\n\n if msg.content.is_empty() {\n return false;\n }\n\n let mut saw_user_text = false;\n for block in &msg.content {\n match block {\n ContentBlock::Text { text, .. } => {\n let trimmed = text.trim();\n if !trimmed.is_empty() && !trimmed.starts_with(\"<system-reminder>\") {\n saw_user_text = true;\n }\n }\n _ => return false,\n }\n }\n\n if saw_user_text {\n return true;\n }\n\n if msg.is_internal_system_reminder() {\n continue;\n }\n\n return false;\n }\n\n false\n}\n\n/// Sanitize a tool ID so it matches the pattern `^[a-zA-Z0-9_-]+$`.\n///\n/// Different providers generate tool IDs in different formats. When switching\n/// from one provider to another mid-conversation, the historical tool IDs may\n/// contain characters that the new provider rejects (e.g., dots in Copilot IDs\n/// sent to Anthropic). This function replaces any invalid characters with\n/// underscores.\npub fn sanitize_tool_id(id: &str) -> String {\n if id.is_empty() {\n return \"unknown\".to_string();\n }\n let sanitized: String = id\n .chars()\n .map(|c| {\n if c.is_ascii_alphanumeric() || c == '_' || c == '-' {\n c\n } else {\n '_'\n }\n })\n .collect();\n if sanitized.is_empty() {\n \"unknown\".to_string()\n } else {\n sanitized\n }\n}\n\n/// Redact likely secrets from persisted tool output.\n///\n/// This is a best-effort safeguard for local session history files. It targets\n/// high-confidence token/key patterns and common `KEY=VALUE` assignments used by\n/// auth flows.\npub fn redact_secrets(text: &str) -> String {\n // Fast path to avoid regex work for most tool outputs.\n let lower = text.to_ascii_lowercase();\n\n if !text.contains(\"sk-\")\n && !text.contains(\"ghp_\")\n && !text.contains(\"github_pat_\")\n && !text.contains(\"AIza\")\n && !text.contains(\"ya29.\")\n && !text.contains(\"xox\")\n && !lower.contains(\"api_key\")\n && !lower.contains(\"token\")\n {\n return text.to_string();\n }\n\n static DIRECT_PATTERNS: OnceLock<Vec<Regex>> = OnceLock::new();\n static ASSIGNMENT_PATTERNS: OnceLock<Vec<Regex>> = OnceLock::new();\n\n let direct_patterns = DIRECT_PATTERNS.get_or_init(|| {\n compile_static_regexes(&[\n r\"sk-ant-(?:oat|ort)01-[A-Za-z0-9_-]{20,}\",\n r\"sk-or-v1-[A-Za-z0-9_-]{20,}\",\n r\"ghp_[A-Za-z0-9]{20,}\",\n r\"github_pat_[A-Za-z0-9_]{20,}\",\n r\"ya29\\.[A-Za-z0-9._-]{20,}\",\n r\"AIza[0-9A-Za-z_-]{20,}\",\n r\"xox[baprs]-[A-Za-z0-9-]{10,}\",\n ])\n });\n\n let assignment_patterns = ASSIGNMENT_PATTERNS.get_or_init(|| {\n compile_static_regexes(&[\n r\"(?m)^\\s*(OPENROUTER_API_KEY\\s*=\\s*)[^\\r\\n]+\",\n r\"(?m)^\\s*(OPENCODE_API_KEY\\s*=\\s*)[^\\r\\n]+\",\n r\"(?m)^\\s*(OPENCODE_GO_API_KEY\\s*=\\s*)[^\\r\\n]+\",\n r\"(?m)^\\s*(ZHIPU_API_KEY\\s*=\\s*)[^\\r\\n]+\",\n r\"(?m)^\\s*(ZAI_API_KEY\\s*=\\s*)[^\\r\\n]+\",\n r\"(?m)^\\s*(302AI_API_KEY\\s*=\\s*)[^\\r\\n]+\",\n r\"(?m)^\\s*(BASETEN_API_KEY\\s*=\\s*)[^\\r\\n]+\",\n r\"(?m)^\\s*(CORTECS_API_KEY\\s*=\\s*)[^\\r\\n]+\",\n r\"(?m)^\\s*(DEEPSEEK_API_KEY\\s*=\\s*)[^\\r\\n]+\",\n r\"(?m)^\\s*(FIRMWARE_API_KEY\\s*=\\s*)[^\\r\\n]+\",\n r\"(?m)^\\s*(HF_TOKEN\\s*=\\s*)[^\\r\\n]+\",\n r\"(?m)^\\s*(MOONSHOT_API_KEY\\s*=\\s*)[^\\r\\n]+\",\n r\"(?m)^\\s*(NEBIUS_API_KEY\\s*=\\s*)[^\\r\\n]+\",\n r\"(?m)^\\s*(SCALEWAY_API_KEY\\s*=\\s*)[^\\r\\n]+\",\n r\"(?m)^\\s*(STACKIT_API_KEY\\s*=\\s*)[^\\r\\n]+\",\n r\"(?m)^\\s*(GROQ_API_KEY\\s*=\\s*)[^\\r\\n]+\",\n r\"(?m)^\\s*(MISTRAL_API_KEY\\s*=\\s*)[^\\r\\n]+\",\n r\"(?m)^\\s*(PERPLEXITY_API_KEY\\s*=\\s*)[^\\r\\n]+\",\n r\"(?m)^\\s*(TOGETHER_API_KEY\\s*=\\s*)[^\\r\\n]+\",\n r\"(?m)^\\s*(DEEPINFRA_API_KEY\\s*=\\s*)[^\\r\\n]+\",\n r\"(?m)^\\s*(XAI_API_KEY\\s*=\\s*)[^\\r\\n]+\",\n r\"(?m)^\\s*(LMSTUDIO_API_KEY\\s*=\\s*)[^\\r\\n]+\",\n r\"(?m)^\\s*(OLLAMA_API_KEY\\s*=\\s*)[^\\r\\n]+\",\n r\"(?m)^\\s*(CHUTES_API_KEY\\s*=\\s*)[^\\r\\n]+\",\n r\"(?m)^\\s*(CEREBRAS_API_KEY\\s*=\\s*)[^\\r\\n]+\",\n r\"(?m)^\\s*(OPENAI_COMPAT_API_KEY\\s*=\\s*)[^\\r\\n]+\",\n r\"(?m)^\\s*(ANTHROPIC_API_KEY\\s*=\\s*)[^\\r\\n]+\",\n r\"(?m)^\\s*(OPENAI_API_KEY\\s*=\\s*)[^\\r\\n]+\",\n r\"(?m)^\\s*(AZURE_OPENAI_API_KEY\\s*=\\s*)[^\\r\\n]+\",\n r\"(?m)^\\s*(CURSOR_API_KEY\\s*=\\s*)[^\\r\\n]+\",\n r\"(?m)^\\s*(GITHUB_TOKEN\\s*=\\s*)[^\\r\\n]+\",\n ])\n });\n\n let mut redacted = text.to_string();\n let mut redacted_keys: HashSet<String> = [\n \"OPENROUTER_API_KEY\",\n \"OPENCODE_API_KEY\",\n \"OPENCODE_GO_API_KEY\",\n \"ZHIPU_API_KEY\",\n \"ZAI_API_KEY\",\n \"302AI_API_KEY\",\n \"BASETEN_API_KEY\",\n \"CORTECS_API_KEY\",\n \"DEEPSEEK_API_KEY\",\n \"FIRMWARE_API_KEY\",\n \"HF_TOKEN\",\n \"MOONSHOT_API_KEY\",\n \"NEBIUS_API_KEY\",\n \"SCALEWAY_API_KEY\",\n \"STACKIT_API_KEY\",\n \"GROQ_API_KEY\",\n \"MISTRAL_API_KEY\",\n \"PERPLEXITY_API_KEY\",\n \"TOGETHER_API_KEY\",\n \"DEEPINFRA_API_KEY\",\n \"XAI_API_KEY\",\n \"LMSTUDIO_API_KEY\",\n \"OLLAMA_API_KEY\",\n \"CHUTES_API_KEY\",\n \"CEREBRAS_API_KEY\",\n \"OPENAI_COMPAT_API_KEY\",\n \"ANTHROPIC_API_KEY\",\n \"OPENAI_API_KEY\",\n \"AZURE_OPENAI_API_KEY\",\n \"CURSOR_API_KEY\",\n \"GITHUB_TOKEN\",\n ]\n .iter()\n .map(|k| (*k).to_string())\n .collect();\n\n for re in direct_patterns {\n redacted = re.replace_all(&redacted, \"[REDACTED_SECRET]\").into_owned();\n }\n\n for re in assignment_patterns {\n redacted = re\n .replace_all(&redacted, \"${1}[REDACTED_SECRET]\")\n .into_owned();\n }\n\n // Also redact custom API key variable names configured at runtime.\n for source in [\n \"JCODE_OPENROUTER_API_KEY_NAME\",\n \"JCODE_OPENAI_COMPAT_API_KEY_NAME\",\n ] {\n let Some(key_name) = std::env::var(source)\n .ok()\n .map(|v| v.trim().to_string())\n .filter(|v| !v.is_empty())\n else {\n continue;\n };\n\n if !key_name\n .chars()\n .all(|c| c.is_ascii_uppercase() || c.is_ascii_digit() || c == '_')\n {\n continue;\n }\n if !redacted_keys.insert(key_name.clone()) {\n continue;\n }\n\n let pattern = format!(r\"(?m)^\\s*({}\\s*=\\s*)[^\\r\\n]+\", regex::escape(&key_name));\n if let Ok(re) = Regex::new(&pattern) {\n redacted = re\n .replace_all(&redacted, \"${1}[REDACTED_SECRET]\")\n .into_owned();\n }\n }\n\n redacted\n}\n\n/// Tool definition for the API\n#[derive(Debug, Clone, Serialize)]\npub struct ToolDefinition {\n pub name: String,\n // Prompt-visible text sent to the model by provider adapters.\n // Approximate prompt cost: description.len() / 4. Use\n // ToolDefinition::description_token_estimate() when reviewing tool bloat.\n pub description: String,\n pub input_schema: serde_json::Value,\n}\n\nimpl ToolDefinition {\n /// Serialized size of the full tool definition payload sent to providers.\n pub fn prompt_chars(&self) -> usize {\n serde_json::json!({\n \"name\": self.name,\n \"description\": self.description,\n \"input_schema\": self.input_schema,\n })\n .to_string()\n .len()\n }\n\n /// Approximate prompt-token cost of this tool's top-level description.\n ///\n /// This uses jcode's standard chars/4 heuristic, matching other token\n /// budget estimates in the codebase.\n pub fn description_token_estimate(&self) -> usize {\n crate::util::estimate_tokens(&self.description)\n }\n\n /// Approximate prompt-token cost of the full tool definition payload.\n pub fn prompt_token_estimate(&self) -> usize {\n crate::util::estimate_tokens(\n &serde_json::json!({\n \"name\": self.name,\n \"description\": self.description,\n \"input_schema\": self.input_schema,\n })\n .to_string(),\n )\n }\n\n pub fn aggregate_prompt_chars(defs: &[ToolDefinition]) -> usize {\n defs.iter().map(Self::prompt_chars).sum()\n }\n\n pub fn aggregate_prompt_token_estimate(defs: &[ToolDefinition]) -> usize {\n defs.iter().map(Self::prompt_token_estimate).sum()\n }\n}\n\n/// A tool call from the model\n#[derive(Debug, Clone, Default, PartialEq, Eq, serde::Serialize, serde::Deserialize)]\npub struct ToolCall {\n #[serde(default)]\n pub id: String,\n #[serde(default)]\n pub name: String,\n #[serde(default)]\n pub input: serde_json::Value,\n #[serde(default, skip_serializing_if = \"Option::is_none\")]\n pub intent: Option<String>,\n}\n\n/// Connection phase for status bar transparency\n#[derive(Debug, Clone, PartialEq)]\npub enum ConnectionPhase {\n /// Refreshing OAuth token\n Authenticating,\n /// TCP + TLS connection to API\n Connecting,\n /// HTTP request sent, waiting for first response byte\n WaitingForResponse,\n /// First byte received, stream is active\n Streaming,\n /// Retrying after a transient error\n Retrying { attempt: u32, max: u32 },\n}\n\nimpl std::fmt::Display for ConnectionPhase {\n fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {\n match self {\n ConnectionPhase::Authenticating => write!(f, \"authenticating\"),\n ConnectionPhase::Connecting => write!(f, \"connecting\"),\n ConnectionPhase::WaitingForResponse => write!(f, \"waiting for response\"),\n ConnectionPhase::Streaming => write!(f, \"streaming\"),\n ConnectionPhase::Retrying { attempt, max } => {\n write!(f, \"retrying ({}/{})\", attempt, max)\n }\n }\n }\n}\n\n/// Streaming event from provider\n#[derive(Debug, Clone)]\npub enum StreamEvent {\n /// Text content delta\n TextDelta(String),\n /// Tool use started\n ToolUseStart { id: String, name: String },\n /// Tool input delta (JSON fragment)\n ToolInputDelta(String),\n /// Tool use complete\n ToolUseEnd,\n /// Tool result from provider (provider already executed the tool)\n ToolResult {\n tool_use_id: String,\n content: String,\n is_error: bool,\n },\n /// Extended thinking started\n ThinkingStart,\n /// Extended thinking delta (reasoning content)\n ThinkingDelta(String),\n /// Extended thinking ended\n ThinkingEnd,\n /// Extended thinking completed with duration\n ThinkingDone { duration_secs: f64 },\n /// Message complete (may have stop reason)\n MessageEnd { stop_reason: Option<String> },\n /// Token usage update\n TokenUsage {\n input_tokens: Option<u64>,\n output_tokens: Option<u64>,\n cache_read_input_tokens: Option<u64>,\n cache_creation_input_tokens: Option<u64>,\n },\n /// Active transport/connection type for this stream\n ConnectionType { connection: String },\n /// Connection phase update (for status bar transparency)\n ConnectionPhase { phase: ConnectionPhase },\n /// Provider-supplied human-readable transport detail for the status line\n StatusDetail { detail: String },\n /// Error occurred\n Error {\n message: String,\n /// Seconds until rate limit resets (if this is a rate limit error)\n retry_after_secs: Option<u64>,\n },\n /// Provider session ID (for conversation resume)\n SessionId(String),\n /// Compaction occurred (context was summarized)\n Compaction {\n trigger: String,\n pre_tokens: Option<u64>,\n /// Provider-native compaction artifact, if one was emitted.\n openai_encrypted_content: Option<String>,\n },\n /// Upstream provider info (e.g., which provider OpenRouter routed to)\n UpstreamProvider { provider: String },\n /// Native tool call from a provider bridge that needs execution by jcode\n NativeToolCall {\n request_id: String,\n tool_name: String,\n input: serde_json::Value,\n },\n}\n\n#[cfg(test)]\nmod tests {\n use super::*;\n\n #[test]\n fn sanitize_tool_id_alphanumeric_passthrough() {\n assert_eq!(\n sanitize_tool_id(\"toolu_01XFDUDYJgAACzvnptvVer6u\"),\n \"toolu_01XFDUDYJgAACzvnptvVer6u\"\n );\n assert_eq!(sanitize_tool_id(\"call_abc123\"), \"call_abc123\");\n assert_eq!(\n sanitize_tool_id(\"call_1234567890_9876543210\"),\n \"call_1234567890_9876543210\"\n );\n }\n\n #[test]\n fn sanitize_tool_id_hyphens_passthrough() {\n assert_eq!(sanitize_tool_id(\"call-abc-123\"), \"call-abc-123\");\n assert_eq!(\n sanitize_tool_id(\"tool_use-id_with-mixed\"),\n \"tool_use-id_with-mixed\"\n );\n }\n\n #[test]\n fn sanitize_tool_id_replaces_dots() {\n assert_eq!(\n sanitize_tool_id(\"chatcmpl-abc.def.ghi\"),\n \"chatcmpl-abc_def_ghi\"\n );\n assert_eq!(sanitize_tool_id(\"call.123\"), \"call_123\");\n }\n\n #[test]\n fn sanitize_tool_id_replaces_colons() {\n assert_eq!(sanitize_tool_id(\"call:123:456\"), \"call_123_456\");\n }\n\n #[test]\n fn sanitize_tool_id_replaces_special_chars() {\n assert_eq!(\n sanitize_tool_id(\"id@with#special$chars\"),\n \"id_with_special_chars\"\n );\n assert_eq!(sanitize_tool_id(\"id with spaces\"), \"id_with_spaces\");\n }\n\n #[test]\n fn sanitize_tool_id_empty_returns_unknown() {\n assert_eq!(sanitize_tool_id(\"\"), \"unknown\");\n }\n\n #[test]\n fn sanitize_tool_id_copilot_to_anthropic() {\n assert_eq!(\n sanitize_tool_id(\"chatcmpl-BF2xX.tool_call.0\"),\n \"chatcmpl-BF2xX_tool_call_0\"\n );\n }\n\n #[test]\n fn sanitize_tool_id_already_valid_unchanged() {\n let valid_ids = [\n \"toolu_01XFDUDYJgAACzvnptvVer6u\",\n \"call_abc123\",\n \"fallback_text_call_call_1234567890_9876543210\",\n \"tool_123\",\n \"a\",\n \"A\",\n \"0\",\n \"_\",\n \"-\",\n \"a-b_c\",\n ];\n for id in valid_ids {\n assert_eq!(sanitize_tool_id(id), id, \"ID '{}' should be unchanged\", id);\n }\n }\n\n #[test]\n fn redact_secrets_redacts_known_direct_token_formats() {\n let input = \"access=sk-ant-oat01-ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789\\nopenrouter=sk-or-v1-abcdefghijklmnopqrstuvwxyz0123456789\\ngithub=ghp_ABCDEFGHIJKLMNOPQRSTUVWXYZ0123\\n\";\n let out = redact_secrets(input);\n assert!(!out.contains(\"sk-ant-oat01-\"));\n assert!(!out.contains(\"sk-or-v1-\"));\n assert!(!out.contains(\"ghp_ABCDEFGHIJKLMNOPQRSTUVWXYZ0123\"));\n assert!(out.matches(\"[REDACTED_SECRET]\").count() >= 3);\n }\n\n #[test]\n fn redact_secrets_redacts_env_style_assignments() {\n let input = \"OPENROUTER_API_KEY=sk-or-v1-abc123abc123abc123abc123\\nOPENCODE_API_KEY=oc_test_secret\\nOPENCODE_GO_API_KEY=ocgo_test_secret\\nZAI_API_KEY=zai_secret\\nCHUTES_API_KEY=chutes_secret\\nCEREBRAS_API_KEY=cerebras_secret\\nOPENAI_COMPAT_API_KEY=compat_secret\\nCURSOR_API_KEY='my_cursor_secret_value'\\nOPENAI_API_KEY=sk-test-openai-example\\nAZURE_OPENAI_API_KEY=azure-openai-secret\\n\";\n let out = redact_secrets(input);\n assert!(out.contains(\"OPENROUTER_API_KEY=[REDACTED_SECRET]\"));\n assert!(out.contains(\"OPENCODE_API_KEY=[REDACTED_SECRET]\"));\n assert!(out.contains(\"OPENCODE_GO_API_KEY=[REDACTED_SECRET]\"));\n assert!(out.contains(\"ZAI_API_KEY=[REDACTED_SECRET]\"));\n assert!(out.contains(\"CHUTES_API_KEY=[REDACTED_SECRET]\"));\n assert!(out.contains(\"CEREBRAS_API_KEY=[REDACTED_SECRET]\"));\n assert!(out.contains(\"OPENAI_COMPAT_API_KEY=[REDACTED_SECRET]\"));\n assert!(out.contains(\"CURSOR_API_KEY=[REDACTED_SECRET]\"));\n assert!(out.contains(\"OPENAI_API_KEY=[REDACTED_SECRET]\"));\n assert!(out.contains(\"AZURE_OPENAI_API_KEY=[REDACTED_SECRET]\"));\n assert!(!out.contains(\"my_cursor_secret_value\"));\n }\n\n #[test]\n fn redact_secrets_redacts_runtime_key_assignment() {\n let key_var = \"JCODE_OPENAI_COMPAT_API_KEY_NAME\";\n let prev = std::env::var(key_var).ok();\n crate::env::set_var(key_var, \"GROQ_API_KEY\");\n\n let input = \"GROQ_API_KEY=my_secret_token_value\";\n let out = redact_secrets(input);\n assert_eq!(out, \"GROQ_API_KEY=[REDACTED_SECRET]\");\n\n if let Some(v) = prev {\n crate::env::set_var(key_var, v);\n } else {\n crate::env::remove_var(key_var);\n }\n }\n\n #[test]\n fn redact_secrets_redacts_mixed_case_token_assignments() {\n let input = \"my_token=ya29.ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789\";\n let out = redact_secrets(input);\n assert!(out.contains(\"[REDACTED_SECRET]\"));\n assert!(!out.contains(\"ya29.ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789\"));\n }\n\n #[test]\n fn redact_secrets_leaves_normal_output_unchanged() {\n let input = \"Found 5 files\\nNo auth errors\\nDone.\";\n assert_eq!(redact_secrets(input), input);\n }\n\n #[test]\n fn format_timestamp_is_stable_utc_rfc3339() {\n let ts = chrono::DateTime::parse_from_rfc3339(\"2025-03-15T02:24:13.250Z\")\n .unwrap()\n .with_timezone(&Utc);\n assert_eq!(Message::format_timestamp(&ts), \"2025-03-15T02:24:13.250Z\");\n }\n\n #[test]\n fn with_timestamps_prepends_utc_prefix_to_user_text() {\n let ts = chrono::DateTime::parse_from_rfc3339(\"2025-03-15T02:24:03Z\")\n .unwrap()\n .with_timezone(&Utc);\n let stamped = Message::with_timestamps(&[Message {\n role: Role::User,\n content: vec![ContentBlock::Text {\n text: \"hello\".to_string(),\n cache_control: None,\n }],\n timestamp: Some(ts),\n tool_duration_ms: None,\n }]);\n match &stamped[0].content[0] {\n ContentBlock::Text { text, .. } => {\n assert_eq!(text, \"[2025-03-15T02:24:03.000Z] hello\");\n }\n other => panic!(\"expected text block, got {other:?}\"),\n }\n }\n\n #[test]\n fn with_timestamps_adds_tool_timing_header_with_duration() {\n let ts = chrono::DateTime::parse_from_rfc3339(\"2025-03-15T02:24:13Z\")\n .unwrap()\n .with_timezone(&Utc);\n let stamped = Message::with_timestamps(&[Message {\n role: Role::User,\n content: vec![ContentBlock::ToolResult {\n tool_use_id: \"call_1\".to_string(),\n content: \"ok\".to_string(),\n is_error: None,\n }],\n timestamp: Some(ts),\n tool_duration_ms: Some(3_200),\n }]);\n match &stamped[0].content[0] {\n ContentBlock::ToolResult { content, .. } => {\n assert_eq!(\n content,\n \"[tool timing: start=2025-03-15T02:24:09.800Z finish=2025-03-15T02:24:13.000Z duration=3.2s] ok\"\n );\n }\n other => panic!(\"expected tool result block, got {other:?}\"),\n }\n }\n\n #[test]\n fn with_timestamps_skips_internal_system_reminders() {\n let ts = chrono::DateTime::parse_from_rfc3339(\"2025-03-15T02:24:13Z\")\n .unwrap()\n .with_timezone(&Utc);\n let original = Message {\n role: Role::User,\n content: vec![ContentBlock::Text {\n text: \"<system-reminder>\\ninternal\\n</system-reminder>\".to_string(),\n cache_control: None,\n }],\n timestamp: Some(ts),\n tool_duration_ms: None,\n };\n let stamped = Message::with_timestamps(std::slice::from_ref(&original));\n match &stamped[0].content[0] {\n ContentBlock::Text { text, .. } => {\n assert_eq!(text, \"<system-reminder>\\ninternal\\n</system-reminder>\");\n }\n other => panic!(\"expected text block, got {other:?}\"),\n }\n }\n\n #[test]\n fn ends_with_fresh_user_turn_accepts_plain_user_text() {\n let messages = vec![Message::user(\"hello\")];\n assert!(ends_with_fresh_user_turn(&messages));\n }\n\n #[test]\n fn ends_with_fresh_user_turn_rejects_trailing_tool_result() {\n let messages = vec![\n Message::user(\"hello\"),\n Message {\n role: Role::Assistant,\n content: vec![ContentBlock::ToolUse {\n id: \"call_1\".to_string(),\n name: \"bash\".to_string(),\n input: serde_json::json!({}),\n }],\n timestamp: Some(Utc::now()),\n tool_duration_ms: None,\n },\n Message::tool_result(\"call_1\", \"ok\", false),\n ];\n assert!(!ends_with_fresh_user_turn(&messages));\n }\n\n #[test]\n fn ends_with_fresh_user_turn_skips_internal_system_reminders() {\n let messages = vec![\n Message::user(\"hello\"),\n Message::user(\"<system-reminder>\\ninternal\\n</system-reminder>\"),\n ];\n assert!(ends_with_fresh_user_turn(&messages));\n }\n\n #[test]\n fn ends_with_fresh_user_turn_rejects_assistant_tail() {\n let messages = vec![\n Message::user(\"hello\"),\n Message::assistant_text(\"working on it\"),\n ];\n assert!(!ends_with_fresh_user_turn(&messages));\n }\n\n #[test]\n fn format_background_task_notification_markdown_uses_code_block_preview() {\n let rendered = format_background_task_notification_markdown(&BackgroundTaskCompleted {\n task_id: \"abc123\".to_string(),\n tool_name: \"bash\".to_string(),\n session_id: \"session\".to_string(),\n status: BackgroundTaskStatus::Completed,\n exit_code: Some(0),\n output_preview: \"[stderr] first line\\n[stdout] second line\\n\".to_string(),\n output_file: std::path::PathBuf::from(\"/tmp/output.log\"),\n duration_secs: 7.1,\n notify: true,\n wake: false,\n });\n\n assert!(\n rendered\n .contains(\"**Background task** `abc123` · `bash` · ✓ completed · 7.1s · exit 0\")\n );\n assert!(rendered.contains(\"```text\\n[stderr] first line\\n[stdout] second line\\n```\"));\n assert!(rendered.contains(\"_Full output:_ `bg action=\\\"output\\\" task_id=\\\"abc123\\\"`\"));\n }\n\n #[test]\n fn format_background_task_notification_markdown_handles_empty_preview() {\n let rendered = format_background_task_notification_markdown(&BackgroundTaskCompleted {\n task_id: \"abc123\".to_string(),\n tool_name: \"bash\".to_string(),\n session_id: \"session\".to_string(),\n status: BackgroundTaskStatus::Failed,\n exit_code: Some(9),\n output_preview: \"\\n\\n\".to_string(),\n output_file: std::path::PathBuf::from(\"/tmp/output.log\"),\n duration_secs: 1.0,\n notify: true,\n wake: false,\n });\n\n assert!(rendered.contains(\"✗ failed\"));\n assert!(rendered.contains(\"_No output captured._\"));\n }\n\n #[test]\n fn format_background_task_notification_markdown_renders_superseded_status() {\n let rendered = format_background_task_notification_markdown(&BackgroundTaskCompleted {\n task_id: \"abc123\".to_string(),\n tool_name: \"selfdev-build\".to_string(),\n session_id: \"session\".to_string(),\n status: BackgroundTaskStatus::Superseded,\n exit_code: Some(0),\n output_preview: \"Build completed, but source changed before activation\".to_string(),\n output_file: std::path::PathBuf::from(\"/tmp/output.log\"),\n duration_secs: 5.0,\n notify: true,\n wake: false,\n });\n\n assert!(rendered.contains(\"↻ superseded\"));\n assert!(rendered.contains(\"exit 0\"));\n assert!(rendered.contains(\"source changed before activation\"));\n }\n\n #[test]\n fn format_background_task_progress_markdown_uses_compact_multiline_layout() {\n let rendered = format_background_task_progress_markdown(&BackgroundTaskProgressEvent {\n task_id: \"bgprogress\".to_string(),\n tool_name: \"bash\".to_string(),\n session_id: \"session\".to_string(),\n progress: crate::bus::BackgroundTaskProgress {\n kind: crate::bus::BackgroundTaskProgressKind::Determinate,\n percent: Some(42.0),\n message: Some(\"Running tests\".to_string()),\n current: Some(21),\n total: Some(50),\n unit: Some(\"tests\".to_string()),\n eta_seconds: None,\n updated_at: Utc::now().to_rfc3339(),\n source: crate::bus::BackgroundTaskProgressSource::Reported,\n },\n });\n\n assert!(rendered.starts_with(\"**Background task progress** `bgprogress` · `bash`\\n\\n\"));\n assert!(rendered.contains(\"42% · Running tests\"));\n assert!(rendered.contains(\"(reported)\"));\n }\n\n #[test]\n fn parse_background_task_progress_notification_extracts_card_fields() {\n let parsed = parse_background_task_progress_notification_markdown(\n \"**Background task progress** `bgprogress` · `bash`\\n\\n[#####-------] 42% · Running tests (reported)\",\n )\n .expect(\"progress notification should parse\");\n\n assert_eq!(parsed.task_id, \"bgprogress\");\n assert_eq!(parsed.tool_name, \"bash\");\n assert_eq!(parsed.summary, \"42% · Running tests\");\n assert_eq!(parsed.source.as_deref(), Some(\"reported\"));\n assert_eq!(parsed.percent, Some(42.0));\n }\n\n #[test]\n fn parse_background_task_progress_notification_supports_legacy_inline_layout() {\n let parsed = parse_background_task_progress_notification_markdown(\n \"**Background task progress** `bgprogress` · `bash` · Release run in_progress: - 7/8 jobs completed (reported)\",\n )\n .expect(\"legacy progress notification should parse\");\n\n assert_eq!(parsed.task_id, \"bgprogress\");\n assert_eq!(\n parsed.summary,\n \"Release run in_progress: - 7/8 jobs completed\"\n );\n assert_eq!(parsed.source.as_deref(), Some(\"reported\"));\n assert_eq!(parsed.percent, None);\n }\n\n #[test]\n fn description_token_estimate_uses_chars_per_token_heuristic() {\n let def = ToolDefinition {\n name: \"read\".to_string(),\n description: \"abcdwxyz\".to_string(),\n input_schema: serde_json::json!({\"type\": \"object\"}),\n };\n\n assert_eq!(def.description_token_estimate(), 2);\n }\n\n #[test]\n fn parse_background_task_notification_markdown_extracts_fields() {\n let rendered = format_background_task_notification_markdown(&BackgroundTaskCompleted {\n task_id: \"abc123\".to_string(),\n tool_name: \"bash\".to_string(),\n session_id: \"session\".to_string(),\n status: BackgroundTaskStatus::Completed,\n exit_code: Some(0),\n output_preview: \"[stderr] first line\\n[stdout] second line\\n\".to_string(),\n output_file: std::path::PathBuf::from(\"/tmp/output.log\"),\n duration_secs: 7.1,\n notify: true,\n wake: false,\n });\n\n let parsed = parse_background_task_notification_markdown(&rendered)\n .expect(\"background task notification should parse\");\n assert_eq!(parsed.task_id, \"abc123\");\n assert_eq!(parsed.tool_name, \"bash\");\n assert_eq!(parsed.status, \"✓ completed\");\n assert_eq!(parsed.duration, \"7.1s\");\n assert_eq!(parsed.exit_label, \"exit 0\");\n assert_eq!(\n parsed.preview.as_deref(),\n Some(\"[stderr] first line\\n[stdout] second line\")\n );\n assert_eq!(\n parsed.full_output_command,\n \"bg action=\\\"output\\\" task_id=\\\"abc123\\\"\"\n );\n }\n}\n"}, "bb4653aa76b3752e8bc4a9ded5e950e24d89940b": {"t": 1785757230.3388035, "n": 35573, "h": "4605174606247743", "c": "use crate::bus::{BackgroundTaskCompleted, BackgroundTaskProgressEvent, BackgroundTaskStatus};\nuse chrono::Utc;\nuse regex::Regex;\nuse serde::{Deserialize, Serialize};\nuse std::collections::HashSet;\nuse std::sync::OnceLock;\n\nmod notifications;\n\npub use notifications::{\n InputShellResult, ParsedBackgroundTaskNotification, background_task_status_notice,\n format_background_task_notification_markdown, format_background_task_progress_markdown,\n format_input_shell_result_markdown, input_shell_status_notice,\n parse_background_task_notification_markdown,\n};\n\nfn compile_static_regex(pattern: &str) -> Option<Regex> {\n match Regex::new(pattern) {\n Ok(regex) => Some(regex),\n Err(err) => {\n eprintln!(\"jcode: failed to compile static regex: {err}\");\n None\n }\n }\n}\n\nfn compile_static_regexes(patterns: &[&str]) -> Vec<Regex> {\n patterns\n .iter()\n .filter_map(|pattern| compile_static_regex(pattern))\n .collect()\n}\n\n/// Role in conversation\n#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]\n#[serde(rename_all = \"lowercase\")]\npub enum Role {\n User,\n Assistant,\n}\n\n/// Plain-text tool output placeholder when execution was interrupted.\npub const TOOL_OUTPUT_MISSING_TEXT: &str =\n \"Tool output missing (session interrupted before tool execution completed)\";\n\n/// A message in the conversation\n#[derive(Debug, Clone, Serialize, Deserialize)]\npub struct Message {\n pub role: Role,\n pub content: Vec<ContentBlock>,\n #[serde(default, skip_serializing_if = \"Option::is_none\")]\n pub timestamp: Option<chrono::DateTime<Utc>>,\n #[serde(default, skip_serializing_if = \"Option::is_none\")]\n pub tool_duration_ms: Option<u64>,\n}\n\n/// Cache control metadata for prompt caching\n#[derive(Debug, Clone, Serialize, Deserialize)]\npub struct CacheControl {\n #[serde(rename = \"type\")]\n pub kind: String,\n #[serde(skip_serializing_if = \"Option::is_none\")]\n pub ttl: Option<String>,\n}\n\nimpl CacheControl {\n pub fn ephemeral(ttl: Option<String>) -> Self {\n Self {\n kind: \"ephemeral\".to_string(),\n ttl,\n }\n }\n}\n\n/// Content block within a message\n#[derive(Debug, Clone, Serialize, Deserialize)]\n#[serde(tag = \"type\", rename_all = \"snake_case\")]\npub enum ContentBlock {\n Text {\n text: String,\n #[serde(skip_serializing_if = \"Option::is_none\")]\n cache_control: Option<CacheControl>,\n },\n /// Hidden reasoning content used for providers that require it (not displayed)\n Reasoning {\n text: String,\n },\n ToolUse {\n id: String,\n name: String,\n input: serde_json::Value,\n },\n ToolResult {\n tool_use_id: String,\n content: String,\n #[serde(skip_serializing_if = \"Option::is_none\")]\n is_error: Option<bool>,\n },\n Image {\n media_type: String,\n data: String,\n },\n /// Hidden OpenAI Responses compaction item used to preserve native\n /// compaction state across turns/saves when jcode explicitly triggers it.\n OpenAICompaction {\n encrypted_content: String,\n },\n}\n\nimpl Message {\n pub fn user(text: &str) -> Self {\n Self {\n role: Role::User,\n content: vec![ContentBlock::Text {\n text: text.to_string(),\n cache_control: None,\n }],\n timestamp: Some(Utc::now()),\n tool_duration_ms: None,\n }\n }\n\n pub fn user_with_images(text: &str, images: Vec<(String, String)>) -> Self {\n let mut content: Vec<ContentBlock> = images\n .into_iter()\n .map(|(media_type, data)| ContentBlock::Image { media_type, data })\n .collect();\n content.push(ContentBlock::Text {\n text: text.to_string(),\n cache_control: None,\n });\n Self {\n role: Role::User,\n content,\n timestamp: Some(Utc::now()),\n tool_duration_ms: None,\n }\n }\n\n pub fn assistant_text(text: &str) -> Self {\n Self {\n role: Role::Assistant,\n content: vec![ContentBlock::Text {\n text: text.to_string(),\n cache_control: None,\n }],\n timestamp: Some(Utc::now()),\n tool_duration_ms: None,\n }\n }\n\n pub fn tool_result(tool_use_id: &str, content: &str, is_error: bool) -> Self {\n Self::tool_result_with_duration(tool_use_id, content, is_error, None)\n }\n\n pub fn tool_result_with_duration(\n tool_use_id: &str,\n content: &str,\n is_error: bool,\n tool_duration_ms: Option<u64>,\n ) -> Self {\n Self {\n role: Role::User,\n content: vec![ContentBlock::ToolResult {\n tool_use_id: tool_use_id.to_string(),\n content: content.to_string(),\n is_error: if is_error { Some(true) } else { None },\n }],\n timestamp: Some(Utc::now()),\n tool_duration_ms,\n }\n }\n\n /// Format a timestamp deterministically in UTC for injection into model-visible content.\n pub fn format_timestamp(ts: &chrono::DateTime<Utc>) -> String {\n ts.to_rfc3339_opts(chrono::SecondsFormat::Millis, true)\n }\n\n pub fn format_duration(duration_ms: u64) -> String {\n match duration_ms {\n 0..=999 => format!(\"{}ms\", duration_ms),\n 1_000..=9_999 => format!(\"{:.1}s\", duration_ms as f64 / 1000.0),\n 10_000..=59_999 => format!(\"{}s\", duration_ms / 1000),\n _ => {\n let total_seconds = duration_ms / 1000;\n let minutes = total_seconds / 60;\n let seconds = total_seconds % 60;\n if seconds == 0 {\n format!(\"{}m\", minutes)\n } else {\n format!(\"{}m {}s\", minutes, seconds)\n }\n }\n }\n }\n\n pub fn is_internal_system_reminder(&self) -> bool {\n self.content\n .iter()\n .find_map(|block| match block {\n ContentBlock::Text { text, .. } => Some(text.trim_start()),\n _ => None,\n })\n .is_some_and(|text| text.starts_with(\"<system-reminder>\"))\n }\n\n fn should_skip_timestamp_injection(&self) -> bool {\n self.is_internal_system_reminder()\n }\n\n fn tool_result_tag(&self, ts: &chrono::DateTime<Utc>) -> String {\n match self.tool_duration_ms {\n Some(duration_ms) => {\n let duration_ms_i64 = i64::try_from(duration_ms).unwrap_or(i64::MAX);\n let start_ts = ts\n .checked_sub_signed(chrono::Duration::milliseconds(duration_ms_i64))\n .unwrap_or(*ts);\n format!(\n \"[tool timing: start={} finish={} duration={}]\",\n Self::format_timestamp(&start_ts),\n Self::format_timestamp(ts),\n Self::format_duration(duration_ms)\n )\n }\n None => format!(\"[{}]\", Self::format_timestamp(ts)),\n }\n }\n\n /// Return a copy of messages with timestamps injected into user-role text content.\n /// Tool results get a stable UTC timing header prepended to content.\n /// User text messages get a stable UTC timestamp prepended to the first text block.\n pub fn with_timestamps(messages: &[Message]) -> Vec<Message> {\n messages\n .iter()\n .map(|msg| {\n let Some(ts) = msg.timestamp else {\n return msg.clone();\n };\n if msg.role != Role::User || msg.should_skip_timestamp_injection() {\n return msg.clone();\n }\n let text_tag = format!(\"[{}]\", Self::format_timestamp(&ts));\n let tool_result_tag = msg.tool_result_tag(&ts);\n let mut msg = msg.clone();\n let mut tagged = false;\n for block in &mut msg.content {\n match block {\n ContentBlock::Text { text, .. } if !tagged => {\n *text = format!(\"{} {}\", text_tag, text);\n tagged = true;\n }\n ContentBlock::ToolResult { content, .. } if !tagged => {\n *content = format!(\"{} {}\", tool_result_tag, content);\n tagged = true;\n }\n _ => {}\n }\n }\n msg\n })\n .collect()\n }\n}\n\nconst STABLE_HASH_SEED: u64 = 0xcbf29ce484222325;\nconst STABLE_HASH_PRIME: u64 = 0x100000001b3;\n\nfn stable_hash_bytes(bytes: &[u8]) -> u64 {\n let mut hash = STABLE_HASH_SEED;\n for byte in bytes {\n hash ^= *byte as u64;\n hash = hash.wrapping_mul(STABLE_HASH_PRIME);\n }\n hash\n}\n\npub(crate) fn extend_stable_hash(acc: u64, next: u64) -> u64 {\n stable_hash_bytes(&[acc.to_le_bytes().as_slice(), next.to_le_bytes().as_slice()].concat())\n}\n\npub(crate) fn stable_message_hash(message: &Message) -> u64 {\n match serde_json::to_vec(message) {\n Ok(bytes) => stable_hash_bytes(&bytes),\n Err(_) => stable_hash_bytes(format!(\"{:?}\", message).as_bytes()),\n }\n}\n\npub fn ends_with_fresh_user_turn(messages: &[Message]) -> bool {\n for msg in messages.iter().rev() {\n if msg.role != Role::User {\n return false;\n }\n\n if msg\n .content\n .iter()\n .any(|block| matches!(block, ContentBlock::ToolResult { .. }))\n {\n return false;\n }\n\n if msg.content.is_empty() {\n return false;\n }\n\n let mut saw_user_text = false;\n for block in &msg.content {\n match block {\n ContentBlock::Text { text, .. } => {\n let trimmed = text.trim();\n if !trimmed.is_empty() && !trimmed.starts_with(\"<system-reminder>\") {\n saw_user_text = true;\n }\n }\n _ => return false,\n }\n }\n\n if saw_user_text {\n return true;\n }\n\n if msg.is_internal_system_reminder() {\n continue;\n }\n\n return false;\n }\n\n false\n}\n\n/// Sanitize a tool ID so it matches the pattern `^[a-zA-Z0-9_-]+$`.\n///\n/// Different providers generate tool IDs in different formats. When switching\n/// from one provider to another mid-conversation, the historical tool IDs may\n/// contain characters that the new provider rejects (e.g., dots in Copilot IDs\n/// sent to Anthropic). This function replaces any invalid characters with\n/// underscores.\npub fn sanitize_tool_id(id: &str) -> String {\n if id.is_empty() {\n return \"unknown\".to_string();\n }\n let sanitized: String = id\n .chars()\n .map(|c| {\n if c.is_ascii_alphanumeric() || c == '_' || c == '-' {\n c\n } else {\n '_'\n }\n })\n .collect();\n if sanitized.is_empty() {\n \"unknown\".to_string()\n } else {\n sanitized\n }\n}\n\n/// Redact likely secrets from persisted tool output.\n///\n/// This is a best-effort safeguard for local session history files. It targets\n/// high-confidence token/key patterns and common `KEY=VALUE` assignments used by\n/// auth flows.\npub fn redact_secrets(text: &str) -> String {\n // Fast path to avoid regex work for most tool outputs.\n let lower = text.to_ascii_lowercase();\n\n if !text.contains(\"sk-\")\n && !text.contains(\"ghp_\")\n && !text.contains(\"github_pat_\")\n && !text.contains(\"AIza\")\n && !text.contains(\"ya29.\")\n && !text.contains(\"xox\")\n && !lower.contains(\"api_key\")\n && !lower.contains(\"token\")\n {\n return text.to_string();\n }\n\n static DIRECT_PATTERNS: OnceLock<Vec<Regex>> = OnceLock::new();\n static ASSIGNMENT_PATTERNS: OnceLock<Vec<Regex>> = OnceLock::new();\n\n let direct_patterns = DIRECT_PATTERNS.get_or_init(|| {\n compile_static_regexes(&[\n r\"sk-ant-(?:oat|ort)01-[A-Za-z0-9_-]{20,}\",\n r\"sk-or-v1-[A-Za-z0-9_-]{20,}\",\n r\"ghp_[A-Za-z0-9]{20,}\",\n r\"github_pat_[A-Za-z0-9_]{20,}\",\n r\"ya29\\.[A-Za-z0-9._-]{20,}\",\n r\"AIza[0-9A-Za-z_-]{20,}\",\n r\"xox[baprs]-[A-Za-z0-9-]{10,}\",\n ])\n });\n\n let assignment_patterns = ASSIGNMENT_PATTERNS.get_or_init(|| {\n compile_static_regexes(&[\n r\"(?m)^\\s*(OPENROUTER_API_KEY\\s*=\\s*)[^\\r\\n]+\",\n r\"(?m)^\\s*(OPENCODE_API_KEY\\s*=\\s*)[^\\r\\n]+\",\n r\"(?m)^\\s*(OPENCODE_GO_API_KEY\\s*=\\s*)[^\\r\\n]+\",\n r\"(?m)^\\s*(ZHIPU_API_KEY\\s*=\\s*)[^\\r\\n]+\",\n r\"(?m)^\\s*(ZAI_API_KEY\\s*=\\s*)[^\\r\\n]+\",\n r\"(?m)^\\s*(302AI_API_KEY\\s*=\\s*)[^\\r\\n]+\",\n r\"(?m)^\\s*(BASETEN_API_KEY\\s*=\\s*)[^\\r\\n]+\",\n r\"(?m)^\\s*(CORTECS_API_KEY\\s*=\\s*)[^\\r\\n]+\",\n r\"(?m)^\\s*(DEEPSEEK_API_KEY\\s*=\\s*)[^\\r\\n]+\",\n r\"(?m)^\\s*(FIRMWARE_API_KEY\\s*=\\s*)[^\\r\\n]+\",\n r\"(?m)^\\s*(HF_TOKEN\\s*=\\s*)[^\\r\\n]+\",\n r\"(?m)^\\s*(MOONSHOT_API_KEY\\s*=\\s*)[^\\r\\n]+\",\n r\"(?m)^\\s*(NEBIUS_API_KEY\\s*=\\s*)[^\\r\\n]+\",\n r\"(?m)^\\s*(SCALEWAY_API_KEY\\s*=\\s*)[^\\r\\n]+\",\n r\"(?m)^\\s*(STACKIT_API_KEY\\s*=\\s*)[^\\r\\n]+\",\n r\"(?m)^\\s*(GROQ_API_KEY\\s*=\\s*)[^\\r\\n]+\",\n r\"(?m)^\\s*(MISTRAL_API_KEY\\s*=\\s*)[^\\r\\n]+\",\n r\"(?m)^\\s*(PERPLEXITY_API_KEY\\s*=\\s*)[^\\r\\n]+\",\n r\"(?m)^\\s*(TOGETHER_API_KEY\\s*=\\s*)[^\\r\\n]+\",\n r\"(?m)^\\s*(DEEPINFRA_API_KEY\\s*=\\s*)[^\\r\\n]+\",\n r\"(?m)^\\s*(XAI_API_KEY\\s*=\\s*)[^\\r\\n]+\",\n r\"(?m)^\\s*(LMSTUDIO_API_KEY\\s*=\\s*)[^\\r\\n]+\",\n r\"(?m)^\\s*(OLLAMA_API_KEY\\s*=\\s*)[^\\r\\n]+\",\n r\"(?m)^\\s*(CHUTES_API_KEY\\s*=\\s*)[^\\r\\n]+\",\n r\"(?m)^\\s*(CEREBRAS_API_KEY\\s*=\\s*)[^\\r\\n]+\",\n r\"(?m)^\\s*(OPENAI_COMPAT_API_KEY\\s*=\\s*)[^\\r\\n]+\",\n r\"(?m)^\\s*(ANTHROPIC_API_KEY\\s*=\\s*)[^\\r\\n]+\",\n r\"(?m)^\\s*(OPENAI_API_KEY\\s*=\\s*)[^\\r\\n]+\",\n r\"(?m)^\\s*(AZURE_OPENAI_API_KEY\\s*=\\s*)[^\\r\\n]+\",\n r\"(?m)^\\s*(CURSOR_API_KEY\\s*=\\s*)[^\\r\\n]+\",\n r\"(?m)^\\s*(GITHUB_TOKEN\\s*=\\s*)[^\\r\\n]+\",\n ])\n });\n\n let mut redacted = text.to_string();\n let mut redacted_keys: HashSet<String> = [\n \"OPENROUTER_API_KEY\",\n \"OPENCODE_API_KEY\",\n \"OPENCODE_GO_API_KEY\",\n \"ZHIPU_API_KEY\",\n \"ZAI_API_KEY\",\n \"302AI_API_KEY\",\n \"BASETEN_API_KEY\",\n \"CORTECS_API_KEY\",\n \"DEEPSEEK_API_KEY\",\n \"FIRMWARE_API_KEY\",\n \"HF_TOKEN\",\n \"MOONSHOT_API_KEY\",\n \"NEBIUS_API_KEY\",\n \"SCALEWAY_API_KEY\",\n \"STACKIT_API_KEY\",\n \"GROQ_API_KEY\",\n \"MISTRAL_API_KEY\",\n \"PERPLEXITY_API_KEY\",\n \"TOGETHER_API_KEY\",\n \"DEEPINFRA_API_KEY\",\n \"XAI_API_KEY\",\n \"LMSTUDIO_API_KEY\",\n \"OLLAMA_API_KEY\",\n \"CHUTES_API_KEY\",\n \"CEREBRAS_API_KEY\",\n \"OPENAI_COMPAT_API_KEY\",\n \"ANTHROPIC_API_KEY\",\n \"OPENAI_API_KEY\",\n \"AZURE_OPENAI_API_KEY\",\n \"CURSOR_API_KEY\",\n \"GITHUB_TOKEN\",\n ]\n .iter()\n .map(|k| (*k).to_string())\n .collect();\n\n for re in direct_patterns {\n redacted = re.replace_all(&redacted, \"[REDACTED_SECRET]\").into_owned();\n }\n\n for re in assignment_patterns {\n redacted = re\n .replace_all(&redacted, \"${1}[REDACTED_SECRET]\")\n .into_owned();\n }\n\n // Also redact custom API key variable names configured at runtime.\n for source in [\n \"JCODE_OPENROUTER_API_KEY_NAME\",\n \"JCODE_OPENAI_COMPAT_API_KEY_NAME\",\n ] {\n let Some(key_name) = std::env::var(source)\n .ok()\n .map(|v| v.trim().to_string())\n .filter(|v| !v.is_empty())\n else {\n continue;\n };\n\n if !key_name\n .chars()\n .all(|c| c.is_ascii_uppercase() || c.is_ascii_digit() || c == '_')\n {\n continue;\n }\n if !redacted_keys.insert(key_name.clone()) {\n continue;\n }\n\n let pattern = format!(r\"(?m)^\\s*({}\\s*=\\s*)[^\\r\\n]+\", regex::escape(&key_name));\n if let Ok(re) = Regex::new(&pattern) {\n redacted = re\n .replace_all(&redacted, \"${1}[REDACTED_SECRET]\")\n .into_owned();\n }\n }\n\n redacted\n}\n\n/// Tool definition for the API\n#[derive(Debug, Clone, Serialize)]\npub struct ToolDefinition {\n pub name: String,\n // Prompt-visible text sent to the model by provider adapters.\n // Approximate prompt cost: description.len() / 4. Use\n // ToolDefinition::description_token_estimate() when reviewing tool bloat.\n pub description: String,\n pub input_schema: serde_json::Value,\n}\n\nimpl ToolDefinition {\n /// Serialized size of the full tool definition payload sent to providers.\n pub fn prompt_chars(&self) -> usize {\n serde_json::json!({\n \"name\": self.name,\n \"description\": self.description,\n \"input_schema\": self.input_schema,\n })\n .to_string()\n .len()\n }\n\n /// Approximate prompt-token cost of this tool's top-level description.\n ///\n /// This uses jcode's standard chars/4 heuristic, matching other token\n /// budget estimates in the codebase.\n pub fn description_token_estimate(&self) -> usize {\n crate::util::estimate_tokens(&self.description)\n }\n\n /// Approximate prompt-token cost of the full tool definition payload.\n pub fn prompt_token_estimate(&self) -> usize {\n crate::util::estimate_tokens(\n &serde_json::json!({\n \"name\": self.name,\n \"description\": self.description,\n \"input_schema\": self.input_schema,\n })\n .to_string(),\n )\n }\n\n pub fn aggregate_prompt_chars(defs: &[ToolDefinition]) -> usize {\n defs.iter().map(Self::prompt_chars).sum()\n }\n\n pub fn aggregate_prompt_token_estimate(defs: &[ToolDefinition]) -> usize {\n defs.iter().map(Self::prompt_token_estimate).sum()\n }\n}\n\n/// A tool call from the model\n#[derive(Debug, Clone, Default, PartialEq, Eq, serde::Serialize, serde::Deserialize)]\npub struct ToolCall {\n #[serde(default)]\n pub id: String,\n #[serde(default)]\n pub name: String,\n #[serde(default)]\n pub input: serde_json::Value,\n #[serde(default, skip_serializing_if = \"Option::is_none\")]\n pub intent: Option<String>,\n}\n\n/// Connection phase for status bar transparency\n#[derive(Debug, Clone, PartialEq)]\npub enum ConnectionPhase {\n /// Refreshing OAuth token\n Authenticating,\n /// TCP + TLS connection to API\n Connecting,\n /// HTTP request sent, waiting for first response byte\n WaitingForResponse,\n /// First byte received, stream is active\n Streaming,\n /// Retrying after a transient error\n Retrying { attempt: u32, max: u32 },\n}\n\nimpl std::fmt::Display for ConnectionPhase {\n fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {\n match self {\n ConnectionPhase::Authenticating => write!(f, \"authenticating\"),\n ConnectionPhase::Connecting => write!(f, \"connecting\"),\n ConnectionPhase::WaitingForResponse => write!(f, \"waiting for response\"),\n ConnectionPhase::Streaming => write!(f, \"streaming\"),\n ConnectionPhase::Retrying { attempt, max } => {\n write!(f, \"retrying ({}/{})\", attempt, max)\n }\n }\n }\n}\n\n/// Streaming event from provider\n#[derive(Debug, Clone)]\npub enum StreamEvent {\n /// Text content delta\n TextDelta(String),\n /// Tool use started\n ToolUseStart { id: String, name: String },\n /// Tool input delta (JSON fragment)\n ToolInputDelta(String),\n /// Tool use complete\n ToolUseEnd,\n /// Tool result from provider (provider already executed the tool)\n ToolResult {\n tool_use_id: String,\n content: String,\n is_error: bool,\n },\n /// Extended thinking started\n ThinkingStart,\n /// Extended thinking delta (reasoning content)\n ThinkingDelta(String),\n /// Extended thinking ended\n ThinkingEnd,\n /// Extended thinking completed with duration\n ThinkingDone { duration_secs: f64 },\n /// Message complete (may have stop reason)\n MessageEnd { stop_reason: Option<String> },\n /// Token usage update\n TokenUsage {\n input_tokens: Option<u64>,\n output_tokens: Option<u64>,\n cache_read_input_tokens: Option<u64>,\n cache_creation_input_tokens: Option<u64>,\n },\n /// Active transport/connection type for this stream\n ConnectionType { connection: String },\n /// Connection phase update (for status bar transparency)\n ConnectionPhase { phase: ConnectionPhase },\n /// Provider-supplied human-readable transport detail for the status line\n StatusDetail { detail: String },\n /// Error occurred\n Error {\n message: String,\n /// Seconds until rate limit resets (if this is a rate limit error)\n retry_after_secs: Option<u64>,\n },\n /// Provider session ID (for conversation resume)\n SessionId(String),\n /// Compaction occurred (context was summarized)\n Compaction {\n trigger: String,\n pre_tokens: Option<u64>,\n /// Provider-native compaction artifact, if one was emitted.\n openai_encrypted_content: Option<String>,\n },\n /// Upstream provider info (e.g., which provider OpenRouter routed to)\n UpstreamProvider { provider: String },\n /// Native tool call from a provider bridge that needs execution by jcode\n NativeToolCall {\n request_id: String,\n tool_name: String,\n input: serde_json::Value,\n },\n}\n\n#[cfg(test)]\nmod tests {\n use super::*;\n\n #[test]\n fn sanitize_tool_id_alphanumeric_passthrough() {\n assert_eq!(\n sanitize_tool_id(\"toolu_01XFDUDYJgAACzvnptvVer6u\"),\n \"toolu_01XFDUDYJgAACzvnptvVer6u\"\n );\n assert_eq!(sanitize_tool_id(\"call_abc123\"), \"call_abc123\");\n assert_eq!(\n sanitize_tool_id(\"call_1234567890_9876543210\"),\n \"call_1234567890_9876543210\"\n );\n }\n\n #[test]\n fn sanitize_tool_id_hyphens_passthrough() {\n assert_eq!(sanitize_tool_id(\"call-abc-123\"), \"call-abc-123\");\n assert_eq!(\n sanitize_tool_id(\"tool_use-id_with-mixed\"),\n \"tool_use-id_with-mixed\"\n );\n }\n\n #[test]\n fn sanitize_tool_id_replaces_dots() {\n assert_eq!(\n sanitize_tool_id(\"chatcmpl-abc.def.ghi\"),\n \"chatcmpl-abc_def_ghi\"\n );\n assert_eq!(sanitize_tool_id(\"call.123\"), \"call_123\");\n }\n\n #[test]\n fn sanitize_tool_id_replaces_colons() {\n assert_eq!(sanitize_tool_id(\"call:123:456\"), \"call_123_456\");\n }\n\n #[test]\n fn sanitize_tool_id_replaces_special_chars() {\n assert_eq!(\n sanitize_tool_id(\"id@with#special$chars\"),\n \"id_with_special_chars\"\n );\n assert_eq!(sanitize_tool_id(\"id with spaces\"), \"id_with_spaces\");\n }\n\n #[test]\n fn sanitize_tool_id_empty_returns_unknown() {\n assert_eq!(sanitize_tool_id(\"\"), \"unknown\");\n }\n\n #[test]\n fn sanitize_tool_id_copilot_to_anthropic() {\n assert_eq!(\n sanitize_tool_id(\"chatcmpl-BF2xX.tool_call.0\"),\n \"chatcmpl-BF2xX_tool_call_0\"\n );\n }\n\n #[test]\n fn sanitize_tool_id_already_valid_unchanged() {\n let valid_ids = [\n \"toolu_01XFDUDYJgAACzvnptvVer6u\",\n \"call_abc123\",\n \"fallback_text_call_call_1234567890_9876543210\",\n \"tool_123\",\n \"a\",\n \"A\",\n \"0\",\n \"_\",\n \"-\",\n \"a-b_c\",\n ];\n for id in valid_ids {\n assert_eq!(sanitize_tool_id(id), id, \"ID '{}' should be unchanged\", id);\n }\n }\n\n #[test]\n fn redact_secrets_redacts_known_direct_token_formats() {\n let input = \"access=sk-ant-oat01-ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789\\nopenrouter=sk-or-v1-abcdefghijklmnopqrstuvwxyz0123456789\\ngithub=ghp_ABCDEFGHIJKLMNOPQRSTUVWXYZ0123\\n\";\n let out = redact_secrets(input);\n assert!(!out.contains(\"sk-ant-oat01-\"));\n assert!(!out.contains(\"sk-or-v1-\"));\n assert!(!out.contains(\"ghp_ABCDEFGHIJKLMNOPQRSTUVWXYZ0123\"));\n assert!(out.matches(\"[REDACTED_SECRET]\").count() >= 3);\n }\n\n #[test]\n fn redact_secrets_redacts_env_style_assignments() {\n let input = \"OPENROUTER_API_KEY=sk-or-v1-abc123abc123abc123abc123\\nOPENCODE_API_KEY=oc_test_secret\\nOPENCODE_GO_API_KEY=ocgo_test_secret\\nZAI_API_KEY=zai_secret\\nCHUTES_API_KEY=chutes_secret\\nCEREBRAS_API_KEY=cerebras_secret\\nOPENAI_COMPAT_API_KEY=compat_secret\\nCURSOR_API_KEY='my_cursor_secret_value'\\nOPENAI_API_KEY=sk-test-openai-example\\nAZURE_OPENAI_API_KEY=azure-openai-secret\\n\";\n let out = redact_secrets(input);\n assert!(out.contains(\"OPENROUTER_API_KEY=[REDACTED_SECRET]\"));\n assert!(out.contains(\"OPENCODE_API_KEY=[REDACTED_SECRET]\"));\n assert!(out.contains(\"OPENCODE_GO_API_KEY=[REDACTED_SECRET]\"));\n assert!(out.contains(\"ZAI_API_KEY=[REDACTED_SECRET]\"));\n assert!(out.contains(\"CHUTES_API_KEY=[REDACTED_SECRET]\"));\n assert!(out.contains(\"CEREBRAS_API_KEY=[REDACTED_SECRET]\"));\n assert!(out.contains(\"OPENAI_COMPAT_API_KEY=[REDACTED_SECRET]\"));\n assert!(out.contains(\"CURSOR_API_KEY=[REDACTED_SECRET]\"));\n assert!(out.contains(\"OPENAI_API_KEY=[REDACTED_SECRET]\"));\n assert!(out.contains(\"AZURE_OPENAI_API_KEY=[REDACTED_SECRET]\"));\n assert!(!out.contains(\"my_cursor_secret_value\"));\n }\n\n #[test]\n fn redact_secrets_redacts_runtime_key_assignment() {\n let key_var = \"JCODE_OPENAI_COMPAT_API_KEY_NAME\";\n let prev = std::env::var(key_var).ok();\n crate::env::set_var(key_var, \"GROQ_API_KEY\");\n\n let input = \"GROQ_API_KEY=my_secret_token_value\";\n let out = redact_secrets(input);\n assert_eq!(out, \"GROQ_API_KEY=[REDACTED_SECRET]\");\n\n if let Some(v) = prev {\n crate::env::set_var(key_var, v);\n } else {\n crate::env::remove_var(key_var);\n }\n }\n\n #[test]\n fn redact_secrets_redacts_mixed_case_token_assignments() {\n let input = \"my_token=ya29.ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789\";\n let out = redact_secrets(input);\n assert!(out.contains(\"[REDACTED_SECRET]\"));\n assert!(!out.contains(\"ya29.ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789\"));\n }\n\n #[test]\n fn redact_secrets_leaves_normal_output_unchanged() {\n let input = \"Found 5 files\\nNo auth errors\\nDone.\";\n assert_eq!(redact_secrets(input), input);\n }\n\n #[test]\n fn format_timestamp_is_stable_utc_rfc3339() {\n let ts = chrono::DateTime::parse_from_rfc3339(\"2025-03-15T02:24:13.250Z\")\n .unwrap()\n .with_timezone(&Utc);\n assert_eq!(Message::format_timestamp(&ts), \"2025-03-15T02:24:13.250Z\");\n }\n\n #[test]\n fn with_timestamps_prepends_utc_prefix_to_user_text() {\n let ts = chrono::DateTime::parse_from_rfc3339(\"2025-03-15T02:24:03Z\")\n .unwrap()\n .with_timezone(&Utc);\n let stamped = Message::with_timestamps(&[Message {\n role: Role::User,\n content: vec![ContentBlock::Text {\n text: \"hello\".to_string(),\n cache_control: None,\n }],\n timestamp: Some(ts),\n tool_duration_ms: None,\n }]);\n match &stamped[0].content[0] {\n ContentBlock::Text { text, .. } => {\n assert_eq!(text, \"[2025-03-15T02:24:03.000Z] hello\");\n }\n other => panic!(\"expected text block, got {other:?}\"),\n }\n }\n\n #[test]\n fn with_timestamps_adds_tool_timing_header_with_duration() {\n let ts = chrono::DateTime::parse_from_rfc3339(\"2025-03-15T02:24:13Z\")\n .unwrap()\n .with_timezone(&Utc);\n let stamped = Message::with_timestamps(&[Message {\n role: Role::User,\n content: vec![ContentBlock::ToolResult {\n tool_use_id: \"call_1\".to_string(),\n content: \"ok\".to_string(),\n is_error: None,\n }],\n timestamp: Some(ts),\n tool_duration_ms: Some(3_200),\n }]);\n match &stamped[0].content[0] {\n ContentBlock::ToolResult { content, .. } => {\n assert_eq!(\n content,\n \"[tool timing: start=2025-03-15T02:24:09.800Z finish=2025-03-15T02:24:13.000Z duration=3.2s] ok\"\n );\n }\n other => panic!(\"expected tool result block, got {other:?}\"),\n }\n }\n\n #[test]\n fn with_timestamps_skips_internal_system_reminders() {\n let ts = chrono::DateTime::parse_from_rfc3339(\"2025-03-15T02:24:13Z\")\n .unwrap()\n .with_timezone(&Utc);\n let original = Message {\n role: Role::User,\n content: vec![ContentBlock::Text {\n text: \"<system-reminder>\\ninternal\\n</system-reminder>\".to_string(),\n cache_control: None,\n }],\n timestamp: Some(ts),\n tool_duration_ms: None,\n };\n let stamped = Message::with_timestamps(std::slice::from_ref(&original));\n match &stamped[0].content[0] {\n ContentBlock::Text { text, .. } => {\n assert_eq!(text, \"<system-reminder>\\ninternal\\n</system-reminder>\");\n }\n other => panic!(\"expected text block, got {other:?}\"),\n }\n }\n\n #[test]\n fn ends_with_fresh_user_turn_accepts_plain_user_text() {\n let messages = vec![Message::user(\"hello\")];\n assert!(ends_with_fresh_user_turn(&messages));\n }\n\n #[test]\n fn ends_with_fresh_user_turn_rejects_trailing_tool_result() {\n let messages = vec![\n Message::user(\"hello\"),\n Message {\n role: Role::Assistant,\n content: vec![ContentBlock::ToolUse {\n id: \"call_1\".to_string(),\n name: \"bash\".to_string(),\n input: serde_json::json!({}),\n }],\n timestamp: Some(Utc::now()),\n tool_duration_ms: None,\n },\n Message::tool_result(\"call_1\", \"ok\", false),\n ];\n assert!(!ends_with_fresh_user_turn(&messages));\n }\n\n #[test]\n fn ends_with_fresh_user_turn_skips_internal_system_reminders() {\n let messages = vec![\n Message::user(\"hello\"),\n Message::user(\"<system-reminder>\\ninternal\\n</system-reminder>\"),\n ];\n assert!(ends_with_fresh_user_turn(&messages));\n }\n\n #[test]\n fn ends_with_fresh_user_turn_rejects_assistant_tail() {\n let messages = vec![\n Message::user(\"hello\"),\n Message::assistant_text(\"working on it\"),\n ];\n assert!(!ends_with_fresh_user_turn(&messages));\n }\n\n #[test]\n fn format_background_task_notification_markdown_uses_code_block_preview() {\n let rendered = format_background_task_notification_markdown(&BackgroundTaskCompleted {\n task_id: \"abc123\".to_string(),\n tool_name: \"bash\".to_string(),\n session_id: \"session\".to_string(),\n status: BackgroundTaskStatus::Completed,\n exit_code: Some(0),\n output_preview: \"[stderr] first line\\n[stdout] second line\\n\".to_string(),\n output_file: std::path::PathBuf::from(\"/tmp/output.log\"),\n duration_secs: 7.1,\n notify: true,\n wake: false,\n });\n\n assert!(\n rendered\n .contains(\"**Background task** `abc123` · `bash` · ✓ completed · 7.1s · exit 0\")\n );\n assert!(rendered.contains(\"```text\\n[stderr] first line\\n[stdout] second line\\n```\"));\n assert!(rendered.contains(\"_Full output:_ `bg action=\\\"output\\\" task_id=\\\"abc123\\\"`\"));\n }\n\n #[test]\n fn format_background_task_notification_markdown_handles_empty_preview() {\n let rendered = format_background_task_notification_markdown(&BackgroundTaskCompleted {\n task_id: \"abc123\".to_string(),\n tool_name: \"bash\".to_string(),\n session_id: \"session\".to_string(),\n status: BackgroundTaskStatus::Failed,\n exit_code: Some(9),\n output_preview: \"\\n\\n\".to_string(),\n output_file: std::path::PathBuf::from(\"/tmp/output.log\"),\n duration_secs: 1.0,\n notify: true,\n wake: false,\n });\n\n assert!(rendered.contains(\"✗ failed\"));\n assert!(rendered.contains(\"_No output captured._\"));\n }\n\n #[test]\n fn format_background_task_notification_markdown_renders_superseded_status() {\n let rendered = format_background_task_notification_markdown(&BackgroundTaskCompleted {\n task_id: \"abc123\".to_string(),\n tool_name: \"selfdev-build\".to_string(),\n session_id: \"session\".to_string(),\n status: BackgroundTaskStatus::Superseded,\n exit_code: Some(0),\n output_preview: \"Build completed, but source changed before activation\".to_string(),\n output_file: std::path::PathBuf::from(\"/tmp/output.log\"),\n duration_secs: 5.0,\n notify: true,\n wake: false,\n });\n\n assert!(rendered.contains(\"↻ superseded\"));\n assert!(rendered.contains(\"exit 0\"));\n assert!(rendered.contains(\"source changed before activation\"));\n }\n\n #[test]\n fn description_token_estimate_uses_chars_per_token_heuristic() {\n let def = ToolDefinition {\n name: \"read\".to_string(),\n description: \"abcdwxyz\".to_string(),\n input_schema: serde_json::json!({\"type\": \"object\"}),\n };\n\n assert_eq!(def.description_token_estimate(), 2);\n }\n\n #[test]\n fn parse_background_task_notification_markdown_extracts_fields() {\n let rendered = format_background_task_notification_markdown(&BackgroundTaskCompleted {\n task_id: \"abc123\".to_string(),\n tool_name: \"bash\".to_string(),\n session_id: \"session\".to_string(),\n status: BackgroundTaskStatus::Completed,\n exit_code: Some(0),\n output_preview: \"[stderr] first line\\n[stdout] second line\\n\".to_string(),\n output_file: std::path::PathBuf::from(\"/tmp/output.log\"),\n duration_secs: 7.1,\n notify: true,\n wake: false,\n });\n\n let parsed = parse_background_task_notification_markdown(&rendered)\n .expect(\"background task notification should parse\");\n assert_eq!(parsed.task_id, \"abc123\");\n assert_eq!(parsed.tool_name, \"bash\");\n assert_eq!(parsed.status, \"✓ completed\");\n assert_eq!(parsed.duration, \"7.1s\");\n assert_eq!(parsed.exit_label, \"exit 0\");\n assert_eq!(\n parsed.preview.as_deref(),\n Some(\"[stderr] first line\\n[stdout] second line\")\n );\n assert_eq!(\n parsed.full_output_command,\n \"bg action=\\\"output\\\" task_id=\\\"abc123\\\"\"\n );\n }\n}\n"}, "acf8096dfc55e9ea39bdbef9193f5c7dde54f44c": {"t": 1785757231.702104, "n": 35498, "h": "13c2634ba45b70bb", "c": "use crate::bus::{BackgroundTaskCompleted, BackgroundTaskStatus};\nuse chrono::Utc;\nuse regex::Regex;\nuse serde::{Deserialize, Serialize};\nuse std::collections::HashSet;\nuse std::sync::OnceLock;\n\nmod notifications;\n\npub use notifications::{\n InputShellResult, ParsedBackgroundTaskNotification, background_task_status_notice,\n format_background_task_notification_markdown, format_input_shell_result_markdown,\n input_shell_status_notice, parse_background_task_notification_markdown,\n};\n\nfn compile_static_regex(pattern: &str) -> Option<Regex> {\n match Regex::new(pattern) {\n Ok(regex) => Some(regex),\n Err(err) => {\n eprintln!(\"jcode: failed to compile static regex: {err}\");\n None\n }\n }\n}\n\nfn compile_static_regexes(patterns: &[&str]) -> Vec<Regex> {\n patterns\n .iter()\n .filter_map(|pattern| compile_static_regex(pattern))\n .collect()\n}\n\n/// Role in conversation\n#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]\n#[serde(rename_all = \"lowercase\")]\npub enum Role {\n User,\n Assistant,\n}\n\n/// Plain-text tool output placeholder when execution was interrupted.\npub const TOOL_OUTPUT_MISSING_TEXT: &str =\n \"Tool output missing (session interrupted before tool execution completed)\";\n\n/// A message in the conversation\n#[derive(Debug, Clone, Serialize, Deserialize)]\npub struct Message {\n pub role: Role,\n pub content: Vec<ContentBlock>,\n #[serde(default, skip_serializing_if = \"Option::is_none\")]\n pub timestamp: Option<chrono::DateTime<Utc>>,\n #[serde(default, skip_serializing_if = \"Option::is_none\")]\n pub tool_duration_ms: Option<u64>,\n}\n\n/// Cache control metadata for prompt caching\n#[derive(Debug, Clone, Serialize, Deserialize)]\npub struct CacheControl {\n #[serde(rename = \"type\")]\n pub kind: String,\n #[serde(skip_serializing_if = \"Option::is_none\")]\n pub ttl: Option<String>,\n}\n\nimpl CacheControl {\n pub fn ephemeral(ttl: Option<String>) -> Self {\n Self {\n kind: \"ephemeral\".to_string(),\n ttl,\n }\n }\n}\n\n/// Content block within a message\n#[derive(Debug, Clone, Serialize, Deserialize)]\n#[serde(tag = \"type\", rename_all = \"snake_case\")]\npub enum ContentBlock {\n Text {\n text: String,\n #[serde(skip_serializing_if = \"Option::is_none\")]\n cache_control: Option<CacheControl>,\n },\n /// Hidden reasoning content used for providers that require it (not displayed)\n Reasoning {\n text: String,\n },\n ToolUse {\n id: String,\n name: String,\n input: serde_json::Value,\n },\n ToolResult {\n tool_use_id: String,\n content: String,\n #[serde(skip_serializing_if = \"Option::is_none\")]\n is_error: Option<bool>,\n },\n Image {\n media_type: String,\n data: String,\n },\n /// Hidden OpenAI Responses compaction item used to preserve native\n /// compaction state across turns/saves when jcode explicitly triggers it.\n OpenAICompaction {\n encrypted_content: String,\n },\n}\n\nimpl Message {\n pub fn user(text: &str) -> Self {\n Self {\n role: Role::User,\n content: vec![ContentBlock::Text {\n text: text.to_string(),\n cache_control: None,\n }],\n timestamp: Some(Utc::now()),\n tool_duration_ms: None,\n }\n }\n\n pub fn user_with_images(text: &str, images: Vec<(String, String)>) -> Self {\n let mut content: Vec<ContentBlock> = images\n .into_iter()\n .map(|(media_type, data)| ContentBlock::Image { media_type, data })\n .collect();\n content.push(ContentBlock::Text {\n text: text.to_string(),\n cache_control: None,\n });\n Self {\n role: Role::User,\n content,\n timestamp: Some(Utc::now()),\n tool_duration_ms: None,\n }\n }\n\n pub fn assistant_text(text: &str) -> Self {\n Self {\n role: Role::Assistant,\n content: vec![ContentBlock::Text {\n text: text.to_string(),\n cache_control: None,\n }],\n timestamp: Some(Utc::now()),\n tool_duration_ms: None,\n }\n }\n\n pub fn tool_result(tool_use_id: &str, content: &str, is_error: bool) -> Self {\n Self::tool_result_with_duration(tool_use_id, content, is_error, None)\n }\n\n pub fn tool_result_with_duration(\n tool_use_id: &str,\n content: &str,\n is_error: bool,\n tool_duration_ms: Option<u64>,\n ) -> Self {\n Self {\n role: Role::User,\n content: vec![ContentBlock::ToolResult {\n tool_use_id: tool_use_id.to_string(),\n content: content.to_string(),\n is_error: if is_error { Some(true) } else { None },\n }],\n timestamp: Some(Utc::now()),\n tool_duration_ms,\n }\n }\n\n /// Format a timestamp deterministically in UTC for injection into model-visible content.\n pub fn format_timestamp(ts: &chrono::DateTime<Utc>) -> String {\n ts.to_rfc3339_opts(chrono::SecondsFormat::Millis, true)\n }\n\n pub fn format_duration(duration_ms: u64) -> String {\n match duration_ms {\n 0..=999 => format!(\"{}ms\", duration_ms),\n 1_000..=9_999 => format!(\"{:.1}s\", duration_ms as f64 / 1000.0),\n 10_000..=59_999 => format!(\"{}s\", duration_ms / 1000),\n _ => {\n let total_seconds = duration_ms / 1000;\n let minutes = total_seconds / 60;\n let seconds = total_seconds % 60;\n if seconds == 0 {\n format!(\"{}m\", minutes)\n } else {\n format!(\"{}m {}s\", minutes, seconds)\n }\n }\n }\n }\n\n pub fn is_internal_system_reminder(&self) -> bool {\n self.content\n .iter()\n .find_map(|block| match block {\n ContentBlock::Text { text, .. } => Some(text.trim_start()),\n _ => None,\n })\n .is_some_and(|text| text.starts_with(\"<system-reminder>\"))\n }\n\n fn should_skip_timestamp_injection(&self) -> bool {\n self.is_internal_system_reminder()\n }\n\n fn tool_result_tag(&self, ts: &chrono::DateTime<Utc>) -> String {\n match self.tool_duration_ms {\n Some(duration_ms) => {\n let duration_ms_i64 = i64::try_from(duration_ms).unwrap_or(i64::MAX);\n let start_ts = ts\n .checked_sub_signed(chrono::Duration::milliseconds(duration_ms_i64))\n .unwrap_or(*ts);\n format!(\n \"[tool timing: start={} finish={} duration={}]\",\n Self::format_timestamp(&start_ts),\n Self::format_timestamp(ts),\n Self::format_duration(duration_ms)\n )\n }\n None => format!(\"[{}]\", Self::format_timestamp(ts)),\n }\n }\n\n /// Return a copy of messages with timestamps injected into user-role text content.\n /// Tool results get a stable UTC timing header prepended to content.\n /// User text messages get a stable UTC timestamp prepended to the first text block.\n pub fn with_timestamps(messages: &[Message]) -> Vec<Message> {\n messages\n .iter()\n .map(|msg| {\n let Some(ts) = msg.timestamp else {\n return msg.clone();\n };\n if msg.role != Role::User || msg.should_skip_timestamp_injection() {\n return msg.clone();\n }\n let text_tag = format!(\"[{}]\", Self::format_timestamp(&ts));\n let tool_result_tag = msg.tool_result_tag(&ts);\n let mut msg = msg.clone();\n let mut tagged = false;\n for block in &mut msg.content {\n match block {\n ContentBlock::Text { text, .. } if !tagged => {\n *text = format!(\"{} {}\", text_tag, text);\n tagged = true;\n }\n ContentBlock::ToolResult { content, .. } if !tagged => {\n *content = format!(\"{} {}\", tool_result_tag, content);\n tagged = true;\n }\n _ => {}\n }\n }\n msg\n })\n .collect()\n }\n}\n\nconst STABLE_HASH_SEED: u64 = 0xcbf29ce484222325;\nconst STABLE_HASH_PRIME: u64 = 0x100000001b3;\n\nfn stable_hash_bytes(bytes: &[u8]) -> u64 {\n let mut hash = STABLE_HASH_SEED;\n for byte in bytes {\n hash ^= *byte as u64;\n hash = hash.wrapping_mul(STABLE_HASH_PRIME);\n }\n hash\n}\n\npub(crate) fn extend_stable_hash(acc: u64, next: u64) -> u64 {\n stable_hash_bytes(&[acc.to_le_bytes().as_slice(), next.to_le_bytes().as_slice()].concat())\n}\n\npub(crate) fn stable_message_hash(message: &Message) -> u64 {\n match serde_json::to_vec(message) {\n Ok(bytes) => stable_hash_bytes(&bytes),\n Err(_) => stable_hash_bytes(format!(\"{:?}\", message).as_bytes()),\n }\n}\n\npub fn ends_with_fresh_user_turn(messages: &[Message]) -> bool {\n for msg in messages.iter().rev() {\n if msg.role != Role::User {\n return false;\n }\n\n if msg\n .content\n .iter()\n .any(|block| matches!(block, ContentBlock::ToolResult { .. }))\n {\n return false;\n }\n\n if msg.content.is_empty() {\n return false;\n }\n\n let mut saw_user_text = false;\n for block in &msg.content {\n match block {\n ContentBlock::Text { text, .. } => {\n let trimmed = text.trim();\n if !trimmed.is_empty() && !trimmed.starts_with(\"<system-reminder>\") {\n saw_user_text = true;\n }\n }\n _ => return false,\n }\n }\n\n if saw_user_text {\n return true;\n }\n\n if msg.is_internal_system_reminder() {\n continue;\n }\n\n return false;\n }\n\n false\n}\n\n/// Sanitize a tool ID so it matches the pattern `^[a-zA-Z0-9_-]+$`.\n///\n/// Different providers generate tool IDs in different formats. When switching\n/// from one provider to another mid-conversation, the historical tool IDs may\n/// contain characters that the new provider rejects (e.g., dots in Copilot IDs\n/// sent to Anthropic). This function replaces any invalid characters with\n/// underscores.\npub fn sanitize_tool_id(id: &str) -> String {\n if id.is_empty() {\n return \"unknown\".to_string();\n }\n let sanitized: String = id\n .chars()\n .map(|c| {\n if c.is_ascii_alphanumeric() || c == '_' || c == '-' {\n c\n } else {\n '_'\n }\n })\n .collect();\n if sanitized.is_empty() {\n \"unknown\".to_string()\n } else {\n sanitized\n }\n}\n\n/// Redact likely secrets from persisted tool output.\n///\n/// This is a best-effort safeguard for local session history files. It targets\n/// high-confidence token/key patterns and common `KEY=VALUE` assignments used by\n/// auth flows.\npub fn redact_secrets(text: &str) -> String {\n // Fast path to avoid regex work for most tool outputs.\n let lower = text.to_ascii_lowercase();\n\n if !text.contains(\"sk-\")\n && !text.contains(\"ghp_\")\n && !text.contains(\"github_pat_\")\n && !text.contains(\"AIza\")\n && !text.contains(\"ya29.\")\n && !text.contains(\"xox\")\n && !lower.contains(\"api_key\")\n && !lower.contains(\"token\")\n {\n return text.to_string();\n }\n\n static DIRECT_PATTERNS: OnceLock<Vec<Regex>> = OnceLock::new();\n static ASSIGNMENT_PATTERNS: OnceLock<Vec<Regex>> = OnceLock::new();\n\n let direct_patterns = DIRECT_PATTERNS.get_or_init(|| {\n compile_static_regexes(&[\n r\"sk-ant-(?:oat|ort)01-[A-Za-z0-9_-]{20,}\",\n r\"sk-or-v1-[A-Za-z0-9_-]{20,}\",\n r\"ghp_[A-Za-z0-9]{20,}\",\n r\"github_pat_[A-Za-z0-9_]{20,}\",\n r\"ya29\\.[A-Za-z0-9._-]{20,}\",\n r\"AIza[0-9A-Za-z_-]{20,}\",\n r\"xox[baprs]-[A-Za-z0-9-]{10,}\",\n ])\n });\n\n let assignment_patterns = ASSIGNMENT_PATTERNS.get_or_init(|| {\n compile_static_regexes(&[\n r\"(?m)^\\s*(OPENROUTER_API_KEY\\s*=\\s*)[^\\r\\n]+\",\n r\"(?m)^\\s*(OPENCODE_API_KEY\\s*=\\s*)[^\\r\\n]+\",\n r\"(?m)^\\s*(OPENCODE_GO_API_KEY\\s*=\\s*)[^\\r\\n]+\",\n r\"(?m)^\\s*(ZHIPU_API_KEY\\s*=\\s*)[^\\r\\n]+\",\n r\"(?m)^\\s*(ZAI_API_KEY\\s*=\\s*)[^\\r\\n]+\",\n r\"(?m)^\\s*(302AI_API_KEY\\s*=\\s*)[^\\r\\n]+\",\n r\"(?m)^\\s*(BASETEN_API_KEY\\s*=\\s*)[^\\r\\n]+\",\n r\"(?m)^\\s*(CORTECS_API_KEY\\s*=\\s*)[^\\r\\n]+\",\n r\"(?m)^\\s*(DEEPSEEK_API_KEY\\s*=\\s*)[^\\r\\n]+\",\n r\"(?m)^\\s*(FIRMWARE_API_KEY\\s*=\\s*)[^\\r\\n]+\",\n r\"(?m)^\\s*(HF_TOKEN\\s*=\\s*)[^\\r\\n]+\",\n r\"(?m)^\\s*(MOONSHOT_API_KEY\\s*=\\s*)[^\\r\\n]+\",\n r\"(?m)^\\s*(NEBIUS_API_KEY\\s*=\\s*)[^\\r\\n]+\",\n r\"(?m)^\\s*(SCALEWAY_API_KEY\\s*=\\s*)[^\\r\\n]+\",\n r\"(?m)^\\s*(STACKIT_API_KEY\\s*=\\s*)[^\\r\\n]+\",\n r\"(?m)^\\s*(GROQ_API_KEY\\s*=\\s*)[^\\r\\n]+\",\n r\"(?m)^\\s*(MISTRAL_API_KEY\\s*=\\s*)[^\\r\\n]+\",\n r\"(?m)^\\s*(PERPLEXITY_API_KEY\\s*=\\s*)[^\\r\\n]+\",\n r\"(?m)^\\s*(TOGETHER_API_KEY\\s*=\\s*)[^\\r\\n]+\",\n r\"(?m)^\\s*(DEEPINFRA_API_KEY\\s*=\\s*)[^\\r\\n]+\",\n r\"(?m)^\\s*(XAI_API_KEY\\s*=\\s*)[^\\r\\n]+\",\n r\"(?m)^\\s*(LMSTUDIO_API_KEY\\s*=\\s*)[^\\r\\n]+\",\n r\"(?m)^\\s*(OLLAMA_API_KEY\\s*=\\s*)[^\\r\\n]+\",\n r\"(?m)^\\s*(CHUTES_API_KEY\\s*=\\s*)[^\\r\\n]+\",\n r\"(?m)^\\s*(CEREBRAS_API_KEY\\s*=\\s*)[^\\r\\n]+\",\n r\"(?m)^\\s*(OPENAI_COMPAT_API_KEY\\s*=\\s*)[^\\r\\n]+\",\n r\"(?m)^\\s*(ANTHROPIC_API_KEY\\s*=\\s*)[^\\r\\n]+\",\n r\"(?m)^\\s*(OPENAI_API_KEY\\s*=\\s*)[^\\r\\n]+\",\n r\"(?m)^\\s*(AZURE_OPENAI_API_KEY\\s*=\\s*)[^\\r\\n]+\",\n r\"(?m)^\\s*(CURSOR_API_KEY\\s*=\\s*)[^\\r\\n]+\",\n r\"(?m)^\\s*(GITHUB_TOKEN\\s*=\\s*)[^\\r\\n]+\",\n ])\n });\n\n let mut redacted = text.to_string();\n let mut redacted_keys: HashSet<String> = [\n \"OPENROUTER_API_KEY\",\n \"OPENCODE_API_KEY\",\n \"OPENCODE_GO_API_KEY\",\n \"ZHIPU_API_KEY\",\n \"ZAI_API_KEY\",\n \"302AI_API_KEY\",\n \"BASETEN_API_KEY\",\n \"CORTECS_API_KEY\",\n \"DEEPSEEK_API_KEY\",\n \"FIRMWARE_API_KEY\",\n \"HF_TOKEN\",\n \"MOONSHOT_API_KEY\",\n \"NEBIUS_API_KEY\",\n \"SCALEWAY_API_KEY\",\n \"STACKIT_API_KEY\",\n \"GROQ_API_KEY\",\n \"MISTRAL_API_KEY\",\n \"PERPLEXITY_API_KEY\",\n \"TOGETHER_API_KEY\",\n \"DEEPINFRA_API_KEY\",\n \"XAI_API_KEY\",\n \"LMSTUDIO_API_KEY\",\n \"OLLAMA_API_KEY\",\n \"CHUTES_API_KEY\",\n \"CEREBRAS_API_KEY\",\n \"OPENAI_COMPAT_API_KEY\",\n \"ANTHROPIC_API_KEY\",\n \"OPENAI_API_KEY\",\n \"AZURE_OPENAI_API_KEY\",\n \"CURSOR_API_KEY\",\n \"GITHUB_TOKEN\",\n ]\n .iter()\n .map(|k| (*k).to_string())\n .collect();\n\n for re in direct_patterns {\n redacted = re.replace_all(&redacted, \"[REDACTED_SECRET]\").into_owned();\n }\n\n for re in assignment_patterns {\n redacted = re\n .replace_all(&redacted, \"${1}[REDACTED_SECRET]\")\n .into_owned();\n }\n\n // Also redact custom API key variable names configured at runtime.\n for source in [\n \"JCODE_OPENROUTER_API_KEY_NAME\",\n \"JCODE_OPENAI_COMPAT_API_KEY_NAME\",\n ] {\n let Some(key_name) = std::env::var(source)\n .ok()\n .map(|v| v.trim().to_string())\n .filter(|v| !v.is_empty())\n else {\n continue;\n };\n\n if !key_name\n .chars()\n .all(|c| c.is_ascii_uppercase() || c.is_ascii_digit() || c == '_')\n {\n continue;\n }\n if !redacted_keys.insert(key_name.clone()) {\n continue;\n }\n\n let pattern = format!(r\"(?m)^\\s*({}\\s*=\\s*)[^\\r\\n]+\", regex::escape(&key_name));\n if let Ok(re) = Regex::new(&pattern) {\n redacted = re\n .replace_all(&redacted, \"${1}[REDACTED_SECRET]\")\n .into_owned();\n }\n }\n\n redacted\n}\n\n/// Tool definition for the API\n#[derive(Debug, Clone, Serialize)]\npub struct ToolDefinition {\n pub name: String,\n // Prompt-visible text sent to the model by provider adapters.\n // Approximate prompt cost: description.len() / 4. Use\n // ToolDefinition::description_token_estimate() when reviewing tool bloat.\n pub description: String,\n pub input_schema: serde_json::Value,\n}\n\nimpl ToolDefinition {\n /// Serialized size of the full tool definition payload sent to providers.\n pub fn prompt_chars(&self) -> usize {\n serde_json::json!({\n \"name\": self.name,\n \"description\": self.description,\n \"input_schema\": self.input_schema,\n })\n .to_string()\n .len()\n }\n\n /// Approximate prompt-token cost of this tool's top-level description.\n ///\n /// This uses jcode's standard chars/4 heuristic, matching other token\n /// budget estimates in the codebase.\n pub fn description_token_estimate(&self) -> usize {\n crate::util::estimate_tokens(&self.description)\n }\n\n /// Approximate prompt-token cost of the full tool definition payload.\n pub fn prompt_token_estimate(&self) -> usize {\n crate::util::estimate_tokens(\n &serde_json::json!({\n \"name\": self.name,\n \"description\": self.description,\n \"input_schema\": self.input_schema,\n })\n .to_string(),\n )\n }\n\n pub fn aggregate_prompt_chars(defs: &[ToolDefinition]) -> usize {\n defs.iter().map(Self::prompt_chars).sum()\n }\n\n pub fn aggregate_prompt_token_estimate(defs: &[ToolDefinition]) -> usize {\n defs.iter().map(Self::prompt_token_estimate).sum()\n }\n}\n\n/// A tool call from the model\n#[derive(Debug, Clone, Default, PartialEq, Eq, serde::Serialize, serde::Deserialize)]\npub struct ToolCall {\n #[serde(default)]\n pub id: String,\n #[serde(default)]\n pub name: String,\n #[serde(default)]\n pub input: serde_json::Value,\n #[serde(default, skip_serializing_if = \"Option::is_none\")]\n pub intent: Option<String>,\n}\n\n/// Connection phase for status bar transparency\n#[derive(Debug, Clone, PartialEq)]\npub enum ConnectionPhase {\n /// Refreshing OAuth token\n Authenticating,\n /// TCP + TLS connection to API\n Connecting,\n /// HTTP request sent, waiting for first response byte\n WaitingForResponse,\n /// First byte received, stream is active\n Streaming,\n /// Retrying after a transient error\n Retrying { attempt: u32, max: u32 },\n}\n\nimpl std::fmt::Display for ConnectionPhase {\n fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {\n match self {\n ConnectionPhase::Authenticating => write!(f, \"authenticating\"),\n ConnectionPhase::Connecting => write!(f, \"connecting\"),\n ConnectionPhase::WaitingForResponse => write!(f, \"waiting for response\"),\n ConnectionPhase::Streaming => write!(f, \"streaming\"),\n ConnectionPhase::Retrying { attempt, max } => {\n write!(f, \"retrying ({}/{})\", attempt, max)\n }\n }\n }\n}\n\n/// Streaming event from provider\n#[derive(Debug, Clone)]\npub enum StreamEvent {\n /// Text content delta\n TextDelta(String),\n /// Tool use started\n ToolUseStart { id: String, name: String },\n /// Tool input delta (JSON fragment)\n ToolInputDelta(String),\n /// Tool use complete\n ToolUseEnd,\n /// Tool result from provider (provider already executed the tool)\n ToolResult {\n tool_use_id: String,\n content: String,\n is_error: bool,\n },\n /// Extended thinking started\n ThinkingStart,\n /// Extended thinking delta (reasoning content)\n ThinkingDelta(String),\n /// Extended thinking ended\n ThinkingEnd,\n /// Extended thinking completed with duration\n ThinkingDone { duration_secs: f64 },\n /// Message complete (may have stop reason)\n MessageEnd { stop_reason: Option<String> },\n /// Token usage update\n TokenUsage {\n input_tokens: Option<u64>,\n output_tokens: Option<u64>,\n cache_read_input_tokens: Option<u64>,\n cache_creation_input_tokens: Option<u64>,\n },\n /// Active transport/connection type for this stream\n ConnectionType { connection: String },\n /// Connection phase update (for status bar transparency)\n ConnectionPhase { phase: ConnectionPhase },\n /// Provider-supplied human-readable transport detail for the status line\n StatusDetail { detail: String },\n /// Error occurred\n Error {\n message: String,\n /// Seconds until rate limit resets (if this is a rate limit error)\n retry_after_secs: Option<u64>,\n },\n /// Provider session ID (for conversation resume)\n SessionId(String),\n /// Compaction occurred (context was summarized)\n Compaction {\n trigger: String,\n pre_tokens: Option<u64>,\n /// Provider-native compaction artifact, if one was emitted.\n openai_encrypted_content: Option<String>,\n },\n /// Upstream provider info (e.g., which provider OpenRouter routed to)\n UpstreamProvider { provider: String },\n /// Native tool call from a provider bridge that needs execution by jcode\n NativeToolCall {\n request_id: String,\n tool_name: String,\n input: serde_json::Value,\n },\n}\n\n#[cfg(test)]\nmod tests {\n use super::*;\n\n #[test]\n fn sanitize_tool_id_alphanumeric_passthrough() {\n assert_eq!(\n sanitize_tool_id(\"toolu_01XFDUDYJgAACzvnptvVer6u\"),\n \"toolu_01XFDUDYJgAACzvnptvVer6u\"\n );\n assert_eq!(sanitize_tool_id(\"call_abc123\"), \"call_abc123\");\n assert_eq!(\n sanitize_tool_id(\"call_1234567890_9876543210\"),\n \"call_1234567890_9876543210\"\n );\n }\n\n #[test]\n fn sanitize_tool_id_hyphens_passthrough() {\n assert_eq!(sanitize_tool_id(\"call-abc-123\"), \"call-abc-123\");\n assert_eq!(\n sanitize_tool_id(\"tool_use-id_with-mixed\"),\n \"tool_use-id_with-mixed\"\n );\n }\n\n #[test]\n fn sanitize_tool_id_replaces_dots() {\n assert_eq!(\n sanitize_tool_id(\"chatcmpl-abc.def.ghi\"),\n \"chatcmpl-abc_def_ghi\"\n );\n assert_eq!(sanitize_tool_id(\"call.123\"), \"call_123\");\n }\n\n #[test]\n fn sanitize_tool_id_replaces_colons() {\n assert_eq!(sanitize_tool_id(\"call:123:456\"), \"call_123_456\");\n }\n\n #[test]\n fn sanitize_tool_id_replaces_special_chars() {\n assert_eq!(\n sanitize_tool_id(\"id@with#special$chars\"),\n \"id_with_special_chars\"\n );\n assert_eq!(sanitize_tool_id(\"id with spaces\"), \"id_with_spaces\");\n }\n\n #[test]\n fn sanitize_tool_id_empty_returns_unknown() {\n assert_eq!(sanitize_tool_id(\"\"), \"unknown\");\n }\n\n #[test]\n fn sanitize_tool_id_copilot_to_anthropic() {\n assert_eq!(\n sanitize_tool_id(\"chatcmpl-BF2xX.tool_call.0\"),\n \"chatcmpl-BF2xX_tool_call_0\"\n );\n }\n\n #[test]\n fn sanitize_tool_id_already_valid_unchanged() {\n let valid_ids = [\n \"toolu_01XFDUDYJgAACzvnptvVer6u\",\n \"call_abc123\",\n \"fallback_text_call_call_1234567890_9876543210\",\n \"tool_123\",\n \"a\",\n \"A\",\n \"0\",\n \"_\",\n \"-\",\n \"a-b_c\",\n ];\n for id in valid_ids {\n assert_eq!(sanitize_tool_id(id), id, \"ID '{}' should be unchanged\", id);\n }\n }\n\n #[test]\n fn redact_secrets_redacts_known_direct_token_formats() {\n let input = \"access=sk-ant-oat01-ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789\\nopenrouter=sk-or-v1-abcdefghijklmnopqrstuvwxyz0123456789\\ngithub=ghp_ABCDEFGHIJKLMNOPQRSTUVWXYZ0123\\n\";\n let out = redact_secrets(input);\n assert!(!out.contains(\"sk-ant-oat01-\"));\n assert!(!out.contains(\"sk-or-v1-\"));\n assert!(!out.contains(\"ghp_ABCDEFGHIJKLMNOPQRSTUVWXYZ0123\"));\n assert!(out.matches(\"[REDACTED_SECRET]\").count() >= 3);\n }\n\n #[test]\n fn redact_secrets_redacts_env_style_assignments() {\n let input = \"OPENROUTER_API_KEY=sk-or-v1-abc123abc123abc123abc123\\nOPENCODE_API_KEY=oc_test_secret\\nOPENCODE_GO_API_KEY=ocgo_test_secret\\nZAI_API_KEY=zai_secret\\nCHUTES_API_KEY=chutes_secret\\nCEREBRAS_API_KEY=cerebras_secret\\nOPENAI_COMPAT_API_KEY=compat_secret\\nCURSOR_API_KEY='my_cursor_secret_value'\\nOPENAI_API_KEY=sk-test-openai-example\\nAZURE_OPENAI_API_KEY=azure-openai-secret\\n\";\n let out = redact_secrets(input);\n assert!(out.contains(\"OPENROUTER_API_KEY=[REDACTED_SECRET]\"));\n assert!(out.contains(\"OPENCODE_API_KEY=[REDACTED_SECRET]\"));\n assert!(out.contains(\"OPENCODE_GO_API_KEY=[REDACTED_SECRET]\"));\n assert!(out.contains(\"ZAI_API_KEY=[REDACTED_SECRET]\"));\n assert!(out.contains(\"CHUTES_API_KEY=[REDACTED_SECRET]\"));\n assert!(out.contains(\"CEREBRAS_API_KEY=[REDACTED_SECRET]\"));\n assert!(out.contains(\"OPENAI_COMPAT_API_KEY=[REDACTED_SECRET]\"));\n assert!(out.contains(\"CURSOR_API_KEY=[REDACTED_SECRET]\"));\n assert!(out.contains(\"OPENAI_API_KEY=[REDACTED_SECRET]\"));\n assert!(out.contains(\"AZURE_OPENAI_API_KEY=[REDACTED_SECRET]\"));\n assert!(!out.contains(\"my_cursor_secret_value\"));\n }\n\n #[test]\n fn redact_secrets_redacts_runtime_key_assignment() {\n let key_var = \"JCODE_OPENAI_COMPAT_API_KEY_NAME\";\n let prev = std::env::var(key_var).ok();\n crate::env::set_var(key_var, \"GROQ_API_KEY\");\n\n let input = \"GROQ_API_KEY=my_secret_token_value\";\n let out = redact_secrets(input);\n assert_eq!(out, \"GROQ_API_KEY=[REDACTED_SECRET]\");\n\n if let Some(v) = prev {\n crate::env::set_var(key_var, v);\n } else {\n crate::env::remove_var(key_var);\n }\n }\n\n #[test]\n fn redact_secrets_redacts_mixed_case_token_assignments() {\n let input = \"my_token=ya29.ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789\";\n let out = redact_secrets(input);\n assert!(out.contains(\"[REDACTED_SECRET]\"));\n assert!(!out.contains(\"ya29.ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789\"));\n }\n\n #[test]\n fn redact_secrets_leaves_normal_output_unchanged() {\n let input = \"Found 5 files\\nNo auth errors\\nDone.\";\n assert_eq!(redact_secrets(input), input);\n }\n\n #[test]\n fn format_timestamp_is_stable_utc_rfc3339() {\n let ts = chrono::DateTime::parse_from_rfc3339(\"2025-03-15T02:24:13.250Z\")\n .unwrap()\n .with_timezone(&Utc);\n assert_eq!(Message::format_timestamp(&ts), \"2025-03-15T02:24:13.250Z\");\n }\n\n #[test]\n fn with_timestamps_prepends_utc_prefix_to_user_text() {\n let ts = chrono::DateTime::parse_from_rfc3339(\"2025-03-15T02:24:03Z\")\n .unwrap()\n .with_timezone(&Utc);\n let stamped = Message::with_timestamps(&[Message {\n role: Role::User,\n content: vec![ContentBlock::Text {\n text: \"hello\".to_string(),\n cache_control: None,\n }],\n timestamp: Some(ts),\n tool_duration_ms: None,\n }]);\n match &stamped[0].content[0] {\n ContentBlock::Text { text, .. } => {\n assert_eq!(text, \"[2025-03-15T02:24:03.000Z] hello\");\n }\n other => panic!(\"expected text block, got {other:?}\"),\n }\n }\n\n #[test]\n fn with_timestamps_adds_tool_timing_header_with_duration() {\n let ts = chrono::DateTime::parse_from_rfc3339(\"2025-03-15T02:24:13Z\")\n .unwrap()\n .with_timezone(&Utc);\n let stamped = Message::with_timestamps(&[Message {\n role: Role::User,\n content: vec![ContentBlock::ToolResult {\n tool_use_id: \"call_1\".to_string(),\n content: \"ok\".to_string(),\n is_error: None,\n }],\n timestamp: Some(ts),\n tool_duration_ms: Some(3_200),\n }]);\n match &stamped[0].content[0] {\n ContentBlock::ToolResult { content, .. } => {\n assert_eq!(\n content,\n \"[tool timing: start=2025-03-15T02:24:09.800Z finish=2025-03-15T02:24:13.000Z duration=3.2s] ok\"\n );\n }\n other => panic!(\"expected tool result block, got {other:?}\"),\n }\n }\n\n #[test]\n fn with_timestamps_skips_internal_system_reminders() {\n let ts = chrono::DateTime::parse_from_rfc3339(\"2025-03-15T02:24:13Z\")\n .unwrap()\n .with_timezone(&Utc);\n let original = Message {\n role: Role::User,\n content: vec![ContentBlock::Text {\n text: \"<system-reminder>\\ninternal\\n</system-reminder>\".to_string(),\n cache_control: None,\n }],\n timestamp: Some(ts),\n tool_duration_ms: None,\n };\n let stamped = Message::with_timestamps(std::slice::from_ref(&original));\n match &stamped[0].content[0] {\n ContentBlock::Text { text, .. } => {\n assert_eq!(text, \"<system-reminder>\\ninternal\\n</system-reminder>\");\n }\n other => panic!(\"expected text block, got {other:?}\"),\n }\n }\n\n #[test]\n fn ends_with_fresh_user_turn_accepts_plain_user_text() {\n let messages = vec![Message::user(\"hello\")];\n assert!(ends_with_fresh_user_turn(&messages));\n }\n\n #[test]\n fn ends_with_fresh_user_turn_rejects_trailing_tool_result() {\n let messages = vec![\n Message::user(\"hello\"),\n Message {\n role: Role::Assistant,\n content: vec![ContentBlock::ToolUse {\n id: \"call_1\".to_string(),\n name: \"bash\".to_string(),\n input: serde_json::json!({}),\n }],\n timestamp: Some(Utc::now()),\n tool_duration_ms: None,\n },\n Message::tool_result(\"call_1\", \"ok\", false),\n ];\n assert!(!ends_with_fresh_user_turn(&messages));\n }\n\n #[test]\n fn ends_with_fresh_user_turn_skips_internal_system_reminders() {\n let messages = vec![\n Message::user(\"hello\"),\n Message::user(\"<system-reminder>\\ninternal\\n</system-reminder>\"),\n ];\n assert!(ends_with_fresh_user_turn(&messages));\n }\n\n #[test]\n fn ends_with_fresh_user_turn_rejects_assistant_tail() {\n let messages = vec![\n Message::user(\"hello\"),\n Message::assistant_text(\"working on it\"),\n ];\n assert!(!ends_with_fresh_user_turn(&messages));\n }\n\n #[test]\n fn format_background_task_notification_markdown_uses_code_block_preview() {\n let rendered = format_background_task_notification_markdown(&BackgroundTaskCompleted {\n task_id: \"abc123\".to_string(),\n tool_name: \"bash\".to_string(),\n session_id: \"session\".to_string(),\n status: BackgroundTaskStatus::Completed,\n exit_code: Some(0),\n output_preview: \"[stderr] first line\\n[stdout] second line\\n\".to_string(),\n output_file: std::path::PathBuf::from(\"/tmp/output.log\"),\n duration_secs: 7.1,\n notify: true,\n wake: false,\n });\n\n assert!(\n rendered\n .contains(\"**Background task** `abc123` · `bash` · ✓ completed · 7.1s · exit 0\")\n );\n assert!(rendered.contains(\"```text\\n[stderr] first line\\n[stdout] second line\\n```\"));\n assert!(rendered.contains(\"_Full output:_ `bg action=\\\"output\\\" task_id=\\\"abc123\\\"`\"));\n }\n\n #[test]\n fn format_background_task_notification_markdown_handles_empty_preview() {\n let rendered = format_background_task_notification_markdown(&BackgroundTaskCompleted {\n task_id: \"abc123\".to_string(),\n tool_name: \"bash\".to_string(),\n session_id: \"session\".to_string(),\n status: BackgroundTaskStatus::Failed,\n exit_code: Some(9),\n output_preview: \"\\n\\n\".to_string(),\n output_file: std::path::PathBuf::from(\"/tmp/output.log\"),\n duration_secs: 1.0,\n notify: true,\n wake: false,\n });\n\n assert!(rendered.contains(\"✗ failed\"));\n assert!(rendered.contains(\"_No output captured._\"));\n }\n\n #[test]\n fn format_background_task_notification_markdown_renders_superseded_status() {\n let rendered = format_background_task_notification_markdown(&BackgroundTaskCompleted {\n task_id: \"abc123\".to_string(),\n tool_name: \"selfdev-build\".to_string(),\n session_id: \"session\".to_string(),\n status: BackgroundTaskStatus::Superseded,\n exit_code: Some(0),\n output_preview: \"Build completed, but source changed before activation\".to_string(),\n output_file: std::path::PathBuf::from(\"/tmp/output.log\"),\n duration_secs: 5.0,\n notify: true,\n wake: false,\n });\n\n assert!(rendered.contains(\"↻ superseded\"));\n assert!(rendered.contains(\"exit 0\"));\n assert!(rendered.contains(\"source changed before activation\"));\n }\n\n #[test]\n fn description_token_estimate_uses_chars_per_token_heuristic() {\n let def = ToolDefinition {\n name: \"read\".to_string(),\n description: \"abcdwxyz\".to_string(),\n input_schema: serde_json::json!({\"type\": \"object\"}),\n };\n\n assert_eq!(def.description_token_estimate(), 2);\n }\n\n #[test]\n fn parse_background_task_notification_markdown_extracts_fields() {\n let rendered = format_background_task_notification_markdown(&BackgroundTaskCompleted {\n task_id: \"abc123\".to_string(),\n tool_name: \"bash\".to_string(),\n session_id: \"session\".to_string(),\n status: BackgroundTaskStatus::Completed,\n exit_code: Some(0),\n output_preview: \"[stderr] first line\\n[stdout] second line\\n\".to_string(),\n output_file: std::path::PathBuf::from(\"/tmp/output.log\"),\n duration_secs: 7.1,\n notify: true,\n wake: false,\n });\n\n let parsed = parse_background_task_notification_markdown(&rendered)\n .expect(\"background task notification should parse\");\n assert_eq!(parsed.task_id, \"abc123\");\n assert_eq!(parsed.tool_name, \"bash\");\n assert_eq!(parsed.status, \"✓ completed\");\n assert_eq!(parsed.duration, \"7.1s\");\n assert_eq!(parsed.exit_label, \"exit 0\");\n assert_eq!(\n parsed.preview.as_deref(),\n Some(\"[stderr] first line\\n[stdout] second line\")\n );\n assert_eq!(\n parsed.full_output_command,\n \"bg action=\\\"output\\\" task_id=\\\"abc123\\\"\"\n );\n }\n}\n"}, "d3cca88d44a69896af3cf7b6a11da9b970d6ef1f": {"t": 1785757233.359118, "n": 35535, "h": "52c1ff52647920bd", "c": "use crate::bus::{BackgroundTaskCompleted, BackgroundTaskStatus};\nuse chrono::Utc;\nuse regex::Regex;\nuse serde::{Deserialize, Serialize};\nuse std::collections::HashSet;\nuse std::sync::OnceLock;\n\n#[path = \"message_notifications.rs\"]\nmod notifications;\n\npub use notifications::{\n InputShellResult, ParsedBackgroundTaskNotification, background_task_status_notice,\n format_background_task_notification_markdown, format_input_shell_result_markdown,\n input_shell_status_notice, parse_background_task_notification_markdown,\n};\n\nfn compile_static_regex(pattern: &str) -> Option<Regex> {\n match Regex::new(pattern) {\n Ok(regex) => Some(regex),\n Err(err) => {\n eprintln!(\"jcode: failed to compile static regex: {err}\");\n None\n }\n }\n}\n\nfn compile_static_regexes(patterns: &[&str]) -> Vec<Regex> {\n patterns\n .iter()\n .filter_map(|pattern| compile_static_regex(pattern))\n .collect()\n}\n\n/// Role in conversation\n#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]\n#[serde(rename_all = \"lowercase\")]\npub enum Role {\n User,\n Assistant,\n}\n\n/// Plain-text tool output placeholder when execution was interrupted.\npub const TOOL_OUTPUT_MISSING_TEXT: &str =\n \"Tool output missing (session interrupted before tool execution completed)\";\n\n/// A message in the conversation\n#[derive(Debug, Clone, Serialize, Deserialize)]\npub struct Message {\n pub role: Role,\n pub content: Vec<ContentBlock>,\n #[serde(default, skip_serializing_if = \"Option::is_none\")]\n pub timestamp: Option<chrono::DateTime<Utc>>,\n #[serde(default, skip_serializing_if = \"Option::is_none\")]\n pub tool_duration_ms: Option<u64>,\n}\n\n/// Cache control metadata for prompt caching\n#[derive(Debug, Clone, Serialize, Deserialize)]\npub struct CacheControl {\n #[serde(rename = \"type\")]\n pub kind: String,\n #[serde(skip_serializing_if = \"Option::is_none\")]\n pub ttl: Option<String>,\n}\n\nimpl CacheControl {\n pub fn ephemeral(ttl: Option<String>) -> Self {\n Self {\n kind: \"ephemeral\".to_string(),\n ttl,\n }\n }\n}\n\n/// Content block within a message\n#[derive(Debug, Clone, Serialize, Deserialize)]\n#[serde(tag = \"type\", rename_all = \"snake_case\")]\npub enum ContentBlock {\n Text {\n text: String,\n #[serde(skip_serializing_if = \"Option::is_none\")]\n cache_control: Option<CacheControl>,\n },\n /// Hidden reasoning content used for providers that require it (not displayed)\n Reasoning {\n text: String,\n },\n ToolUse {\n id: String,\n name: String,\n input: serde_json::Value,\n },\n ToolResult {\n tool_use_id: String,\n content: String,\n #[serde(skip_serializing_if = \"Option::is_none\")]\n is_error: Option<bool>,\n },\n Image {\n media_type: String,\n data: String,\n },\n /// Hidden OpenAI Responses compaction item used to preserve native\n /// compaction state across turns/saves when jcode explicitly triggers it.\n OpenAICompaction {\n encrypted_content: String,\n },\n}\n\nimpl Message {\n pub fn user(text: &str) -> Self {\n Self {\n role: Role::User,\n content: vec![ContentBlock::Text {\n text: text.to_string(),\n cache_control: None,\n }],\n timestamp: Some(Utc::now()),\n tool_duration_ms: None,\n }\n }\n\n pub fn user_with_images(text: &str, images: Vec<(String, String)>) -> Self {\n let mut content: Vec<ContentBlock> = images\n .into_iter()\n .map(|(media_type, data)| ContentBlock::Image { media_type, data })\n .collect();\n content.push(ContentBlock::Text {\n text: text.to_string(),\n cache_control: None,\n });\n Self {\n role: Role::User,\n content,\n timestamp: Some(Utc::now()),\n tool_duration_ms: None,\n }\n }\n\n pub fn assistant_text(text: &str) -> Self {\n Self {\n role: Role::Assistant,\n content: vec![ContentBlock::Text {\n text: text.to_string(),\n cache_control: None,\n }],\n timestamp: Some(Utc::now()),\n tool_duration_ms: None,\n }\n }\n\n pub fn tool_result(tool_use_id: &str, content: &str, is_error: bool) -> Self {\n Self::tool_result_with_duration(tool_use_id, content, is_error, None)\n }\n\n pub fn tool_result_with_duration(\n tool_use_id: &str,\n content: &str,\n is_error: bool,\n tool_duration_ms: Option<u64>,\n ) -> Self {\n Self {\n role: Role::User,\n content: vec![ContentBlock::ToolResult {\n tool_use_id: tool_use_id.to_string(),\n content: content.to_string(),\n is_error: if is_error { Some(true) } else { None },\n }],\n timestamp: Some(Utc::now()),\n tool_duration_ms,\n }\n }\n\n /// Format a timestamp deterministically in UTC for injection into model-visible content.\n pub fn format_timestamp(ts: &chrono::DateTime<Utc>) -> String {\n ts.to_rfc3339_opts(chrono::SecondsFormat::Millis, true)\n }\n\n pub fn format_duration(duration_ms: u64) -> String {\n match duration_ms {\n 0..=999 => format!(\"{}ms\", duration_ms),\n 1_000..=9_999 => format!(\"{:.1}s\", duration_ms as f64 / 1000.0),\n 10_000..=59_999 => format!(\"{}s\", duration_ms / 1000),\n _ => {\n let total_seconds = duration_ms / 1000;\n let minutes = total_seconds / 60;\n let seconds = total_seconds % 60;\n if seconds == 0 {\n format!(\"{}m\", minutes)\n } else {\n format!(\"{}m {}s\", minutes, seconds)\n }\n }\n }\n }\n\n pub fn is_internal_system_reminder(&self) -> bool {\n self.content\n .iter()\n .find_map(|block| match block {\n ContentBlock::Text { text, .. } => Some(text.trim_start()),\n _ => None,\n })\n .is_some_and(|text| text.starts_with(\"<system-reminder>\"))\n }\n\n fn should_skip_timestamp_injection(&self) -> bool {\n self.is_internal_system_reminder()\n }\n\n fn tool_result_tag(&self, ts: &chrono::DateTime<Utc>) -> String {\n match self.tool_duration_ms {\n Some(duration_ms) => {\n let duration_ms_i64 = i64::try_from(duration_ms).unwrap_or(i64::MAX);\n let start_ts = ts\n .checked_sub_signed(chrono::Duration::milliseconds(duration_ms_i64))\n .unwrap_or(*ts);\n format!(\n \"[tool timing: start={} finish={} duration={}]\",\n Self::format_timestamp(&start_ts),\n Self::format_timestamp(ts),\n Self::format_duration(duration_ms)\n )\n }\n None => format!(\"[{}]\", Self::format_timestamp(ts)),\n }\n }\n\n /// Return a copy of messages with timestamps injected into user-role text content.\n /// Tool results get a stable UTC timing header prepended to content.\n /// User text messages get a stable UTC timestamp prepended to the first text block.\n pub fn with_timestamps(messages: &[Message]) -> Vec<Message> {\n messages\n .iter()\n .map(|msg| {\n let Some(ts) = msg.timestamp else {\n return msg.clone();\n };\n if msg.role != Role::User || msg.should_skip_timestamp_injection() {\n return msg.clone();\n }\n let text_tag = format!(\"[{}]\", Self::format_timestamp(&ts));\n let tool_result_tag = msg.tool_result_tag(&ts);\n let mut msg = msg.clone();\n let mut tagged = false;\n for block in &mut msg.content {\n match block {\n ContentBlock::Text { text, .. } if !tagged => {\n *text = format!(\"{} {}\", text_tag, text);\n tagged = true;\n }\n ContentBlock::ToolResult { content, .. } if !tagged => {\n *content = format!(\"{} {}\", tool_result_tag, content);\n tagged = true;\n }\n _ => {}\n }\n }\n msg\n })\n .collect()\n }\n}\n\nconst STABLE_HASH_SEED: u64 = 0xcbf29ce484222325;\nconst STABLE_HASH_PRIME: u64 = 0x100000001b3;\n\nfn stable_hash_bytes(bytes: &[u8]) -> u64 {\n let mut hash = STABLE_HASH_SEED;\n for byte in bytes {\n hash ^= *byte as u64;\n hash = hash.wrapping_mul(STABLE_HASH_PRIME);\n }\n hash\n}\n\npub(crate) fn extend_stable_hash(acc: u64, next: u64) -> u64 {\n stable_hash_bytes(&[acc.to_le_bytes().as_slice(), next.to_le_bytes().as_slice()].concat())\n}\n\npub(crate) fn stable_message_hash(message: &Message) -> u64 {\n match serde_json::to_vec(message) {\n Ok(bytes) => stable_hash_bytes(&bytes),\n Err(_) => stable_hash_bytes(format!(\"{:?}\", message).as_bytes()),\n }\n}\n\npub fn ends_with_fresh_user_turn(messages: &[Message]) -> bool {\n for msg in messages.iter().rev() {\n if msg.role != Role::User {\n return false;\n }\n\n if msg\n .content\n .iter()\n .any(|block| matches!(block, ContentBlock::ToolResult { .. }))\n {\n return false;\n }\n\n if msg.content.is_empty() {\n return false;\n }\n\n let mut saw_user_text = false;\n for block in &msg.content {\n match block {\n ContentBlock::Text { text, .. } => {\n let trimmed = text.trim();\n if !trimmed.is_empty() && !trimmed.starts_with(\"<system-reminder>\") {\n saw_user_text = true;\n }\n }\n _ => return false,\n }\n }\n\n if saw_user_text {\n return true;\n }\n\n if msg.is_internal_system_reminder() {\n continue;\n }\n\n return false;\n }\n\n false\n}\n\n/// Sanitize a tool ID so it matches the pattern `^[a-zA-Z0-9_-]+$`.\n///\n/// Different providers generate tool IDs in different formats. When switching\n/// from one provider to another mid-conversation, the historical tool IDs may\n/// contain characters that the new provider rejects (e.g., dots in Copilot IDs\n/// sent to Anthropic). This function replaces any invalid characters with\n/// underscores.\npub fn sanitize_tool_id(id: &str) -> String {\n if id.is_empty() {\n return \"unknown\".to_string();\n }\n let sanitized: String = id\n .chars()\n .map(|c| {\n if c.is_ascii_alphanumeric() || c == '_' || c == '-' {\n c\n } else {\n '_'\n }\n })\n .collect();\n if sanitized.is_empty() {\n \"unknown\".to_string()\n } else {\n sanitized\n }\n}\n\n/// Redact likely secrets from persisted tool output.\n///\n/// This is a best-effort safeguard for local session history files. It targets\n/// high-confidence token/key patterns and common `KEY=VALUE` assignments used by\n/// auth flows.\npub fn redact_secrets(text: &str) -> String {\n // Fast path to avoid regex work for most tool outputs.\n let lower = text.to_ascii_lowercase();\n\n if !text.contains(\"sk-\")\n && !text.contains(\"ghp_\")\n && !text.contains(\"github_pat_\")\n && !text.contains(\"AIza\")\n && !text.contains(\"ya29.\")\n && !text.contains(\"xox\")\n && !lower.contains(\"api_key\")\n && !lower.contains(\"token\")\n {\n return text.to_string();\n }\n\n static DIRECT_PATTERNS: OnceLock<Vec<Regex>> = OnceLock::new();\n static ASSIGNMENT_PATTERNS: OnceLock<Vec<Regex>> = OnceLock::new();\n\n let direct_patterns = DIRECT_PATTERNS.get_or_init(|| {\n compile_static_regexes(&[\n r\"sk-ant-(?:oat|ort)01-[A-Za-z0-9_-]{20,}\",\n r\"sk-or-v1-[A-Za-z0-9_-]{20,}\",\n r\"ghp_[A-Za-z0-9]{20,}\",\n r\"github_pat_[A-Za-z0-9_]{20,}\",\n r\"ya29\\.[A-Za-z0-9._-]{20,}\",\n r\"AIza[0-9A-Za-z_-]{20,}\",\n r\"xox[baprs]-[A-Za-z0-9-]{10,}\",\n ])\n });\n\n let assignment_patterns = ASSIGNMENT_PATTERNS.get_or_init(|| {\n compile_static_regexes(&[\n r\"(?m)^\\s*(OPENROUTER_API_KEY\\s*=\\s*)[^\\r\\n]+\",\n r\"(?m)^\\s*(OPENCODE_API_KEY\\s*=\\s*)[^\\r\\n]+\",\n r\"(?m)^\\s*(OPENCODE_GO_API_KEY\\s*=\\s*)[^\\r\\n]+\",\n r\"(?m)^\\s*(ZHIPU_API_KEY\\s*=\\s*)[^\\r\\n]+\",\n r\"(?m)^\\s*(ZAI_API_KEY\\s*=\\s*)[^\\r\\n]+\",\n r\"(?m)^\\s*(302AI_API_KEY\\s*=\\s*)[^\\r\\n]+\",\n r\"(?m)^\\s*(BASETEN_API_KEY\\s*=\\s*)[^\\r\\n]+\",\n r\"(?m)^\\s*(CORTECS_API_KEY\\s*=\\s*)[^\\r\\n]+\",\n r\"(?m)^\\s*(DEEPSEEK_API_KEY\\s*=\\s*)[^\\r\\n]+\",\n r\"(?m)^\\s*(FIRMWARE_API_KEY\\s*=\\s*)[^\\r\\n]+\",\n r\"(?m)^\\s*(HF_TOKEN\\s*=\\s*)[^\\r\\n]+\",\n r\"(?m)^\\s*(MOONSHOT_API_KEY\\s*=\\s*)[^\\r\\n]+\",\n r\"(?m)^\\s*(NEBIUS_API_KEY\\s*=\\s*)[^\\r\\n]+\",\n r\"(?m)^\\s*(SCALEWAY_API_KEY\\s*=\\s*)[^\\r\\n]+\",\n r\"(?m)^\\s*(STACKIT_API_KEY\\s*=\\s*)[^\\r\\n]+\",\n r\"(?m)^\\s*(GROQ_API_KEY\\s*=\\s*)[^\\r\\n]+\",\n r\"(?m)^\\s*(MISTRAL_API_KEY\\s*=\\s*)[^\\r\\n]+\",\n r\"(?m)^\\s*(PERPLEXITY_API_KEY\\s*=\\s*)[^\\r\\n]+\",\n r\"(?m)^\\s*(TOGETHER_API_KEY\\s*=\\s*)[^\\r\\n]+\",\n r\"(?m)^\\s*(DEEPINFRA_API_KEY\\s*=\\s*)[^\\r\\n]+\",\n r\"(?m)^\\s*(XAI_API_KEY\\s*=\\s*)[^\\r\\n]+\",\n r\"(?m)^\\s*(LMSTUDIO_API_KEY\\s*=\\s*)[^\\r\\n]+\",\n r\"(?m)^\\s*(OLLAMA_API_KEY\\s*=\\s*)[^\\r\\n]+\",\n r\"(?m)^\\s*(CHUTES_API_KEY\\s*=\\s*)[^\\r\\n]+\",\n r\"(?m)^\\s*(CEREBRAS_API_KEY\\s*=\\s*)[^\\r\\n]+\",\n r\"(?m)^\\s*(OPENAI_COMPAT_API_KEY\\s*=\\s*)[^\\r\\n]+\",\n r\"(?m)^\\s*(ANTHROPIC_API_KEY\\s*=\\s*)[^\\r\\n]+\",\n r\"(?m)^\\s*(OPENAI_API_KEY\\s*=\\s*)[^\\r\\n]+\",\n r\"(?m)^\\s*(AZURE_OPENAI_API_KEY\\s*=\\s*)[^\\r\\n]+\",\n r\"(?m)^\\s*(CURSOR_API_KEY\\s*=\\s*)[^\\r\\n]+\",\n r\"(?m)^\\s*(GITHUB_TOKEN\\s*=\\s*)[^\\r\\n]+\",\n ])\n });\n\n let mut redacted = text.to_string();\n let mut redacted_keys: HashSet<String> = [\n \"OPENROUTER_API_KEY\",\n \"OPENCODE_API_KEY\",\n \"OPENCODE_GO_API_KEY\",\n \"ZHIPU_API_KEY\",\n \"ZAI_API_KEY\",\n \"302AI_API_KEY\",\n \"BASETEN_API_KEY\",\n \"CORTECS_API_KEY\",\n \"DEEPSEEK_API_KEY\",\n \"FIRMWARE_API_KEY\",\n \"HF_TOKEN\",\n \"MOONSHOT_API_KEY\",\n \"NEBIUS_API_KEY\",\n \"SCALEWAY_API_KEY\",\n \"STACKIT_API_KEY\",\n \"GROQ_API_KEY\",\n \"MISTRAL_API_KEY\",\n \"PERPLEXITY_API_KEY\",\n \"TOGETHER_API_KEY\",\n \"DEEPINFRA_API_KEY\",\n \"XAI_API_KEY\",\n \"LMSTUDIO_API_KEY\",\n \"OLLAMA_API_KEY\",\n \"CHUTES_API_KEY\",\n \"CEREBRAS_API_KEY\",\n \"OPENAI_COMPAT_API_KEY\",\n \"ANTHROPIC_API_KEY\",\n \"OPENAI_API_KEY\",\n \"AZURE_OPENAI_API_KEY\",\n \"CURSOR_API_KEY\",\n \"GITHUB_TOKEN\",\n ]\n .iter()\n .map(|k| (*k).to_string())\n .collect();\n\n for re in direct_patterns {\n redacted = re.replace_all(&redacted, \"[REDACTED_SECRET]\").into_owned();\n }\n\n for re in assignment_patterns {\n redacted = re\n .replace_all(&redacted, \"${1}[REDACTED_SECRET]\")\n .into_owned();\n }\n\n // Also redact custom API key variable names configured at runtime.\n for source in [\n \"JCODE_OPENROUTER_API_KEY_NAME\",\n \"JCODE_OPENAI_COMPAT_API_KEY_NAME\",\n ] {\n let Some(key_name) = std::env::var(source)\n .ok()\n .map(|v| v.trim().to_string())\n .filter(|v| !v.is_empty())\n else {\n continue;\n };\n\n if !key_name\n .chars()\n .all(|c| c.is_ascii_uppercase() || c.is_ascii_digit() || c == '_')\n {\n continue;\n }\n if !redacted_keys.insert(key_name.clone()) {\n continue;\n }\n\n let pattern = format!(r\"(?m)^\\s*({}\\s*=\\s*)[^\\r\\n]+\", regex::escape(&key_name));\n if let Ok(re) = Regex::new(&pattern) {\n redacted = re\n .replace_all(&redacted, \"${1}[REDACTED_SECRET]\")\n .into_owned();\n }\n }\n\n redacted\n}\n\n/// Tool definition for the API\n#[derive(Debug, Clone, Serialize)]\npub struct ToolDefinition {\n pub name: String,\n // Prompt-visible text sent to the model by provider adapters.\n // Approximate prompt cost: description.len() / 4. Use\n // ToolDefinition::description_token_estimate() when reviewing tool bloat.\n pub description: String,\n pub input_schema: serde_json::Value,\n}\n\nimpl ToolDefinition {\n /// Serialized size of the full tool definition payload sent to providers.\n pub fn prompt_chars(&self) -> usize {\n serde_json::json!({\n \"name\": self.name,\n \"description\": self.description,\n \"input_schema\": self.input_schema,\n })\n .to_string()\n .len()\n }\n\n /// Approximate prompt-token cost of this tool's top-level description.\n ///\n /// This uses jcode's standard chars/4 heuristic, matching other token\n /// budget estimates in the codebase.\n pub fn description_token_estimate(&self) -> usize {\n crate::util::estimate_tokens(&self.description)\n }\n\n /// Approximate prompt-token cost of the full tool definition payload.\n pub fn prompt_token_estimate(&self) -> usize {\n crate::util::estimate_tokens(\n &serde_json::json!({\n \"name\": self.name,\n \"description\": self.description,\n \"input_schema\": self.input_schema,\n })\n .to_string(),\n )\n }\n\n pub fn aggregate_prompt_chars(defs: &[ToolDefinition]) -> usize {\n defs.iter().map(Self::prompt_chars).sum()\n }\n\n pub fn aggregate_prompt_token_estimate(defs: &[ToolDefinition]) -> usize {\n defs.iter().map(Self::prompt_token_estimate).sum()\n }\n}\n\n/// A tool call from the model\n#[derive(Debug, Clone, Default, PartialEq, Eq, serde::Serialize, serde::Deserialize)]\npub struct ToolCall {\n #[serde(default)]\n pub id: String,\n #[serde(default)]\n pub name: String,\n #[serde(default)]\n pub input: serde_json::Value,\n #[serde(default, skip_serializing_if = \"Option::is_none\")]\n pub intent: Option<String>,\n}\n\n/// Connection phase for status bar transparency\n#[derive(Debug, Clone, PartialEq)]\npub enum ConnectionPhase {\n /// Refreshing OAuth token\n Authenticating,\n /// TCP + TLS connection to API\n Connecting,\n /// HTTP request sent, waiting for first response byte\n WaitingForResponse,\n /// First byte received, stream is active\n Streaming,\n /// Retrying after a transient error\n Retrying { attempt: u32, max: u32 },\n}\n\nimpl std::fmt::Display for ConnectionPhase {\n fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {\n match self {\n ConnectionPhase::Authenticating => write!(f, \"authenticating\"),\n ConnectionPhase::Connecting => write!(f, \"connecting\"),\n ConnectionPhase::WaitingForResponse => write!(f, \"waiting for response\"),\n ConnectionPhase::Streaming => write!(f, \"streaming\"),\n ConnectionPhase::Retrying { attempt, max } => {\n write!(f, \"retrying ({}/{})\", attempt, max)\n }\n }\n }\n}\n\n/// Streaming event from provider\n#[derive(Debug, Clone)]\npub enum StreamEvent {\n /// Text content delta\n TextDelta(String),\n /// Tool use started\n ToolUseStart { id: String, name: String },\n /// Tool input delta (JSON fragment)\n ToolInputDelta(String),\n /// Tool use complete\n ToolUseEnd,\n /// Tool result from provider (provider already executed the tool)\n ToolResult {\n tool_use_id: String,\n content: String,\n is_error: bool,\n },\n /// Extended thinking started\n ThinkingStart,\n /// Extended thinking delta (reasoning content)\n ThinkingDelta(String),\n /// Extended thinking ended\n ThinkingEnd,\n /// Extended thinking completed with duration\n ThinkingDone { duration_secs: f64 },\n /// Message complete (may have stop reason)\n MessageEnd { stop_reason: Option<String> },\n /// Token usage update\n TokenUsage {\n input_tokens: Option<u64>,\n output_tokens: Option<u64>,\n cache_read_input_tokens: Option<u64>,\n cache_creation_input_tokens: Option<u64>,\n },\n /// Active transport/connection type for this stream\n ConnectionType { connection: String },\n /// Connection phase update (for status bar transparency)\n ConnectionPhase { phase: ConnectionPhase },\n /// Provider-supplied human-readable transport detail for the status line\n StatusDetail { detail: String },\n /// Error occurred\n Error {\n message: String,\n /// Seconds until rate limit resets (if this is a rate limit error)\n retry_after_secs: Option<u64>,\n },\n /// Provider session ID (for conversation resume)\n SessionId(String),\n /// Compaction occurred (context was summarized)\n Compaction {\n trigger: String,\n pre_tokens: Option<u64>,\n /// Provider-native compaction artifact, if one was emitted.\n openai_encrypted_content: Option<String>,\n },\n /// Upstream provider info (e.g., which provider OpenRouter routed to)\n UpstreamProvider { provider: String },\n /// Native tool call from a provider bridge that needs execution by jcode\n NativeToolCall {\n request_id: String,\n tool_name: String,\n input: serde_json::Value,\n },\n}\n\n#[cfg(test)]\nmod tests {\n use super::*;\n\n #[test]\n fn sanitize_tool_id_alphanumeric_passthrough() {\n assert_eq!(\n sanitize_tool_id(\"toolu_01XFDUDYJgAACzvnptvVer6u\"),\n \"toolu_01XFDUDYJgAACzvnptvVer6u\"\n );\n assert_eq!(sanitize_tool_id(\"call_abc123\"), \"call_abc123\");\n assert_eq!(\n sanitize_tool_id(\"call_1234567890_9876543210\"),\n \"call_1234567890_9876543210\"\n );\n }\n\n #[test]\n fn sanitize_tool_id_hyphens_passthrough() {\n assert_eq!(sanitize_tool_id(\"call-abc-123\"), \"call-abc-123\");\n assert_eq!(\n sanitize_tool_id(\"tool_use-id_with-mixed\"),\n \"tool_use-id_with-mixed\"\n );\n }\n\n #[test]\n fn sanitize_tool_id_replaces_dots() {\n assert_eq!(\n sanitize_tool_id(\"chatcmpl-abc.def.ghi\"),\n \"chatcmpl-abc_def_ghi\"\n );\n assert_eq!(sanitize_tool_id(\"call.123\"), \"call_123\");\n }\n\n #[test]\n fn sanitize_tool_id_replaces_colons() {\n assert_eq!(sanitize_tool_id(\"call:123:456\"), \"call_123_456\");\n }\n\n #[test]\n fn sanitize_tool_id_replaces_special_chars() {\n assert_eq!(\n sanitize_tool_id(\"id@with#special$chars\"),\n \"id_with_special_chars\"\n );\n assert_eq!(sanitize_tool_id(\"id with spaces\"), \"id_with_spaces\");\n }\n\n #[test]\n fn sanitize_tool_id_empty_returns_unknown() {\n assert_eq!(sanitize_tool_id(\"\"), \"unknown\");\n }\n\n #[test]\n fn sanitize_tool_id_copilot_to_anthropic() {\n assert_eq!(\n sanitize_tool_id(\"chatcmpl-BF2xX.tool_call.0\"),\n \"chatcmpl-BF2xX_tool_call_0\"\n );\n }\n\n #[test]\n fn sanitize_tool_id_already_valid_unchanged() {\n let valid_ids = [\n \"toolu_01XFDUDYJgAACzvnptvVer6u\",\n \"call_abc123\",\n \"fallback_text_call_call_1234567890_9876543210\",\n \"tool_123\",\n \"a\",\n \"A\",\n \"0\",\n \"_\",\n \"-\",\n \"a-b_c\",\n ];\n for id in valid_ids {\n assert_eq!(sanitize_tool_id(id), id, \"ID '{}' should be unchanged\", id);\n }\n }\n\n #[test]\n fn redact_secrets_redacts_known_direct_token_formats() {\n let input = \"access=sk-ant-oat01-ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789\\nopenrouter=sk-or-v1-abcdefghijklmnopqrstuvwxyz0123456789\\ngithub=ghp_ABCDEFGHIJKLMNOPQRSTUVWXYZ0123\\n\";\n let out = redact_secrets(input);\n assert!(!out.contains(\"sk-ant-oat01-\"));\n assert!(!out.contains(\"sk-or-v1-\"));\n assert!(!out.contains(\"ghp_ABCDEFGHIJKLMNOPQRSTUVWXYZ0123\"));\n assert!(out.matches(\"[REDACTED_SECRET]\").count() >= 3);\n }\n\n #[test]\n fn redact_secrets_redacts_env_style_assignments() {\n let input = \"OPENROUTER_API_KEY=sk-or-v1-abc123abc123abc123abc123\\nOPENCODE_API_KEY=oc_test_secret\\nOPENCODE_GO_API_KEY=ocgo_test_secret\\nZAI_API_KEY=zai_secret\\nCHUTES_API_KEY=chutes_secret\\nCEREBRAS_API_KEY=cerebras_secret\\nOPENAI_COMPAT_API_KEY=compat_secret\\nCURSOR_API_KEY='my_cursor_secret_value'\\nOPENAI_API_KEY=sk-test-openai-example\\nAZURE_OPENAI_API_KEY=azure-openai-secret\\n\";\n let out = redact_secrets(input);\n assert!(out.contains(\"OPENROUTER_API_KEY=[REDACTED_SECRET]\"));\n assert!(out.contains(\"OPENCODE_API_KEY=[REDACTED_SECRET]\"));\n assert!(out.contains(\"OPENCODE_GO_API_KEY=[REDACTED_SECRET]\"));\n assert!(out.contains(\"ZAI_API_KEY=[REDACTED_SECRET]\"));\n assert!(out.contains(\"CHUTES_API_KEY=[REDACTED_SECRET]\"));\n assert!(out.contains(\"CEREBRAS_API_KEY=[REDACTED_SECRET]\"));\n assert!(out.contains(\"OPENAI_COMPAT_API_KEY=[REDACTED_SECRET]\"));\n assert!(out.contains(\"CURSOR_API_KEY=[REDACTED_SECRET]\"));\n assert!(out.contains(\"OPENAI_API_KEY=[REDACTED_SECRET]\"));\n assert!(out.contains(\"AZURE_OPENAI_API_KEY=[REDACTED_SECRET]\"));\n assert!(!out.contains(\"my_cursor_secret_value\"));\n }\n\n #[test]\n fn redact_secrets_redacts_runtime_key_assignment() {\n let key_var = \"JCODE_OPENAI_COMPAT_API_KEY_NAME\";\n let prev = std::env::var(key_var).ok();\n crate::env::set_var(key_var, \"GROQ_API_KEY\");\n\n let input = \"GROQ_API_KEY=my_secret_token_value\";\n let out = redact_secrets(input);\n assert_eq!(out, \"GROQ_API_KEY=[REDACTED_SECRET]\");\n\n if let Some(v) = prev {\n crate::env::set_var(key_var, v);\n } else {\n crate::env::remove_var(key_var);\n }\n }\n\n #[test]\n fn redact_secrets_redacts_mixed_case_token_assignments() {\n let input = \"my_token=ya29.ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789\";\n let out = redact_secrets(input);\n assert!(out.contains(\"[REDACTED_SECRET]\"));\n assert!(!out.contains(\"ya29.ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789\"));\n }\n\n #[test]\n fn redact_secrets_leaves_normal_output_unchanged() {\n let input = \"Found 5 files\\nNo auth errors\\nDone.\";\n assert_eq!(redact_secrets(input), input);\n }\n\n #[test]\n fn format_timestamp_is_stable_utc_rfc3339() {\n let ts = chrono::DateTime::parse_from_rfc3339(\"2025-03-15T02:24:13.250Z\")\n .unwrap()\n .with_timezone(&Utc);\n assert_eq!(Message::format_timestamp(&ts), \"2025-03-15T02:24:13.250Z\");\n }\n\n #[test]\n fn with_timestamps_prepends_utc_prefix_to_user_text() {\n let ts = chrono::DateTime::parse_from_rfc3339(\"2025-03-15T02:24:03Z\")\n .unwrap()\n .with_timezone(&Utc);\n let stamped = Message::with_timestamps(&[Message {\n role: Role::User,\n content: vec![ContentBlock::Text {\n text: \"hello\".to_string(),\n cache_control: None,\n }],\n timestamp: Some(ts),\n tool_duration_ms: None,\n }]);\n match &stamped[0].content[0] {\n ContentBlock::Text { text, .. } => {\n assert_eq!(text, \"[2025-03-15T02:24:03.000Z] hello\");\n }\n other => panic!(\"expected text block, got {other:?}\"),\n }\n }\n\n #[test]\n fn with_timestamps_adds_tool_timing_header_with_duration() {\n let ts = chrono::DateTime::parse_from_rfc3339(\"2025-03-15T02:24:13Z\")\n .unwrap()\n .with_timezone(&Utc);\n let stamped = Message::with_timestamps(&[Message {\n role: Role::User,\n content: vec![ContentBlock::ToolResult {\n tool_use_id: \"call_1\".to_string(),\n content: \"ok\".to_string(),\n is_error: None,\n }],\n timestamp: Some(ts),\n tool_duration_ms: Some(3_200),\n }]);\n match &stamped[0].content[0] {\n ContentBlock::ToolResult { content, .. } => {\n assert_eq!(\n content,\n \"[tool timing: start=2025-03-15T02:24:09.800Z finish=2025-03-15T02:24:13.000Z duration=3.2s] ok\"\n );\n }\n other => panic!(\"expected tool result block, got {other:?}\"),\n }\n }\n\n #[test]\n fn with_timestamps_skips_internal_system_reminders() {\n let ts = chrono::DateTime::parse_from_rfc3339(\"2025-03-15T02:24:13Z\")\n .unwrap()\n .with_timezone(&Utc);\n let original = Message {\n role: Role::User,\n content: vec![ContentBlock::Text {\n text: \"<system-reminder>\\ninternal\\n</system-reminder>\".to_string(),\n cache_control: None,\n }],\n timestamp: Some(ts),\n tool_duration_ms: None,\n };\n let stamped = Message::with_timestamps(std::slice::from_ref(&original));\n match &stamped[0].content[0] {\n ContentBlock::Text { text, .. } => {\n assert_eq!(text, \"<system-reminder>\\ninternal\\n</system-reminder>\");\n }\n other => panic!(\"expected text block, got {other:?}\"),\n }\n }\n\n #[test]\n fn ends_with_fresh_user_turn_accepts_plain_user_text() {\n let messages = vec![Message::user(\"hello\")];\n assert!(ends_with_fresh_user_turn(&messages));\n }\n\n #[test]\n fn ends_with_fresh_user_turn_rejects_trailing_tool_result() {\n let messages = vec![\n Message::user(\"hello\"),\n Message {\n role: Role::Assistant,\n content: vec![ContentBlock::ToolUse {\n id: \"call_1\".to_string(),\n name: \"bash\".to_string(),\n input: serde_json::json!({}),\n }],\n timestamp: Some(Utc::now()),\n tool_duration_ms: None,\n },\n Message::tool_result(\"call_1\", \"ok\", false),\n ];\n assert!(!ends_with_fresh_user_turn(&messages));\n }\n\n #[test]\n fn ends_with_fresh_user_turn_skips_internal_system_reminders() {\n let messages = vec![\n Message::user(\"hello\"),\n Message::user(\"<system-reminder>\\ninternal\\n</system-reminder>\"),\n ];\n assert!(ends_with_fresh_user_turn(&messages));\n }\n\n #[test]\n fn ends_with_fresh_user_turn_rejects_assistant_tail() {\n let messages = vec![\n Message::user(\"hello\"),\n Message::assistant_text(\"working on it\"),\n ];\n assert!(!ends_with_fresh_user_turn(&messages));\n }\n\n #[test]\n fn format_background_task_notification_markdown_uses_code_block_preview() {\n let rendered = format_background_task_notification_markdown(&BackgroundTaskCompleted {\n task_id: \"abc123\".to_string(),\n tool_name: \"bash\".to_string(),\n session_id: \"session\".to_string(),\n status: BackgroundTaskStatus::Completed,\n exit_code: Some(0),\n output_preview: \"[stderr] first line\\n[stdout] second line\\n\".to_string(),\n output_file: std::path::PathBuf::from(\"/tmp/output.log\"),\n duration_secs: 7.1,\n notify: true,\n wake: false,\n });\n\n assert!(\n rendered\n .contains(\"**Background task** `abc123` · `bash` · ✓ completed · 7.1s · exit 0\")\n );\n assert!(rendered.contains(\"```text\\n[stderr] first line\\n[stdout] second line\\n```\"));\n assert!(rendered.contains(\"_Full output:_ `bg action=\\\"output\\\" task_id=\\\"abc123\\\"`\"));\n }\n\n #[test]\n fn format_background_task_notification_markdown_handles_empty_preview() {\n let rendered = format_background_task_notification_markdown(&BackgroundTaskCompleted {\n task_id: \"abc123\".to_string(),\n tool_name: \"bash\".to_string(),\n session_id: \"session\".to_string(),\n status: BackgroundTaskStatus::Failed,\n exit_code: Some(9),\n output_preview: \"\\n\\n\".to_string(),\n output_file: std::path::PathBuf::from(\"/tmp/output.log\"),\n duration_secs: 1.0,\n notify: true,\n wake: false,\n });\n\n assert!(rendered.contains(\"✗ failed\"));\n assert!(rendered.contains(\"_No output captured._\"));\n }\n\n #[test]\n fn format_background_task_notification_markdown_renders_superseded_status() {\n let rendered = format_background_task_notification_markdown(&BackgroundTaskCompleted {\n task_id: \"abc123\".to_string(),\n tool_name: \"selfdev-build\".to_string(),\n session_id: \"session\".to_string(),\n status: BackgroundTaskStatus::Superseded,\n exit_code: Some(0),\n output_preview: \"Build completed, but source changed before activation\".to_string(),\n output_file: std::path::PathBuf::from(\"/tmp/output.log\"),\n duration_secs: 5.0,\n notify: true,\n wake: false,\n });\n\n assert!(rendered.contains(\"↻ superseded\"));\n assert!(rendered.contains(\"exit 0\"));\n assert!(rendered.contains(\"source changed before activation\"));\n }\n\n #[test]\n fn description_token_estimate_uses_chars_per_token_heuristic() {\n let def = ToolDefinition {\n name: \"read\".to_string(),\n description: \"abcdwxyz\".to_string(),\n input_schema: serde_json::json!({\"type\": \"object\"}),\n };\n\n assert_eq!(def.description_token_estimate(), 2);\n }\n\n #[test]\n fn parse_background_task_notification_markdown_extracts_fields() {\n let rendered = format_background_task_notification_markdown(&BackgroundTaskCompleted {\n task_id: \"abc123\".to_string(),\n tool_name: \"bash\".to_string(),\n session_id: \"session\".to_string(),\n status: BackgroundTaskStatus::Completed,\n exit_code: Some(0),\n output_preview: \"[stderr] first line\\n[stdout] second line\\n\".to_string(),\n output_file: std::path::PathBuf::from(\"/tmp/output.log\"),\n duration_secs: 7.1,\n notify: true,\n wake: false,\n });\n\n let parsed = parse_background_task_notification_markdown(&rendered)\n .expect(\"background task notification should parse\");\n assert_eq!(parsed.task_id, \"abc123\");\n assert_eq!(parsed.tool_name, \"bash\");\n assert_eq!(parsed.status, \"✓ completed\");\n assert_eq!(parsed.duration, \"7.1s\");\n assert_eq!(parsed.exit_label, \"exit 0\");\n assert_eq!(\n parsed.preview.as_deref(),\n Some(\"[stderr] first line\\n[stdout] second line\")\n );\n assert_eq!(\n parsed.full_output_command,\n \"bg action=\\\"output\\\" task_id=\\\"abc123\\\"\"\n );\n }\n}\n"}, "c0fafa75e20ac41197c66a22667261ad236b0759": {"t": 1785757234.910225, "n": 41788, "h": "b9a7510db283c289", "c": "use crate::bus::{BackgroundTaskCompleted, BackgroundTaskStatus};\nuse chrono::Utc;\nuse regex::Regex;\nuse serde::{Deserialize, Serialize};\nuse std::collections::HashSet;\nuse std::sync::OnceLock;\n\nfn compile_static_regex(pattern: &str) -> Option<Regex> {\n match Regex::new(pattern) {\n Ok(regex) => Some(regex),\n Err(err) => {\n eprintln!(\"jcode: failed to compile static regex: {err}\");\n None\n }\n }\n}\n\nfn compile_static_regexes(patterns: &[&str]) -> Vec<Regex> {\n patterns\n .iter()\n .filter_map(|pattern| compile_static_regex(pattern))\n .collect()\n}\n\n#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]\npub struct InputShellResult {\n pub command: String,\n #[serde(default, skip_serializing_if = \"Option::is_none\")]\n pub cwd: Option<String>,\n pub output: String,\n #[serde(default, skip_serializing_if = \"Option::is_none\")]\n pub exit_code: Option<i32>,\n pub duration_ms: u64,\n #[serde(default)]\n pub truncated: bool,\n #[serde(default)]\n pub failed_to_start: bool,\n}\n\nfn sanitize_fenced_block(text: &str) -> String {\n text.replace(\"```\", \"``\\u{200b}`\")\n}\n\npub fn format_input_shell_result_markdown(shell: &InputShellResult) -> String {\n let status = if shell.failed_to_start {\n \"✗ failed to start\".to_string()\n } else if shell.exit_code == Some(0) {\n \"✓ exit 0\".to_string()\n } else if let Some(code) = shell.exit_code {\n format!(\"✗ exit {}\", code)\n } else {\n \"✗ terminated\".to_string()\n };\n\n let mut meta = vec![status, Message::format_duration(shell.duration_ms)];\n if let Some(cwd) = shell.cwd.as_deref() {\n meta.push(format!(\"cwd `{}`\", cwd));\n }\n if shell.truncated {\n meta.push(\"truncated\".to_string());\n }\n\n let mut message = format!(\n \"**Shell command** · {}\\n\\n```bash\\n{}\\n```\",\n meta.join(\" · \"),\n sanitize_fenced_block(&shell.command)\n );\n\n if shell.output.trim().is_empty() {\n message.push_str(\"\\n\\n_No output._\");\n } else {\n message.push_str(&format!(\n \"\\n\\n```text\\n{}\\n```\",\n sanitize_fenced_block(shell.output.trim_end())\n ));\n }\n\n message\n}\n\npub fn input_shell_status_notice(shell: &InputShellResult) -> String {\n if shell.failed_to_start {\n \"Shell command failed to start\".to_string()\n } else if shell.exit_code == Some(0) {\n \"Shell command completed\".to_string()\n } else if let Some(code) = shell.exit_code {\n format!(\"Shell command failed (exit {})\", code)\n } else {\n \"Shell command terminated\".to_string()\n }\n}\n\nfn format_background_task_status(status: &BackgroundTaskStatus) -> &'static str {\n match status {\n BackgroundTaskStatus::Completed => \"✓ completed\",\n BackgroundTaskStatus::Superseded => \"↻ superseded\",\n BackgroundTaskStatus::Failed => \"✗ failed\",\n BackgroundTaskStatus::Running => \"running\",\n }\n}\n\nfn normalize_background_task_preview(preview: &str) -> Option<String> {\n let normalized = preview.replace(\"\\r\\n\", \"\\n\").replace('\\r', \"\\n\");\n let trimmed = normalized.trim_end();\n if trimmed.trim().is_empty() {\n None\n } else {\n Some(sanitize_fenced_block(trimmed))\n }\n}\n\npub fn format_background_task_notification_markdown(task: &BackgroundTaskCompleted) -> String {\n let exit_code = task\n .exit_code\n .map(|code| format!(\"exit {}\", code))\n .unwrap_or_else(|| \"exit n/a\".to_string());\n\n let mut message = format!(\n \"**Background task** `{}` · `{}` · {} · {:.1}s · {}\",\n task.task_id,\n task.tool_name,\n format_background_task_status(&task.status),\n task.duration_secs,\n exit_code,\n );\n\n if let Some(preview) = normalize_background_task_preview(&task.output_preview) {\n message.push_str(&format!(\"\\n\\n```text\\n{}\\n```\", preview));\n } else {\n message.push_str(\"\\n\\n_No output captured._\");\n }\n\n message.push_str(&format!(\n \"\\n\\n_Full output:_ `bg action=\\\"output\\\" task_id=\\\"{}\\\"`\",\n task.task_id\n ));\n\n message\n}\n\n#[derive(Debug, Clone, PartialEq, Eq)]\npub struct ParsedBackgroundTaskNotification {\n pub task_id: String,\n pub tool_name: String,\n pub status: String,\n pub duration: String,\n pub exit_label: String,\n pub preview: Option<String>,\n pub full_output_command: String,\n}\n\npub fn parse_background_task_notification_markdown(\n content: &str,\n) -> Option<ParsedBackgroundTaskNotification> {\n static HEADER_RE: OnceLock<Option<Regex>> = OnceLock::new();\n static FULL_OUTPUT_RE: OnceLock<Option<Regex>> = OnceLock::new();\n\n let header_re = HEADER_RE\n .get_or_init(|| {\n compile_static_regex(\n r\"^\\*\\*Background task\\*\\* `(?P<task_id>[^`]+)` · `(?P<tool_name>[^`]+)` · (?P<status>.+?) · (?P<duration>[0-9]+(?:\\.[0-9]+)?s) · (?P<exit_label>.+)$\",\n )\n })\n .as_ref()?;\n let full_output_re = FULL_OUTPUT_RE\n .get_or_init(|| compile_static_regex(r#\"^_Full output:_ `(?P<command>[^`]+)`$\"#))\n .as_ref()?;\n\n let normalized = content.replace(\"\\r\\n\", \"\\n\").replace('\\r', \"\\n\");\n let mut sections = normalized.split(\"\\n\\n\");\n let header = sections.next()?.trim();\n let captures = header_re.captures(header)?;\n\n let mut preview: Option<String> = None;\n let mut full_output_command: Option<String> = None;\n\n for section in sections {\n let trimmed = section.trim();\n if trimmed.is_empty() {\n continue;\n }\n\n if let Some(captures) = full_output_re.captures(trimmed) {\n full_output_command = Some(captures[\"command\"].to_string());\n continue;\n }\n\n if trimmed == \"_No output captured._\" {\n preview = None;\n continue;\n }\n\n if let Some(fenced) = trimmed\n .strip_prefix(\"```text\\n\")\n .and_then(|body| body.strip_suffix(\"\\n```\"))\n {\n preview = Some(fenced.to_string());\n }\n }\n\n Some(ParsedBackgroundTaskNotification {\n task_id: captures[\"task_id\"].to_string(),\n tool_name: captures[\"tool_name\"].to_string(),\n status: captures[\"status\"].to_string(),\n duration: captures[\"duration\"].to_string(),\n exit_label: captures[\"exit_label\"].to_string(),\n preview,\n full_output_command: full_output_command?,\n })\n}\n\npub fn background_task_status_notice(task: &BackgroundTaskCompleted) -> String {\n match task.status {\n BackgroundTaskStatus::Completed => {\n format!(\"Background task completed · {}\", task.tool_name)\n }\n BackgroundTaskStatus::Superseded => {\n format!(\"Background task superseded · {}\", task.tool_name)\n }\n BackgroundTaskStatus::Failed => match task.exit_code {\n Some(code) => format!(\n \"Background task failed · {} · exit {}\",\n task.tool_name, code\n ),\n None => format!(\"Background task failed · {}\", task.tool_name),\n },\n BackgroundTaskStatus::Running => format!(\"Background task running · {}\", task.tool_name),\n }\n}\n\n/// Role in conversation\n#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]\n#[serde(rename_all = \"lowercase\")]\npub enum Role {\n User,\n Assistant,\n}\n\n/// Plain-text tool output placeholder when execution was interrupted.\npub const TOOL_OUTPUT_MISSING_TEXT: &str =\n \"Tool output missing (session interrupted before tool execution completed)\";\n\n/// A message in the conversation\n#[derive(Debug, Clone, Serialize, Deserialize)]\npub struct Message {\n pub role: Role,\n pub content: Vec<ContentBlock>,\n #[serde(default, skip_serializing_if = \"Option::is_none\")]\n pub timestamp: Option<chrono::DateTime<Utc>>,\n #[serde(default, skip_serializing_if = \"Option::is_none\")]\n pub tool_duration_ms: Option<u64>,\n}\n\n/// Cache control metadata for prompt caching\n#[derive(Debug, Clone, Serialize, Deserialize)]\npub struct CacheControl {\n #[serde(rename = \"type\")]\n pub kind: String,\n #[serde(skip_serializing_if = \"Option::is_none\")]\n pub ttl: Option<String>,\n}\n\nimpl CacheControl {\n pub fn ephemeral(ttl: Option<String>) -> Self {\n Self {\n kind: \"ephemeral\".to_string(),\n ttl,\n }\n }\n}\n\n/// Content block within a message\n#[derive(Debug, Clone, Serialize, Deserialize)]\n#[serde(tag = \"type\", rename_all = \"snake_case\")]\npub enum ContentBlock {\n Text {\n text: String,\n #[serde(skip_serializing_if = \"Option::is_none\")]\n cache_control: Option<CacheControl>,\n },\n /// Hidden reasoning content used for providers that require it (not displayed)\n Reasoning {\n text: String,\n },\n ToolUse {\n id: String,\n name: String,\n input: serde_json::Value,\n },\n ToolResult {\n tool_use_id: String,\n content: String,\n #[serde(skip_serializing_if = \"Option::is_none\")]\n is_error: Option<bool>,\n },\n Image {\n media_type: String,\n data: String,\n },\n /// Hidden OpenAI Responses compaction item used to preserve native\n /// compaction state across turns/saves when jcode explicitly triggers it.\n OpenAICompaction {\n encrypted_content: String,\n },\n}\n\nimpl Message {\n pub fn user(text: &str) -> Self {\n Self {\n role: Role::User,\n content: vec![ContentBlock::Text {\n text: text.to_string(),\n cache_control: None,\n }],\n timestamp: Some(Utc::now()),\n tool_duration_ms: None,\n }\n }\n\n pub fn user_with_images(text: &str, images: Vec<(String, String)>) -> Self {\n let mut content: Vec<ContentBlock> = images\n .into_iter()\n .map(|(media_type, data)| ContentBlock::Image { media_type, data })\n .collect();\n content.push(ContentBlock::Text {\n text: text.to_string(),\n cache_control: None,\n });\n Self {\n role: Role::User,\n content,\n timestamp: Some(Utc::now()),\n tool_duration_ms: None,\n }\n }\n\n pub fn assistant_text(text: &str) -> Self {\n Self {\n role: Role::Assistant,\n content: vec![ContentBlock::Text {\n text: text.to_string(),\n cache_control: None,\n }],\n timestamp: Some(Utc::now()),\n tool_duration_ms: None,\n }\n }\n\n pub fn tool_result(tool_use_id: &str, content: &str, is_error: bool) -> Self {\n Self::tool_result_with_duration(tool_use_id, content, is_error, None)\n }\n\n pub fn tool_result_with_duration(\n tool_use_id: &str,\n content: &str,\n is_error: bool,\n tool_duration_ms: Option<u64>,\n ) -> Self {\n Self {\n role: Role::User,\n content: vec![ContentBlock::ToolResult {\n tool_use_id: tool_use_id.to_string(),\n content: content.to_string(),\n is_error: if is_error { Some(true) } else { None },\n }],\n timestamp: Some(Utc::now()),\n tool_duration_ms,\n }\n }\n\n /// Format a timestamp deterministically in UTC for injection into model-visible content.\n pub fn format_timestamp(ts: &chrono::DateTime<Utc>) -> String {\n ts.to_rfc3339_opts(chrono::SecondsFormat::Millis, true)\n }\n\n pub fn format_duration(duration_ms: u64) -> String {\n match duration_ms {\n 0..=999 => format!(\"{}ms\", duration_ms),\n 1_000..=9_999 => format!(\"{:.1}s\", duration_ms as f64 / 1000.0),\n 10_000..=59_999 => format!(\"{}s\", duration_ms / 1000),\n _ => {\n let total_seconds = duration_ms / 1000;\n let minutes = total_seconds / 60;\n let seconds = total_seconds % 60;\n if seconds == 0 {\n format!(\"{}m\", minutes)\n } else {\n format!(\"{}m {}s\", minutes, seconds)\n }\n }\n }\n }\n\n pub fn is_internal_system_reminder(&self) -> bool {\n self.content\n .iter()\n .find_map(|block| match block {\n ContentBlock::Text { text, .. } => Some(text.trim_start()),\n _ => None,\n })\n .is_some_and(|text| text.starts_with(\"<system-reminder>\"))\n }\n\n fn should_skip_timestamp_injection(&self) -> bool {\n self.is_internal_system_reminder()\n }\n\n fn tool_result_tag(&self, ts: &chrono::DateTime<Utc>) -> String {\n match self.tool_duration_ms {\n Some(duration_ms) => {\n let duration_ms_i64 = i64::try_from(duration_ms).unwrap_or(i64::MAX);\n let start_ts = ts\n .checked_sub_signed(chrono::Duration::milliseconds(duration_ms_i64))\n .unwrap_or(*ts);\n format!(\n \"[tool timing: start={} finish={} duration={}]\",\n Self::format_timestamp(&start_ts),\n Self::format_timestamp(ts),\n Self::format_duration(duration_ms)\n )\n }\n None => format!(\"[{}]\", Self::format_timestamp(ts)),\n }\n }\n\n /// Return a copy of messages with timestamps injected into user-role text content.\n /// Tool results get a stable UTC timing header prepended to content.\n /// User text messages get a stable UTC timestamp prepended to the first text block.\n pub fn with_timestamps(messages: &[Message]) -> Vec<Message> {\n messages\n .iter()\n .map(|msg| {\n let Some(ts) = msg.timestamp else {\n return msg.clone();\n };\n if msg.role != Role::User || msg.should_skip_timestamp_injection() {\n return msg.clone();\n }\n let text_tag = format!(\"[{}]\", Self::format_timestamp(&ts));\n let tool_result_tag = msg.tool_result_tag(&ts);\n let mut msg = msg.clone();\n let mut tagged = false;\n for block in &mut msg.content {\n match block {\n ContentBlock::Text { text, .. } if !tagged => {\n *text = format!(\"{} {}\", text_tag, text);\n tagged = true;\n }\n ContentBlock::ToolResult { content, .. } if !tagged => {\n *content = format!(\"{} {}\", tool_result_tag, content);\n tagged = true;\n }\n _ => {}\n }\n }\n msg\n })\n .collect()\n }\n}\n\nconst STABLE_HASH_SEED: u64 = 0xcbf29ce484222325;\nconst STABLE_HASH_PRIME: u64 = 0x100000001b3;\n\nfn stable_hash_bytes(bytes: &[u8]) -> u64 {\n let mut hash = STABLE_HASH_SEED;\n for byte in bytes {\n hash ^= *byte as u64;\n hash = hash.wrapping_mul(STABLE_HASH_PRIME);\n }\n hash\n}\n\npub(crate) fn extend_stable_hash(acc: u64, next: u64) -> u64 {\n stable_hash_bytes(&[acc.to_le_bytes().as_slice(), next.to_le_bytes().as_slice()].concat())\n}\n\npub(crate) fn stable_message_hash(message: &Message) -> u64 {\n match serde_json::to_vec(message) {\n Ok(bytes) => stable_hash_bytes(&bytes),\n Err(_) => stable_hash_bytes(format!(\"{:?}\", message).as_bytes()),\n }\n}\n\npub fn ends_with_fresh_user_turn(messages: &[Message]) -> bool {\n for msg in messages.iter().rev() {\n if msg.role != Role::User {\n return false;\n }\n\n if msg\n .content\n .iter()\n .any(|block| matches!(block, ContentBlock::ToolResult { .. }))\n {\n return false;\n }\n\n if msg.content.is_empty() {\n return false;\n }\n\n let mut saw_user_text = false;\n for block in &msg.content {\n match block {\n ContentBlock::Text { text, .. } => {\n let trimmed = text.trim();\n if !trimmed.is_empty() && !trimmed.starts_with(\"<system-reminder>\") {\n saw_user_text = true;\n }\n }\n _ => return false,\n }\n }\n\n if saw_user_text {\n return true;\n }\n\n if msg.is_internal_system_reminder() {\n continue;\n }\n\n return false;\n }\n\n false\n}\n\n/// Sanitize a tool ID so it matches the pattern `^[a-zA-Z0-9_-]+$`.\n///\n/// Different providers generate tool IDs in different formats. When switching\n/// from one provider to another mid-conversation, the historical tool IDs may\n/// contain characters that the new provider rejects (e.g., dots in Copilot IDs\n/// sent to Anthropic). This function replaces any invalid characters with\n/// underscores.\npub fn sanitize_tool_id(id: &str) -> String {\n if id.is_empty() {\n return \"unknown\".to_string();\n }\n let sanitized: String = id\n .chars()\n .map(|c| {\n if c.is_ascii_alphanumeric() || c == '_' || c == '-' {\n c\n } else {\n '_'\n }\n })\n .collect();\n if sanitized.is_empty() {\n \"unknown\".to_string()\n } else {\n sanitized\n }\n}\n\n/// Redact likely secrets from persisted tool output.\n///\n/// This is a best-effort safeguard for local session history files. It targets\n/// high-confidence token/key patterns and common `KEY=VALUE` assignments used by\n/// auth flows.\npub fn redact_secrets(text: &str) -> String {\n // Fast path to avoid regex work for most tool outputs.\n let lower = text.to_ascii_lowercase();\n\n if !text.contains(\"sk-\")\n && !text.contains(\"ghp_\")\n && !text.contains(\"github_pat_\")\n && !text.contains(\"AIza\")\n && !text.contains(\"ya29.\")\n && !text.contains(\"xox\")\n && !lower.contains(\"api_key\")\n && !lower.contains(\"token\")\n {\n return text.to_string();\n }\n\n static DIRECT_PATTERNS: OnceLock<Vec<Regex>> = OnceLock::new();\n static ASSIGNMENT_PATTERNS: OnceLock<Vec<Regex>> = OnceLock::new();\n\n let direct_patterns = DIRECT_PATTERNS.get_or_init(|| {\n compile_static_regexes(&[\n r\"sk-ant-(?:oat|ort)01-[A-Za-z0-9_-]{20,}\",\n r\"sk-or-v1-[A-Za-z0-9_-]{20,}\",\n r\"ghp_[A-Za-z0-9]{20,}\",\n r\"github_pat_[A-Za-z0-9_]{20,}\",\n r\"ya29\\.[A-Za-z0-9._-]{20,}\",\n r\"AIza[0-9A-Za-z_-]{20,}\",\n r\"xox[baprs]-[A-Za-z0-9-]{10,}\",\n ])\n });\n\n let assignment_patterns = ASSIGNMENT_PATTERNS.get_or_init(|| {\n compile_static_regexes(&[\n r\"(?m)^\\s*(OPENROUTER_API_KEY\\s*=\\s*)[^\\r\\n]+\",\n r\"(?m)^\\s*(OPENCODE_API_KEY\\s*=\\s*)[^\\r\\n]+\",\n r\"(?m)^\\s*(OPENCODE_GO_API_KEY\\s*=\\s*)[^\\r\\n]+\",\n r\"(?m)^\\s*(ZHIPU_API_KEY\\s*=\\s*)[^\\r\\n]+\",\n r\"(?m)^\\s*(ZAI_API_KEY\\s*=\\s*)[^\\r\\n]+\",\n r\"(?m)^\\s*(302AI_API_KEY\\s*=\\s*)[^\\r\\n]+\",\n r\"(?m)^\\s*(BASETEN_API_KEY\\s*=\\s*)[^\\r\\n]+\",\n r\"(?m)^\\s*(CORTECS_API_KEY\\s*=\\s*)[^\\r\\n]+\",\n r\"(?m)^\\s*(DEEPSEEK_API_KEY\\s*=\\s*)[^\\r\\n]+\",\n r\"(?m)^\\s*(FIRMWARE_API_KEY\\s*=\\s*)[^\\r\\n]+\",\n r\"(?m)^\\s*(HF_TOKEN\\s*=\\s*)[^\\r\\n]+\",\n r\"(?m)^\\s*(MOONSHOT_API_KEY\\s*=\\s*)[^\\r\\n]+\",\n r\"(?m)^\\s*(NEBIUS_API_KEY\\s*=\\s*)[^\\r\\n]+\",\n r\"(?m)^\\s*(SCALEWAY_API_KEY\\s*=\\s*)[^\\r\\n]+\",\n r\"(?m)^\\s*(STACKIT_API_KEY\\s*=\\s*)[^\\r\\n]+\",\n r\"(?m)^\\s*(GROQ_API_KEY\\s*=\\s*)[^\\r\\n]+\",\n r\"(?m)^\\s*(MISTRAL_API_KEY\\s*=\\s*)[^\\r\\n]+\",\n r\"(?m)^\\s*(PERPLEXITY_API_KEY\\s*=\\s*)[^\\r\\n]+\",\n r\"(?m)^\\s*(TOGETHER_API_KEY\\s*=\\s*)[^\\r\\n]+\",\n r\"(?m)^\\s*(DEEPINFRA_API_KEY\\s*=\\s*)[^\\r\\n]+\",\n r\"(?m)^\\s*(XAI_API_KEY\\s*=\\s*)[^\\r\\n]+\",\n r\"(?m)^\\s*(LMSTUDIO_API_KEY\\s*=\\s*)[^\\r\\n]+\",\n r\"(?m)^\\s*(OLLAMA_API_KEY\\s*=\\s*)[^\\r\\n]+\",\n r\"(?m)^\\s*(CHUTES_API_KEY\\s*=\\s*)[^\\r\\n]+\",\n r\"(?m)^\\s*(CEREBRAS_API_KEY\\s*=\\s*)[^\\r\\n]+\",\n r\"(?m)^\\s*(OPENAI_COMPAT_API_KEY\\s*=\\s*)[^\\r\\n]+\",\n r\"(?m)^\\s*(ANTHROPIC_API_KEY\\s*=\\s*)[^\\r\\n]+\",\n r\"(?m)^\\s*(OPENAI_API_KEY\\s*=\\s*)[^\\r\\n]+\",\n r\"(?m)^\\s*(AZURE_OPENAI_API_KEY\\s*=\\s*)[^\\r\\n]+\",\n r\"(?m)^\\s*(CURSOR_API_KEY\\s*=\\s*)[^\\r\\n]+\",\n r\"(?m)^\\s*(GITHUB_TOKEN\\s*=\\s*)[^\\r\\n]+\",\n ])\n });\n\n let mut redacted = text.to_string();\n let mut redacted_keys: HashSet<String> = [\n \"OPENROUTER_API_KEY\",\n \"OPENCODE_API_KEY\",\n \"OPENCODE_GO_API_KEY\",\n \"ZHIPU_API_KEY\",\n \"ZAI_API_KEY\",\n \"302AI_API_KEY\",\n \"BASETEN_API_KEY\",\n \"CORTECS_API_KEY\",\n \"DEEPSEEK_API_KEY\",\n \"FIRMWARE_API_KEY\",\n \"HF_TOKEN\",\n \"MOONSHOT_API_KEY\",\n \"NEBIUS_API_KEY\",\n \"SCALEWAY_API_KEY\",\n \"STACKIT_API_KEY\",\n \"GROQ_API_KEY\",\n \"MISTRAL_API_KEY\",\n \"PERPLEXITY_API_KEY\",\n \"TOGETHER_API_KEY\",\n \"DEEPINFRA_API_KEY\",\n \"XAI_API_KEY\",\n \"LMSTUDIO_API_KEY\",\n \"OLLAMA_API_KEY\",\n \"CHUTES_API_KEY\",\n \"CEREBRAS_API_KEY\",\n \"OPENAI_COMPAT_API_KEY\",\n \"ANTHROPIC_API_KEY\",\n \"OPENAI_API_KEY\",\n \"AZURE_OPENAI_API_KEY\",\n \"CURSOR_API_KEY\",\n \"GITHUB_TOKEN\",\n ]\n .iter()\n .map(|k| (*k).to_string())\n .collect();\n\n for re in direct_patterns {\n redacted = re.replace_all(&redacted, \"[REDACTED_SECRET]\").into_owned();\n }\n\n for re in assignment_patterns {\n redacted = re\n .replace_all(&redacted, \"${1}[REDACTED_SECRET]\")\n .into_owned();\n }\n\n // Also redact custom API key variable names configured at runtime.\n for source in [\n \"JCODE_OPENROUTER_API_KEY_NAME\",\n \"JCODE_OPENAI_COMPAT_API_KEY_NAME\",\n ] {\n let Some(key_name) = std::env::var(source)\n .ok()\n .map(|v| v.trim().to_string())\n .filter(|v| !v.is_empty())\n else {\n continue;\n };\n\n if !key_name\n .chars()\n .all(|c| c.is_ascii_uppercase() || c.is_ascii_digit() || c == '_')\n {\n continue;\n }\n if !redacted_keys.insert(key_name.clone()) {\n continue;\n }\n\n let pattern = format!(r\"(?m)^\\s*({}\\s*=\\s*)[^\\r\\n]+\", regex::escape(&key_name));\n if let Ok(re) = Regex::new(&pattern) {\n redacted = re\n .replace_all(&redacted, \"${1}[REDACTED_SECRET]\")\n .into_owned();\n }\n }\n\n redacted\n}\n\n/// Tool definition for the API\n#[derive(Debug, Clone, Serialize)]\npub struct ToolDefinition {\n pub name: String,\n // Prompt-visible text sent to the model by provider adapters.\n // Approximate prompt cost: description.len() / 4. Use\n // ToolDefinition::description_token_estimate() when reviewing tool bloat.\n pub description: String,\n pub input_schema: serde_json::Value,\n}\n\nimpl ToolDefinition {\n /// Serialized size of the full tool definition payload sent to providers.\n pub fn prompt_chars(&self) -> usize {\n serde_json::json!({\n \"name\": self.name,\n \"description\": self.description,\n \"input_schema\": self.input_schema,\n })\n .to_string()\n .len()\n }\n\n /// Approximate prompt-token cost of this tool's top-level description.\n ///\n /// This uses jcode's standard chars/4 heuristic, matching other token\n /// budget estimates in the codebase.\n pub fn description_token_estimate(&self) -> usize {\n crate::util::estimate_tokens(&self.description)\n }\n\n /// Approximate prompt-token cost of the full tool definition payload.\n pub fn prompt_token_estimate(&self) -> usize {\n crate::util::estimate_tokens(\n &serde_json::json!({\n \"name\": self.name,\n \"description\": self.description,\n \"input_schema\": self.input_schema,\n })\n .to_string(),\n )\n }\n\n pub fn aggregate_prompt_chars(defs: &[ToolDefinition]) -> usize {\n defs.iter().map(Self::prompt_chars).sum()\n }\n\n pub fn aggregate_prompt_token_estimate(defs: &[ToolDefinition]) -> usize {\n defs.iter().map(Self::prompt_token_estimate).sum()\n }\n}\n\n/// A tool call from the model\n#[derive(Debug, Clone, Default, PartialEq, Eq, serde::Serialize, serde::Deserialize)]\npub struct ToolCall {\n #[serde(default)]\n pub id: String,\n #[serde(default)]\n pub name: String,\n #[serde(default)]\n pub input: serde_json::Value,\n #[serde(default, skip_serializing_if = \"Option::is_none\")]\n pub intent: Option<String>,\n}\n\n/// Connection phase for status bar transparency\n#[derive(Debug, Clone, PartialEq)]\npub enum ConnectionPhase {\n /// Refreshing OAuth token\n Authenticating,\n /// TCP + TLS connection to API\n Connecting,\n /// HTTP request sent, waiting for first response byte\n WaitingForResponse,\n /// First byte received, stream is active\n Streaming,\n /// Retrying after a transient error\n Retrying { attempt: u32, max: u32 },\n}\n\nimpl std::fmt::Display for ConnectionPhase {\n fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {\n match self {\n ConnectionPhase::Authenticating => write!(f, \"authenticating\"),\n ConnectionPhase::Connecting => write!(f, \"connecting\"),\n ConnectionPhase::WaitingForResponse => write!(f, \"waiting for response\"),\n ConnectionPhase::Streaming => write!(f, \"streaming\"),\n ConnectionPhase::Retrying { attempt, max } => {\n write!(f, \"retrying ({}/{})\", attempt, max)\n }\n }\n }\n}\n\n/// Streaming event from provider\n#[derive(Debug, Clone)]\npub enum StreamEvent {\n /// Text content delta\n TextDelta(String),\n /// Tool use started\n ToolUseStart { id: String, name: String },\n /// Tool input delta (JSON fragment)\n ToolInputDelta(String),\n /// Tool use complete\n ToolUseEnd,\n /// Tool result from provider (provider already executed the tool)\n ToolResult {\n tool_use_id: String,\n content: String,\n is_error: bool,\n },\n /// Extended thinking started\n ThinkingStart,\n /// Extended thinking delta (reasoning content)\n ThinkingDelta(String),\n /// Extended thinking ended\n ThinkingEnd,\n /// Extended thinking completed with duration\n ThinkingDone { duration_secs: f64 },\n /// Message complete (may have stop reason)\n MessageEnd { stop_reason: Option<String> },\n /// Token usage update\n TokenUsage {\n input_tokens: Option<u64>,\n output_tokens: Option<u64>,\n cache_read_input_tokens: Option<u64>,\n cache_creation_input_tokens: Option<u64>,\n },\n /// Active transport/connection type for this stream\n ConnectionType { connection: String },\n /// Connection phase update (for status bar transparency)\n ConnectionPhase { phase: ConnectionPhase },\n /// Provider-supplied human-readable transport detail for the status line\n StatusDetail { detail: String },\n /// Error occurred\n Error {\n message: String,\n /// Seconds until rate limit resets (if this is a rate limit error)\n retry_after_secs: Option<u64>,\n },\n /// Provider session ID (for conversation resume)\n SessionId(String),\n /// Compaction occurred (context was summarized)\n Compaction {\n trigger: String,\n pre_tokens: Option<u64>,\n /// Provider-native compaction artifact, if one was emitted.\n openai_encrypted_content: Option<String>,\n },\n /// Upstream provider info (e.g., which provider OpenRouter routed to)\n UpstreamProvider { provider: String },\n /// Native tool call from a provider bridge that needs execution by jcode\n NativeToolCall {\n request_id: String,\n tool_name: String,\n input: serde_json::Value,\n },\n}\n\n#[cfg(test)]\nmod tests {\n use super::*;\n\n #[test]\n fn sanitize_tool_id_alphanumeric_passthrough() {\n assert_eq!(\n sanitize_tool_id(\"toolu_01XFDUDYJgAACzvnptvVer6u\"),\n \"toolu_01XFDUDYJgAACzvnptvVer6u\"\n );\n assert_eq!(sanitize_tool_id(\"call_abc123\"), \"call_abc123\");\n assert_eq!(\n sanitize_tool_id(\"call_1234567890_9876543210\"),\n \"call_1234567890_9876543210\"\n );\n }\n\n #[test]\n fn sanitize_tool_id_hyphens_passthrough() {\n assert_eq!(sanitize_tool_id(\"call-abc-123\"), \"call-abc-123\");\n assert_eq!(\n sanitize_tool_id(\"tool_use-id_with-mixed\"),\n \"tool_use-id_with-mixed\"\n );\n }\n\n #[test]\n fn sanitize_tool_id_replaces_dots() {\n assert_eq!(\n sanitize_tool_id(\"chatcmpl-abc.def.ghi\"),\n \"chatcmpl-abc_def_ghi\"\n );\n assert_eq!(sanitize_tool_id(\"call.123\"), \"call_123\");\n }\n\n #[test]\n fn sanitize_tool_id_replaces_colons() {\n assert_eq!(sanitize_tool_id(\"call:123:456\"), \"call_123_456\");\n }\n\n #[test]\n fn sanitize_tool_id_replaces_special_chars() {\n assert_eq!(\n sanitize_tool_id(\"id@with#special$chars\"),\n \"id_with_special_chars\"\n );\n assert_eq!(sanitize_tool_id(\"id with spaces\"), \"id_with_spaces\");\n }\n\n #[test]\n fn sanitize_tool_id_empty_returns_unknown() {\n assert_eq!(sanitize_tool_id(\"\"), \"unknown\");\n }\n\n #[test]\n fn sanitize_tool_id_copilot_to_anthropic() {\n assert_eq!(\n sanitize_tool_id(\"chatcmpl-BF2xX.tool_call.0\"),\n \"chatcmpl-BF2xX_tool_call_0\"\n );\n }\n\n #[test]\n fn sanitize_tool_id_already_valid_unchanged() {\n let valid_ids = [\n \"toolu_01XFDUDYJgAACzvnptvVer6u\",\n \"call_abc123\",\n \"fallback_text_call_call_1234567890_9876543210\",\n \"tool_123\",\n \"a\",\n \"A\",\n \"0\",\n \"_\",\n \"-\",\n \"a-b_c\",\n ];\n for id in valid_ids {\n assert_eq!(sanitize_tool_id(id), id, \"ID '{}' should be unchanged\", id);\n }\n }\n\n #[test]\n fn redact_secrets_redacts_known_direct_token_formats() {\n let input = \"access=sk-ant-oat01-ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789\\nopenrouter=sk-or-v1-abcdefghijklmnopqrstuvwxyz0123456789\\ngithub=ghp_ABCDEFGHIJKLMNOPQRSTUVWXYZ0123\\n\";\n let out = redact_secrets(input);\n assert!(!out.contains(\"sk-ant-oat01-\"));\n assert!(!out.contains(\"sk-or-v1-\"));\n assert!(!out.contains(\"ghp_ABCDEFGHIJKLMNOPQRSTUVWXYZ0123\"));\n assert!(out.matches(\"[REDACTED_SECRET]\").count() >= 3);\n }\n\n #[test]\n fn redact_secrets_redacts_env_style_assignments() {\n let input = \"OPENROUTER_API_KEY=sk-or-v1-abc123abc123abc123abc123\\nOPENCODE_API_KEY=oc_test_secret\\nOPENCODE_GO_API_KEY=ocgo_test_secret\\nZAI_API_KEY=zai_secret\\nCHUTES_API_KEY=chutes_secret\\nCEREBRAS_API_KEY=cerebras_secret\\nOPENAI_COMPAT_API_KEY=compat_secret\\nCURSOR_API_KEY='my_cursor_secret_value'\\nOPENAI_API_KEY=sk-test-openai-example\\nAZURE_OPENAI_API_KEY=azure-openai-secret\\n\";\n let out = redact_secrets(input);\n assert!(out.contains(\"OPENROUTER_API_KEY=[REDACTED_SECRET]\"));\n assert!(out.contains(\"OPENCODE_API_KEY=[REDACTED_SECRET]\"));\n assert!(out.contains(\"OPENCODE_GO_API_KEY=[REDACTED_SECRET]\"));\n assert!(out.contains(\"ZAI_API_KEY=[REDACTED_SECRET]\"));\n assert!(out.contains(\"CHUTES_API_KEY=[REDACTED_SECRET]\"));\n assert!(out.contains(\"CEREBRAS_API_KEY=[REDACTED_SECRET]\"));\n assert!(out.contains(\"OPENAI_COMPAT_API_KEY=[REDACTED_SECRET]\"));\n assert!(out.contains(\"CURSOR_API_KEY=[REDACTED_SECRET]\"));\n assert!(out.contains(\"OPENAI_API_KEY=[REDACTED_SECRET]\"));\n assert!(out.contains(\"AZURE_OPENAI_API_KEY=[REDACTED_SECRET]\"));\n assert!(!out.contains(\"my_cursor_secret_value\"));\n }\n\n #[test]\n fn redact_secrets_redacts_runtime_key_assignment() {\n let key_var = \"JCODE_OPENAI_COMPAT_API_KEY_NAME\";\n let prev = std::env::var(key_var).ok();\n crate::env::set_var(key_var, \"GROQ_API_KEY\");\n\n let input = \"GROQ_API_KEY=my_secret_token_value\";\n let out = redact_secrets(input);\n assert_eq!(out, \"GROQ_API_KEY=[REDACTED_SECRET]\");\n\n if let Some(v) = prev {\n crate::env::set_var(key_var, v);\n } else {\n crate::env::remove_var(key_var);\n }\n }\n\n #[test]\n fn redact_secrets_redacts_mixed_case_token_assignments() {\n let input = \"my_token=ya29.ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789\";\n let out = redact_secrets(input);\n assert!(out.contains(\"[REDACTED_SECRET]\"));\n assert!(!out.contains(\"ya29.ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789\"));\n }\n\n #[test]\n fn redact_secrets_leaves_normal_output_unchanged() {\n let input = \"Found 5 files\\nNo auth errors\\nDone.\";\n assert_eq!(redact_secrets(input), input);\n }\n\n #[test]\n fn format_timestamp_is_stable_utc_rfc3339() {\n let ts = chrono::DateTime::parse_from_rfc3339(\"2025-03-15T02:24:13.250Z\")\n .unwrap()\n .with_timezone(&Utc);\n assert_eq!(Message::format_timestamp(&ts), \"2025-03-15T02:24:13.250Z\");\n }\n\n #[test]\n fn with_timestamps_prepends_utc_prefix_to_user_text() {\n let ts = chrono::DateTime::parse_from_rfc3339(\"2025-03-15T02:24:03Z\")\n .unwrap()\n .with_timezone(&Utc);\n let stamped = Message::with_timestamps(&[Message {\n role: Role::User,\n content: vec![ContentBlock::Text {\n text: \"hello\".to_string(),\n cache_control: None,\n }],\n timestamp: Some(ts),\n tool_duration_ms: None,\n }]);\n match &stamped[0].content[0] {\n ContentBlock::Text { text, .. } => {\n assert_eq!(text, \"[2025-03-15T02:24:03.000Z] hello\");\n }\n other => panic!(\"expected text block, got {other:?}\"),\n }\n }\n\n #[test]\n fn with_timestamps_adds_tool_timing_header_with_duration() {\n let ts = chrono::DateTime::parse_from_rfc3339(\"2025-03-15T02:24:13Z\")\n .unwrap()\n .with_timezone(&Utc);\n let stamped = Message::with_timestamps(&[Message {\n role: Role::User,\n content: vec![ContentBlock::ToolResult {\n tool_use_id: \"call_1\".to_string(),\n content: \"ok\".to_string(),\n is_error: None,\n }],\n timestamp: Some(ts),\n tool_duration_ms: Some(3_200),\n }]);\n match &stamped[0].content[0] {\n ContentBlock::ToolResult { content, .. } => {\n assert_eq!(\n content,\n \"[tool timing: start=2025-03-15T02:24:09.800Z finish=2025-03-15T02:24:13.000Z duration=3.2s] ok\"\n );\n }\n other => panic!(\"expected tool result block, got {other:?}\"),\n }\n }\n\n #[test]\n fn with_timestamps_skips_internal_system_reminders() {\n let ts = chrono::DateTime::parse_from_rfc3339(\"2025-03-15T02:24:13Z\")\n .unwrap()\n .with_timezone(&Utc);\n let original = Message {\n role: Role::User,\n content: vec![ContentBlock::Text {\n text: \"<system-reminder>\\ninternal\\n</system-reminder>\".to_string(),\n cache_control: None,\n }],\n timestamp: Some(ts),\n tool_duration_ms: None,\n };\n let stamped = Message::with_timestamps(std::slice::from_ref(&original));\n match &stamped[0].content[0] {\n ContentBlock::Text { text, .. } => {\n assert_eq!(text, \"<system-reminder>\\ninternal\\n</system-reminder>\");\n }\n other => panic!(\"expected text block, got {other:?}\"),\n }\n }\n\n #[test]\n fn ends_with_fresh_user_turn_accepts_plain_user_text() {\n let messages = vec![Message::user(\"hello\")];\n assert!(ends_with_fresh_user_turn(&messages));\n }\n\n #[test]\n fn ends_with_fresh_user_turn_rejects_trailing_tool_result() {\n let messages = vec![\n Message::user(\"hello\"),\n Message {\n role: Role::Assistant,\n content: vec![ContentBlock::ToolUse {\n id: \"call_1\".to_string(),\n name: \"bash\".to_string(),\n input: serde_json::json!({}),\n }],\n timestamp: Some(Utc::now()),\n tool_duration_ms: None,\n },\n Message::tool_result(\"call_1\", \"ok\", false),\n ];\n assert!(!ends_with_fresh_user_turn(&messages));\n }\n\n #[test]\n fn ends_with_fresh_user_turn_skips_internal_system_reminders() {\n let messages = vec![\n Message::user(\"hello\"),\n Message::user(\"<system-reminder>\\ninternal\\n</system-reminder>\"),\n ];\n assert!(ends_with_fresh_user_turn(&messages));\n }\n\n #[test]\n fn ends_with_fresh_user_turn_rejects_assistant_tail() {\n let messages = vec![\n Message::user(\"hello\"),\n Message::assistant_text(\"working on it\"),\n ];\n assert!(!ends_with_fresh_user_turn(&messages));\n }\n\n #[test]\n fn format_background_task_notification_markdown_uses_code_block_preview() {\n let rendered = format_background_task_notification_markdown(&BackgroundTaskCompleted {\n task_id: \"abc123\".to_string(),\n tool_name: \"bash\".to_string(),\n session_id: \"session\".to_string(),\n status: BackgroundTaskStatus::Completed,\n exit_code: Some(0),\n output_preview: \"[stderr] first line\\n[stdout] second line\\n\".to_string(),\n output_file: std::path::PathBuf::from(\"/tmp/output.log\"),\n duration_secs: 7.1,\n notify: true,\n wake: false,\n });\n\n assert!(\n rendered\n .contains(\"**Background task** `abc123` · `bash` · ✓ completed · 7.1s · exit 0\")\n );\n assert!(rendered.contains(\"```text\\n[stderr] first line\\n[stdout] second line\\n```\"));\n assert!(rendered.contains(\"_Full output:_ `bg action=\\\"output\\\" task_id=\\\"abc123\\\"`\"));\n }\n\n #[test]\n fn format_background_task_notification_markdown_handles_empty_preview() {\n let rendered = format_background_task_notification_markdown(&BackgroundTaskCompleted {\n task_id: \"abc123\".to_string(),\n tool_name: \"bash\".to_string(),\n session_id: \"session\".to_string(),\n status: BackgroundTaskStatus::Failed,\n exit_code: Some(9),\n output_preview: \"\\n\\n\".to_string(),\n output_file: std::path::PathBuf::from(\"/tmp/output.log\"),\n duration_secs: 1.0,\n notify: true,\n wake: false,\n });\n\n assert!(rendered.contains(\"✗ failed\"));\n assert!(rendered.contains(\"_No output captured._\"));\n }\n\n #[test]\n fn format_background_task_notification_markdown_renders_superseded_status() {\n let rendered = format_background_task_notification_markdown(&BackgroundTaskCompleted {\n task_id: \"abc123\".to_string(),\n tool_name: \"selfdev-build\".to_string(),\n session_id: \"session\".to_string(),\n status: BackgroundTaskStatus::Superseded,\n exit_code: Some(0),\n output_preview: \"Build completed, but source changed before activation\".to_string(),\n output_file: std::path::PathBuf::from(\"/tmp/output.log\"),\n duration_secs: 5.0,\n notify: true,\n wake: false,\n });\n\n assert!(rendered.contains(\"↻ superseded\"));\n assert!(rendered.contains(\"exit 0\"));\n assert!(rendered.contains(\"source changed before activation\"));\n }\n\n #[test]\n fn description_token_estimate_uses_chars_per_token_heuristic() {\n let def = ToolDefinition {\n name: \"read\".to_string(),\n description: \"abcdwxyz\".to_string(),\n input_schema: serde_json::json!({\"type\": \"object\"}),\n };\n\n assert_eq!(def.description_token_estimate(), 2);\n }\n\n #[test]\n fn parse_background_task_notification_markdown_extracts_fields() {\n let rendered = format_background_task_notification_markdown(&BackgroundTaskCompleted {\n task_id: \"abc123\".to_string(),\n tool_name: \"bash\".to_string(),\n session_id: \"session\".to_string(),\n status: BackgroundTaskStatus::Completed,\n exit_code: Some(0),\n output_preview: \"[stderr] first line\\n[stdout] second line\\n\".to_string(),\n output_file: std::path::PathBuf::from(\"/tmp/output.log\"),\n duration_secs: 7.1,\n notify: true,\n wake: false,\n });\n\n let parsed = parse_background_task_notification_markdown(&rendered)\n .expect(\"background task notification should parse\");\n assert_eq!(parsed.task_id, \"abc123\");\n assert_eq!(parsed.tool_name, \"bash\");\n assert_eq!(parsed.status, \"✓ completed\");\n assert_eq!(parsed.duration, \"7.1s\");\n assert_eq!(parsed.exit_label, \"exit 0\");\n assert_eq!(\n parsed.preview.as_deref(),\n Some(\"[stderr] first line\\n[stdout] second line\")\n );\n assert_eq!(\n parsed.full_output_command,\n \"bg action=\\\"output\\\" task_id=\\\"abc123\\\"\"\n );\n }\n}\n"}, "80aba6b02f0296a6a8c31c6a674f1276aecb5dd8": {"t": 1785757236.502416, "n": 44156, "h": "3ca56bd506c8a8ea", "c": "use crate::bus::{BackgroundTaskCompleted, BackgroundTaskStatus};\nuse chrono::Utc;\nuse regex::Regex;\nuse serde::{Deserialize, Serialize};\nuse std::collections::HashSet;\nuse std::sync::OnceLock;\n\n#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]\npub struct InputShellResult {\n pub command: String,\n #[serde(default, skip_serializing_if = \"Option::is_none\")]\n pub cwd: Option<String>,\n pub output: String,\n #[serde(default, skip_serializing_if = \"Option::is_none\")]\n pub exit_code: Option<i32>,\n pub duration_ms: u64,\n #[serde(default)]\n pub truncated: bool,\n #[serde(default)]\n pub failed_to_start: bool,\n}\n\nfn sanitize_fenced_block(text: &str) -> String {\n text.replace(\"```\", \"``\\u{200b}`\")\n}\n\npub fn format_input_shell_result_markdown(shell: &InputShellResult) -> String {\n let status = if shell.failed_to_start {\n \"✗ failed to start\".to_string()\n } else if shell.exit_code == Some(0) {\n \"✓ exit 0\".to_string()\n } else if let Some(code) = shell.exit_code {\n format!(\"✗ exit {}\", code)\n } else {\n \"✗ terminated\".to_string()\n };\n\n let mut meta = vec![status, Message::format_duration(shell.duration_ms)];\n if let Some(cwd) = shell.cwd.as_deref() {\n meta.push(format!(\"cwd `{}`\", cwd));\n }\n if shell.truncated {\n meta.push(\"truncated\".to_string());\n }\n\n let mut message = format!(\n \"**Shell command** · {}\\n\\n```bash\\n{}\\n```\",\n meta.join(\" · \"),\n sanitize_fenced_block(&shell.command)\n );\n\n if shell.output.trim().is_empty() {\n message.push_str(\"\\n\\n_No output._\");\n } else {\n message.push_str(&format!(\n \"\\n\\n```text\\n{}\\n```\",\n sanitize_fenced_block(shell.output.trim_end())\n ));\n }\n\n message\n}\n\npub fn input_shell_status_notice(shell: &InputShellResult) -> String {\n if shell.failed_to_start {\n \"Shell command failed to start\".to_string()\n } else if shell.exit_code == Some(0) {\n \"Shell command completed\".to_string()\n } else if let Some(code) = shell.exit_code {\n format!(\"Shell command failed (exit {})\", code)\n } else {\n \"Shell command terminated\".to_string()\n }\n}\n\nfn format_background_task_status(status: &BackgroundTaskStatus) -> &'static str {\n match status {\n BackgroundTaskStatus::Completed => \"✓ completed\",\n BackgroundTaskStatus::Superseded => \"↻ superseded\",\n BackgroundTaskStatus::Failed => \"✗ failed\",\n BackgroundTaskStatus::Running => \"running\",\n }\n}\n\nfn normalize_background_task_preview(preview: &str) -> Option<String> {\n let normalized = preview.replace(\"\\r\\n\", \"\\n\").replace('\\r', \"\\n\");\n let trimmed = normalized.trim_end();\n if trimmed.trim().is_empty() {\n None\n } else {\n Some(sanitize_fenced_block(trimmed))\n }\n}\n\npub fn format_background_task_notification_markdown(task: &BackgroundTaskCompleted) -> String {\n let exit_code = task\n .exit_code\n .map(|code| format!(\"exit {}\", code))\n .unwrap_or_else(|| \"exit n/a\".to_string());\n\n let mut message = format!(\n \"**Background task** `{}` · `{}` · {} · {:.1}s · {}\",\n task.task_id,\n task.tool_name,\n format_background_task_status(&task.status),\n task.duration_secs,\n exit_code,\n );\n\n if let Some(preview) = normalize_background_task_preview(&task.output_preview) {\n message.push_str(&format!(\"\\n\\n```text\\n{}\\n```\", preview));\n } else {\n message.push_str(\"\\n\\n_No output captured._\");\n }\n\n message.push_str(&format!(\n \"\\n\\n_Full output:_ `bg action=\\\"output\\\" task_id=\\\"{}\\\"`\",\n task.task_id\n ));\n\n message\n}\n\n#[derive(Debug, Clone, PartialEq, Eq)]\npub struct ParsedBackgroundTaskNotification {\n pub task_id: String,\n pub tool_name: String,\n pub status: String,\n pub duration: String,\n pub exit_label: String,\n pub preview: Option<String>,\n pub full_output_command: String,\n}\n\npub fn parse_background_task_notification_markdown(\n content: &str,\n) -> Option<ParsedBackgroundTaskNotification> {\n static HEADER_RE: OnceLock<Regex> = OnceLock::new();\n static FULL_OUTPUT_RE: OnceLock<Regex> = OnceLock::new();\n\n let header_re = HEADER_RE.get_or_init(|| {\n Regex::new(\n r\"^\\*\\*Background task\\*\\* `(?P<task_id>[^`]+)` · `(?P<tool_name>[^`]+)` · (?P<status>.+?) · (?P<duration>[0-9]+(?:\\.[0-9]+)?s) · (?P<exit_label>.+)$\",\n )\n .expect(\"valid background task header regex\")\n });\n let full_output_re = FULL_OUTPUT_RE.get_or_init(|| {\n Regex::new(r#\"^_Full output:_ `(?P<command>[^`]+)`$\"#)\n .expect(\"valid background task full output regex\")\n });\n\n let normalized = content.replace(\"\\r\\n\", \"\\n\").replace('\\r', \"\\n\");\n let mut sections = normalized.split(\"\\n\\n\");\n let header = sections.next()?.trim();\n let captures = header_re.captures(header)?;\n\n let mut preview: Option<String> = None;\n let mut full_output_command: Option<String> = None;\n\n for section in sections {\n let trimmed = section.trim();\n if trimmed.is_empty() {\n continue;\n }\n\n if let Some(captures) = full_output_re.captures(trimmed) {\n full_output_command = Some(captures[\"command\"].to_string());\n continue;\n }\n\n if trimmed == \"_No output captured._\" {\n preview = None;\n continue;\n }\n\n if let Some(fenced) = trimmed\n .strip_prefix(\"```text\\n\")\n .and_then(|body| body.strip_suffix(\"\\n```\"))\n {\n preview = Some(fenced.to_string());\n }\n }\n\n Some(ParsedBackgroundTaskNotification {\n task_id: captures[\"task_id\"].to_string(),\n tool_name: captures[\"tool_name\"].to_string(),\n status: captures[\"status\"].to_string(),\n duration: captures[\"duration\"].to_string(),\n exit_label: captures[\"exit_label\"].to_string(),\n preview,\n full_output_command: full_output_command?,\n })\n}\n\npub fn background_task_status_notice(task: &BackgroundTaskCompleted) -> String {\n match task.status {\n BackgroundTaskStatus::Completed => {\n format!(\"Background task completed · {}\", task.tool_name)\n }\n BackgroundTaskStatus::Superseded => {\n format!(\"Background task superseded · {}\", task.tool_name)\n }\n BackgroundTaskStatus::Failed => match task.exit_code {\n Some(code) => format!(\n \"Background task failed · {} · exit {}\",\n task.tool_name, code\n ),\n None => format!(\"Background task failed · {}\", task.tool_name),\n },\n BackgroundTaskStatus::Running => format!(\"Background task running · {}\", task.tool_name),\n }\n}\n\n/// Role in conversation\n#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]\n#[serde(rename_all = \"lowercase\")]\npub enum Role {\n User,\n Assistant,\n}\n\n/// Plain-text tool output placeholder when execution was interrupted.\npub const TOOL_OUTPUT_MISSING_TEXT: &str =\n \"Tool output missing (session interrupted before tool execution completed)\";\n\n/// A message in the conversation\n#[derive(Debug, Clone, Serialize, Deserialize)]\npub struct Message {\n pub role: Role,\n pub content: Vec<ContentBlock>,\n #[serde(default, skip_serializing_if = \"Option::is_none\")]\n pub timestamp: Option<chrono::DateTime<Utc>>,\n #[serde(default, skip_serializing_if = \"Option::is_none\")]\n pub tool_duration_ms: Option<u64>,\n}\n\n/// Cache control metadata for prompt caching\n#[derive(Debug, Clone, Serialize, Deserialize)]\npub struct CacheControl {\n #[serde(rename = \"type\")]\n pub kind: String,\n #[serde(skip_serializing_if = \"Option::is_none\")]\n pub ttl: Option<String>,\n}\n\nimpl CacheControl {\n pub fn ephemeral(ttl: Option<String>) -> Self {\n Self {\n kind: \"ephemeral\".to_string(),\n ttl,\n }\n }\n}\n\n/// Content block within a message\n#[derive(Debug, Clone, Serialize, Deserialize)]\n#[serde(tag = \"type\", rename_all = \"snake_case\")]\npub enum ContentBlock {\n Text {\n text: String,\n #[serde(skip_serializing_if = \"Option::is_none\")]\n cache_control: Option<CacheControl>,\n },\n /// Hidden reasoning content used for providers that require it (not displayed)\n Reasoning {\n text: String,\n },\n ToolUse {\n id: String,\n name: String,\n input: serde_json::Value,\n },\n ToolResult {\n tool_use_id: String,\n content: String,\n #[serde(skip_serializing_if = \"Option::is_none\")]\n is_error: Option<bool>,\n },\n Image {\n media_type: String,\n data: String,\n },\n /// Hidden OpenAI Responses compaction item used to preserve native\n /// compaction state across turns/saves when jcode explicitly triggers it.\n OpenAICompaction {\n encrypted_content: String,\n },\n}\n\nimpl Message {\n pub fn user(text: &str) -> Self {\n Self {\n role: Role::User,\n content: vec![ContentBlock::Text {\n text: text.to_string(),\n cache_control: None,\n }],\n timestamp: Some(Utc::now()),\n tool_duration_ms: None,\n }\n }\n\n pub fn user_with_images(text: &str, images: Vec<(String, String)>) -> Self {\n let mut content: Vec<ContentBlock> = images\n .into_iter()\n .map(|(media_type, data)| ContentBlock::Image { media_type, data })\n .collect();\n content.push(ContentBlock::Text {\n text: text.to_string(),\n cache_control: None,\n });\n Self {\n role: Role::User,\n content,\n timestamp: Some(Utc::now()),\n tool_duration_ms: None,\n }\n }\n\n pub fn assistant_text(text: &str) -> Self {\n Self {\n role: Role::Assistant,\n content: vec![ContentBlock::Text {\n text: text.to_string(),\n cache_control: None,\n }],\n timestamp: Some(Utc::now()),\n tool_duration_ms: None,\n }\n }\n\n pub fn tool_result(tool_use_id: &str, content: &str, is_error: bool) -> Self {\n Self::tool_result_with_duration(tool_use_id, content, is_error, None)\n }\n\n pub fn tool_result_with_duration(\n tool_use_id: &str,\n content: &str,\n is_error: bool,\n tool_duration_ms: Option<u64>,\n ) -> Self {\n Self {\n role: Role::User,\n content: vec![ContentBlock::ToolResult {\n tool_use_id: tool_use_id.to_string(),\n content: content.to_string(),\n is_error: if is_error { Some(true) } else { None },\n }],\n timestamp: Some(Utc::now()),\n tool_duration_ms,\n }\n }\n\n /// Format a timestamp deterministically in UTC for injection into model-visible content.\n pub fn format_timestamp(ts: &chrono::DateTime<Utc>) -> String {\n ts.to_rfc3339_opts(chrono::SecondsFormat::Millis, true)\n }\n\n pub fn format_duration(duration_ms: u64) -> String {\n match duration_ms {\n 0..=999 => format!(\"{}ms\", duration_ms),\n 1_000..=9_999 => format!(\"{:.1}s\", duration_ms as f64 / 1000.0),\n 10_000..=59_999 => format!(\"{}s\", duration_ms / 1000),\n _ => {\n let total_seconds = duration_ms / 1000;\n let minutes = total_seconds / 60;\n let seconds = total_seconds % 60;\n if seconds == 0 {\n format!(\"{}m\", minutes)\n } else {\n format!(\"{}m {}s\", minutes, seconds)\n }\n }\n }\n }\n\n pub fn is_internal_system_reminder(&self) -> bool {\n self.content\n .iter()\n .find_map(|block| match block {\n ContentBlock::Text { text, .. } => Some(text.trim_start()),\n _ => None,\n })\n .is_some_and(|text| text.starts_with(\"<system-reminder>\"))\n }\n\n fn should_skip_timestamp_injection(&self) -> bool {\n self.is_internal_system_reminder()\n }\n\n fn tool_result_tag(&self, ts: &chrono::DateTime<Utc>) -> String {\n match self.tool_duration_ms {\n Some(duration_ms) => {\n let duration_ms_i64 = i64::try_from(duration_ms).unwrap_or(i64::MAX);\n let start_ts = ts\n .checked_sub_signed(chrono::Duration::milliseconds(duration_ms_i64))\n .unwrap_or(*ts);\n format!(\n \"[tool timing: start={} finish={} duration={}]\",\n Self::format_timestamp(&start_ts),\n Self::format_timestamp(ts),\n Self::format_duration(duration_ms)\n )\n }\n None => format!(\"[{}]\", Self::format_timestamp(ts)),\n }\n }\n\n /// Return a copy of messages with timestamps injected into user-role text content.\n /// Tool results get a stable UTC timing header prepended to content.\n /// User text messages get a stable UTC timestamp prepended to the first text block.\n pub fn with_timestamps(messages: &[Message]) -> Vec<Message> {\n messages\n .iter()\n .map(|msg| {\n let Some(ts) = msg.timestamp else {\n return msg.clone();\n };\n if msg.role != Role::User || msg.should_skip_timestamp_injection() {\n return msg.clone();\n }\n let text_tag = format!(\"[{}]\", Self::format_timestamp(&ts));\n let tool_result_tag = msg.tool_result_tag(&ts);\n let mut msg = msg.clone();\n let mut tagged = false;\n for block in &mut msg.content {\n match block {\n ContentBlock::Text { text, .. } if !tagged => {\n *text = format!(\"{} {}\", text_tag, text);\n tagged = true;\n }\n ContentBlock::ToolResult { content, .. } if !tagged => {\n *content = format!(\"{} {}\", tool_result_tag, content);\n tagged = true;\n }\n _ => {}\n }\n }\n msg\n })\n .collect()\n }\n}\n\nconst STABLE_HASH_SEED: u64 = 0xcbf29ce484222325;\nconst STABLE_HASH_PRIME: u64 = 0x100000001b3;\n\nfn stable_hash_bytes(bytes: &[u8]) -> u64 {\n let mut hash = STABLE_HASH_SEED;\n for byte in bytes {\n hash ^= *byte as u64;\n hash = hash.wrapping_mul(STABLE_HASH_PRIME);\n }\n hash\n}\n\npub(crate) fn extend_stable_hash(acc: u64, next: u64) -> u64 {\n stable_hash_bytes(&[acc.to_le_bytes().as_slice(), next.to_le_bytes().as_slice()].concat())\n}\n\npub(crate) fn stable_message_hash(message: &Message) -> u64 {\n match serde_json::to_vec(message) {\n Ok(bytes) => stable_hash_bytes(&bytes),\n Err(_) => stable_hash_bytes(format!(\"{:?}\", message).as_bytes()),\n }\n}\n\npub fn ends_with_fresh_user_turn(messages: &[Message]) -> bool {\n for msg in messages.iter().rev() {\n if msg.role != Role::User {\n return false;\n }\n\n if msg\n .content\n .iter()\n .any(|block| matches!(block, ContentBlock::ToolResult { .. }))\n {\n return false;\n }\n\n if msg.content.is_empty() {\n return false;\n }\n\n let mut saw_user_text = false;\n for block in &msg.content {\n match block {\n ContentBlock::Text { text, .. } => {\n let trimmed = text.trim();\n if !trimmed.is_empty() && !trimmed.starts_with(\"<system-reminder>\") {\n saw_user_text = true;\n }\n }\n _ => return false,\n }\n }\n\n if saw_user_text {\n return true;\n }\n\n if msg.is_internal_system_reminder() {\n continue;\n }\n\n return false;\n }\n\n false\n}\n\n/// Sanitize a tool ID so it matches the pattern `^[a-zA-Z0-9_-]+$`.\n///\n/// Different providers generate tool IDs in different formats. When switching\n/// from one provider to another mid-conversation, the historical tool IDs may\n/// contain characters that the new provider rejects (e.g., dots in Copilot IDs\n/// sent to Anthropic). This function replaces any invalid characters with\n/// underscores.\npub fn sanitize_tool_id(id: &str) -> String {\n if id.is_empty() {\n return \"unknown\".to_string();\n }\n let sanitized: String = id\n .chars()\n .map(|c| {\n if c.is_ascii_alphanumeric() || c == '_' || c == '-' {\n c\n } else {\n '_'\n }\n })\n .collect();\n if sanitized.is_empty() {\n \"unknown\".to_string()\n } else {\n sanitized\n }\n}\n\n/// Redact likely secrets from persisted tool output.\n///\n/// This is a best-effort safeguard for local session history files. It targets\n/// high-confidence token/key patterns and common `KEY=VALUE` assignments used by\n/// auth flows.\npub fn redact_secrets(text: &str) -> String {\n // Fast path to avoid regex work for most tool outputs.\n let lower = text.to_ascii_lowercase();\n\n if !text.contains(\"sk-\")\n && !text.contains(\"ghp_\")\n && !text.contains(\"github_pat_\")\n && !text.contains(\"AIza\")\n && !text.contains(\"ya29.\")\n && !text.contains(\"xox\")\n && !lower.contains(\"api_key\")\n && !lower.contains(\"token\")\n {\n return text.to_string();\n }\n\n static DIRECT_PATTERNS: OnceLock<Vec<Regex>> = OnceLock::new();\n static ASSIGNMENT_PATTERNS: OnceLock<Vec<Regex>> = OnceLock::new();\n\n let direct_patterns = DIRECT_PATTERNS.get_or_init(|| {\n vec![\n Regex::new(r\"sk-ant-(?:oat|ort)01-[A-Za-z0-9_-]{20,}\")\n .expect(\"valid Anthropic OAuth token regex\"),\n Regex::new(r\"sk-or-v1-[A-Za-z0-9_-]{20,}\").expect(\"valid OpenRouter key regex\"),\n Regex::new(r\"ghp_[A-Za-z0-9]{20,}\").expect(\"valid GitHub PAT regex\"),\n Regex::new(r\"github_pat_[A-Za-z0-9_]{20,}\")\n .expect(\"valid GitHub fine-grained PAT regex\"),\n Regex::new(r\"ya29\\.[A-Za-z0-9._-]{20,}\").expect(\"valid Google OAuth token regex\"),\n Regex::new(r\"AIza[0-9A-Za-z_-]{20,}\").expect(\"valid Google API key regex\"),\n Regex::new(r\"xox[baprs]-[A-Za-z0-9-]{10,}\").expect(\"valid Slack token regex\"),\n ]\n });\n\n let assignment_patterns = ASSIGNMENT_PATTERNS.get_or_init(|| {\n vec![\n Regex::new(r\"(?m)^\\s*(OPENROUTER_API_KEY\\s*=\\s*)[^\\r\\n]+\")\n .expect(\"valid OPENROUTER_API_KEY assignment regex\"),\n Regex::new(r\"(?m)^\\s*(OPENCODE_API_KEY\\s*=\\s*)[^\\r\\n]+\")\n .expect(\"valid OPENCODE_API_KEY assignment regex\"),\n Regex::new(r\"(?m)^\\s*(OPENCODE_GO_API_KEY\\s*=\\s*)[^\\r\\n]+\")\n .expect(\"valid OPENCODE_GO_API_KEY assignment regex\"),\n Regex::new(r\"(?m)^\\s*(ZHIPU_API_KEY\\s*=\\s*)[^\\r\\n]+\")\n .expect(\"valid ZHIPU_API_KEY assignment regex\"),\n Regex::new(r\"(?m)^\\s*(ZAI_API_KEY\\s*=\\s*)[^\\r\\n]+\")\n .expect(\"valid ZAI_API_KEY assignment regex\"),\n Regex::new(r\"(?m)^\\s*(302AI_API_KEY\\s*=\\s*)[^\\r\\n]+\")\n .expect(\"valid 302AI_API_KEY assignment regex\"),\n Regex::new(r\"(?m)^\\s*(BASETEN_API_KEY\\s*=\\s*)[^\\r\\n]+\")\n .expect(\"valid BASETEN_API_KEY assignment regex\"),\n Regex::new(r\"(?m)^\\s*(CORTECS_API_KEY\\s*=\\s*)[^\\r\\n]+\")\n .expect(\"valid CORTECS_API_KEY assignment regex\"),\n Regex::new(r\"(?m)^\\s*(DEEPSEEK_API_KEY\\s*=\\s*)[^\\r\\n]+\")\n .expect(\"valid DEEPSEEK_API_KEY assignment regex\"),\n Regex::new(r\"(?m)^\\s*(FIRMWARE_API_KEY\\s*=\\s*)[^\\r\\n]+\")\n .expect(\"valid FIRMWARE_API_KEY assignment regex\"),\n Regex::new(r\"(?m)^\\s*(HF_TOKEN\\s*=\\s*)[^\\r\\n]+\")\n .expect(\"valid HF_TOKEN assignment regex\"),\n Regex::new(r\"(?m)^\\s*(MOONSHOT_API_KEY\\s*=\\s*)[^\\r\\n]+\")\n .expect(\"valid MOONSHOT_API_KEY assignment regex\"),\n Regex::new(r\"(?m)^\\s*(NEBIUS_API_KEY\\s*=\\s*)[^\\r\\n]+\")\n .expect(\"valid NEBIUS_API_KEY assignment regex\"),\n Regex::new(r\"(?m)^\\s*(SCALEWAY_API_KEY\\s*=\\s*)[^\\r\\n]+\")\n .expect(\"valid SCALEWAY_API_KEY assignment regex\"),\n Regex::new(r\"(?m)^\\s*(STACKIT_API_KEY\\s*=\\s*)[^\\r\\n]+\")\n .expect(\"valid STACKIT_API_KEY assignment regex\"),\n Regex::new(r\"(?m)^\\s*(GROQ_API_KEY\\s*=\\s*)[^\\r\\n]+\")\n .expect(\"valid GROQ_API_KEY assignment regex\"),\n Regex::new(r\"(?m)^\\s*(MISTRAL_API_KEY\\s*=\\s*)[^\\r\\n]+\")\n .expect(\"valid MISTRAL_API_KEY assignment regex\"),\n Regex::new(r\"(?m)^\\s*(PERPLEXITY_API_KEY\\s*=\\s*)[^\\r\\n]+\")\n .expect(\"valid PERPLEXITY_API_KEY assignment regex\"),\n Regex::new(r\"(?m)^\\s*(TOGETHER_API_KEY\\s*=\\s*)[^\\r\\n]+\")\n .expect(\"valid TOGETHER_API_KEY assignment regex\"),\n Regex::new(r\"(?m)^\\s*(DEEPINFRA_API_KEY\\s*=\\s*)[^\\r\\n]+\")\n .expect(\"valid DEEPINFRA_API_KEY assignment regex\"),\n Regex::new(r\"(?m)^\\s*(XAI_API_KEY\\s*=\\s*)[^\\r\\n]+\")\n .expect(\"valid XAI_API_KEY assignment regex\"),\n Regex::new(r\"(?m)^\\s*(LMSTUDIO_API_KEY\\s*=\\s*)[^\\r\\n]+\")\n .expect(\"valid LMSTUDIO_API_KEY assignment regex\"),\n Regex::new(r\"(?m)^\\s*(OLLAMA_API_KEY\\s*=\\s*)[^\\r\\n]+\")\n .expect(\"valid OLLAMA_API_KEY assignment regex\"),\n Regex::new(r\"(?m)^\\s*(CHUTES_API_KEY\\s*=\\s*)[^\\r\\n]+\")\n .expect(\"valid CHUTES_API_KEY assignment regex\"),\n Regex::new(r\"(?m)^\\s*(CEREBRAS_API_KEY\\s*=\\s*)[^\\r\\n]+\")\n .expect(\"valid CEREBRAS_API_KEY assignment regex\"),\n Regex::new(r\"(?m)^\\s*(OPENAI_COMPAT_API_KEY\\s*=\\s*)[^\\r\\n]+\")\n .expect(\"valid OPENAI_COMPAT_API_KEY assignment regex\"),\n Regex::new(r\"(?m)^\\s*(ANTHROPIC_API_KEY\\s*=\\s*)[^\\r\\n]+\")\n .expect(\"valid ANTHROPIC_API_KEY assignment regex\"),\n Regex::new(r\"(?m)^\\s*(OPENAI_API_KEY\\s*=\\s*)[^\\r\\n]+\")\n .expect(\"valid OPENAI_API_KEY assignment regex\"),\n Regex::new(r\"(?m)^\\s*(AZURE_OPENAI_API_KEY\\s*=\\s*)[^\\r\\n]+\")\n .expect(\"valid AZURE_OPENAI_API_KEY assignment regex\"),\n Regex::new(r\"(?m)^\\s*(CURSOR_API_KEY\\s*=\\s*)[^\\r\\n]+\")\n .expect(\"valid CURSOR_API_KEY assignment regex\"),\n Regex::new(r\"(?m)^\\s*(GITHUB_TOKEN\\s*=\\s*)[^\\r\\n]+\")\n .expect(\"valid GITHUB_TOKEN assignment regex\"),\n ]\n });\n\n let mut redacted = text.to_string();\n let mut redacted_keys: HashSet<String> = [\n \"OPENROUTER_API_KEY\",\n \"OPENCODE_API_KEY\",\n \"OPENCODE_GO_API_KEY\",\n \"ZHIPU_API_KEY\",\n \"ZAI_API_KEY\",\n \"302AI_API_KEY\",\n \"BASETEN_API_KEY\",\n \"CORTECS_API_KEY\",\n \"DEEPSEEK_API_KEY\",\n \"FIRMWARE_API_KEY\",\n \"HF_TOKEN\",\n \"MOONSHOT_API_KEY\",\n \"NEBIUS_API_KEY\",\n \"SCALEWAY_API_KEY\",\n \"STACKIT_API_KEY\",\n \"GROQ_API_KEY\",\n \"MISTRAL_API_KEY\",\n \"PERPLEXITY_API_KEY\",\n \"TOGETHER_API_KEY\",\n \"DEEPINFRA_API_KEY\",\n \"XAI_API_KEY\",\n \"LMSTUDIO_API_KEY\",\n \"OLLAMA_API_KEY\",\n \"CHUTES_API_KEY\",\n \"CEREBRAS_API_KEY\",\n \"OPENAI_COMPAT_API_KEY\",\n \"ANTHROPIC_API_KEY\",\n \"OPENAI_API_KEY\",\n \"AZURE_OPENAI_API_KEY\",\n \"CURSOR_API_KEY\",\n \"GITHUB_TOKEN\",\n ]\n .iter()\n .map(|k| (*k).to_string())\n .collect();\n\n for re in direct_patterns {\n redacted = re.replace_all(&redacted, \"[REDACTED_SECRET]\").into_owned();\n }\n\n for re in assignment_patterns {\n redacted = re\n .replace_all(&redacted, \"${1}[REDACTED_SECRET]\")\n .into_owned();\n }\n\n // Also redact custom API key variable names configured at runtime.\n for source in [\n \"JCODE_OPENROUTER_API_KEY_NAME\",\n \"JCODE_OPENAI_COMPAT_API_KEY_NAME\",\n ] {\n let Some(key_name) = std::env::var(source)\n .ok()\n .map(|v| v.trim().to_string())\n .filter(|v| !v.is_empty())\n else {\n continue;\n };\n\n if !key_name\n .chars()\n .all(|c| c.is_ascii_uppercase() || c.is_ascii_digit() || c == '_')\n {\n continue;\n }\n if !redacted_keys.insert(key_name.clone()) {\n continue;\n }\n\n let pattern = format!(r\"(?m)^\\s*({}\\s*=\\s*)[^\\r\\n]+\", regex::escape(&key_name));\n if let Ok(re) = Regex::new(&pattern) {\n redacted = re\n .replace_all(&redacted, \"${1}[REDACTED_SECRET]\")\n .into_owned();\n }\n }\n\n redacted\n}\n\n/// Tool definition for the API\n#[derive(Debug, Clone, Serialize)]\npub struct ToolDefinition {\n pub name: String,\n // Prompt-visible text sent to the model by provider adapters.\n // Approximate prompt cost: description.len() / 4. Use\n // ToolDefinition::description_token_estimate() when reviewing tool bloat.\n pub description: String,\n pub input_schema: serde_json::Value,\n}\n\nimpl ToolDefinition {\n /// Serialized size of the full tool definition payload sent to providers.\n pub fn prompt_chars(&self) -> usize {\n serde_json::json!({\n \"name\": self.name,\n \"description\": self.description,\n \"input_schema\": self.input_schema,\n })\n .to_string()\n .len()\n }\n\n /// Approximate prompt-token cost of this tool's top-level description.\n ///\n /// This uses jcode's standard chars/4 heuristic, matching other token\n /// budget estimates in the codebase.\n pub fn description_token_estimate(&self) -> usize {\n crate::util::estimate_tokens(&self.description)\n }\n\n /// Approximate prompt-token cost of the full tool definition payload.\n pub fn prompt_token_estimate(&self) -> usize {\n crate::util::estimate_tokens(\n &serde_json::json!({\n \"name\": self.name,\n \"description\": self.description,\n \"input_schema\": self.input_schema,\n })\n .to_string(),\n )\n }\n\n pub fn aggregate_prompt_chars(defs: &[ToolDefinition]) -> usize {\n defs.iter().map(Self::prompt_chars).sum()\n }\n\n pub fn aggregate_prompt_token_estimate(defs: &[ToolDefinition]) -> usize {\n defs.iter().map(Self::prompt_token_estimate).sum()\n }\n}\n\n/// A tool call from the model\n#[derive(Debug, Clone, Default, PartialEq, Eq, serde::Serialize, serde::Deserialize)]\npub struct ToolCall {\n #[serde(default)]\n pub id: String,\n #[serde(default)]\n pub name: String,\n #[serde(default)]\n pub input: serde_json::Value,\n #[serde(default, skip_serializing_if = \"Option::is_none\")]\n pub intent: Option<String>,\n}\n\n/// Connection phase for status bar transparency\n#[derive(Debug, Clone, PartialEq)]\npub enum ConnectionPhase {\n /// Refreshing OAuth token\n Authenticating,\n /// TCP + TLS connection to API\n Connecting,\n /// HTTP request sent, waiting for first response byte\n WaitingForResponse,\n /// First byte received, stream is active\n Streaming,\n /// Retrying after a transient error\n Retrying { attempt: u32, max: u32 },\n}\n\nimpl std::fmt::Display for ConnectionPhase {\n fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {\n match self {\n ConnectionPhase::Authenticating => write!(f, \"authenticating\"),\n ConnectionPhase::Connecting => write!(f, \"connecting\"),\n ConnectionPhase::WaitingForResponse => write!(f, \"waiting for response\"),\n ConnectionPhase::Streaming => write!(f, \"streaming\"),\n ConnectionPhase::Retrying { attempt, max } => {\n write!(f, \"retrying ({}/{})\", attempt, max)\n }\n }\n }\n}\n\n/// Streaming event from provider\n#[derive(Debug, Clone)]\npub enum StreamEvent {\n /// Text content delta\n TextDelta(String),\n /// Tool use started\n ToolUseStart { id: String, name: String },\n /// Tool input delta (JSON fragment)\n ToolInputDelta(String),\n /// Tool use complete\n ToolUseEnd,\n /// Tool result from provider (provider already executed the tool)\n ToolResult {\n tool_use_id: String,\n content: String,\n is_error: bool,\n },\n /// Extended thinking started\n ThinkingStart,\n /// Extended thinking delta (reasoning content)\n ThinkingDelta(String),\n /// Extended thinking ended\n ThinkingEnd,\n /// Extended thinking completed with duration\n ThinkingDone { duration_secs: f64 },\n /// Message complete (may have stop reason)\n MessageEnd { stop_reason: Option<String> },\n /// Token usage update\n TokenUsage {\n input_tokens: Option<u64>,\n output_tokens: Option<u64>,\n cache_read_input_tokens: Option<u64>,\n cache_creation_input_tokens: Option<u64>,\n },\n /// Active transport/connection type for this stream\n ConnectionType { connection: String },\n /// Connection phase update (for status bar transparency)\n ConnectionPhase { phase: ConnectionPhase },\n /// Provider-supplied human-readable transport detail for the status line\n StatusDetail { detail: String },\n /// Error occurred\n Error {\n message: String,\n /// Seconds until rate limit resets (if this is a rate limit error)\n retry_after_secs: Option<u64>,\n },\n /// Provider session ID (for conversation resume)\n SessionId(String),\n /// Compaction occurred (context was summarized)\n Compaction {\n trigger: String,\n pre_tokens: Option<u64>,\n /// Provider-native compaction artifact, if one was emitted.\n openai_encrypted_content: Option<String>,\n },\n /// Upstream provider info (e.g., which provider OpenRouter routed to)\n UpstreamProvider { provider: String },\n /// Native tool call from a provider bridge that needs execution by jcode\n NativeToolCall {\n request_id: String,\n tool_name: String,\n input: serde_json::Value,\n },\n}\n\n#[cfg(test)]\nmod tests {\n use super::*;\n\n #[test]\n fn sanitize_tool_id_alphanumeric_passthrough() {\n assert_eq!(\n sanitize_tool_id(\"toolu_01XFDUDYJgAACzvnptvVer6u\"),\n \"toolu_01XFDUDYJgAACzvnptvVer6u\"\n );\n assert_eq!(sanitize_tool_id(\"call_abc123\"), \"call_abc123\");\n assert_eq!(\n sanitize_tool_id(\"call_1234567890_9876543210\"),\n \"call_1234567890_9876543210\"\n );\n }\n\n #[test]\n fn sanitize_tool_id_hyphens_passthrough() {\n assert_eq!(sanitize_tool_id(\"call-abc-123\"), \"call-abc-123\");\n assert_eq!(\n sanitize_tool_id(\"tool_use-id_with-mixed\"),\n \"tool_use-id_with-mixed\"\n );\n }\n\n #[test]\n fn sanitize_tool_id_replaces_dots() {\n assert_eq!(\n sanitize_tool_id(\"chatcmpl-abc.def.ghi\"),\n \"chatcmpl-abc_def_ghi\"\n );\n assert_eq!(sanitize_tool_id(\"call.123\"), \"call_123\");\n }\n\n #[test]\n fn sanitize_tool_id_replaces_colons() {\n assert_eq!(sanitize_tool_id(\"call:123:456\"), \"call_123_456\");\n }\n\n #[test]\n fn sanitize_tool_id_replaces_special_chars() {\n assert_eq!(\n sanitize_tool_id(\"id@with#special$chars\"),\n \"id_with_special_chars\"\n );\n assert_eq!(sanitize_tool_id(\"id with spaces\"), \"id_with_spaces\");\n }\n\n #[test]\n fn sanitize_tool_id_empty_returns_unknown() {\n assert_eq!(sanitize_tool_id(\"\"), \"unknown\");\n }\n\n #[test]\n fn sanitize_tool_id_copilot_to_anthropic() {\n assert_eq!(\n sanitize_tool_id(\"chatcmpl-BF2xX.tool_call.0\"),\n \"chatcmpl-BF2xX_tool_call_0\"\n );\n }\n\n #[test]\n fn sanitize_tool_id_already_valid_unchanged() {\n let valid_ids = [\n \"toolu_01XFDUDYJgAACzvnptvVer6u\",\n \"call_abc123\",\n \"fallback_text_call_call_1234567890_9876543210\",\n \"tool_123\",\n \"a\",\n \"A\",\n \"0\",\n \"_\",\n \"-\",\n \"a-b_c\",\n ];\n for id in valid_ids {\n assert_eq!(sanitize_tool_id(id), id, \"ID '{}' should be unchanged\", id);\n }\n }\n\n #[test]\n fn redact_secrets_redacts_known_direct_token_formats() {\n let input = \"access=sk-ant-oat01-ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789\\nopenrouter=sk-or-v1-abcdefghijklmnopqrstuvwxyz0123456789\\ngithub=ghp_ABCDEFGHIJKLMNOPQRSTUVWXYZ0123\\n\";\n let out = redact_secrets(input);\n assert!(!out.contains(\"sk-ant-oat01-\"));\n assert!(!out.contains(\"sk-or-v1-\"));\n assert!(!out.contains(\"ghp_ABCDEFGHIJKLMNOPQRSTUVWXYZ0123\"));\n assert!(out.matches(\"[REDACTED_SECRET]\").count() >= 3);\n }\n\n #[test]\n fn redact_secrets_redacts_env_style_assignments() {\n let input = \"OPENROUTER_API_KEY=sk-or-v1-abc123abc123abc123abc123\\nOPENCODE_API_KEY=oc_test_secret\\nOPENCODE_GO_API_KEY=ocgo_test_secret\\nZAI_API_KEY=zai_secret\\nCHUTES_API_KEY=chutes_secret\\nCEREBRAS_API_KEY=cerebras_secret\\nOPENAI_COMPAT_API_KEY=compat_secret\\nCURSOR_API_KEY='my_cursor_secret_value'\\nOPENAI_API_KEY=sk-test-openai-example\\nAZURE_OPENAI_API_KEY=azure-openai-secret\\n\";\n let out = redact_secrets(input);\n assert!(out.contains(\"OPENROUTER_API_KEY=[REDACTED_SECRET]\"));\n assert!(out.contains(\"OPENCODE_API_KEY=[REDACTED_SECRET]\"));\n assert!(out.contains(\"OPENCODE_GO_API_KEY=[REDACTED_SECRET]\"));\n assert!(out.contains(\"ZAI_API_KEY=[REDACTED_SECRET]\"));\n assert!(out.contains(\"CHUTES_API_KEY=[REDACTED_SECRET]\"));\n assert!(out.contains(\"CEREBRAS_API_KEY=[REDACTED_SECRET]\"));\n assert!(out.contains(\"OPENAI_COMPAT_API_KEY=[REDACTED_SECRET]\"));\n assert!(out.contains(\"CURSOR_API_KEY=[REDACTED_SECRET]\"));\n assert!(out.contains(\"OPENAI_API_KEY=[REDACTED_SECRET]\"));\n assert!(out.contains(\"AZURE_OPENAI_API_KEY=[REDACTED_SECRET]\"));\n assert!(!out.contains(\"my_cursor_secret_value\"));\n }\n\n #[test]\n fn redact_secrets_redacts_runtime_key_assignment() {\n let key_var = \"JCODE_OPENAI_COMPAT_API_KEY_NAME\";\n let prev = std::env::var(key_var).ok();\n crate::env::set_var(key_var, \"GROQ_API_KEY\");\n\n let input = \"GROQ_API_KEY=my_secret_token_value\";\n let out = redact_secrets(input);\n assert_eq!(out, \"GROQ_API_KEY=[REDACTED_SECRET]\");\n\n if let Some(v) = prev {\n crate::env::set_var(key_var, v);\n } else {\n crate::env::remove_var(key_var);\n }\n }\n\n #[test]\n fn redact_secrets_redacts_mixed_case_token_assignments() {\n let input = \"my_token=ya29.ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789\";\n let out = redact_secrets(input);\n assert!(out.contains(\"[REDACTED_SECRET]\"));\n assert!(!out.contains(\"ya29.ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789\"));\n }\n\n #[test]\n fn redact_secrets_leaves_normal_output_unchanged() {\n let input = \"Found 5 files\\nNo auth errors\\nDone.\";\n assert_eq!(redact_secrets(input), input);\n }\n\n #[test]\n fn format_timestamp_is_stable_utc_rfc3339() {\n let ts = chrono::DateTime::parse_from_rfc3339(\"2025-03-15T02:24:13.250Z\")\n .unwrap()\n .with_timezone(&Utc);\n assert_eq!(Message::format_timestamp(&ts), \"2025-03-15T02:24:13.250Z\");\n }\n\n #[test]\n fn with_timestamps_prepends_utc_prefix_to_user_text() {\n let ts = chrono::DateTime::parse_from_rfc3339(\"2025-03-15T02:24:03Z\")\n .unwrap()\n .with_timezone(&Utc);\n let stamped = Message::with_timestamps(&[Message {\n role: Role::User,\n content: vec![ContentBlock::Text {\n text: \"hello\".to_string(),\n cache_control: None,\n }],\n timestamp: Some(ts),\n tool_duration_ms: None,\n }]);\n match &stamped[0].content[0] {\n ContentBlock::Text { text, .. } => {\n assert_eq!(text, \"[2025-03-15T02:24:03.000Z] hello\");\n }\n other => panic!(\"expected text block, got {other:?}\"),\n }\n }\n\n #[test]\n fn with_timestamps_adds_tool_timing_header_with_duration() {\n let ts = chrono::DateTime::parse_from_rfc3339(\"2025-03-15T02:24:13Z\")\n .unwrap()\n .with_timezone(&Utc);\n let stamped = Message::with_timestamps(&[Message {\n role: Role::User,\n content: vec![ContentBlock::ToolResult {\n tool_use_id: \"call_1\".to_string(),\n content: \"ok\".to_string(),\n is_error: None,\n }],\n timestamp: Some(ts),\n tool_duration_ms: Some(3_200),\n }]);\n match &stamped[0].content[0] {\n ContentBlock::ToolResult { content, .. } => {\n assert_eq!(\n content,\n \"[tool timing: start=2025-03-15T02:24:09.800Z finish=2025-03-15T02:24:13.000Z duration=3.2s] ok\"\n );\n }\n other => panic!(\"expected tool result block, got {other:?}\"),\n }\n }\n\n #[test]\n fn with_timestamps_skips_internal_system_reminders() {\n let ts = chrono::DateTime::parse_from_rfc3339(\"2025-03-15T02:24:13Z\")\n .unwrap()\n .with_timezone(&Utc);\n let original = Message {\n role: Role::User,\n content: vec![ContentBlock::Text {\n text: \"<system-reminder>\\ninternal\\n</system-reminder>\".to_string(),\n cache_control: None,\n }],\n timestamp: Some(ts),\n tool_duration_ms: None,\n };\n let stamped = Message::with_timestamps(std::slice::from_ref(&original));\n match &stamped[0].content[0] {\n ContentBlock::Text { text, .. } => {\n assert_eq!(text, \"<system-reminder>\\ninternal\\n</system-reminder>\");\n }\n other => panic!(\"expected text block, got {other:?}\"),\n }\n }\n\n #[test]\n fn ends_with_fresh_user_turn_accepts_plain_user_text() {\n let messages = vec![Message::user(\"hello\")];\n assert!(ends_with_fresh_user_turn(&messages));\n }\n\n #[test]\n fn ends_with_fresh_user_turn_rejects_trailing_tool_result() {\n let messages = vec![\n Message::user(\"hello\"),\n Message {\n role: Role::Assistant,\n content: vec![ContentBlock::ToolUse {\n id: \"call_1\".to_string(),\n name: \"bash\".to_string(),\n input: serde_json::json!({}),\n }],\n timestamp: Some(Utc::now()),\n tool_duration_ms: None,\n },\n Message::tool_result(\"call_1\", \"ok\", false),\n ];\n assert!(!ends_with_fresh_user_turn(&messages));\n }\n\n #[test]\n fn ends_with_fresh_user_turn_skips_internal_system_reminders() {\n let messages = vec![\n Message::user(\"hello\"),\n Message::user(\"<system-reminder>\\ninternal\\n</system-reminder>\"),\n ];\n assert!(ends_with_fresh_user_turn(&messages));\n }\n\n #[test]\n fn ends_with_fresh_user_turn_rejects_assistant_tail() {\n let messages = vec![\n Message::user(\"hello\"),\n Message::assistant_text(\"working on it\"),\n ];\n assert!(!ends_with_fresh_user_turn(&messages));\n }\n\n #[test]\n fn format_background_task_notification_markdown_uses_code_block_preview() {\n let rendered = format_background_task_notification_markdown(&BackgroundTaskCompleted {\n task_id: \"abc123\".to_string(),\n tool_name: \"bash\".to_string(),\n session_id: \"session\".to_string(),\n status: BackgroundTaskStatus::Completed,\n exit_code: Some(0),\n output_preview: \"[stderr] first line\\n[stdout] second line\\n\".to_string(),\n output_file: std::path::PathBuf::from(\"/tmp/output.log\"),\n duration_secs: 7.1,\n notify: true,\n wake: false,\n });\n\n assert!(\n rendered\n .contains(\"**Background task** `abc123` · `bash` · ✓ completed · 7.1s · exit 0\")\n );\n assert!(rendered.contains(\"```text\\n[stderr] first line\\n[stdout] second line\\n```\"));\n assert!(rendered.contains(\"_Full output:_ `bg action=\\\"output\\\" task_id=\\\"abc123\\\"`\"));\n }\n\n #[test]\n fn format_background_task_notification_markdown_handles_empty_preview() {\n let rendered = format_background_task_notification_markdown(&BackgroundTaskCompleted {\n task_id: \"abc123\".to_string(),\n tool_name: \"bash\".to_string(),\n session_id: \"session\".to_string(),\n status: BackgroundTaskStatus::Failed,\n exit_code: Some(9),\n output_preview: \"\\n\\n\".to_string(),\n output_file: std::path::PathBuf::from(\"/tmp/output.log\"),\n duration_secs: 1.0,\n notify: true,\n wake: false,\n });\n\n assert!(rendered.contains(\"✗ failed\"));\n assert!(rendered.contains(\"_No output captured._\"));\n }\n\n #[test]\n fn format_background_task_notification_markdown_renders_superseded_status() {\n let rendered = format_background_task_notification_markdown(&BackgroundTaskCompleted {\n task_id: \"abc123\".to_string(),\n tool_name: \"selfdev-build\".to_string(),\n session_id: \"session\".to_string(),\n status: BackgroundTaskStatus::Superseded,\n exit_code: Some(0),\n output_preview: \"Build completed, but source changed before activation\".to_string(),\n output_file: std::path::PathBuf::from(\"/tmp/output.log\"),\n duration_secs: 5.0,\n notify: true,\n wake: false,\n });\n\n assert!(rendered.contains(\"↻ superseded\"));\n assert!(rendered.contains(\"exit 0\"));\n assert!(rendered.contains(\"source changed before activation\"));\n }\n\n #[test]\n fn description_token_estimate_uses_chars_per_token_heuristic() {\n let def = ToolDefinition {\n name: \"read\".to_string(),\n description: \"abcdwxyz\".to_string(),\n input_schema: serde_json::json!({\"type\": \"object\"}),\n };\n\n assert_eq!(def.description_token_estimate(), 2);\n }\n\n #[test]\n fn parse_background_task_notification_markdown_extracts_fields() {\n let rendered = format_background_task_notification_markdown(&BackgroundTaskCompleted {\n task_id: \"abc123\".to_string(),\n tool_name: \"bash\".to_string(),\n session_id: \"session\".to_string(),\n status: BackgroundTaskStatus::Completed,\n exit_code: Some(0),\n output_preview: \"[stderr] first line\\n[stdout] second line\\n\".to_string(),\n output_file: std::path::PathBuf::from(\"/tmp/output.log\"),\n duration_secs: 7.1,\n notify: true,\n wake: false,\n });\n\n let parsed = parse_background_task_notification_markdown(&rendered)\n .expect(\"background task notification should parse\");\n assert_eq!(parsed.task_id, \"abc123\");\n assert_eq!(parsed.tool_name, \"bash\");\n assert_eq!(parsed.status, \"✓ completed\");\n assert_eq!(parsed.duration, \"7.1s\");\n assert_eq!(parsed.exit_label, \"exit 0\");\n assert_eq!(\n parsed.preview.as_deref(),\n Some(\"[stderr] first line\\n[stdout] second line\")\n );\n assert_eq!(\n parsed.full_output_command,\n \"bg action=\\\"output\\\" task_id=\\\"abc123\\\"\"\n );\n }\n}\n"}, "b9d1ce332e0c8a68c9287dfd886f25e1b1bcb44a": {"t": 1785757237.9849153, "n": 44178, "h": "57762fd2a161d1d9", "c": "#![allow(dead_code)]\n\nuse crate::bus::{BackgroundTaskCompleted, BackgroundTaskStatus};\nuse chrono::Utc;\nuse regex::Regex;\nuse serde::{Deserialize, Serialize};\nuse std::collections::HashSet;\nuse std::sync::OnceLock;\n\n#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]\npub struct InputShellResult {\n pub command: String,\n #[serde(default, skip_serializing_if = \"Option::is_none\")]\n pub cwd: Option<String>,\n pub output: String,\n #[serde(default, skip_serializing_if = \"Option::is_none\")]\n pub exit_code: Option<i32>,\n pub duration_ms: u64,\n #[serde(default)]\n pub truncated: bool,\n #[serde(default)]\n pub failed_to_start: bool,\n}\n\nfn sanitize_fenced_block(text: &str) -> String {\n text.replace(\"```\", \"``\\u{200b}`\")\n}\n\npub fn format_input_shell_result_markdown(shell: &InputShellResult) -> String {\n let status = if shell.failed_to_start {\n \"✗ failed to start\".to_string()\n } else if shell.exit_code == Some(0) {\n \"✓ exit 0\".to_string()\n } else if let Some(code) = shell.exit_code {\n format!(\"✗ exit {}\", code)\n } else {\n \"✗ terminated\".to_string()\n };\n\n let mut meta = vec![status, Message::format_duration(shell.duration_ms)];\n if let Some(cwd) = shell.cwd.as_deref() {\n meta.push(format!(\"cwd `{}`\", cwd));\n }\n if shell.truncated {\n meta.push(\"truncated\".to_string());\n }\n\n let mut message = format!(\n \"**Shell command** · {}\\n\\n```bash\\n{}\\n```\",\n meta.join(\" · \"),\n sanitize_fenced_block(&shell.command)\n );\n\n if shell.output.trim().is_empty() {\n message.push_str(\"\\n\\n_No output._\");\n } else {\n message.push_str(&format!(\n \"\\n\\n```text\\n{}\\n```\",\n sanitize_fenced_block(shell.output.trim_end())\n ));\n }\n\n message\n}\n\npub fn input_shell_status_notice(shell: &InputShellResult) -> String {\n if shell.failed_to_start {\n \"Shell command failed to start\".to_string()\n } else if shell.exit_code == Some(0) {\n \"Shell command completed\".to_string()\n } else if let Some(code) = shell.exit_code {\n format!(\"Shell command failed (exit {})\", code)\n } else {\n \"Shell command terminated\".to_string()\n }\n}\n\nfn format_background_task_status(status: &BackgroundTaskStatus) -> &'static str {\n match status {\n BackgroundTaskStatus::Completed => \"✓ completed\",\n BackgroundTaskStatus::Superseded => \"↻ superseded\",\n BackgroundTaskStatus::Failed => \"✗ failed\",\n BackgroundTaskStatus::Running => \"running\",\n }\n}\n\nfn normalize_background_task_preview(preview: &str) -> Option<String> {\n let normalized = preview.replace(\"\\r\\n\", \"\\n\").replace('\\r', \"\\n\");\n let trimmed = normalized.trim_end();\n if trimmed.trim().is_empty() {\n None\n } else {\n Some(sanitize_fenced_block(trimmed))\n }\n}\n\npub fn format_background_task_notification_markdown(task: &BackgroundTaskCompleted) -> String {\n let exit_code = task\n .exit_code\n .map(|code| format!(\"exit {}\", code))\n .unwrap_or_else(|| \"exit n/a\".to_string());\n\n let mut message = format!(\n \"**Background task** `{}` · `{}` · {} · {:.1}s · {}\",\n task.task_id,\n task.tool_name,\n format_background_task_status(&task.status),\n task.duration_secs,\n exit_code,\n );\n\n if let Some(preview) = normalize_background_task_preview(&task.output_preview) {\n message.push_str(&format!(\"\\n\\n```text\\n{}\\n```\", preview));\n } else {\n message.push_str(\"\\n\\n_No output captured._\");\n }\n\n message.push_str(&format!(\n \"\\n\\n_Full output:_ `bg action=\\\"output\\\" task_id=\\\"{}\\\"`\",\n task.task_id\n ));\n\n message\n}\n\n#[derive(Debug, Clone, PartialEq, Eq)]\npub struct ParsedBackgroundTaskNotification {\n pub task_id: String,\n pub tool_name: String,\n pub status: String,\n pub duration: String,\n pub exit_label: String,\n pub preview: Option<String>,\n pub full_output_command: String,\n}\n\npub fn parse_background_task_notification_markdown(\n content: &str,\n) -> Option<ParsedBackgroundTaskNotification> {\n static HEADER_RE: OnceLock<Regex> = OnceLock::new();\n static FULL_OUTPUT_RE: OnceLock<Regex> = OnceLock::new();\n\n let header_re = HEADER_RE.get_or_init(|| {\n Regex::new(\n r\"^\\*\\*Background task\\*\\* `(?P<task_id>[^`]+)` · `(?P<tool_name>[^`]+)` · (?P<status>.+?) · (?P<duration>[0-9]+(?:\\.[0-9]+)?s) · (?P<exit_label>.+)$\",\n )\n .expect(\"valid background task header regex\")\n });\n let full_output_re = FULL_OUTPUT_RE.get_or_init(|| {\n Regex::new(r#\"^_Full output:_ `(?P<command>[^`]+)`$\"#)\n .expect(\"valid background task full output regex\")\n });\n\n let normalized = content.replace(\"\\r\\n\", \"\\n\").replace('\\r', \"\\n\");\n let mut sections = normalized.split(\"\\n\\n\");\n let header = sections.next()?.trim();\n let captures = header_re.captures(header)?;\n\n let mut preview: Option<String> = None;\n let mut full_output_command: Option<String> = None;\n\n for section in sections {\n let trimmed = section.trim();\n if trimmed.is_empty() {\n continue;\n }\n\n if let Some(captures) = full_output_re.captures(trimmed) {\n full_output_command = Some(captures[\"command\"].to_string());\n continue;\n }\n\n if trimmed == \"_No output captured._\" {\n preview = None;\n continue;\n }\n\n if let Some(fenced) = trimmed\n .strip_prefix(\"```text\\n\")\n .and_then(|body| body.strip_suffix(\"\\n```\"))\n {\n preview = Some(fenced.to_string());\n }\n }\n\n Some(ParsedBackgroundTaskNotification {\n task_id: captures[\"task_id\"].to_string(),\n tool_name: captures[\"tool_name\"].to_string(),\n status: captures[\"status\"].to_string(),\n duration: captures[\"duration\"].to_string(),\n exit_label: captures[\"exit_label\"].to_string(),\n preview,\n full_output_command: full_output_command?,\n })\n}\n\npub fn background_task_status_notice(task: &BackgroundTaskCompleted) -> String {\n match task.status {\n BackgroundTaskStatus::Completed => {\n format!(\"Background task completed · {}\", task.tool_name)\n }\n BackgroundTaskStatus::Superseded => {\n format!(\"Background task superseded · {}\", task.tool_name)\n }\n BackgroundTaskStatus::Failed => match task.exit_code {\n Some(code) => format!(\n \"Background task failed · {} · exit {}\",\n task.tool_name, code\n ),\n None => format!(\"Background task failed · {}\", task.tool_name),\n },\n BackgroundTaskStatus::Running => format!(\"Background task running · {}\", task.tool_name),\n }\n}\n\n/// Role in conversation\n#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]\n#[serde(rename_all = \"lowercase\")]\npub enum Role {\n User,\n Assistant,\n}\n\n/// Plain-text tool output placeholder when execution was interrupted.\npub const TOOL_OUTPUT_MISSING_TEXT: &str =\n \"Tool output missing (session interrupted before tool execution completed)\";\n\n/// A message in the conversation\n#[derive(Debug, Clone, Serialize, Deserialize)]\npub struct Message {\n pub role: Role,\n pub content: Vec<ContentBlock>,\n #[serde(default, skip_serializing_if = \"Option::is_none\")]\n pub timestamp: Option<chrono::DateTime<Utc>>,\n #[serde(default, skip_serializing_if = \"Option::is_none\")]\n pub tool_duration_ms: Option<u64>,\n}\n\n/// Cache control metadata for prompt caching\n#[derive(Debug, Clone, Serialize, Deserialize)]\npub struct CacheControl {\n #[serde(rename = \"type\")]\n pub kind: String,\n #[serde(skip_serializing_if = \"Option::is_none\")]\n pub ttl: Option<String>,\n}\n\nimpl CacheControl {\n pub fn ephemeral(ttl: Option<String>) -> Self {\n Self {\n kind: \"ephemeral\".to_string(),\n ttl,\n }\n }\n}\n\n/// Content block within a message\n#[derive(Debug, Clone, Serialize, Deserialize)]\n#[serde(tag = \"type\", rename_all = \"snake_case\")]\npub enum ContentBlock {\n Text {\n text: String,\n #[serde(skip_serializing_if = \"Option::is_none\")]\n cache_control: Option<CacheControl>,\n },\n /// Hidden reasoning content used for providers that require it (not displayed)\n Reasoning {\n text: String,\n },\n ToolUse {\n id: String,\n name: String,\n input: serde_json::Value,\n },\n ToolResult {\n tool_use_id: String,\n content: String,\n #[serde(skip_serializing_if = \"Option::is_none\")]\n is_error: Option<bool>,\n },\n Image {\n media_type: String,\n data: String,\n },\n /// Hidden OpenAI Responses compaction item used to preserve native\n /// compaction state across turns/saves when jcode explicitly triggers it.\n OpenAICompaction {\n encrypted_content: String,\n },\n}\n\nimpl Message {\n pub fn user(text: &str) -> Self {\n Self {\n role: Role::User,\n content: vec![ContentBlock::Text {\n text: text.to_string(),\n cache_control: None,\n }],\n timestamp: Some(Utc::now()),\n tool_duration_ms: None,\n }\n }\n\n pub fn user_with_images(text: &str, images: Vec<(String, String)>) -> Self {\n let mut content: Vec<ContentBlock> = images\n .into_iter()\n .map(|(media_type, data)| ContentBlock::Image { media_type, data })\n .collect();\n content.push(ContentBlock::Text {\n text: text.to_string(),\n cache_control: None,\n });\n Self {\n role: Role::User,\n content,\n timestamp: Some(Utc::now()),\n tool_duration_ms: None,\n }\n }\n\n pub fn assistant_text(text: &str) -> Self {\n Self {\n role: Role::Assistant,\n content: vec![ContentBlock::Text {\n text: text.to_string(),\n cache_control: None,\n }],\n timestamp: Some(Utc::now()),\n tool_duration_ms: None,\n }\n }\n\n pub fn tool_result(tool_use_id: &str, content: &str, is_error: bool) -> Self {\n Self::tool_result_with_duration(tool_use_id, content, is_error, None)\n }\n\n pub fn tool_result_with_duration(\n tool_use_id: &str,\n content: &str,\n is_error: bool,\n tool_duration_ms: Option<u64>,\n ) -> Self {\n Self {\n role: Role::User,\n content: vec![ContentBlock::ToolResult {\n tool_use_id: tool_use_id.to_string(),\n content: content.to_string(),\n is_error: if is_error { Some(true) } else { None },\n }],\n timestamp: Some(Utc::now()),\n tool_duration_ms,\n }\n }\n\n /// Format a timestamp deterministically in UTC for injection into model-visible content.\n pub fn format_timestamp(ts: &chrono::DateTime<Utc>) -> String {\n ts.to_rfc3339_opts(chrono::SecondsFormat::Millis, true)\n }\n\n pub fn format_duration(duration_ms: u64) -> String {\n match duration_ms {\n 0..=999 => format!(\"{}ms\", duration_ms),\n 1_000..=9_999 => format!(\"{:.1}s\", duration_ms as f64 / 1000.0),\n 10_000..=59_999 => format!(\"{}s\", duration_ms / 1000),\n _ => {\n let total_seconds = duration_ms / 1000;\n let minutes = total_seconds / 60;\n let seconds = total_seconds % 60;\n if seconds == 0 {\n format!(\"{}m\", minutes)\n } else {\n format!(\"{}m {}s\", minutes, seconds)\n }\n }\n }\n }\n\n pub fn is_internal_system_reminder(&self) -> bool {\n self.content\n .iter()\n .find_map(|block| match block {\n ContentBlock::Text { text, .. } => Some(text.trim_start()),\n _ => None,\n })\n .is_some_and(|text| text.starts_with(\"<system-reminder>\"))\n }\n\n fn should_skip_timestamp_injection(&self) -> bool {\n self.is_internal_system_reminder()\n }\n\n fn tool_result_tag(&self, ts: &chrono::DateTime<Utc>) -> String {\n match self.tool_duration_ms {\n Some(duration_ms) => {\n let duration_ms_i64 = i64::try_from(duration_ms).unwrap_or(i64::MAX);\n let start_ts = ts\n .checked_sub_signed(chrono::Duration::milliseconds(duration_ms_i64))\n .unwrap_or(*ts);\n format!(\n \"[tool timing: start={} finish={} duration={}]\",\n Self::format_timestamp(&start_ts),\n Self::format_timestamp(ts),\n Self::format_duration(duration_ms)\n )\n }\n None => format!(\"[{}]\", Self::format_timestamp(ts)),\n }\n }\n\n /// Return a copy of messages with timestamps injected into user-role text content.\n /// Tool results get a stable UTC timing header prepended to content.\n /// User text messages get a stable UTC timestamp prepended to the first text block.\n pub fn with_timestamps(messages: &[Message]) -> Vec<Message> {\n messages\n .iter()\n .map(|msg| {\n let Some(ts) = msg.timestamp else {\n return msg.clone();\n };\n if msg.role != Role::User || msg.should_skip_timestamp_injection() {\n return msg.clone();\n }\n let text_tag = format!(\"[{}]\", Self::format_timestamp(&ts));\n let tool_result_tag = msg.tool_result_tag(&ts);\n let mut msg = msg.clone();\n let mut tagged = false;\n for block in &mut msg.content {\n match block {\n ContentBlock::Text { text, .. } if !tagged => {\n *text = format!(\"{} {}\", text_tag, text);\n tagged = true;\n }\n ContentBlock::ToolResult { content, .. } if !tagged => {\n *content = format!(\"{} {}\", tool_result_tag, content);\n tagged = true;\n }\n _ => {}\n }\n }\n msg\n })\n .collect()\n }\n}\n\nconst STABLE_HASH_SEED: u64 = 0xcbf29ce484222325;\nconst STABLE_HASH_PRIME: u64 = 0x100000001b3;\n\nfn stable_hash_bytes(bytes: &[u8]) -> u64 {\n let mut hash = STABLE_HASH_SEED;\n for byte in bytes {\n hash ^= *byte as u64;\n hash = hash.wrapping_mul(STABLE_HASH_PRIME);\n }\n hash\n}\n\npub(crate) fn extend_stable_hash(acc: u64, next: u64) -> u64 {\n stable_hash_bytes(&[acc.to_le_bytes().as_slice(), next.to_le_bytes().as_slice()].concat())\n}\n\npub(crate) fn stable_message_hash(message: &Message) -> u64 {\n match serde_json::to_vec(message) {\n Ok(bytes) => stable_hash_bytes(&bytes),\n Err(_) => stable_hash_bytes(format!(\"{:?}\", message).as_bytes()),\n }\n}\n\npub fn ends_with_fresh_user_turn(messages: &[Message]) -> bool {\n for msg in messages.iter().rev() {\n if msg.role != Role::User {\n return false;\n }\n\n if msg\n .content\n .iter()\n .any(|block| matches!(block, ContentBlock::ToolResult { .. }))\n {\n return false;\n }\n\n if msg.content.is_empty() {\n return false;\n }\n\n let mut saw_user_text = false;\n for block in &msg.content {\n match block {\n ContentBlock::Text { text, .. } => {\n let trimmed = text.trim();\n if !trimmed.is_empty() && !trimmed.starts_with(\"<system-reminder>\") {\n saw_user_text = true;\n }\n }\n _ => return false,\n }\n }\n\n if saw_user_text {\n return true;\n }\n\n if msg.is_internal_system_reminder() {\n continue;\n }\n\n return false;\n }\n\n false\n}\n\n/// Sanitize a tool ID so it matches the pattern `^[a-zA-Z0-9_-]+$`.\n///\n/// Different providers generate tool IDs in different formats. When switching\n/// from one provider to another mid-conversation, the historical tool IDs may\n/// contain characters that the new provider rejects (e.g., dots in Copilot IDs\n/// sent to Anthropic). This function replaces any invalid characters with\n/// underscores.\npub fn sanitize_tool_id(id: &str) -> String {\n if id.is_empty() {\n return \"unknown\".to_string();\n }\n let sanitized: String = id\n .chars()\n .map(|c| {\n if c.is_ascii_alphanumeric() || c == '_' || c == '-' {\n c\n } else {\n '_'\n }\n })\n .collect();\n if sanitized.is_empty() {\n \"unknown\".to_string()\n } else {\n sanitized\n }\n}\n\n/// Redact likely secrets from persisted tool output.\n///\n/// This is a best-effort safeguard for local session history files. It targets\n/// high-confidence token/key patterns and common `KEY=VALUE` assignments used by\n/// auth flows.\npub fn redact_secrets(text: &str) -> String {\n // Fast path to avoid regex work for most tool outputs.\n let lower = text.to_ascii_lowercase();\n\n if !text.contains(\"sk-\")\n && !text.contains(\"ghp_\")\n && !text.contains(\"github_pat_\")\n && !text.contains(\"AIza\")\n && !text.contains(\"ya29.\")\n && !text.contains(\"xox\")\n && !lower.contains(\"api_key\")\n && !lower.contains(\"token\")\n {\n return text.to_string();\n }\n\n static DIRECT_PATTERNS: OnceLock<Vec<Regex>> = OnceLock::new();\n static ASSIGNMENT_PATTERNS: OnceLock<Vec<Regex>> = OnceLock::new();\n\n let direct_patterns = DIRECT_PATTERNS.get_or_init(|| {\n vec![\n Regex::new(r\"sk-ant-(?:oat|ort)01-[A-Za-z0-9_-]{20,}\")\n .expect(\"valid Anthropic OAuth token regex\"),\n Regex::new(r\"sk-or-v1-[A-Za-z0-9_-]{20,}\").expect(\"valid OpenRouter key regex\"),\n Regex::new(r\"ghp_[A-Za-z0-9]{20,}\").expect(\"valid GitHub PAT regex\"),\n Regex::new(r\"github_pat_[A-Za-z0-9_]{20,}\")\n .expect(\"valid GitHub fine-grained PAT regex\"),\n Regex::new(r\"ya29\\.[A-Za-z0-9._-]{20,}\").expect(\"valid Google OAuth token regex\"),\n Regex::new(r\"AIza[0-9A-Za-z_-]{20,}\").expect(\"valid Google API key regex\"),\n Regex::new(r\"xox[baprs]-[A-Za-z0-9-]{10,}\").expect(\"valid Slack token regex\"),\n ]\n });\n\n let assignment_patterns = ASSIGNMENT_PATTERNS.get_or_init(|| {\n vec![\n Regex::new(r\"(?m)^\\s*(OPENROUTER_API_KEY\\s*=\\s*)[^\\r\\n]+\")\n .expect(\"valid OPENROUTER_API_KEY assignment regex\"),\n Regex::new(r\"(?m)^\\s*(OPENCODE_API_KEY\\s*=\\s*)[^\\r\\n]+\")\n .expect(\"valid OPENCODE_API_KEY assignment regex\"),\n Regex::new(r\"(?m)^\\s*(OPENCODE_GO_API_KEY\\s*=\\s*)[^\\r\\n]+\")\n .expect(\"valid OPENCODE_GO_API_KEY assignment regex\"),\n Regex::new(r\"(?m)^\\s*(ZHIPU_API_KEY\\s*=\\s*)[^\\r\\n]+\")\n .expect(\"valid ZHIPU_API_KEY assignment regex\"),\n Regex::new(r\"(?m)^\\s*(ZAI_API_KEY\\s*=\\s*)[^\\r\\n]+\")\n .expect(\"valid ZAI_API_KEY assignment regex\"),\n Regex::new(r\"(?m)^\\s*(302AI_API_KEY\\s*=\\s*)[^\\r\\n]+\")\n .expect(\"valid 302AI_API_KEY assignment regex\"),\n Regex::new(r\"(?m)^\\s*(BASETEN_API_KEY\\s*=\\s*)[^\\r\\n]+\")\n .expect(\"valid BASETEN_API_KEY assignment regex\"),\n Regex::new(r\"(?m)^\\s*(CORTECS_API_KEY\\s*=\\s*)[^\\r\\n]+\")\n .expect(\"valid CORTECS_API_KEY assignment regex\"),\n Regex::new(r\"(?m)^\\s*(DEEPSEEK_API_KEY\\s*=\\s*)[^\\r\\n]+\")\n .expect(\"valid DEEPSEEK_API_KEY assignment regex\"),\n Regex::new(r\"(?m)^\\s*(FIRMWARE_API_KEY\\s*=\\s*)[^\\r\\n]+\")\n .expect(\"valid FIRMWARE_API_KEY assignment regex\"),\n Regex::new(r\"(?m)^\\s*(HF_TOKEN\\s*=\\s*)[^\\r\\n]+\")\n .expect(\"valid HF_TOKEN assignment regex\"),\n Regex::new(r\"(?m)^\\s*(MOONSHOT_API_KEY\\s*=\\s*)[^\\r\\n]+\")\n .expect(\"valid MOONSHOT_API_KEY assignment regex\"),\n Regex::new(r\"(?m)^\\s*(NEBIUS_API_KEY\\s*=\\s*)[^\\r\\n]+\")\n .expect(\"valid NEBIUS_API_KEY assignment regex\"),\n Regex::new(r\"(?m)^\\s*(SCALEWAY_API_KEY\\s*=\\s*)[^\\r\\n]+\")\n .expect(\"valid SCALEWAY_API_KEY assignment regex\"),\n Regex::new(r\"(?m)^\\s*(STACKIT_API_KEY\\s*=\\s*)[^\\r\\n]+\")\n .expect(\"valid STACKIT_API_KEY assignment regex\"),\n Regex::new(r\"(?m)^\\s*(GROQ_API_KEY\\s*=\\s*)[^\\r\\n]+\")\n .expect(\"valid GROQ_API_KEY assignment regex\"),\n Regex::new(r\"(?m)^\\s*(MISTRAL_API_KEY\\s*=\\s*)[^\\r\\n]+\")\n .expect(\"valid MISTRAL_API_KEY assignment regex\"),\n Regex::new(r\"(?m)^\\s*(PERPLEXITY_API_KEY\\s*=\\s*)[^\\r\\n]+\")\n .expect(\"valid PERPLEXITY_API_KEY assignment regex\"),\n Regex::new(r\"(?m)^\\s*(TOGETHER_API_KEY\\s*=\\s*)[^\\r\\n]+\")\n .expect(\"valid TOGETHER_API_KEY assignment regex\"),\n Regex::new(r\"(?m)^\\s*(DEEPINFRA_API_KEY\\s*=\\s*)[^\\r\\n]+\")\n .expect(\"valid DEEPINFRA_API_KEY assignment regex\"),\n Regex::new(r\"(?m)^\\s*(XAI_API_KEY\\s*=\\s*)[^\\r\\n]+\")\n .expect(\"valid XAI_API_KEY assignment regex\"),\n Regex::new(r\"(?m)^\\s*(LMSTUDIO_API_KEY\\s*=\\s*)[^\\r\\n]+\")\n .expect(\"valid LMSTUDIO_API_KEY assignment regex\"),\n Regex::new(r\"(?m)^\\s*(OLLAMA_API_KEY\\s*=\\s*)[^\\r\\n]+\")\n .expect(\"valid OLLAMA_API_KEY assignment regex\"),\n Regex::new(r\"(?m)^\\s*(CHUTES_API_KEY\\s*=\\s*)[^\\r\\n]+\")\n .expect(\"valid CHUTES_API_KEY assignment regex\"),\n Regex::new(r\"(?m)^\\s*(CEREBRAS_API_KEY\\s*=\\s*)[^\\r\\n]+\")\n .expect(\"valid CEREBRAS_API_KEY assignment regex\"),\n Regex::new(r\"(?m)^\\s*(OPENAI_COMPAT_API_KEY\\s*=\\s*)[^\\r\\n]+\")\n .expect(\"valid OPENAI_COMPAT_API_KEY assignment regex\"),\n Regex::new(r\"(?m)^\\s*(ANTHROPIC_API_KEY\\s*=\\s*)[^\\r\\n]+\")\n .expect(\"valid ANTHROPIC_API_KEY assignment regex\"),\n Regex::new(r\"(?m)^\\s*(OPENAI_API_KEY\\s*=\\s*)[^\\r\\n]+\")\n .expect(\"valid OPENAI_API_KEY assignment regex\"),\n Regex::new(r\"(?m)^\\s*(AZURE_OPENAI_API_KEY\\s*=\\s*)[^\\r\\n]+\")\n .expect(\"valid AZURE_OPENAI_API_KEY assignment regex\"),\n Regex::new(r\"(?m)^\\s*(CURSOR_API_KEY\\s*=\\s*)[^\\r\\n]+\")\n .expect(\"valid CURSOR_API_KEY assignment regex\"),\n Regex::new(r\"(?m)^\\s*(GITHUB_TOKEN\\s*=\\s*)[^\\r\\n]+\")\n .expect(\"valid GITHUB_TOKEN assignment regex\"),\n ]\n });\n\n let mut redacted = text.to_string();\n let mut redacted_keys: HashSet<String> = [\n \"OPENROUTER_API_KEY\",\n \"OPENCODE_API_KEY\",\n \"OPENCODE_GO_API_KEY\",\n \"ZHIPU_API_KEY\",\n \"ZAI_API_KEY\",\n \"302AI_API_KEY\",\n \"BASETEN_API_KEY\",\n \"CORTECS_API_KEY\",\n \"DEEPSEEK_API_KEY\",\n \"FIRMWARE_API_KEY\",\n \"HF_TOKEN\",\n \"MOONSHOT_API_KEY\",\n \"NEBIUS_API_KEY\",\n \"SCALEWAY_API_KEY\",\n \"STACKIT_API_KEY\",\n \"GROQ_API_KEY\",\n \"MISTRAL_API_KEY\",\n \"PERPLEXITY_API_KEY\",\n \"TOGETHER_API_KEY\",\n \"DEEPINFRA_API_KEY\",\n \"XAI_API_KEY\",\n \"LMSTUDIO_API_KEY\",\n \"OLLAMA_API_KEY\",\n \"CHUTES_API_KEY\",\n \"CEREBRAS_API_KEY\",\n \"OPENAI_COMPAT_API_KEY\",\n \"ANTHROPIC_API_KEY\",\n \"OPENAI_API_KEY\",\n \"AZURE_OPENAI_API_KEY\",\n \"CURSOR_API_KEY\",\n \"GITHUB_TOKEN\",\n ]\n .iter()\n .map(|k| (*k).to_string())\n .collect();\n\n for re in direct_patterns {\n redacted = re.replace_all(&redacted, \"[REDACTED_SECRET]\").into_owned();\n }\n\n for re in assignment_patterns {\n redacted = re\n .replace_all(&redacted, \"${1}[REDACTED_SECRET]\")\n .into_owned();\n }\n\n // Also redact custom API key variable names configured at runtime.\n for source in [\n \"JCODE_OPENROUTER_API_KEY_NAME\",\n \"JCODE_OPENAI_COMPAT_API_KEY_NAME\",\n ] {\n let Some(key_name) = std::env::var(source)\n .ok()\n .map(|v| v.trim().to_string())\n .filter(|v| !v.is_empty())\n else {\n continue;\n };\n\n if !key_name\n .chars()\n .all(|c| c.is_ascii_uppercase() || c.is_ascii_digit() || c == '_')\n {\n continue;\n }\n if !redacted_keys.insert(key_name.clone()) {\n continue;\n }\n\n let pattern = format!(r\"(?m)^\\s*({}\\s*=\\s*)[^\\r\\n]+\", regex::escape(&key_name));\n if let Ok(re) = Regex::new(&pattern) {\n redacted = re\n .replace_all(&redacted, \"${1}[REDACTED_SECRET]\")\n .into_owned();\n }\n }\n\n redacted\n}\n\n/// Tool definition for the API\n#[derive(Debug, Clone, Serialize)]\npub struct ToolDefinition {\n pub name: String,\n // Prompt-visible text sent to the model by provider adapters.\n // Approximate prompt cost: description.len() / 4. Use\n // ToolDefinition::description_token_estimate() when reviewing tool bloat.\n pub description: String,\n pub input_schema: serde_json::Value,\n}\n\nimpl ToolDefinition {\n /// Serialized size of the full tool definition payload sent to providers.\n pub fn prompt_chars(&self) -> usize {\n serde_json::json!({\n \"name\": self.name,\n \"description\": self.description,\n \"input_schema\": self.input_schema,\n })\n .to_string()\n .len()\n }\n\n /// Approximate prompt-token cost of this tool's top-level description.\n ///\n /// This uses jcode's standard chars/4 heuristic, matching other token\n /// budget estimates in the codebase.\n pub fn description_token_estimate(&self) -> usize {\n crate::util::estimate_tokens(&self.description)\n }\n\n /// Approximate prompt-token cost of the full tool definition payload.\n pub fn prompt_token_estimate(&self) -> usize {\n crate::util::estimate_tokens(\n &serde_json::json!({\n \"name\": self.name,\n \"description\": self.description,\n \"input_schema\": self.input_schema,\n })\n .to_string(),\n )\n }\n\n pub fn aggregate_prompt_chars(defs: &[ToolDefinition]) -> usize {\n defs.iter().map(Self::prompt_chars).sum()\n }\n\n pub fn aggregate_prompt_token_estimate(defs: &[ToolDefinition]) -> usize {\n defs.iter().map(Self::prompt_token_estimate).sum()\n }\n}\n\n/// A tool call from the model\n#[derive(Debug, Clone, Default, PartialEq, Eq, serde::Serialize, serde::Deserialize)]\npub struct ToolCall {\n #[serde(default)]\n pub id: String,\n #[serde(default)]\n pub name: String,\n #[serde(default)]\n pub input: serde_json::Value,\n #[serde(default, skip_serializing_if = \"Option::is_none\")]\n pub intent: Option<String>,\n}\n\n/// Connection phase for status bar transparency\n#[derive(Debug, Clone, PartialEq)]\npub enum ConnectionPhase {\n /// Refreshing OAuth token\n Authenticating,\n /// TCP + TLS connection to API\n Connecting,\n /// HTTP request sent, waiting for first response byte\n WaitingForResponse,\n /// First byte received, stream is active\n Streaming,\n /// Retrying after a transient error\n Retrying { attempt: u32, max: u32 },\n}\n\nimpl std::fmt::Display for ConnectionPhase {\n fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {\n match self {\n ConnectionPhase::Authenticating => write!(f, \"authenticating\"),\n ConnectionPhase::Connecting => write!(f, \"connecting\"),\n ConnectionPhase::WaitingForResponse => write!(f, \"waiting for response\"),\n ConnectionPhase::Streaming => write!(f, \"streaming\"),\n ConnectionPhase::Retrying { attempt, max } => {\n write!(f, \"retrying ({}/{})\", attempt, max)\n }\n }\n }\n}\n\n/// Streaming event from provider\n#[derive(Debug, Clone)]\npub enum StreamEvent {\n /// Text content delta\n TextDelta(String),\n /// Tool use started\n ToolUseStart { id: String, name: String },\n /// Tool input delta (JSON fragment)\n ToolInputDelta(String),\n /// Tool use complete\n ToolUseEnd,\n /// Tool result from provider (provider already executed the tool)\n ToolResult {\n tool_use_id: String,\n content: String,\n is_error: bool,\n },\n /// Extended thinking started\n ThinkingStart,\n /// Extended thinking delta (reasoning content)\n ThinkingDelta(String),\n /// Extended thinking ended\n ThinkingEnd,\n /// Extended thinking completed with duration\n ThinkingDone { duration_secs: f64 },\n /// Message complete (may have stop reason)\n MessageEnd { stop_reason: Option<String> },\n /// Token usage update\n TokenUsage {\n input_tokens: Option<u64>,\n output_tokens: Option<u64>,\n cache_read_input_tokens: Option<u64>,\n cache_creation_input_tokens: Option<u64>,\n },\n /// Active transport/connection type for this stream\n ConnectionType { connection: String },\n /// Connection phase update (for status bar transparency)\n ConnectionPhase { phase: ConnectionPhase },\n /// Provider-supplied human-readable transport detail for the status line\n StatusDetail { detail: String },\n /// Error occurred\n Error {\n message: String,\n /// Seconds until rate limit resets (if this is a rate limit error)\n retry_after_secs: Option<u64>,\n },\n /// Provider session ID (for conversation resume)\n SessionId(String),\n /// Compaction occurred (context was summarized)\n Compaction {\n trigger: String,\n pre_tokens: Option<u64>,\n /// Provider-native compaction artifact, if one was emitted.\n openai_encrypted_content: Option<String>,\n },\n /// Upstream provider info (e.g., which provider OpenRouter routed to)\n UpstreamProvider { provider: String },\n /// Native tool call from a provider bridge that needs execution by jcode\n NativeToolCall {\n request_id: String,\n tool_name: String,\n input: serde_json::Value,\n },\n}\n\n#[cfg(test)]\nmod tests {\n use super::*;\n\n #[test]\n fn sanitize_tool_id_alphanumeric_passthrough() {\n assert_eq!(\n sanitize_tool_id(\"toolu_01XFDUDYJgAACzvnptvVer6u\"),\n \"toolu_01XFDUDYJgAACzvnptvVer6u\"\n );\n assert_eq!(sanitize_tool_id(\"call_abc123\"), \"call_abc123\");\n assert_eq!(\n sanitize_tool_id(\"call_1234567890_9876543210\"),\n \"call_1234567890_9876543210\"\n );\n }\n\n #[test]\n fn sanitize_tool_id_hyphens_passthrough() {\n assert_eq!(sanitize_tool_id(\"call-abc-123\"), \"call-abc-123\");\n assert_eq!(\n sanitize_tool_id(\"tool_use-id_with-mixed\"),\n \"tool_use-id_with-mixed\"\n );\n }\n\n #[test]\n fn sanitize_tool_id_replaces_dots() {\n assert_eq!(\n sanitize_tool_id(\"chatcmpl-abc.def.ghi\"),\n \"chatcmpl-abc_def_ghi\"\n );\n assert_eq!(sanitize_tool_id(\"call.123\"), \"call_123\");\n }\n\n #[test]\n fn sanitize_tool_id_replaces_colons() {\n assert_eq!(sanitize_tool_id(\"call:123:456\"), \"call_123_456\");\n }\n\n #[test]\n fn sanitize_tool_id_replaces_special_chars() {\n assert_eq!(\n sanitize_tool_id(\"id@with#special$chars\"),\n \"id_with_special_chars\"\n );\n assert_eq!(sanitize_tool_id(\"id with spaces\"), \"id_with_spaces\");\n }\n\n #[test]\n fn sanitize_tool_id_empty_returns_unknown() {\n assert_eq!(sanitize_tool_id(\"\"), \"unknown\");\n }\n\n #[test]\n fn sanitize_tool_id_copilot_to_anthropic() {\n assert_eq!(\n sanitize_tool_id(\"chatcmpl-BF2xX.tool_call.0\"),\n \"chatcmpl-BF2xX_tool_call_0\"\n );\n }\n\n #[test]\n fn sanitize_tool_id_already_valid_unchanged() {\n let valid_ids = [\n \"toolu_01XFDUDYJgAACzvnptvVer6u\",\n \"call_abc123\",\n \"fallback_text_call_call_1234567890_9876543210\",\n \"tool_123\",\n \"a\",\n \"A\",\n \"0\",\n \"_\",\n \"-\",\n \"a-b_c\",\n ];\n for id in valid_ids {\n assert_eq!(sanitize_tool_id(id), id, \"ID '{}' should be unchanged\", id);\n }\n }\n\n #[test]\n fn redact_secrets_redacts_known_direct_token_formats() {\n let input = \"access=sk-ant-oat01-ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789\\nopenrouter=sk-or-v1-abcdefghijklmnopqrstuvwxyz0123456789\\ngithub=ghp_ABCDEFGHIJKLMNOPQRSTUVWXYZ0123\\n\";\n let out = redact_secrets(input);\n assert!(!out.contains(\"sk-ant-oat01-\"));\n assert!(!out.contains(\"sk-or-v1-\"));\n assert!(!out.contains(\"ghp_ABCDEFGHIJKLMNOPQRSTUVWXYZ0123\"));\n assert!(out.matches(\"[REDACTED_SECRET]\").count() >= 3);\n }\n\n #[test]\n fn redact_secrets_redacts_env_style_assignments() {\n let input = \"OPENROUTER_API_KEY=sk-or-v1-abc123abc123abc123abc123\\nOPENCODE_API_KEY=oc_test_secret\\nOPENCODE_GO_API_KEY=ocgo_test_secret\\nZAI_API_KEY=zai_secret\\nCHUTES_API_KEY=chutes_secret\\nCEREBRAS_API_KEY=cerebras_secret\\nOPENAI_COMPAT_API_KEY=compat_secret\\nCURSOR_API_KEY='my_cursor_secret_value'\\nOPENAI_API_KEY=sk-test-openai-example\\nAZURE_OPENAI_API_KEY=azure-openai-secret\\n\";\n let out = redact_secrets(input);\n assert!(out.contains(\"OPENROUTER_API_KEY=[REDACTED_SECRET]\"));\n assert!(out.contains(\"OPENCODE_API_KEY=[REDACTED_SECRET]\"));\n assert!(out.contains(\"OPENCODE_GO_API_KEY=[REDACTED_SECRET]\"));\n assert!(out.contains(\"ZAI_API_KEY=[REDACTED_SECRET]\"));\n assert!(out.contains(\"CHUTES_API_KEY=[REDACTED_SECRET]\"));\n assert!(out.contains(\"CEREBRAS_API_KEY=[REDACTED_SECRET]\"));\n assert!(out.contains(\"OPENAI_COMPAT_API_KEY=[REDACTED_SECRET]\"));\n assert!(out.contains(\"CURSOR_API_KEY=[REDACTED_SECRET]\"));\n assert!(out.contains(\"OPENAI_API_KEY=[REDACTED_SECRET]\"));\n assert!(out.contains(\"AZURE_OPENAI_API_KEY=[REDACTED_SECRET]\"));\n assert!(!out.contains(\"my_cursor_secret_value\"));\n }\n\n #[test]\n fn redact_secrets_redacts_runtime_key_assignment() {\n let key_var = \"JCODE_OPENAI_COMPAT_API_KEY_NAME\";\n let prev = std::env::var(key_var).ok();\n crate::env::set_var(key_var, \"GROQ_API_KEY\");\n\n let input = \"GROQ_API_KEY=my_secret_token_value\";\n let out = redact_secrets(input);\n assert_eq!(out, \"GROQ_API_KEY=[REDACTED_SECRET]\");\n\n if let Some(v) = prev {\n crate::env::set_var(key_var, v);\n } else {\n crate::env::remove_var(key_var);\n }\n }\n\n #[test]\n fn redact_secrets_redacts_mixed_case_token_assignments() {\n let input = \"my_token=ya29.ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789\";\n let out = redact_secrets(input);\n assert!(out.contains(\"[REDACTED_SECRET]\"));\n assert!(!out.contains(\"ya29.ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789\"));\n }\n\n #[test]\n fn redact_secrets_leaves_normal_output_unchanged() {\n let input = \"Found 5 files\\nNo auth errors\\nDone.\";\n assert_eq!(redact_secrets(input), input);\n }\n\n #[test]\n fn format_timestamp_is_stable_utc_rfc3339() {\n let ts = chrono::DateTime::parse_from_rfc3339(\"2025-03-15T02:24:13.250Z\")\n .unwrap()\n .with_timezone(&Utc);\n assert_eq!(Message::format_timestamp(&ts), \"2025-03-15T02:24:13.250Z\");\n }\n\n #[test]\n fn with_timestamps_prepends_utc_prefix_to_user_text() {\n let ts = chrono::DateTime::parse_from_rfc3339(\"2025-03-15T02:24:03Z\")\n .unwrap()\n .with_timezone(&Utc);\n let stamped = Message::with_timestamps(&[Message {\n role: Role::User,\n content: vec![ContentBlock::Text {\n text: \"hello\".to_string(),\n cache_control: None,\n }],\n timestamp: Some(ts),\n tool_duration_ms: None,\n }]);\n match &stamped[0].content[0] {\n ContentBlock::Text { text, .. } => {\n assert_eq!(text, \"[2025-03-15T02:24:03.000Z] hello\");\n }\n other => panic!(\"expected text block, got {other:?}\"),\n }\n }\n\n #[test]\n fn with_timestamps_adds_tool_timing_header_with_duration() {\n let ts = chrono::DateTime::parse_from_rfc3339(\"2025-03-15T02:24:13Z\")\n .unwrap()\n .with_timezone(&Utc);\n let stamped = Message::with_timestamps(&[Message {\n role: Role::User,\n content: vec![ContentBlock::ToolResult {\n tool_use_id: \"call_1\".to_string(),\n content: \"ok\".to_string(),\n is_error: None,\n }],\n timestamp: Some(ts),\n tool_duration_ms: Some(3_200),\n }]);\n match &stamped[0].content[0] {\n ContentBlock::ToolResult { content, .. } => {\n assert_eq!(\n content,\n \"[tool timing: start=2025-03-15T02:24:09.800Z finish=2025-03-15T02:24:13.000Z duration=3.2s] ok\"\n );\n }\n other => panic!(\"expected tool result block, got {other:?}\"),\n }\n }\n\n #[test]\n fn with_timestamps_skips_internal_system_reminders() {\n let ts = chrono::DateTime::parse_from_rfc3339(\"2025-03-15T02:24:13Z\")\n .unwrap()\n .with_timezone(&Utc);\n let original = Message {\n role: Role::User,\n content: vec![ContentBlock::Text {\n text: \"<system-reminder>\\ninternal\\n</system-reminder>\".to_string(),\n cache_control: None,\n }],\n timestamp: Some(ts),\n tool_duration_ms: None,\n };\n let stamped = Message::with_timestamps(std::slice::from_ref(&original));\n match &stamped[0].content[0] {\n ContentBlock::Text { text, .. } => {\n assert_eq!(text, \"<system-reminder>\\ninternal\\n</system-reminder>\");\n }\n other => panic!(\"expected text block, got {other:?}\"),\n }\n }\n\n #[test]\n fn ends_with_fresh_user_turn_accepts_plain_user_text() {\n let messages = vec![Message::user(\"hello\")];\n assert!(ends_with_fresh_user_turn(&messages));\n }\n\n #[test]\n fn ends_with_fresh_user_turn_rejects_trailing_tool_result() {\n let messages = vec![\n Message::user(\"hello\"),\n Message {\n role: Role::Assistant,\n content: vec![ContentBlock::ToolUse {\n id: \"call_1\".to_string(),\n name: \"bash\".to_string(),\n input: serde_json::json!({}),\n }],\n timestamp: Some(Utc::now()),\n tool_duration_ms: None,\n },\n Message::tool_result(\"call_1\", \"ok\", false),\n ];\n assert!(!ends_with_fresh_user_turn(&messages));\n }\n\n #[test]\n fn ends_with_fresh_user_turn_skips_internal_system_reminders() {\n let messages = vec![\n Message::user(\"hello\"),\n Message::user(\"<system-reminder>\\ninternal\\n</system-reminder>\"),\n ];\n assert!(ends_with_fresh_user_turn(&messages));\n }\n\n #[test]\n fn ends_with_fresh_user_turn_rejects_assistant_tail() {\n let messages = vec![\n Message::user(\"hello\"),\n Message::assistant_text(\"working on it\"),\n ];\n assert!(!ends_with_fresh_user_turn(&messages));\n }\n\n #[test]\n fn format_background_task_notification_markdown_uses_code_block_preview() {\n let rendered = format_background_task_notification_markdown(&BackgroundTaskCompleted {\n task_id: \"abc123\".to_string(),\n tool_name: \"bash\".to_string(),\n session_id: \"session\".to_string(),\n status: BackgroundTaskStatus::Completed,\n exit_code: Some(0),\n output_preview: \"[stderr] first line\\n[stdout] second line\\n\".to_string(),\n output_file: std::path::PathBuf::from(\"/tmp/output.log\"),\n duration_secs: 7.1,\n notify: true,\n wake: false,\n });\n\n assert!(\n rendered\n .contains(\"**Background task** `abc123` · `bash` · ✓ completed · 7.1s · exit 0\")\n );\n assert!(rendered.contains(\"```text\\n[stderr] first line\\n[stdout] second line\\n```\"));\n assert!(rendered.contains(\"_Full output:_ `bg action=\\\"output\\\" task_id=\\\"abc123\\\"`\"));\n }\n\n #[test]\n fn format_background_task_notification_markdown_handles_empty_preview() {\n let rendered = format_background_task_notification_markdown(&BackgroundTaskCompleted {\n task_id: \"abc123\".to_string(),\n tool_name: \"bash\".to_string(),\n session_id: \"session\".to_string(),\n status: BackgroundTaskStatus::Failed,\n exit_code: Some(9),\n output_preview: \"\\n\\n\".to_string(),\n output_file: std::path::PathBuf::from(\"/tmp/output.log\"),\n duration_secs: 1.0,\n notify: true,\n wake: false,\n });\n\n assert!(rendered.contains(\"✗ failed\"));\n assert!(rendered.contains(\"_No output captured._\"));\n }\n\n #[test]\n fn format_background_task_notification_markdown_renders_superseded_status() {\n let rendered = format_background_task_notification_markdown(&BackgroundTaskCompleted {\n task_id: \"abc123\".to_string(),\n tool_name: \"selfdev-build\".to_string(),\n session_id: \"session\".to_string(),\n status: BackgroundTaskStatus::Superseded,\n exit_code: Some(0),\n output_preview: \"Build completed, but source changed before activation\".to_string(),\n output_file: std::path::PathBuf::from(\"/tmp/output.log\"),\n duration_secs: 5.0,\n notify: true,\n wake: false,\n });\n\n assert!(rendered.contains(\"↻ superseded\"));\n assert!(rendered.contains(\"exit 0\"));\n assert!(rendered.contains(\"source changed before activation\"));\n }\n\n #[test]\n fn description_token_estimate_uses_chars_per_token_heuristic() {\n let def = ToolDefinition {\n name: \"read\".to_string(),\n description: \"abcdwxyz\".to_string(),\n input_schema: serde_json::json!({\"type\": \"object\"}),\n };\n\n assert_eq!(def.description_token_estimate(), 2);\n }\n\n #[test]\n fn parse_background_task_notification_markdown_extracts_fields() {\n let rendered = format_background_task_notification_markdown(&BackgroundTaskCompleted {\n task_id: \"abc123\".to_string(),\n tool_name: \"bash\".to_string(),\n session_id: \"session\".to_string(),\n status: BackgroundTaskStatus::Completed,\n exit_code: Some(0),\n output_preview: \"[stderr] first line\\n[stdout] second line\\n\".to_string(),\n output_file: std::path::PathBuf::from(\"/tmp/output.log\"),\n duration_secs: 7.1,\n notify: true,\n wake: false,\n });\n\n let parsed = parse_background_task_notification_markdown(&rendered)\n .expect(\"background task notification should parse\");\n assert_eq!(parsed.task_id, \"abc123\");\n assert_eq!(parsed.tool_name, \"bash\");\n assert_eq!(parsed.status, \"✓ completed\");\n assert_eq!(parsed.duration, \"7.1s\");\n assert_eq!(parsed.exit_label, \"exit 0\");\n assert_eq!(\n parsed.preview.as_deref(),\n Some(\"[stderr] first line\\n[stdout] second line\")\n );\n assert_eq!(\n parsed.full_output_command,\n \"bg action=\\\"output\\\" task_id=\\\"abc123\\\"\"\n );\n }\n}\n"}, "5845c2412645c3ab9b2ebed068aa0de048886273": {"t": 1785757239.500089, "n": 44166, "h": "6c4b9ebf9ecace1a", "c": "#![allow(dead_code)]\n\nuse crate::bus::{BackgroundTaskCompleted, BackgroundTaskStatus};\nuse chrono::Utc;\nuse regex::Regex;\nuse serde::{Deserialize, Serialize};\nuse std::collections::HashSet;\nuse std::sync::OnceLock;\n\n#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]\npub struct InputShellResult {\n pub command: String,\n #[serde(default, skip_serializing_if = \"Option::is_none\")]\n pub cwd: Option<String>,\n pub output: String,\n #[serde(default, skip_serializing_if = \"Option::is_none\")]\n pub exit_code: Option<i32>,\n pub duration_ms: u64,\n #[serde(default)]\n pub truncated: bool,\n #[serde(default)]\n pub failed_to_start: bool,\n}\n\nfn sanitize_fenced_block(text: &str) -> String {\n text.replace(\"```\", \"``\\u{200b}`\")\n}\n\npub fn format_input_shell_result_markdown(shell: &InputShellResult) -> String {\n let status = if shell.failed_to_start {\n \"✗ failed to start\".to_string()\n } else if shell.exit_code == Some(0) {\n \"✓ exit 0\".to_string()\n } else if let Some(code) = shell.exit_code {\n format!(\"✗ exit {}\", code)\n } else {\n \"✗ terminated\".to_string()\n };\n\n let mut meta = vec![status, Message::format_duration(shell.duration_ms)];\n if let Some(cwd) = shell.cwd.as_deref() {\n meta.push(format!(\"cwd `{}`\", cwd));\n }\n if shell.truncated {\n meta.push(\"truncated\".to_string());\n }\n\n let mut message = format!(\n \"**Shell command** · {}\\n\\n```bash\\n{}\\n```\",\n meta.join(\" · \"),\n sanitize_fenced_block(&shell.command)\n );\n\n if shell.output.trim().is_empty() {\n message.push_str(\"\\n\\n_No output._\");\n } else {\n message.push_str(&format!(\n \"\\n\\n```text\\n{}\\n```\",\n sanitize_fenced_block(shell.output.trim_end())\n ));\n }\n\n message\n}\n\npub fn input_shell_status_notice(shell: &InputShellResult) -> String {\n if shell.failed_to_start {\n \"Shell command failed to start\".to_string()\n } else if shell.exit_code == Some(0) {\n \"Shell command completed\".to_string()\n } else if let Some(code) = shell.exit_code {\n format!(\"Shell command failed (exit {})\", code)\n } else {\n \"Shell command terminated\".to_string()\n }\n}\n\nfn format_background_task_status(status: &BackgroundTaskStatus) -> &'static str {\n match status {\n BackgroundTaskStatus::Completed => \"✓ completed\",\n BackgroundTaskStatus::Superseded => \"↻ superseded\",\n BackgroundTaskStatus::Failed => \"✗ failed\",\n BackgroundTaskStatus::Running => \"running\",\n }\n}\n\nfn normalize_background_task_preview(preview: &str) -> Option<String> {\n let normalized = preview.replace(\"\\r\\n\", \"\\n\").replace('\\r', \"\\n\");\n let trimmed = normalized.trim_end();\n if trimmed.trim().is_empty() {\n None\n } else {\n Some(sanitize_fenced_block(trimmed))\n }\n}\n\npub fn format_background_task_notification_markdown(task: &BackgroundTaskCompleted) -> String {\n let exit_code = task\n .exit_code\n .map(|code| format!(\"exit {}\", code))\n .unwrap_or_else(|| \"exit n/a\".to_string());\n\n let mut message = format!(\n \"**Background task** `{}` · `{}` · {} · {:.1}s · {}\",\n task.task_id,\n task.tool_name,\n format_background_task_status(&task.status),\n task.duration_secs,\n exit_code,\n );\n\n if let Some(preview) = normalize_background_task_preview(&task.output_preview) {\n message.push_str(&format!(\"\\n\\n```text\\n{}\\n```\", preview));\n } else {\n message.push_str(\"\\n\\n_No output captured._\");\n }\n\n message.push_str(&format!(\n \"\\n\\n_Full output:_ `bg action=\\\"output\\\" task_id=\\\"{}\\\"`\",\n task.task_id\n ));\n\n message\n}\n\n#[derive(Debug, Clone, PartialEq, Eq)]\npub struct ParsedBackgroundTaskNotification {\n pub task_id: String,\n pub tool_name: String,\n pub status: String,\n pub duration: String,\n pub exit_label: String,\n pub preview: Option<String>,\n pub full_output_command: String,\n}\n\npub fn parse_background_task_notification_markdown(\n content: &str,\n) -> Option<ParsedBackgroundTaskNotification> {\n static HEADER_RE: OnceLock<Regex> = OnceLock::new();\n static FULL_OUTPUT_RE: OnceLock<Regex> = OnceLock::new();\n\n let header_re = HEADER_RE.get_or_init(|| {\n Regex::new(\n r\"^\\*\\*Background task\\*\\* `(?P<task_id>[^`]+)` · `(?P<tool_name>[^`]+)` · (?P<status>.+?) · (?P<duration>[0-9]+(?:\\.[0-9]+)?s) · (?P<exit_label>.+)$\",\n )\n .expect(\"valid background task header regex\")\n });\n let full_output_re = FULL_OUTPUT_RE.get_or_init(|| {\n Regex::new(r#\"^_Full output:_ `(?P<command>[^`]+)`$\"#)\n .expect(\"valid background task full output regex\")\n });\n\n let normalized = content.replace(\"\\r\\n\", \"\\n\").replace('\\r', \"\\n\");\n let mut sections = normalized.split(\"\\n\\n\");\n let header = sections.next()?.trim();\n let captures = header_re.captures(header)?;\n\n let mut preview: Option<String> = None;\n let mut full_output_command: Option<String> = None;\n\n for section in sections {\n let trimmed = section.trim();\n if trimmed.is_empty() {\n continue;\n }\n\n if let Some(captures) = full_output_re.captures(trimmed) {\n full_output_command = Some(captures[\"command\"].to_string());\n continue;\n }\n\n if trimmed == \"_No output captured._\" {\n preview = None;\n continue;\n }\n\n if let Some(fenced) = trimmed\n .strip_prefix(\"```text\\n\")\n .and_then(|body| body.strip_suffix(\"\\n```\"))\n {\n preview = Some(fenced.to_string());\n }\n }\n\n Some(ParsedBackgroundTaskNotification {\n task_id: captures[\"task_id\"].to_string(),\n tool_name: captures[\"tool_name\"].to_string(),\n status: captures[\"status\"].to_string(),\n duration: captures[\"duration\"].to_string(),\n exit_label: captures[\"exit_label\"].to_string(),\n preview,\n full_output_command: full_output_command?,\n })\n}\n\npub fn background_task_status_notice(task: &BackgroundTaskCompleted) -> String {\n match task.status {\n BackgroundTaskStatus::Completed => {\n format!(\"Background task completed · {}\", task.tool_name)\n }\n BackgroundTaskStatus::Superseded => {\n format!(\"Background task superseded · {}\", task.tool_name)\n }\n BackgroundTaskStatus::Failed => match task.exit_code {\n Some(code) => format!(\n \"Background task failed · {} · exit {}\",\n task.tool_name, code\n ),\n None => format!(\"Background task failed · {}\", task.tool_name),\n },\n BackgroundTaskStatus::Running => format!(\"Background task running · {}\", task.tool_name),\n }\n}\n\n/// Role in conversation\n#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]\n#[serde(rename_all = \"lowercase\")]\npub enum Role {\n User,\n Assistant,\n}\n\n/// Plain-text tool output placeholder when execution was interrupted.\npub const TOOL_OUTPUT_MISSING_TEXT: &str =\n \"Tool output missing (session interrupted before tool execution completed)\";\n\n/// A message in the conversation\n#[derive(Debug, Clone, Serialize, Deserialize)]\npub struct Message {\n pub role: Role,\n pub content: Vec<ContentBlock>,\n #[serde(default, skip_serializing_if = \"Option::is_none\")]\n pub timestamp: Option<chrono::DateTime<Utc>>,\n #[serde(default, skip_serializing_if = \"Option::is_none\")]\n pub tool_duration_ms: Option<u64>,\n}\n\n/// Cache control metadata for prompt caching\n#[derive(Debug, Clone, Serialize, Deserialize)]\npub struct CacheControl {\n #[serde(rename = \"type\")]\n pub kind: String,\n #[serde(skip_serializing_if = \"Option::is_none\")]\n pub ttl: Option<String>,\n}\n\nimpl CacheControl {\n pub fn ephemeral(ttl: Option<String>) -> Self {\n Self {\n kind: \"ephemeral\".to_string(),\n ttl,\n }\n }\n}\n\n/// Content block within a message\n#[derive(Debug, Clone, Serialize, Deserialize)]\n#[serde(tag = \"type\", rename_all = \"snake_case\")]\npub enum ContentBlock {\n Text {\n text: String,\n #[serde(skip_serializing_if = \"Option::is_none\")]\n cache_control: Option<CacheControl>,\n },\n /// Hidden reasoning content used for providers that require it (not displayed)\n Reasoning {\n text: String,\n },\n ToolUse {\n id: String,\n name: String,\n input: serde_json::Value,\n },\n ToolResult {\n tool_use_id: String,\n content: String,\n #[serde(skip_serializing_if = \"Option::is_none\")]\n is_error: Option<bool>,\n },\n Image {\n media_type: String,\n data: String,\n },\n /// Hidden OpenAI Responses compaction item used to preserve native\n /// compaction state across turns/saves when jcode explicitly triggers it.\n OpenAICompaction {\n encrypted_content: String,\n },\n}\n\nimpl Message {\n pub fn user(text: &str) -> Self {\n Self {\n role: Role::User,\n content: vec![ContentBlock::Text {\n text: text.to_string(),\n cache_control: None,\n }],\n timestamp: Some(Utc::now()),\n tool_duration_ms: None,\n }\n }\n\n pub fn user_with_images(text: &str, images: Vec<(String, String)>) -> Self {\n let mut content: Vec<ContentBlock> = images\n .into_iter()\n .map(|(media_type, data)| ContentBlock::Image { media_type, data })\n .collect();\n content.push(ContentBlock::Text {\n text: text.to_string(),\n cache_control: None,\n });\n Self {\n role: Role::User,\n content,\n timestamp: Some(Utc::now()),\n tool_duration_ms: None,\n }\n }\n\n pub fn assistant_text(text: &str) -> Self {\n Self {\n role: Role::Assistant,\n content: vec![ContentBlock::Text {\n text: text.to_string(),\n cache_control: None,\n }],\n timestamp: Some(Utc::now()),\n tool_duration_ms: None,\n }\n }\n\n pub fn tool_result(tool_use_id: &str, content: &str, is_error: bool) -> Self {\n Self::tool_result_with_duration(tool_use_id, content, is_error, None)\n }\n\n pub fn tool_result_with_duration(\n tool_use_id: &str,\n content: &str,\n is_error: bool,\n tool_duration_ms: Option<u64>,\n ) -> Self {\n Self {\n role: Role::User,\n content: vec![ContentBlock::ToolResult {\n tool_use_id: tool_use_id.to_string(),\n content: content.to_string(),\n is_error: if is_error { Some(true) } else { None },\n }],\n timestamp: Some(Utc::now()),\n tool_duration_ms,\n }\n }\n\n /// Format a timestamp deterministically in UTC for injection into model-visible content.\n pub fn format_timestamp(ts: &chrono::DateTime<Utc>) -> String {\n ts.to_rfc3339_opts(chrono::SecondsFormat::Millis, true)\n }\n\n pub fn format_duration(duration_ms: u64) -> String {\n match duration_ms {\n 0..=999 => format!(\"{}ms\", duration_ms),\n 1_000..=9_999 => format!(\"{:.1}s\", duration_ms as f64 / 1000.0),\n 10_000..=59_999 => format!(\"{}s\", duration_ms / 1000),\n _ => {\n let total_seconds = duration_ms / 1000;\n let minutes = total_seconds / 60;\n let seconds = total_seconds % 60;\n if seconds == 0 {\n format!(\"{}m\", minutes)\n } else {\n format!(\"{}m {}s\", minutes, seconds)\n }\n }\n }\n }\n\n pub fn is_internal_system_reminder(&self) -> bool {\n self.content\n .iter()\n .find_map(|block| match block {\n ContentBlock::Text { text, .. } => Some(text.trim_start()),\n _ => None,\n })\n .is_some_and(|text| text.starts_with(\"<system-reminder>\"))\n }\n\n fn should_skip_timestamp_injection(&self) -> bool {\n self.is_internal_system_reminder()\n }\n\n fn tool_result_tag(&self, ts: &chrono::DateTime<Utc>) -> String {\n match self.tool_duration_ms {\n Some(duration_ms) => {\n let duration_ms_i64 = i64::try_from(duration_ms).unwrap_or(i64::MAX);\n let start_ts = ts\n .checked_sub_signed(chrono::Duration::milliseconds(duration_ms_i64))\n .unwrap_or(*ts);\n format!(\n \"[tool timing: start={} finish={} duration={}]\",\n Self::format_timestamp(&start_ts),\n Self::format_timestamp(ts),\n Self::format_duration(duration_ms)\n )\n }\n None => format!(\"[{}]\", Self::format_timestamp(ts)),\n }\n }\n\n /// Return a copy of messages with timestamps injected into user-role text content.\n /// Tool results get a stable UTC timing header prepended to content.\n /// User text messages get a stable UTC timestamp prepended to the first text block.\n pub fn with_timestamps(messages: &[Message]) -> Vec<Message> {\n messages\n .iter()\n .map(|msg| {\n let Some(ts) = msg.timestamp else {\n return msg.clone();\n };\n if msg.role != Role::User || msg.should_skip_timestamp_injection() {\n return msg.clone();\n }\n let text_tag = format!(\"[{}]\", Self::format_timestamp(&ts));\n let tool_result_tag = msg.tool_result_tag(&ts);\n let mut msg = msg.clone();\n let mut tagged = false;\n for block in &mut msg.content {\n match block {\n ContentBlock::Text { text, .. } if !tagged => {\n *text = format!(\"{} {}\", text_tag, text);\n tagged = true;\n }\n ContentBlock::ToolResult { content, .. } if !tagged => {\n *content = format!(\"{} {}\", tool_result_tag, content);\n tagged = true;\n }\n _ => {}\n }\n }\n msg\n })\n .collect()\n }\n}\n\nconst STABLE_HASH_SEED: u64 = 0xcbf29ce484222325;\nconst STABLE_HASH_PRIME: u64 = 0x100000001b3;\n\nfn stable_hash_bytes(bytes: &[u8]) -> u64 {\n let mut hash = STABLE_HASH_SEED;\n for byte in bytes {\n hash ^= *byte as u64;\n hash = hash.wrapping_mul(STABLE_HASH_PRIME);\n }\n hash\n}\n\npub(crate) fn extend_stable_hash(acc: u64, next: u64) -> u64 {\n stable_hash_bytes(&[acc.to_le_bytes().as_slice(), next.to_le_bytes().as_slice()].concat())\n}\n\npub(crate) fn stable_message_hash(message: &Message) -> u64 {\n match serde_json::to_vec(message) {\n Ok(bytes) => stable_hash_bytes(&bytes),\n Err(_) => stable_hash_bytes(format!(\"{:?}\", message).as_bytes()),\n }\n}\n\npub fn ends_with_fresh_user_turn(messages: &[Message]) -> bool {\n for msg in messages.iter().rev() {\n if msg.role != Role::User {\n return false;\n }\n\n if msg\n .content\n .iter()\n .any(|block| matches!(block, ContentBlock::ToolResult { .. }))\n {\n return false;\n }\n\n if msg.content.is_empty() {\n return false;\n }\n\n let mut saw_user_text = false;\n for block in &msg.content {\n match block {\n ContentBlock::Text { text, .. } => {\n let trimmed = text.trim();\n if !trimmed.is_empty() && !trimmed.starts_with(\"<system-reminder>\") {\n saw_user_text = true;\n }\n }\n _ => return false,\n }\n }\n\n if saw_user_text {\n return true;\n }\n\n if msg.is_internal_system_reminder() {\n continue;\n }\n\n return false;\n }\n\n false\n}\n\n/// Sanitize a tool ID so it matches the pattern `^[a-zA-Z0-9_-]+$`.\n///\n/// Different providers generate tool IDs in different formats. When switching\n/// from one provider to another mid-conversation, the historical tool IDs may\n/// contain characters that the new provider rejects (e.g., dots in Copilot IDs\n/// sent to Anthropic). This function replaces any invalid characters with\n/// underscores.\npub fn sanitize_tool_id(id: &str) -> String {\n if id.is_empty() {\n return \"unknown\".to_string();\n }\n let sanitized: String = id\n .chars()\n .map(|c| {\n if c.is_ascii_alphanumeric() || c == '_' || c == '-' {\n c\n } else {\n '_'\n }\n })\n .collect();\n if sanitized.is_empty() {\n \"unknown\".to_string()\n } else {\n sanitized\n }\n}\n\n/// Redact likely secrets from persisted tool output.\n///\n/// This is a best-effort safeguard for local session history files. It targets\n/// high-confidence token/key patterns and common `KEY=VALUE` assignments used by\n/// auth flows.\npub fn redact_secrets(text: &str) -> String {\n // Fast path to avoid regex work for most tool outputs.\n let lower = text.to_ascii_lowercase();\n\n if !text.contains(\"sk-\")\n && !text.contains(\"ghp_\")\n && !text.contains(\"github_pat_\")\n && !text.contains(\"AIza\")\n && !text.contains(\"ya29.\")\n && !text.contains(\"xox\")\n && !lower.contains(\"api_key\")\n && !lower.contains(\"token\")\n {\n return text.to_string();\n }\n\n static DIRECT_PATTERNS: OnceLock<Vec<Regex>> = OnceLock::new();\n static ASSIGNMENT_PATTERNS: OnceLock<Vec<Regex>> = OnceLock::new();\n\n let direct_patterns = DIRECT_PATTERNS.get_or_init(|| {\n vec![\n Regex::new(r\"sk-ant-(?:oat|ort)01-[A-Za-z0-9_-]{20,}\")\n .expect(\"valid Anthropic OAuth token regex\"),\n Regex::new(r\"sk-or-v1-[A-Za-z0-9_-]{20,}\").expect(\"valid OpenRouter key regex\"),\n Regex::new(r\"ghp_[A-Za-z0-9]{20,}\").expect(\"valid GitHub PAT regex\"),\n Regex::new(r\"github_pat_[A-Za-z0-9_]{20,}\")\n .expect(\"valid GitHub fine-grained PAT regex\"),\n Regex::new(r\"ya29\\.[A-Za-z0-9._-]{20,}\").expect(\"valid Google OAuth token regex\"),\n Regex::new(r\"AIza[0-9A-Za-z_-]{20,}\").expect(\"valid Google API key regex\"),\n Regex::new(r\"xox[baprs]-[A-Za-z0-9-]{10,}\").expect(\"valid Slack token regex\"),\n ]\n });\n\n let assignment_patterns = ASSIGNMENT_PATTERNS.get_or_init(|| {\n vec![\n Regex::new(r\"(?m)^\\s*(OPENROUTER_API_KEY\\s*=\\s*)[^\\r\\n]+\")\n .expect(\"valid OPENROUTER_API_KEY assignment regex\"),\n Regex::new(r\"(?m)^\\s*(OPENCODE_API_KEY\\s*=\\s*)[^\\r\\n]+\")\n .expect(\"valid OPENCODE_API_KEY assignment regex\"),\n Regex::new(r\"(?m)^\\s*(OPENCODE_GO_API_KEY\\s*=\\s*)[^\\r\\n]+\")\n .expect(\"valid OPENCODE_GO_API_KEY assignment regex\"),\n Regex::new(r\"(?m)^\\s*(ZHIPU_API_KEY\\s*=\\s*)[^\\r\\n]+\")\n .expect(\"valid ZHIPU_API_KEY assignment regex\"),\n Regex::new(r\"(?m)^\\s*(ZAI_API_KEY\\s*=\\s*)[^\\r\\n]+\")\n .expect(\"valid ZAI_API_KEY assignment regex\"),\n Regex::new(r\"(?m)^\\s*(302AI_API_KEY\\s*=\\s*)[^\\r\\n]+\")\n .expect(\"valid 302AI_API_KEY assignment regex\"),\n Regex::new(r\"(?m)^\\s*(BASETEN_API_KEY\\s*=\\s*)[^\\r\\n]+\")\n .expect(\"valid BASETEN_API_KEY assignment regex\"),\n Regex::new(r\"(?m)^\\s*(CORTECS_API_KEY\\s*=\\s*)[^\\r\\n]+\")\n .expect(\"valid CORTECS_API_KEY assignment regex\"),\n Regex::new(r\"(?m)^\\s*(DEEPSEEK_API_KEY\\s*=\\s*)[^\\r\\n]+\")\n .expect(\"valid DEEPSEEK_API_KEY assignment regex\"),\n Regex::new(r\"(?m)^\\s*(FIRMWARE_API_KEY\\s*=\\s*)[^\\r\\n]+\")\n .expect(\"valid FIRMWARE_API_KEY assignment regex\"),\n Regex::new(r\"(?m)^\\s*(HF_TOKEN\\s*=\\s*)[^\\r\\n]+\")\n .expect(\"valid HF_TOKEN assignment regex\"),\n Regex::new(r\"(?m)^\\s*(MOONSHOT_API_KEY\\s*=\\s*)[^\\r\\n]+\")\n .expect(\"valid MOONSHOT_API_KEY assignment regex\"),\n Regex::new(r\"(?m)^\\s*(NEBIUS_API_KEY\\s*=\\s*)[^\\r\\n]+\")\n .expect(\"valid NEBIUS_API_KEY assignment regex\"),\n Regex::new(r\"(?m)^\\s*(SCALEWAY_API_KEY\\s*=\\s*)[^\\r\\n]+\")\n .expect(\"valid SCALEWAY_API_KEY assignment regex\"),\n Regex::new(r\"(?m)^\\s*(STACKIT_API_KEY\\s*=\\s*)[^\\r\\n]+\")\n .expect(\"valid STACKIT_API_KEY assignment regex\"),\n Regex::new(r\"(?m)^\\s*(GROQ_API_KEY\\s*=\\s*)[^\\r\\n]+\")\n .expect(\"valid GROQ_API_KEY assignment regex\"),\n Regex::new(r\"(?m)^\\s*(MISTRAL_API_KEY\\s*=\\s*)[^\\r\\n]+\")\n .expect(\"valid MISTRAL_API_KEY assignment regex\"),\n Regex::new(r\"(?m)^\\s*(PERPLEXITY_API_KEY\\s*=\\s*)[^\\r\\n]+\")\n .expect(\"valid PERPLEXITY_API_KEY assignment regex\"),\n Regex::new(r\"(?m)^\\s*(TOGETHER_API_KEY\\s*=\\s*)[^\\r\\n]+\")\n .expect(\"valid TOGETHER_API_KEY assignment regex\"),\n Regex::new(r\"(?m)^\\s*(DEEPINFRA_API_KEY\\s*=\\s*)[^\\r\\n]+\")\n .expect(\"valid DEEPINFRA_API_KEY assignment regex\"),\n Regex::new(r\"(?m)^\\s*(XAI_API_KEY\\s*=\\s*)[^\\r\\n]+\")\n .expect(\"valid XAI_API_KEY assignment regex\"),\n Regex::new(r\"(?m)^\\s*(LMSTUDIO_API_KEY\\s*=\\s*)[^\\r\\n]+\")\n .expect(\"valid LMSTUDIO_API_KEY assignment regex\"),\n Regex::new(r\"(?m)^\\s*(OLLAMA_API_KEY\\s*=\\s*)[^\\r\\n]+\")\n .expect(\"valid OLLAMA_API_KEY assignment regex\"),\n Regex::new(r\"(?m)^\\s*(CHUTES_API_KEY\\s*=\\s*)[^\\r\\n]+\")\n .expect(\"valid CHUTES_API_KEY assignment regex\"),\n Regex::new(r\"(?m)^\\s*(CEREBRAS_API_KEY\\s*=\\s*)[^\\r\\n]+\")\n .expect(\"valid CEREBRAS_API_KEY assignment regex\"),\n Regex::new(r\"(?m)^\\s*(OPENAI_COMPAT_API_KEY\\s*=\\s*)[^\\r\\n]+\")\n .expect(\"valid OPENAI_COMPAT_API_KEY assignment regex\"),\n Regex::new(r\"(?m)^\\s*(ANTHROPIC_API_KEY\\s*=\\s*)[^\\r\\n]+\")\n .expect(\"valid ANTHROPIC_API_KEY assignment regex\"),\n Regex::new(r\"(?m)^\\s*(OPENAI_API_KEY\\s*=\\s*)[^\\r\\n]+\")\n .expect(\"valid OPENAI_API_KEY assignment regex\"),\n Regex::new(r\"(?m)^\\s*(AZURE_OPENAI_API_KEY\\s*=\\s*)[^\\r\\n]+\")\n .expect(\"valid AZURE_OPENAI_API_KEY assignment regex\"),\n Regex::new(r\"(?m)^\\s*(CURSOR_API_KEY\\s*=\\s*)[^\\r\\n]+\")\n .expect(\"valid CURSOR_API_KEY assignment regex\"),\n Regex::new(r\"(?m)^\\s*(GITHUB_TOKEN\\s*=\\s*)[^\\r\\n]+\")\n .expect(\"valid GITHUB_TOKEN assignment regex\"),\n ]\n });\n\n let mut redacted = text.to_string();\n let mut redacted_keys: HashSet<String> = [\n \"OPENROUTER_API_KEY\",\n \"OPENCODE_API_KEY\",\n \"OPENCODE_GO_API_KEY\",\n \"ZHIPU_API_KEY\",\n \"ZAI_API_KEY\",\n \"302AI_API_KEY\",\n \"BASETEN_API_KEY\",\n \"CORTECS_API_KEY\",\n \"DEEPSEEK_API_KEY\",\n \"FIRMWARE_API_KEY\",\n \"HF_TOKEN\",\n \"MOONSHOT_API_KEY\",\n \"NEBIUS_API_KEY\",\n \"SCALEWAY_API_KEY\",\n \"STACKIT_API_KEY\",\n \"GROQ_API_KEY\",\n \"MISTRAL_API_KEY\",\n \"PERPLEXITY_API_KEY\",\n \"TOGETHER_API_KEY\",\n \"DEEPINFRA_API_KEY\",\n \"XAI_API_KEY\",\n \"LMSTUDIO_API_KEY\",\n \"OLLAMA_API_KEY\",\n \"CHUTES_API_KEY\",\n \"CEREBRAS_API_KEY\",\n \"OPENAI_COMPAT_API_KEY\",\n \"ANTHROPIC_API_KEY\",\n \"OPENAI_API_KEY\",\n \"AZURE_OPENAI_API_KEY\",\n \"CURSOR_API_KEY\",\n \"GITHUB_TOKEN\",\n ]\n .iter()\n .map(|k| (*k).to_string())\n .collect();\n\n for re in direct_patterns {\n redacted = re.replace_all(&redacted, \"[REDACTED_SECRET]\").into_owned();\n }\n\n for re in assignment_patterns {\n redacted = re\n .replace_all(&redacted, \"${1}[REDACTED_SECRET]\")\n .into_owned();\n }\n\n // Also redact custom API key variable names configured at runtime.\n for source in [\n \"JCODE_OPENROUTER_API_KEY_NAME\",\n \"JCODE_OPENAI_COMPAT_API_KEY_NAME\",\n ] {\n let Some(key_name) = std::env::var(source)\n .ok()\n .map(|v| v.trim().to_string())\n .filter(|v| !v.is_empty())\n else {\n continue;\n };\n\n if !key_name\n .chars()\n .all(|c| c.is_ascii_uppercase() || c.is_ascii_digit() || c == '_')\n {\n continue;\n }\n if !redacted_keys.insert(key_name.clone()) {\n continue;\n }\n\n let pattern = format!(r\"(?m)^\\s*({}\\s*=\\s*)[^\\r\\n]+\", regex::escape(&key_name));\n if let Ok(re) = Regex::new(&pattern) {\n redacted = re\n .replace_all(&redacted, \"${1}[REDACTED_SECRET]\")\n .into_owned();\n }\n }\n\n redacted\n}\n\n/// Tool definition for the API\n#[derive(Debug, Clone, Serialize)]\npub struct ToolDefinition {\n pub name: String,\n // Prompt-visible text sent to the model by provider adapters.\n // Approximate prompt cost: description.len() / 4. Use\n // ToolDefinition::description_token_estimate() when reviewing tool bloat.\n pub description: String,\n pub input_schema: serde_json::Value,\n}\n\nimpl ToolDefinition {\n /// Serialized size of the full tool definition payload sent to providers.\n pub fn prompt_chars(&self) -> usize {\n serde_json::json!({\n \"name\": self.name,\n \"description\": self.description,\n \"input_schema\": self.input_schema,\n })\n .to_string()\n .len()\n }\n\n /// Approximate prompt-token cost of this tool's top-level description.\n ///\n /// This uses jcode's standard chars/4 heuristic, matching other token\n /// budget estimates in the codebase.\n pub fn description_token_estimate(&self) -> usize {\n crate::util::estimate_tokens(&self.description)\n }\n\n /// Approximate prompt-token cost of the full tool definition payload.\n pub fn prompt_token_estimate(&self) -> usize {\n crate::util::estimate_tokens(\n &serde_json::json!({\n \"name\": self.name,\n \"description\": self.description,\n \"input_schema\": self.input_schema,\n })\n .to_string(),\n )\n }\n\n pub fn aggregate_prompt_chars(defs: &[ToolDefinition]) -> usize {\n defs.iter().map(Self::prompt_chars).sum()\n }\n\n pub fn aggregate_prompt_token_estimate(defs: &[ToolDefinition]) -> usize {\n defs.iter().map(Self::prompt_token_estimate).sum()\n }\n}\n\n/// A tool call from the model\n#[derive(Debug, Clone, Default, PartialEq, Eq, serde::Serialize, serde::Deserialize)]\npub struct ToolCall {\n #[serde(default)]\n pub id: String,\n #[serde(default)]\n pub name: String,\n #[serde(default)]\n pub input: serde_json::Value,\n #[serde(default, skip_serializing_if = \"Option::is_none\")]\n pub intent: Option<String>,\n}\n\n/// Connection phase for status bar transparency\n#[derive(Debug, Clone, PartialEq)]\npub enum ConnectionPhase {\n /// Refreshing OAuth token\n Authenticating,\n /// TCP + TLS connection to API\n Connecting,\n /// HTTP request sent, waiting for first response byte\n WaitingForResponse,\n /// First byte received, stream is active\n Streaming,\n /// Retrying after a transient error\n Retrying { attempt: u32, max: u32 },\n}\n\nimpl std::fmt::Display for ConnectionPhase {\n fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {\n match self {\n ConnectionPhase::Authenticating => write!(f, \"authenticating\"),\n ConnectionPhase::Connecting => write!(f, \"connecting\"),\n ConnectionPhase::WaitingForResponse => write!(f, \"waiting for response\"),\n ConnectionPhase::Streaming => write!(f, \"streaming\"),\n ConnectionPhase::Retrying { attempt, max } => {\n write!(f, \"retrying ({}/{})\", attempt, max)\n }\n }\n }\n}\n\n/// Streaming event from provider\n#[derive(Debug, Clone)]\npub enum StreamEvent {\n /// Text content delta\n TextDelta(String),\n /// Tool use started\n ToolUseStart { id: String, name: String },\n /// Tool input delta (JSON fragment)\n ToolInputDelta(String),\n /// Tool use complete\n ToolUseEnd,\n /// Tool result from provider (provider already executed the tool)\n ToolResult {\n tool_use_id: String,\n content: String,\n is_error: bool,\n },\n /// Extended thinking started\n ThinkingStart,\n /// Extended thinking delta (reasoning content)\n ThinkingDelta(String),\n /// Extended thinking ended\n ThinkingEnd,\n /// Extended thinking completed with duration\n ThinkingDone { duration_secs: f64 },\n /// Message complete (may have stop reason)\n MessageEnd { stop_reason: Option<String> },\n /// Token usage update\n TokenUsage {\n input_tokens: Option<u64>,\n output_tokens: Option<u64>,\n cache_read_input_tokens: Option<u64>,\n cache_creation_input_tokens: Option<u64>,\n },\n /// Active transport/connection type for this stream\n ConnectionType { connection: String },\n /// Connection phase update (for status bar transparency)\n ConnectionPhase { phase: ConnectionPhase },\n /// Provider-supplied human-readable transport detail for the status line\n StatusDetail { detail: String },\n /// Error occurred\n Error {\n message: String,\n /// Seconds until rate limit resets (if this is a rate limit error)\n retry_after_secs: Option<u64>,\n },\n /// Provider session ID (for conversation resume)\n SessionId(String),\n /// Compaction occurred (context was summarized)\n Compaction {\n trigger: String,\n pre_tokens: Option<u64>,\n /// Provider-native compaction artifact, if one was emitted.\n openai_encrypted_content: Option<String>,\n },\n /// Upstream provider info (e.g., which provider OpenRouter routed to)\n UpstreamProvider { provider: String },\n /// Native tool call from a provider bridge that needs execution by jcode\n NativeToolCall {\n request_id: String,\n tool_name: String,\n input: serde_json::Value,\n },\n}\n\n#[cfg(test)]\nmod tests {\n use super::*;\n\n #[test]\n fn sanitize_tool_id_alphanumeric_passthrough() {\n assert_eq!(\n sanitize_tool_id(\"toolu_01XFDUDYJgAACzvnptvVer6u\"),\n \"toolu_01XFDUDYJgAACzvnptvVer6u\"\n );\n assert_eq!(sanitize_tool_id(\"call_abc123\"), \"call_abc123\");\n assert_eq!(\n sanitize_tool_id(\"call_1234567890_9876543210\"),\n \"call_1234567890_9876543210\"\n );\n }\n\n #[test]\n fn sanitize_tool_id_hyphens_passthrough() {\n assert_eq!(sanitize_tool_id(\"call-abc-123\"), \"call-abc-123\");\n assert_eq!(\n sanitize_tool_id(\"tool_use-id_with-mixed\"),\n \"tool_use-id_with-mixed\"\n );\n }\n\n #[test]\n fn sanitize_tool_id_replaces_dots() {\n assert_eq!(\n sanitize_tool_id(\"chatcmpl-abc.def.ghi\"),\n \"chatcmpl-abc_def_ghi\"\n );\n assert_eq!(sanitize_tool_id(\"call.123\"), \"call_123\");\n }\n\n #[test]\n fn sanitize_tool_id_replaces_colons() {\n assert_eq!(sanitize_tool_id(\"call:123:456\"), \"call_123_456\");\n }\n\n #[test]\n fn sanitize_tool_id_replaces_special_chars() {\n assert_eq!(\n sanitize_tool_id(\"id@with#special$chars\"),\n \"id_with_special_chars\"\n );\n assert_eq!(sanitize_tool_id(\"id with spaces\"), \"id_with_spaces\");\n }\n\n #[test]\n fn sanitize_tool_id_empty_returns_unknown() {\n assert_eq!(sanitize_tool_id(\"\"), \"unknown\");\n }\n\n #[test]\n fn sanitize_tool_id_copilot_to_anthropic() {\n assert_eq!(\n sanitize_tool_id(\"chatcmpl-BF2xX.tool_call.0\"),\n \"chatcmpl-BF2xX_tool_call_0\"\n );\n }\n\n #[test]\n fn sanitize_tool_id_already_valid_unchanged() {\n let valid_ids = [\n \"toolu_01XFDUDYJgAACzvnptvVer6u\",\n \"call_abc123\",\n \"fallback_text_call_call_1234567890_9876543210\",\n \"tool_123\",\n \"a\",\n \"A\",\n \"0\",\n \"_\",\n \"-\",\n \"a-b_c\",\n ];\n for id in valid_ids {\n assert_eq!(sanitize_tool_id(id), id, \"ID '{}' should be unchanged\", id);\n }\n }\n\n #[test]\n fn redact_secrets_redacts_known_direct_token_formats() {\n let input = \"access=sk-ant-oat01-ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789\\nopenrouter=sk-or-v1-abcdefghijklmnopqrstuvwxyz0123456789\\ngithub=ghp_ABCDEFGHIJKLMNOPQRSTUVWXYZ0123\\n\";\n let out = redact_secrets(input);\n assert!(!out.contains(\"sk-ant-oat01-\"));\n assert!(!out.contains(\"sk-or-v1-\"));\n assert!(!out.contains(\"ghp_ABCDEFGHIJKLMNOPQRSTUVWXYZ0123\"));\n assert!(out.matches(\"[REDACTED_SECRET]\").count() >= 3);\n }\n\n #[test]\n fn redact_secrets_redacts_env_style_assignments() {\n let input = \"OPENROUTER_API_KEY=sk-or-v1-abc123abc123abc123abc123\\nOPENCODE_API_KEY=oc_test_secret\\nOPENCODE_GO_API_KEY=ocgo_test_secret\\nZAI_API_KEY=zai_secret\\nCHUTES_API_KEY=chutes_secret\\nCEREBRAS_API_KEY=cerebras_secret\\nOPENAI_COMPAT_API_KEY=compat_secret\\nCURSOR_API_KEY='my_cursor_secret_value'\\nOPENAI_API_KEY=sk-test-openai-example\\nAZURE_OPENAI_API_KEY=azure-openai-secret\\n\";\n let out = redact_secrets(input);\n assert!(out.contains(\"OPENROUTER_API_KEY=[REDACTED_SECRET]\"));\n assert!(out.contains(\"OPENCODE_API_KEY=[REDACTED_SECRET]\"));\n assert!(out.contains(\"OPENCODE_GO_API_KEY=[REDACTED_SECRET]\"));\n assert!(out.contains(\"ZAI_API_KEY=[REDACTED_SECRET]\"));\n assert!(out.contains(\"CHUTES_API_KEY=[REDACTED_SECRET]\"));\n assert!(out.contains(\"CEREBRAS_API_KEY=[REDACTED_SECRET]\"));\n assert!(out.contains(\"OPENAI_COMPAT_API_KEY=[REDACTED_SECRET]\"));\n assert!(out.contains(\"CURSOR_API_KEY=[REDACTED_SECRET]\"));\n assert!(out.contains(\"OPENAI_API_KEY=[REDACTED_SECRET]\"));\n assert!(out.contains(\"AZURE_OPENAI_API_KEY=[REDACTED_SECRET]\"));\n assert!(!out.contains(\"my_cursor_secret_value\"));\n }\n\n #[test]\n fn redact_secrets_redacts_runtime_key_assignment() {\n let key_var = \"JCODE_OPENAI_COMPAT_API_KEY_NAME\";\n let prev = std::env::var(key_var).ok();\n crate::env::set_var(key_var, \"GROQ_API_KEY\");\n\n let input = \"GROQ_API_KEY=my_secret_token_value\";\n let out = redact_secrets(input);\n assert_eq!(out, \"GROQ_API_KEY=[REDACTED_SECRET]\");\n\n if let Some(v) = prev {\n crate::env::set_var(key_var, v);\n } else {\n crate::env::remove_var(key_var);\n }\n }\n\n #[test]\n fn redact_secrets_redacts_mixed_case_token_assignments() {\n let input = \"my_token=ya29.ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789\";\n let out = redact_secrets(input);\n assert!(out.contains(\"[REDACTED_SECRET]\"));\n assert!(!out.contains(\"ya29.ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789\"));\n }\n\n #[test]\n fn redact_secrets_leaves_normal_output_unchanged() {\n let input = \"Found 5 files\\nNo auth errors\\nDone.\";\n assert_eq!(redact_secrets(input), input);\n }\n\n #[test]\n fn format_timestamp_is_stable_utc_rfc3339() {\n let ts = chrono::DateTime::parse_from_rfc3339(\"2025-03-15T02:24:13.250Z\")\n .unwrap()\n .with_timezone(&Utc);\n assert_eq!(Message::format_timestamp(&ts), \"2025-03-15T02:24:13.250Z\");\n }\n\n #[test]\n fn with_timestamps_prepends_utc_prefix_to_user_text() {\n let ts = chrono::DateTime::parse_from_rfc3339(\"2025-03-15T02:24:03Z\")\n .unwrap()\n .with_timezone(&Utc);\n let stamped = Message::with_timestamps(&[Message {\n role: Role::User,\n content: vec![ContentBlock::Text {\n text: \"hello\".to_string(),\n cache_control: None,\n }],\n timestamp: Some(ts),\n tool_duration_ms: None,\n }]);\n match &stamped[0].content[0] {\n ContentBlock::Text { text, .. } => {\n assert_eq!(text, \"[2025-03-15T02:24:03.000Z] hello\");\n }\n other => panic!(\"expected text block, got {other:?}\"),\n }\n }\n\n #[test]\n fn with_timestamps_adds_tool_timing_header_with_duration() {\n let ts = chrono::DateTime::parse_from_rfc3339(\"2025-03-15T02:24:13Z\")\n .unwrap()\n .with_timezone(&Utc);\n let stamped = Message::with_timestamps(&[Message {\n role: Role::User,\n content: vec![ContentBlock::ToolResult {\n tool_use_id: \"call_1\".to_string(),\n content: \"ok\".to_string(),\n is_error: None,\n }],\n timestamp: Some(ts),\n tool_duration_ms: Some(3_200),\n }]);\n match &stamped[0].content[0] {\n ContentBlock::ToolResult { content, .. } => {\n assert_eq!(\n content,\n \"[tool timing: start=2025-03-15T02:24:09.800Z finish=2025-03-15T02:24:13.000Z duration=3.2s] ok\"\n );\n }\n other => panic!(\"expected tool result block, got {other:?}\"),\n }\n }\n\n #[test]\n fn with_timestamps_skips_internal_system_reminders() {\n let ts = chrono::DateTime::parse_from_rfc3339(\"2025-03-15T02:24:13Z\")\n .unwrap()\n .with_timezone(&Utc);\n let original = Message {\n role: Role::User,\n content: vec![ContentBlock::Text {\n text: \"<system-reminder>\\ninternal\\n</system-reminder>\".to_string(),\n cache_control: None,\n }],\n timestamp: Some(ts),\n tool_duration_ms: None,\n };\n let stamped = Message::with_timestamps(&[original.clone()]);\n match &stamped[0].content[0] {\n ContentBlock::Text { text, .. } => {\n assert_eq!(text, \"<system-reminder>\\ninternal\\n</system-reminder>\");\n }\n other => panic!(\"expected text block, got {other:?}\"),\n }\n }\n\n #[test]\n fn ends_with_fresh_user_turn_accepts_plain_user_text() {\n let messages = vec![Message::user(\"hello\")];\n assert!(ends_with_fresh_user_turn(&messages));\n }\n\n #[test]\n fn ends_with_fresh_user_turn_rejects_trailing_tool_result() {\n let messages = vec![\n Message::user(\"hello\"),\n Message {\n role: Role::Assistant,\n content: vec![ContentBlock::ToolUse {\n id: \"call_1\".to_string(),\n name: \"bash\".to_string(),\n input: serde_json::json!({}),\n }],\n timestamp: Some(Utc::now()),\n tool_duration_ms: None,\n },\n Message::tool_result(\"call_1\", \"ok\", false),\n ];\n assert!(!ends_with_fresh_user_turn(&messages));\n }\n\n #[test]\n fn ends_with_fresh_user_turn_skips_internal_system_reminders() {\n let messages = vec![\n Message::user(\"hello\"),\n Message::user(\"<system-reminder>\\ninternal\\n</system-reminder>\"),\n ];\n assert!(ends_with_fresh_user_turn(&messages));\n }\n\n #[test]\n fn ends_with_fresh_user_turn_rejects_assistant_tail() {\n let messages = vec![\n Message::user(\"hello\"),\n Message::assistant_text(\"working on it\"),\n ];\n assert!(!ends_with_fresh_user_turn(&messages));\n }\n\n #[test]\n fn format_background_task_notification_markdown_uses_code_block_preview() {\n let rendered = format_background_task_notification_markdown(&BackgroundTaskCompleted {\n task_id: \"abc123\".to_string(),\n tool_name: \"bash\".to_string(),\n session_id: \"session\".to_string(),\n status: BackgroundTaskStatus::Completed,\n exit_code: Some(0),\n output_preview: \"[stderr] first line\\n[stdout] second line\\n\".to_string(),\n output_file: std::path::PathBuf::from(\"/tmp/output.log\"),\n duration_secs: 7.1,\n notify: true,\n wake: false,\n });\n\n assert!(\n rendered\n .contains(\"**Background task** `abc123` · `bash` · ✓ completed · 7.1s · exit 0\")\n );\n assert!(rendered.contains(\"```text\\n[stderr] first line\\n[stdout] second line\\n```\"));\n assert!(rendered.contains(\"_Full output:_ `bg action=\\\"output\\\" task_id=\\\"abc123\\\"`\"));\n }\n\n #[test]\n fn format_background_task_notification_markdown_handles_empty_preview() {\n let rendered = format_background_task_notification_markdown(&BackgroundTaskCompleted {\n task_id: \"abc123\".to_string(),\n tool_name: \"bash\".to_string(),\n session_id: \"session\".to_string(),\n status: BackgroundTaskStatus::Failed,\n exit_code: Some(9),\n output_preview: \"\\n\\n\".to_string(),\n output_file: std::path::PathBuf::from(\"/tmp/output.log\"),\n duration_secs: 1.0,\n notify: true,\n wake: false,\n });\n\n assert!(rendered.contains(\"✗ failed\"));\n assert!(rendered.contains(\"_No output captured._\"));\n }\n\n #[test]\n fn format_background_task_notification_markdown_renders_superseded_status() {\n let rendered = format_background_task_notification_markdown(&BackgroundTaskCompleted {\n task_id: \"abc123\".to_string(),\n tool_name: \"selfdev-build\".to_string(),\n session_id: \"session\".to_string(),\n status: BackgroundTaskStatus::Superseded,\n exit_code: Some(0),\n output_preview: \"Build completed, but source changed before activation\".to_string(),\n output_file: std::path::PathBuf::from(\"/tmp/output.log\"),\n duration_secs: 5.0,\n notify: true,\n wake: false,\n });\n\n assert!(rendered.contains(\"↻ superseded\"));\n assert!(rendered.contains(\"exit 0\"));\n assert!(rendered.contains(\"source changed before activation\"));\n }\n\n #[test]\n fn description_token_estimate_uses_chars_per_token_heuristic() {\n let def = ToolDefinition {\n name: \"read\".to_string(),\n description: \"abcdwxyz\".to_string(),\n input_schema: serde_json::json!({\"type\": \"object\"}),\n };\n\n assert_eq!(def.description_token_estimate(), 2);\n }\n\n #[test]\n fn parse_background_task_notification_markdown_extracts_fields() {\n let rendered = format_background_task_notification_markdown(&BackgroundTaskCompleted {\n task_id: \"abc123\".to_string(),\n tool_name: \"bash\".to_string(),\n session_id: \"session\".to_string(),\n status: BackgroundTaskStatus::Completed,\n exit_code: Some(0),\n output_preview: \"[stderr] first line\\n[stdout] second line\\n\".to_string(),\n output_file: std::path::PathBuf::from(\"/tmp/output.log\"),\n duration_secs: 7.1,\n notify: true,\n wake: false,\n });\n\n let parsed = parse_background_task_notification_markdown(&rendered)\n .expect(\"background task notification should parse\");\n assert_eq!(parsed.task_id, \"abc123\");\n assert_eq!(parsed.tool_name, \"bash\");\n assert_eq!(parsed.status, \"✓ completed\");\n assert_eq!(parsed.duration, \"7.1s\");\n assert_eq!(parsed.exit_label, \"exit 0\");\n assert_eq!(\n parsed.preview.as_deref(),\n Some(\"[stderr] first line\\n[stdout] second line\")\n );\n assert_eq!(\n parsed.full_output_command,\n \"bg action=\\\"output\\\" task_id=\\\"abc123\\\"\"\n );\n }\n}\n"}, "a9ed34a1e16806e6403539d57af109648fb8d094": {"t": 1785757241.1163883, "n": 43568, "h": "fa915f5b81305d4f", "c": "#![allow(dead_code)]\n\nuse crate::bus::{BackgroundTaskCompleted, BackgroundTaskStatus};\nuse chrono::Utc;\nuse regex::Regex;\nuse serde::{Deserialize, Serialize};\nuse std::collections::HashSet;\nuse std::sync::OnceLock;\n\n#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]\npub struct InputShellResult {\n pub command: String,\n #[serde(default, skip_serializing_if = \"Option::is_none\")]\n pub cwd: Option<String>,\n pub output: String,\n #[serde(default, skip_serializing_if = \"Option::is_none\")]\n pub exit_code: Option<i32>,\n pub duration_ms: u64,\n #[serde(default)]\n pub truncated: bool,\n #[serde(default)]\n pub failed_to_start: bool,\n}\n\nfn sanitize_fenced_block(text: &str) -> String {\n text.replace(\"```\", \"``\\u{200b}`\")\n}\n\npub fn format_input_shell_result_markdown(shell: &InputShellResult) -> String {\n let status = if shell.failed_to_start {\n \"✗ failed to start\".to_string()\n } else if shell.exit_code == Some(0) {\n \"✓ exit 0\".to_string()\n } else if let Some(code) = shell.exit_code {\n format!(\"✗ exit {}\", code)\n } else {\n \"✗ terminated\".to_string()\n };\n\n let mut meta = vec![status, Message::format_duration(shell.duration_ms)];\n if let Some(cwd) = shell.cwd.as_deref() {\n meta.push(format!(\"cwd `{}`\", cwd));\n }\n if shell.truncated {\n meta.push(\"truncated\".to_string());\n }\n\n let mut message = format!(\n \"**Shell command** · {}\\n\\n```bash\\n{}\\n```\",\n meta.join(\" · \"),\n sanitize_fenced_block(&shell.command)\n );\n\n if shell.output.trim().is_empty() {\n message.push_str(\"\\n\\n_No output._\");\n } else {\n message.push_str(&format!(\n \"\\n\\n```text\\n{}\\n```\",\n sanitize_fenced_block(shell.output.trim_end())\n ));\n }\n\n message\n}\n\npub fn input_shell_status_notice(shell: &InputShellResult) -> String {\n if shell.failed_to_start {\n \"Shell command failed to start\".to_string()\n } else if shell.exit_code == Some(0) {\n \"Shell command completed\".to_string()\n } else if let Some(code) = shell.exit_code {\n format!(\"Shell command failed (exit {})\", code)\n } else {\n \"Shell command terminated\".to_string()\n }\n}\n\nfn format_background_task_status(status: &BackgroundTaskStatus) -> &'static str {\n match status {\n BackgroundTaskStatus::Completed => \"✓ completed\",\n BackgroundTaskStatus::Superseded => \"↻ superseded\",\n BackgroundTaskStatus::Failed => \"✗ failed\",\n BackgroundTaskStatus::Running => \"running\",\n }\n}\n\nfn normalize_background_task_preview(preview: &str) -> Option<String> {\n let normalized = preview.replace(\"\\r\\n\", \"\\n\").replace('\\r', \"\\n\");\n let trimmed = normalized.trim_end();\n if trimmed.trim().is_empty() {\n None\n } else {\n Some(sanitize_fenced_block(trimmed))\n }\n}\n\npub fn format_background_task_notification_markdown(task: &BackgroundTaskCompleted) -> String {\n let exit_code = task\n .exit_code\n .map(|code| format!(\"exit {}\", code))\n .unwrap_or_else(|| \"exit n/a\".to_string());\n\n let mut message = format!(\n \"**Background task** `{}` · `{}` · {} · {:.1}s · {}\",\n task.task_id,\n task.tool_name,\n format_background_task_status(&task.status),\n task.duration_secs,\n exit_code,\n );\n\n if let Some(preview) = normalize_background_task_preview(&task.output_preview) {\n message.push_str(&format!(\"\\n\\n```text\\n{}\\n```\", preview));\n } else {\n message.push_str(\"\\n\\n_No output captured._\");\n }\n\n message.push_str(&format!(\n \"\\n\\n_Full output:_ `bg action=\\\"output\\\" task_id=\\\"{}\\\"`\",\n task.task_id\n ));\n\n message\n}\n\n#[derive(Debug, Clone, PartialEq, Eq)]\npub struct ParsedBackgroundTaskNotification {\n pub task_id: String,\n pub tool_name: String,\n pub status: String,\n pub duration: String,\n pub exit_label: String,\n pub preview: Option<String>,\n pub full_output_command: String,\n}\n\npub fn parse_background_task_notification_markdown(\n content: &str,\n) -> Option<ParsedBackgroundTaskNotification> {\n static HEADER_RE: OnceLock<Regex> = OnceLock::new();\n static FULL_OUTPUT_RE: OnceLock<Regex> = OnceLock::new();\n\n let header_re = HEADER_RE.get_or_init(|| {\n Regex::new(\n r\"^\\*\\*Background task\\*\\* `(?P<task_id>[^`]+)` · `(?P<tool_name>[^`]+)` · (?P<status>.+?) · (?P<duration>[0-9]+(?:\\.[0-9]+)?s) · (?P<exit_label>.+)$\",\n )\n .expect(\"valid background task header regex\")\n });\n let full_output_re = FULL_OUTPUT_RE.get_or_init(|| {\n Regex::new(r#\"^_Full output:_ `(?P<command>[^`]+)`$\"#)\n .expect(\"valid background task full output regex\")\n });\n\n let normalized = content.replace(\"\\r\\n\", \"\\n\").replace('\\r', \"\\n\");\n let mut sections = normalized.split(\"\\n\\n\");\n let header = sections.next()?.trim();\n let captures = header_re.captures(header)?;\n\n let mut preview: Option<String> = None;\n let mut full_output_command: Option<String> = None;\n\n for section in sections {\n let trimmed = section.trim();\n if trimmed.is_empty() {\n continue;\n }\n\n if let Some(captures) = full_output_re.captures(trimmed) {\n full_output_command = Some(captures[\"command\"].to_string());\n continue;\n }\n\n if trimmed == \"_No output captured._\" {\n preview = None;\n continue;\n }\n\n if let Some(fenced) = trimmed\n .strip_prefix(\"```text\\n\")\n .and_then(|body| body.strip_suffix(\"\\n```\"))\n {\n preview = Some(fenced.to_string());\n }\n }\n\n Some(ParsedBackgroundTaskNotification {\n task_id: captures[\"task_id\"].to_string(),\n tool_name: captures[\"tool_name\"].to_string(),\n status: captures[\"status\"].to_string(),\n duration: captures[\"duration\"].to_string(),\n exit_label: captures[\"exit_label\"].to_string(),\n preview,\n full_output_command: full_output_command?,\n })\n}\n\npub fn background_task_status_notice(task: &BackgroundTaskCompleted) -> String {\n match task.status {\n BackgroundTaskStatus::Completed => {\n format!(\"Background task completed · {}\", task.tool_name)\n }\n BackgroundTaskStatus::Superseded => {\n format!(\"Background task superseded · {}\", task.tool_name)\n }\n BackgroundTaskStatus::Failed => match task.exit_code {\n Some(code) => format!(\n \"Background task failed · {} · exit {}\",\n task.tool_name, code\n ),\n None => format!(\"Background task failed · {}\", task.tool_name),\n },\n BackgroundTaskStatus::Running => format!(\"Background task running · {}\", task.tool_name),\n }\n}\n\n/// Role in conversation\n#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]\n#[serde(rename_all = \"lowercase\")]\npub enum Role {\n User,\n Assistant,\n}\n\n/// Plain-text tool output placeholder when execution was interrupted.\npub const TOOL_OUTPUT_MISSING_TEXT: &str =\n \"Tool output missing (session interrupted before tool execution completed)\";\n\n/// A message in the conversation\n#[derive(Debug, Clone, Serialize, Deserialize)]\npub struct Message {\n pub role: Role,\n pub content: Vec<ContentBlock>,\n #[serde(default, skip_serializing_if = \"Option::is_none\")]\n pub timestamp: Option<chrono::DateTime<Utc>>,\n #[serde(default, skip_serializing_if = \"Option::is_none\")]\n pub tool_duration_ms: Option<u64>,\n}\n\n/// Cache control metadata for prompt caching\n#[derive(Debug, Clone, Serialize, Deserialize)]\npub struct CacheControl {\n #[serde(rename = \"type\")]\n pub kind: String,\n #[serde(skip_serializing_if = \"Option::is_none\")]\n pub ttl: Option<String>,\n}\n\nimpl CacheControl {\n pub fn ephemeral(ttl: Option<String>) -> Self {\n Self {\n kind: \"ephemeral\".to_string(),\n ttl,\n }\n }\n}\n\n/// Content block within a message\n#[derive(Debug, Clone, Serialize, Deserialize)]\n#[serde(tag = \"type\", rename_all = \"snake_case\")]\npub enum ContentBlock {\n Text {\n text: String,\n #[serde(skip_serializing_if = \"Option::is_none\")]\n cache_control: Option<CacheControl>,\n },\n /// Hidden reasoning content used for providers that require it (not displayed)\n Reasoning {\n text: String,\n },\n ToolUse {\n id: String,\n name: String,\n input: serde_json::Value,\n },\n ToolResult {\n tool_use_id: String,\n content: String,\n #[serde(skip_serializing_if = \"Option::is_none\")]\n is_error: Option<bool>,\n },\n Image {\n media_type: String,\n data: String,\n },\n /// Hidden OpenAI Responses compaction item used to preserve native\n /// compaction state across turns/saves when jcode explicitly triggers it.\n OpenAICompaction {\n encrypted_content: String,\n },\n}\n\nimpl Message {\n pub fn user(text: &str) -> Self {\n Self {\n role: Role::User,\n content: vec![ContentBlock::Text {\n text: text.to_string(),\n cache_control: None,\n }],\n timestamp: Some(Utc::now()),\n tool_duration_ms: None,\n }\n }\n\n pub fn user_with_images(text: &str, images: Vec<(String, String)>) -> Self {\n let mut content: Vec<ContentBlock> = images\n .into_iter()\n .map(|(media_type, data)| ContentBlock::Image { media_type, data })\n .collect();\n content.push(ContentBlock::Text {\n text: text.to_string(),\n cache_control: None,\n });\n Self {\n role: Role::User,\n content,\n timestamp: Some(Utc::now()),\n tool_duration_ms: None,\n }\n }\n\n pub fn assistant_text(text: &str) -> Self {\n Self {\n role: Role::Assistant,\n content: vec![ContentBlock::Text {\n text: text.to_string(),\n cache_control: None,\n }],\n timestamp: Some(Utc::now()),\n tool_duration_ms: None,\n }\n }\n\n pub fn tool_result(tool_use_id: &str, content: &str, is_error: bool) -> Self {\n Self::tool_result_with_duration(tool_use_id, content, is_error, None)\n }\n\n pub fn tool_result_with_duration(\n tool_use_id: &str,\n content: &str,\n is_error: bool,\n tool_duration_ms: Option<u64>,\n ) -> Self {\n Self {\n role: Role::User,\n content: vec![ContentBlock::ToolResult {\n tool_use_id: tool_use_id.to_string(),\n content: content.to_string(),\n is_error: if is_error { Some(true) } else { None },\n }],\n timestamp: Some(Utc::now()),\n tool_duration_ms,\n }\n }\n\n /// Format a timestamp deterministically in UTC for injection into model-visible content.\n pub fn format_timestamp(ts: &chrono::DateTime<Utc>) -> String {\n ts.to_rfc3339_opts(chrono::SecondsFormat::Millis, true)\n }\n\n pub fn format_duration(duration_ms: u64) -> String {\n match duration_ms {\n 0..=999 => format!(\"{}ms\", duration_ms),\n 1_000..=9_999 => format!(\"{:.1}s\", duration_ms as f64 / 1000.0),\n 10_000..=59_999 => format!(\"{}s\", duration_ms / 1000),\n _ => {\n let total_seconds = duration_ms / 1000;\n let minutes = total_seconds / 60;\n let seconds = total_seconds % 60;\n if seconds == 0 {\n format!(\"{}m\", minutes)\n } else {\n format!(\"{}m {}s\", minutes, seconds)\n }\n }\n }\n }\n\n pub fn is_internal_system_reminder(&self) -> bool {\n self.content\n .iter()\n .find_map(|block| match block {\n ContentBlock::Text { text, .. } => Some(text.trim_start()),\n _ => None,\n })\n .is_some_and(|text| text.starts_with(\"<system-reminder>\"))\n }\n\n fn should_skip_timestamp_injection(&self) -> bool {\n self.is_internal_system_reminder()\n }\n\n fn tool_result_tag(&self, ts: &chrono::DateTime<Utc>) -> String {\n match self.tool_duration_ms {\n Some(duration_ms) => {\n let duration_ms_i64 = i64::try_from(duration_ms).unwrap_or(i64::MAX);\n let start_ts = ts\n .checked_sub_signed(chrono::Duration::milliseconds(duration_ms_i64))\n .unwrap_or(*ts);\n format!(\n \"[tool timing: start={} finish={} duration={}]\",\n Self::format_timestamp(&start_ts),\n Self::format_timestamp(ts),\n Self::format_duration(duration_ms)\n )\n }\n None => format!(\"[{}]\", Self::format_timestamp(ts)),\n }\n }\n\n /// Return a copy of messages with timestamps injected into user-role text content.\n /// Tool results get a stable UTC timing header prepended to content.\n /// User text messages get a stable UTC timestamp prepended to the first text block.\n pub fn with_timestamps(messages: &[Message]) -> Vec<Message> {\n messages\n .iter()\n .map(|msg| {\n let Some(ts) = msg.timestamp else {\n return msg.clone();\n };\n if msg.role != Role::User || msg.should_skip_timestamp_injection() {\n return msg.clone();\n }\n let text_tag = format!(\"[{}]\", Self::format_timestamp(&ts));\n let tool_result_tag = msg.tool_result_tag(&ts);\n let mut msg = msg.clone();\n let mut tagged = false;\n for block in &mut msg.content {\n match block {\n ContentBlock::Text { text, .. } if !tagged => {\n *text = format!(\"{} {}\", text_tag, text);\n tagged = true;\n }\n ContentBlock::ToolResult { content, .. } if !tagged => {\n *content = format!(\"{} {}\", tool_result_tag, content);\n tagged = true;\n }\n _ => {}\n }\n }\n msg\n })\n .collect()\n }\n}\n\nconst STABLE_HASH_SEED: u64 = 0xcbf29ce484222325;\nconst STABLE_HASH_PRIME: u64 = 0x100000001b3;\n\nfn stable_hash_bytes(bytes: &[u8]) -> u64 {\n let mut hash = STABLE_HASH_SEED;\n for byte in bytes {\n hash ^= *byte as u64;\n hash = hash.wrapping_mul(STABLE_HASH_PRIME);\n }\n hash\n}\n\npub(crate) fn extend_stable_hash(acc: u64, next: u64) -> u64 {\n stable_hash_bytes(&[acc.to_le_bytes().as_slice(), next.to_le_bytes().as_slice()].concat())\n}\n\npub(crate) fn stable_message_hash(message: &Message) -> u64 {\n match serde_json::to_vec(message) {\n Ok(bytes) => stable_hash_bytes(&bytes),\n Err(_) => stable_hash_bytes(format!(\"{:?}\", message).as_bytes()),\n }\n}\n\npub fn ends_with_fresh_user_turn(messages: &[Message]) -> bool {\n for msg in messages.iter().rev() {\n if msg.role != Role::User {\n return false;\n }\n\n if msg\n .content\n .iter()\n .any(|block| matches!(block, ContentBlock::ToolResult { .. }))\n {\n return false;\n }\n\n if msg.content.is_empty() {\n return false;\n }\n\n let mut saw_user_text = false;\n for block in &msg.content {\n match block {\n ContentBlock::Text { text, .. } => {\n let trimmed = text.trim();\n if !trimmed.is_empty() && !trimmed.starts_with(\"<system-reminder>\") {\n saw_user_text = true;\n }\n }\n _ => return false,\n }\n }\n\n if saw_user_text {\n return true;\n }\n\n if msg.is_internal_system_reminder() {\n continue;\n }\n\n return false;\n }\n\n false\n}\n\n/// Sanitize a tool ID so it matches the pattern `^[a-zA-Z0-9_-]+$`.\n///\n/// Different providers generate tool IDs in different formats. When switching\n/// from one provider to another mid-conversation, the historical tool IDs may\n/// contain characters that the new provider rejects (e.g., dots in Copilot IDs\n/// sent to Anthropic). This function replaces any invalid characters with\n/// underscores.\npub fn sanitize_tool_id(id: &str) -> String {\n if id.is_empty() {\n return \"unknown\".to_string();\n }\n let sanitized: String = id\n .chars()\n .map(|c| {\n if c.is_ascii_alphanumeric() || c == '_' || c == '-' {\n c\n } else {\n '_'\n }\n })\n .collect();\n if sanitized.is_empty() {\n \"unknown\".to_string()\n } else {\n sanitized\n }\n}\n\n/// Redact likely secrets from persisted tool output.\n///\n/// This is a best-effort safeguard for local session history files. It targets\n/// high-confidence token/key patterns and common `KEY=VALUE` assignments used by\n/// auth flows.\npub fn redact_secrets(text: &str) -> String {\n // Fast path to avoid regex work for most tool outputs.\n let lower = text.to_ascii_lowercase();\n\n if !text.contains(\"sk-\")\n && !text.contains(\"ghp_\")\n && !text.contains(\"github_pat_\")\n && !text.contains(\"AIza\")\n && !text.contains(\"ya29.\")\n && !text.contains(\"xox\")\n && !lower.contains(\"api_key\")\n && !lower.contains(\"token\")\n {\n return text.to_string();\n }\n\n static DIRECT_PATTERNS: OnceLock<Vec<Regex>> = OnceLock::new();\n static ASSIGNMENT_PATTERNS: OnceLock<Vec<Regex>> = OnceLock::new();\n\n let direct_patterns = DIRECT_PATTERNS.get_or_init(|| {\n vec![\n Regex::new(r\"sk-ant-(?:oat|ort)01-[A-Za-z0-9_-]{20,}\")\n .expect(\"valid Anthropic OAuth token regex\"),\n Regex::new(r\"sk-or-v1-[A-Za-z0-9_-]{20,}\").expect(\"valid OpenRouter key regex\"),\n Regex::new(r\"ghp_[A-Za-z0-9]{20,}\").expect(\"valid GitHub PAT regex\"),\n Regex::new(r\"github_pat_[A-Za-z0-9_]{20,}\")\n .expect(\"valid GitHub fine-grained PAT regex\"),\n Regex::new(r\"ya29\\.[A-Za-z0-9._-]{20,}\").expect(\"valid Google OAuth token regex\"),\n Regex::new(r\"AIza[0-9A-Za-z_-]{20,}\").expect(\"valid Google API key regex\"),\n Regex::new(r\"xox[baprs]-[A-Za-z0-9-]{10,}\").expect(\"valid Slack token regex\"),\n ]\n });\n\n let assignment_patterns = ASSIGNMENT_PATTERNS.get_or_init(|| {\n vec![\n Regex::new(r\"(?m)^\\s*(OPENROUTER_API_KEY\\s*=\\s*)[^\\r\\n]+\")\n .expect(\"valid OPENROUTER_API_KEY assignment regex\"),\n Regex::new(r\"(?m)^\\s*(OPENCODE_API_KEY\\s*=\\s*)[^\\r\\n]+\")\n .expect(\"valid OPENCODE_API_KEY assignment regex\"),\n Regex::new(r\"(?m)^\\s*(OPENCODE_GO_API_KEY\\s*=\\s*)[^\\r\\n]+\")\n .expect(\"valid OPENCODE_GO_API_KEY assignment regex\"),\n Regex::new(r\"(?m)^\\s*(ZHIPU_API_KEY\\s*=\\s*)[^\\r\\n]+\")\n .expect(\"valid ZHIPU_API_KEY assignment regex\"),\n Regex::new(r\"(?m)^\\s*(ZAI_API_KEY\\s*=\\s*)[^\\r\\n]+\")\n .expect(\"valid ZAI_API_KEY assignment regex\"),\n Regex::new(r\"(?m)^\\s*(302AI_API_KEY\\s*=\\s*)[^\\r\\n]+\")\n .expect(\"valid 302AI_API_KEY assignment regex\"),\n Regex::new(r\"(?m)^\\s*(BASETEN_API_KEY\\s*=\\s*)[^\\r\\n]+\")\n .expect(\"valid BASETEN_API_KEY assignment regex\"),\n Regex::new(r\"(?m)^\\s*(CORTECS_API_KEY\\s*=\\s*)[^\\r\\n]+\")\n .expect(\"valid CORTECS_API_KEY assignment regex\"),\n Regex::new(r\"(?m)^\\s*(DEEPSEEK_API_KEY\\s*=\\s*)[^\\r\\n]+\")\n .expect(\"valid DEEPSEEK_API_KEY assignment regex\"),\n Regex::new(r\"(?m)^\\s*(FIRMWARE_API_KEY\\s*=\\s*)[^\\r\\n]+\")\n .expect(\"valid FIRMWARE_API_KEY assignment regex\"),\n Regex::new(r\"(?m)^\\s*(HF_TOKEN\\s*=\\s*)[^\\r\\n]+\")\n .expect(\"valid HF_TOKEN assignment regex\"),\n Regex::new(r\"(?m)^\\s*(MOONSHOT_API_KEY\\s*=\\s*)[^\\r\\n]+\")\n .expect(\"valid MOONSHOT_API_KEY assignment regex\"),\n Regex::new(r\"(?m)^\\s*(NEBIUS_API_KEY\\s*=\\s*)[^\\r\\n]+\")\n .expect(\"valid NEBIUS_API_KEY assignment regex\"),\n Regex::new(r\"(?m)^\\s*(SCALEWAY_API_KEY\\s*=\\s*)[^\\r\\n]+\")\n .expect(\"valid SCALEWAY_API_KEY assignment regex\"),\n Regex::new(r\"(?m)^\\s*(STACKIT_API_KEY\\s*=\\s*)[^\\r\\n]+\")\n .expect(\"valid STACKIT_API_KEY assignment regex\"),\n Regex::new(r\"(?m)^\\s*(GROQ_API_KEY\\s*=\\s*)[^\\r\\n]+\")\n .expect(\"valid GROQ_API_KEY assignment regex\"),\n Regex::new(r\"(?m)^\\s*(MISTRAL_API_KEY\\s*=\\s*)[^\\r\\n]+\")\n .expect(\"valid MISTRAL_API_KEY assignment regex\"),\n Regex::new(r\"(?m)^\\s*(PERPLEXITY_API_KEY\\s*=\\s*)[^\\r\\n]+\")\n .expect(\"valid PERPLEXITY_API_KEY assignment regex\"),\n Regex::new(r\"(?m)^\\s*(TOGETHER_API_KEY\\s*=\\s*)[^\\r\\n]+\")\n .expect(\"valid TOGETHER_API_KEY assignment regex\"),\n Regex::new(r\"(?m)^\\s*(DEEPINFRA_API_KEY\\s*=\\s*)[^\\r\\n]+\")\n .expect(\"valid DEEPINFRA_API_KEY assignment regex\"),\n Regex::new(r\"(?m)^\\s*(XAI_API_KEY\\s*=\\s*)[^\\r\\n]+\")\n .expect(\"valid XAI_API_KEY assignment regex\"),\n Regex::new(r\"(?m)^\\s*(LMSTUDIO_API_KEY\\s*=\\s*)[^\\r\\n]+\")\n .expect(\"valid LMSTUDIO_API_KEY assignment regex\"),\n Regex::new(r\"(?m)^\\s*(OLLAMA_API_KEY\\s*=\\s*)[^\\r\\n]+\")\n .expect(\"valid OLLAMA_API_KEY assignment regex\"),\n Regex::new(r\"(?m)^\\s*(CHUTES_API_KEY\\s*=\\s*)[^\\r\\n]+\")\n .expect(\"valid CHUTES_API_KEY assignment regex\"),\n Regex::new(r\"(?m)^\\s*(CEREBRAS_API_KEY\\s*=\\s*)[^\\r\\n]+\")\n .expect(\"valid CEREBRAS_API_KEY assignment regex\"),\n Regex::new(r\"(?m)^\\s*(OPENAI_COMPAT_API_KEY\\s*=\\s*)[^\\r\\n]+\")\n .expect(\"valid OPENAI_COMPAT_API_KEY assignment regex\"),\n Regex::new(r\"(?m)^\\s*(ANTHROPIC_API_KEY\\s*=\\s*)[^\\r\\n]+\")\n .expect(\"valid ANTHROPIC_API_KEY assignment regex\"),\n Regex::new(r\"(?m)^\\s*(OPENAI_API_KEY\\s*=\\s*)[^\\r\\n]+\")\n .expect(\"valid OPENAI_API_KEY assignment regex\"),\n Regex::new(r\"(?m)^\\s*(AZURE_OPENAI_API_KEY\\s*=\\s*)[^\\r\\n]+\")\n .expect(\"valid AZURE_OPENAI_API_KEY assignment regex\"),\n Regex::new(r\"(?m)^\\s*(CURSOR_API_KEY\\s*=\\s*)[^\\r\\n]+\")\n .expect(\"valid CURSOR_API_KEY assignment regex\"),\n Regex::new(r\"(?m)^\\s*(GITHUB_TOKEN\\s*=\\s*)[^\\r\\n]+\")\n .expect(\"valid GITHUB_TOKEN assignment regex\"),\n ]\n });\n\n let mut redacted = text.to_string();\n let mut redacted_keys: HashSet<String> = [\n \"OPENROUTER_API_KEY\",\n \"OPENCODE_API_KEY\",\n \"OPENCODE_GO_API_KEY\",\n \"ZHIPU_API_KEY\",\n \"ZAI_API_KEY\",\n \"302AI_API_KEY\",\n \"BASETEN_API_KEY\",\n \"CORTECS_API_KEY\",\n \"DEEPSEEK_API_KEY\",\n \"FIRMWARE_API_KEY\",\n \"HF_TOKEN\",\n \"MOONSHOT_API_KEY\",\n \"NEBIUS_API_KEY\",\n \"SCALEWAY_API_KEY\",\n \"STACKIT_API_KEY\",\n \"GROQ_API_KEY\",\n \"MISTRAL_API_KEY\",\n \"PERPLEXITY_API_KEY\",\n \"TOGETHER_API_KEY\",\n \"DEEPINFRA_API_KEY\",\n \"XAI_API_KEY\",\n \"LMSTUDIO_API_KEY\",\n \"OLLAMA_API_KEY\",\n \"CHUTES_API_KEY\",\n \"CEREBRAS_API_KEY\",\n \"OPENAI_COMPAT_API_KEY\",\n \"ANTHROPIC_API_KEY\",\n \"OPENAI_API_KEY\",\n \"AZURE_OPENAI_API_KEY\",\n \"CURSOR_API_KEY\",\n \"GITHUB_TOKEN\",\n ]\n .iter()\n .map(|k| (*k).to_string())\n .collect();\n\n for re in direct_patterns {\n redacted = re.replace_all(&redacted, \"[REDACTED_SECRET]\").into_owned();\n }\n\n for re in assignment_patterns {\n redacted = re\n .replace_all(&redacted, \"${1}[REDACTED_SECRET]\")\n .into_owned();\n }\n\n // Also redact custom API key variable names configured at runtime.\n for source in [\n \"JCODE_OPENROUTER_API_KEY_NAME\",\n \"JCODE_OPENAI_COMPAT_API_KEY_NAME\",\n ] {\n let Some(key_name) = std::env::var(source)\n .ok()\n .map(|v| v.trim().to_string())\n .filter(|v| !v.is_empty())\n else {\n continue;\n };\n\n if !key_name\n .chars()\n .all(|c| c.is_ascii_uppercase() || c.is_ascii_digit() || c == '_')\n {\n continue;\n }\n if !redacted_keys.insert(key_name.clone()) {\n continue;\n }\n\n let pattern = format!(r\"(?m)^\\s*({}\\s*=\\s*)[^\\r\\n]+\", regex::escape(&key_name));\n if let Ok(re) = Regex::new(&pattern) {\n redacted = re\n .replace_all(&redacted, \"${1}[REDACTED_SECRET]\")\n .into_owned();\n }\n }\n\n redacted\n}\n\n/// Tool definition for the API\n#[derive(Debug, Clone, Serialize)]\npub struct ToolDefinition {\n pub name: String,\n // Prompt-visible text sent to the model by provider adapters.\n // Approximate prompt cost: description.len() / 4. Use\n // ToolDefinition::description_token_estimate() when reviewing tool bloat.\n pub description: String,\n pub input_schema: serde_json::Value,\n}\n\nimpl ToolDefinition {\n /// Approximate prompt-token cost of this tool's top-level description.\n ///\n /// This uses jcode's standard chars/4 heuristic, matching other token\n /// budget estimates in the codebase.\n pub fn description_token_estimate(&self) -> usize {\n crate::util::estimate_tokens(&self.description)\n }\n\n /// Approximate prompt-token cost of the full tool definition payload.\n pub fn prompt_token_estimate(&self) -> usize {\n crate::util::estimate_tokens(\n &serde_json::json!({\n \"name\": self.name,\n \"description\": self.description,\n \"input_schema\": self.input_schema,\n })\n .to_string(),\n )\n }\n}\n\n/// A tool call from the model\n#[derive(Debug, Clone, Default, PartialEq, Eq, serde::Serialize, serde::Deserialize)]\npub struct ToolCall {\n #[serde(default)]\n pub id: String,\n #[serde(default)]\n pub name: String,\n #[serde(default)]\n pub input: serde_json::Value,\n #[serde(default, skip_serializing_if = \"Option::is_none\")]\n pub intent: Option<String>,\n}\n\n/// Connection phase for status bar transparency\n#[derive(Debug, Clone, PartialEq)]\npub enum ConnectionPhase {\n /// Refreshing OAuth token\n Authenticating,\n /// TCP + TLS connection to API\n Connecting,\n /// HTTP request sent, waiting for first response byte\n WaitingForResponse,\n /// First byte received, stream is active\n Streaming,\n /// Retrying after a transient error\n Retrying { attempt: u32, max: u32 },\n}\n\nimpl std::fmt::Display for ConnectionPhase {\n fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {\n match self {\n ConnectionPhase::Authenticating => write!(f, \"authenticating\"),\n ConnectionPhase::Connecting => write!(f, \"connecting\"),\n ConnectionPhase::WaitingForResponse => write!(f, \"waiting for response\"),\n ConnectionPhase::Streaming => write!(f, \"streaming\"),\n ConnectionPhase::Retrying { attempt, max } => {\n write!(f, \"retrying ({}/{})\", attempt, max)\n }\n }\n }\n}\n\n/// Streaming event from provider\n#[derive(Debug, Clone)]\npub enum StreamEvent {\n /// Text content delta\n TextDelta(String),\n /// Tool use started\n ToolUseStart { id: String, name: String },\n /// Tool input delta (JSON fragment)\n ToolInputDelta(String),\n /// Tool use complete\n ToolUseEnd,\n /// Tool result from provider (provider already executed the tool)\n ToolResult {\n tool_use_id: String,\n content: String,\n is_error: bool,\n },\n /// Extended thinking started\n ThinkingStart,\n /// Extended thinking delta (reasoning content)\n ThinkingDelta(String),\n /// Extended thinking ended\n ThinkingEnd,\n /// Extended thinking completed with duration\n ThinkingDone { duration_secs: f64 },\n /// Message complete (may have stop reason)\n MessageEnd { stop_reason: Option<String> },\n /// Token usage update\n TokenUsage {\n input_tokens: Option<u64>,\n output_tokens: Option<u64>,\n cache_read_input_tokens: Option<u64>,\n cache_creation_input_tokens: Option<u64>,\n },\n /// Active transport/connection type for this stream\n ConnectionType { connection: String },\n /// Connection phase update (for status bar transparency)\n ConnectionPhase { phase: ConnectionPhase },\n /// Provider-supplied human-readable transport detail for the status line\n StatusDetail { detail: String },\n /// Error occurred\n Error {\n message: String,\n /// Seconds until rate limit resets (if this is a rate limit error)\n retry_after_secs: Option<u64>,\n },\n /// Provider session ID (for conversation resume)\n SessionId(String),\n /// Compaction occurred (context was summarized)\n Compaction {\n trigger: String,\n pre_tokens: Option<u64>,\n /// Provider-native compaction artifact, if one was emitted.\n openai_encrypted_content: Option<String>,\n },\n /// Upstream provider info (e.g., which provider OpenRouter routed to)\n UpstreamProvider { provider: String },\n /// Native tool call from a provider bridge that needs execution by jcode\n NativeToolCall {\n request_id: String,\n tool_name: String,\n input: serde_json::Value,\n },\n}\n\n#[cfg(test)]\nmod tests {\n use super::*;\n\n #[test]\n fn sanitize_tool_id_alphanumeric_passthrough() {\n assert_eq!(\n sanitize_tool_id(\"toolu_01XFDUDYJgAACzvnptvVer6u\"),\n \"toolu_01XFDUDYJgAACzvnptvVer6u\"\n );\n assert_eq!(sanitize_tool_id(\"call_abc123\"), \"call_abc123\");\n assert_eq!(\n sanitize_tool_id(\"call_1234567890_9876543210\"),\n \"call_1234567890_9876543210\"\n );\n }\n\n #[test]\n fn sanitize_tool_id_hyphens_passthrough() {\n assert_eq!(sanitize_tool_id(\"call-abc-123\"), \"call-abc-123\");\n assert_eq!(\n sanitize_tool_id(\"tool_use-id_with-mixed\"),\n \"tool_use-id_with-mixed\"\n );\n }\n\n #[test]\n fn sanitize_tool_id_replaces_dots() {\n assert_eq!(\n sanitize_tool_id(\"chatcmpl-abc.def.ghi\"),\n \"chatcmpl-abc_def_ghi\"\n );\n assert_eq!(sanitize_tool_id(\"call.123\"), \"call_123\");\n }\n\n #[test]\n fn sanitize_tool_id_replaces_colons() {\n assert_eq!(sanitize_tool_id(\"call:123:456\"), \"call_123_456\");\n }\n\n #[test]\n fn sanitize_tool_id_replaces_special_chars() {\n assert_eq!(\n sanitize_tool_id(\"id@with#special$chars\"),\n \"id_with_special_chars\"\n );\n assert_eq!(sanitize_tool_id(\"id with spaces\"), \"id_with_spaces\");\n }\n\n #[test]\n fn sanitize_tool_id_empty_returns_unknown() {\n assert_eq!(sanitize_tool_id(\"\"), \"unknown\");\n }\n\n #[test]\n fn sanitize_tool_id_copilot_to_anthropic() {\n assert_eq!(\n sanitize_tool_id(\"chatcmpl-BF2xX.tool_call.0\"),\n \"chatcmpl-BF2xX_tool_call_0\"\n );\n }\n\n #[test]\n fn sanitize_tool_id_already_valid_unchanged() {\n let valid_ids = [\n \"toolu_01XFDUDYJgAACzvnptvVer6u\",\n \"call_abc123\",\n \"fallback_text_call_call_1234567890_9876543210\",\n \"tool_123\",\n \"a\",\n \"A\",\n \"0\",\n \"_\",\n \"-\",\n \"a-b_c\",\n ];\n for id in valid_ids {\n assert_eq!(sanitize_tool_id(id), id, \"ID '{}' should be unchanged\", id);\n }\n }\n\n #[test]\n fn redact_secrets_redacts_known_direct_token_formats() {\n let input = \"access=sk-ant-oat01-ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789\\nopenrouter=sk-or-v1-abcdefghijklmnopqrstuvwxyz0123456789\\ngithub=ghp_ABCDEFGHIJKLMNOPQRSTUVWXYZ0123\\n\";\n let out = redact_secrets(input);\n assert!(!out.contains(\"sk-ant-oat01-\"));\n assert!(!out.contains(\"sk-or-v1-\"));\n assert!(!out.contains(\"ghp_ABCDEFGHIJKLMNOPQRSTUVWXYZ0123\"));\n assert!(out.matches(\"[REDACTED_SECRET]\").count() >= 3);\n }\n\n #[test]\n fn redact_secrets_redacts_env_style_assignments() {\n let input = \"OPENROUTER_API_KEY=sk-or-v1-abc123abc123abc123abc123\\nOPENCODE_API_KEY=oc_test_secret\\nOPENCODE_GO_API_KEY=ocgo_test_secret\\nZAI_API_KEY=zai_secret\\nCHUTES_API_KEY=chutes_secret\\nCEREBRAS_API_KEY=cerebras_secret\\nOPENAI_COMPAT_API_KEY=compat_secret\\nCURSOR_API_KEY='my_cursor_secret_value'\\nOPENAI_API_KEY=sk-test-openai-example\\nAZURE_OPENAI_API_KEY=azure-openai-secret\\n\";\n let out = redact_secrets(input);\n assert!(out.contains(\"OPENROUTER_API_KEY=[REDACTED_SECRET]\"));\n assert!(out.contains(\"OPENCODE_API_KEY=[REDACTED_SECRET]\"));\n assert!(out.contains(\"OPENCODE_GO_API_KEY=[REDACTED_SECRET]\"));\n assert!(out.contains(\"ZAI_API_KEY=[REDACTED_SECRET]\"));\n assert!(out.contains(\"CHUTES_API_KEY=[REDACTED_SECRET]\"));\n assert!(out.contains(\"CEREBRAS_API_KEY=[REDACTED_SECRET]\"));\n assert!(out.contains(\"OPENAI_COMPAT_API_KEY=[REDACTED_SECRET]\"));\n assert!(out.contains(\"CURSOR_API_KEY=[REDACTED_SECRET]\"));\n assert!(out.contains(\"OPENAI_API_KEY=[REDACTED_SECRET]\"));\n assert!(out.contains(\"AZURE_OPENAI_API_KEY=[REDACTED_SECRET]\"));\n assert!(!out.contains(\"my_cursor_secret_value\"));\n }\n\n #[test]\n fn redact_secrets_redacts_runtime_key_assignment() {\n let key_var = \"JCODE_OPENAI_COMPAT_API_KEY_NAME\";\n let prev = std::env::var(key_var).ok();\n crate::env::set_var(key_var, \"GROQ_API_KEY\");\n\n let input = \"GROQ_API_KEY=my_secret_token_value\";\n let out = redact_secrets(input);\n assert_eq!(out, \"GROQ_API_KEY=[REDACTED_SECRET]\");\n\n if let Some(v) = prev {\n crate::env::set_var(key_var, v);\n } else {\n crate::env::remove_var(key_var);\n }\n }\n\n #[test]\n fn redact_secrets_redacts_mixed_case_token_assignments() {\n let input = \"my_token=ya29.ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789\";\n let out = redact_secrets(input);\n assert!(out.contains(\"[REDACTED_SECRET]\"));\n assert!(!out.contains(\"ya29.ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789\"));\n }\n\n #[test]\n fn redact_secrets_leaves_normal_output_unchanged() {\n let input = \"Found 5 files\\nNo auth errors\\nDone.\";\n assert_eq!(redact_secrets(input), input);\n }\n\n #[test]\n fn format_timestamp_is_stable_utc_rfc3339() {\n let ts = chrono::DateTime::parse_from_rfc3339(\"2025-03-15T02:24:13.250Z\")\n .unwrap()\n .with_timezone(&Utc);\n assert_eq!(Message::format_timestamp(&ts), \"2025-03-15T02:24:13.250Z\");\n }\n\n #[test]\n fn with_timestamps_prepends_utc_prefix_to_user_text() {\n let ts = chrono::DateTime::parse_from_rfc3339(\"2025-03-15T02:24:03Z\")\n .unwrap()\n .with_timezone(&Utc);\n let stamped = Message::with_timestamps(&[Message {\n role: Role::User,\n content: vec![ContentBlock::Text {\n text: \"hello\".to_string(),\n cache_control: None,\n }],\n timestamp: Some(ts),\n tool_duration_ms: None,\n }]);\n match &stamped[0].content[0] {\n ContentBlock::Text { text, .. } => {\n assert_eq!(text, \"[2025-03-15T02:24:03.000Z] hello\");\n }\n other => panic!(\"expected text block, got {other:?}\"),\n }\n }\n\n #[test]\n fn with_timestamps_adds_tool_timing_header_with_duration() {\n let ts = chrono::DateTime::parse_from_rfc3339(\"2025-03-15T02:24:13Z\")\n .unwrap()\n .with_timezone(&Utc);\n let stamped = Message::with_timestamps(&[Message {\n role: Role::User,\n content: vec![ContentBlock::ToolResult {\n tool_use_id: \"call_1\".to_string(),\n content: \"ok\".to_string(),\n is_error: None,\n }],\n timestamp: Some(ts),\n tool_duration_ms: Some(3_200),\n }]);\n match &stamped[0].content[0] {\n ContentBlock::ToolResult { content, .. } => {\n assert_eq!(\n content,\n \"[tool timing: start=2025-03-15T02:24:09.800Z finish=2025-03-15T02:24:13.000Z duration=3.2s] ok\"\n );\n }\n other => panic!(\"expected tool result block, got {other:?}\"),\n }\n }\n\n #[test]\n fn with_timestamps_skips_internal_system_reminders() {\n let ts = chrono::DateTime::parse_from_rfc3339(\"2025-03-15T02:24:13Z\")\n .unwrap()\n .with_timezone(&Utc);\n let original = Message {\n role: Role::User,\n content: vec![ContentBlock::Text {\n text: \"<system-reminder>\\ninternal\\n</system-reminder>\".to_string(),\n cache_control: None,\n }],\n timestamp: Some(ts),\n tool_duration_ms: None,\n };\n let stamped = Message::with_timestamps(&[original.clone()]);\n match &stamped[0].content[0] {\n ContentBlock::Text { text, .. } => {\n assert_eq!(text, \"<system-reminder>\\ninternal\\n</system-reminder>\");\n }\n other => panic!(\"expected text block, got {other:?}\"),\n }\n }\n\n #[test]\n fn ends_with_fresh_user_turn_accepts_plain_user_text() {\n let messages = vec![Message::user(\"hello\")];\n assert!(ends_with_fresh_user_turn(&messages));\n }\n\n #[test]\n fn ends_with_fresh_user_turn_rejects_trailing_tool_result() {\n let messages = vec![\n Message::user(\"hello\"),\n Message {\n role: Role::Assistant,\n content: vec![ContentBlock::ToolUse {\n id: \"call_1\".to_string(),\n name: \"bash\".to_string(),\n input: serde_json::json!({}),\n }],\n timestamp: Some(Utc::now()),\n tool_duration_ms: None,\n },\n Message::tool_result(\"call_1\", \"ok\", false),\n ];\n assert!(!ends_with_fresh_user_turn(&messages));\n }\n\n #[test]\n fn ends_with_fresh_user_turn_skips_internal_system_reminders() {\n let messages = vec![\n Message::user(\"hello\"),\n Message::user(\"<system-reminder>\\ninternal\\n</system-reminder>\"),\n ];\n assert!(ends_with_fresh_user_turn(&messages));\n }\n\n #[test]\n fn ends_with_fresh_user_turn_rejects_assistant_tail() {\n let messages = vec![\n Message::user(\"hello\"),\n Message::assistant_text(\"working on it\"),\n ];\n assert!(!ends_with_fresh_user_turn(&messages));\n }\n\n #[test]\n fn format_background_task_notification_markdown_uses_code_block_preview() {\n let rendered = format_background_task_notification_markdown(&BackgroundTaskCompleted {\n task_id: \"abc123\".to_string(),\n tool_name: \"bash\".to_string(),\n session_id: \"session\".to_string(),\n status: BackgroundTaskStatus::Completed,\n exit_code: Some(0),\n output_preview: \"[stderr] first line\\n[stdout] second line\\n\".to_string(),\n output_file: std::path::PathBuf::from(\"/tmp/output.log\"),\n duration_secs: 7.1,\n notify: true,\n wake: false,\n });\n\n assert!(\n rendered\n .contains(\"**Background task** `abc123` · `bash` · ✓ completed · 7.1s · exit 0\")\n );\n assert!(rendered.contains(\"```text\\n[stderr] first line\\n[stdout] second line\\n```\"));\n assert!(rendered.contains(\"_Full output:_ `bg action=\\\"output\\\" task_id=\\\"abc123\\\"`\"));\n }\n\n #[test]\n fn format_background_task_notification_markdown_handles_empty_preview() {\n let rendered = format_background_task_notification_markdown(&BackgroundTaskCompleted {\n task_id: \"abc123\".to_string(),\n tool_name: \"bash\".to_string(),\n session_id: \"session\".to_string(),\n status: BackgroundTaskStatus::Failed,\n exit_code: Some(9),\n output_preview: \"\\n\\n\".to_string(),\n output_file: std::path::PathBuf::from(\"/tmp/output.log\"),\n duration_secs: 1.0,\n notify: true,\n wake: false,\n });\n\n assert!(rendered.contains(\"✗ failed\"));\n assert!(rendered.contains(\"_No output captured._\"));\n }\n\n #[test]\n fn format_background_task_notification_markdown_renders_superseded_status() {\n let rendered = format_background_task_notification_markdown(&BackgroundTaskCompleted {\n task_id: \"abc123\".to_string(),\n tool_name: \"selfdev-build\".to_string(),\n session_id: \"session\".to_string(),\n status: BackgroundTaskStatus::Superseded,\n exit_code: Some(0),\n output_preview: \"Build completed, but source changed before activation\".to_string(),\n output_file: std::path::PathBuf::from(\"/tmp/output.log\"),\n duration_secs: 5.0,\n notify: true,\n wake: false,\n });\n\n assert!(rendered.contains(\"↻ superseded\"));\n assert!(rendered.contains(\"exit 0\"));\n assert!(rendered.contains(\"source changed before activation\"));\n }\n\n #[test]\n fn description_token_estimate_uses_chars_per_token_heuristic() {\n let def = ToolDefinition {\n name: \"read\".to_string(),\n description: \"abcdwxyz\".to_string(),\n input_schema: serde_json::json!({\"type\": \"object\"}),\n };\n\n assert_eq!(def.description_token_estimate(), 2);\n }\n\n #[test]\n fn parse_background_task_notification_markdown_extracts_fields() {\n let rendered = format_background_task_notification_markdown(&BackgroundTaskCompleted {\n task_id: \"abc123\".to_string(),\n tool_name: \"bash\".to_string(),\n session_id: \"session\".to_string(),\n status: BackgroundTaskStatus::Completed,\n exit_code: Some(0),\n output_preview: \"[stderr] first line\\n[stdout] second line\\n\".to_string(),\n output_file: std::path::PathBuf::from(\"/tmp/output.log\"),\n duration_secs: 7.1,\n notify: true,\n wake: false,\n });\n\n let parsed = parse_background_task_notification_markdown(&rendered)\n .expect(\"background task notification should parse\");\n assert_eq!(parsed.task_id, \"abc123\");\n assert_eq!(parsed.tool_name, \"bash\");\n assert_eq!(parsed.status, \"✓ completed\");\n assert_eq!(parsed.duration, \"7.1s\");\n assert_eq!(parsed.exit_label, \"exit 0\");\n assert_eq!(\n parsed.preview.as_deref(),\n Some(\"[stderr] first line\\n[stdout] second line\")\n );\n assert_eq!(\n parsed.full_output_command,\n \"bg action=\\\"output\\\" task_id=\\\"abc123\\\"\"\n );\n }\n}\n"}, "4921c56d8f39d44a0f8916f070c4e75ba4e461ec": {"t": 1785757242.8350463, "n": 42285, "h": "084caefa5c7e9a91", "c": "#![allow(dead_code)]\n\nuse crate::bus::{BackgroundTaskCompleted, BackgroundTaskStatus};\nuse chrono::Utc;\nuse regex::Regex;\nuse serde::{Deserialize, Serialize};\nuse std::collections::HashSet;\nuse std::sync::OnceLock;\n\n#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]\npub struct InputShellResult {\n pub command: String,\n #[serde(default, skip_serializing_if = \"Option::is_none\")]\n pub cwd: Option<String>,\n pub output: String,\n #[serde(default, skip_serializing_if = \"Option::is_none\")]\n pub exit_code: Option<i32>,\n pub duration_ms: u64,\n #[serde(default)]\n pub truncated: bool,\n #[serde(default)]\n pub failed_to_start: bool,\n}\n\nfn sanitize_fenced_block(text: &str) -> String {\n text.replace(\"```\", \"``\\u{200b}`\")\n}\n\npub fn format_input_shell_result_markdown(shell: &InputShellResult) -> String {\n let status = if shell.failed_to_start {\n \"✗ failed to start\".to_string()\n } else if shell.exit_code == Some(0) {\n \"✓ exit 0\".to_string()\n } else if let Some(code) = shell.exit_code {\n format!(\"✗ exit {}\", code)\n } else {\n \"✗ terminated\".to_string()\n };\n\n let mut meta = vec![status, Message::format_duration(shell.duration_ms)];\n if let Some(cwd) = shell.cwd.as_deref() {\n meta.push(format!(\"cwd `{}`\", cwd));\n }\n if shell.truncated {\n meta.push(\"truncated\".to_string());\n }\n\n let mut message = format!(\n \"**Shell command** · {}\\n\\n```bash\\n{}\\n```\",\n meta.join(\" · \"),\n sanitize_fenced_block(&shell.command)\n );\n\n if shell.output.trim().is_empty() {\n message.push_str(\"\\n\\n_No output._\");\n } else {\n message.push_str(&format!(\n \"\\n\\n```text\\n{}\\n```\",\n sanitize_fenced_block(shell.output.trim_end())\n ));\n }\n\n message\n}\n\npub fn input_shell_status_notice(shell: &InputShellResult) -> String {\n if shell.failed_to_start {\n \"Shell command failed to start\".to_string()\n } else if shell.exit_code == Some(0) {\n \"Shell command completed\".to_string()\n } else if let Some(code) = shell.exit_code {\n format!(\"Shell command failed (exit {})\", code)\n } else {\n \"Shell command terminated\".to_string()\n }\n}\n\nfn format_background_task_status(status: &BackgroundTaskStatus) -> &'static str {\n match status {\n BackgroundTaskStatus::Completed => \"✓ completed\",\n BackgroundTaskStatus::Superseded => \"↻ superseded\",\n BackgroundTaskStatus::Failed => \"✗ failed\",\n BackgroundTaskStatus::Running => \"running\",\n }\n}\n\nfn normalize_background_task_preview(preview: &str) -> Option<String> {\n let normalized = preview.replace(\"\\r\\n\", \"\\n\").replace('\\r', \"\\n\");\n let trimmed = normalized.trim_end();\n if trimmed.trim().is_empty() {\n None\n } else {\n Some(sanitize_fenced_block(trimmed))\n }\n}\n\npub fn format_background_task_notification_markdown(task: &BackgroundTaskCompleted) -> String {\n let exit_code = task\n .exit_code\n .map(|code| format!(\"exit {}\", code))\n .unwrap_or_else(|| \"exit n/a\".to_string());\n\n let mut message = format!(\n \"**Background task** `{}` · `{}` · {} · {:.1}s · {}\",\n task.task_id,\n task.tool_name,\n format_background_task_status(&task.status),\n task.duration_secs,\n exit_code,\n );\n\n if let Some(preview) = normalize_background_task_preview(&task.output_preview) {\n message.push_str(&format!(\"\\n\\n```text\\n{}\\n```\", preview));\n } else {\n message.push_str(\"\\n\\n_No output captured._\");\n }\n\n message.push_str(&format!(\n \"\\n\\n_Full output:_ `bg action=\\\"output\\\" task_id=\\\"{}\\\"`\",\n task.task_id\n ));\n\n message\n}\n\n#[derive(Debug, Clone, PartialEq, Eq)]\npub struct ParsedBackgroundTaskNotification {\n pub task_id: String,\n pub tool_name: String,\n pub status: String,\n pub duration: String,\n pub exit_label: String,\n pub preview: Option<String>,\n pub full_output_command: String,\n}\n\npub fn parse_background_task_notification_markdown(\n content: &str,\n) -> Option<ParsedBackgroundTaskNotification> {\n static HEADER_RE: OnceLock<Regex> = OnceLock::new();\n static FULL_OUTPUT_RE: OnceLock<Regex> = OnceLock::new();\n\n let header_re = HEADER_RE.get_or_init(|| {\n Regex::new(\n r\"^\\*\\*Background task\\*\\* `(?P<task_id>[^`]+)` · `(?P<tool_name>[^`]+)` · (?P<status>.+?) · (?P<duration>[0-9]+(?:\\.[0-9]+)?s) · (?P<exit_label>.+)$\",\n )\n .expect(\"valid background task header regex\")\n });\n let full_output_re = FULL_OUTPUT_RE.get_or_init(|| {\n Regex::new(r#\"^_Full output:_ `(?P<command>[^`]+)`$\"#)\n .expect(\"valid background task full output regex\")\n });\n\n let normalized = content.replace(\"\\r\\n\", \"\\n\").replace('\\r', \"\\n\");\n let mut sections = normalized.split(\"\\n\\n\");\n let header = sections.next()?.trim();\n let captures = header_re.captures(header)?;\n\n let mut preview: Option<String> = None;\n let mut full_output_command: Option<String> = None;\n\n for section in sections {\n let trimmed = section.trim();\n if trimmed.is_empty() {\n continue;\n }\n\n if let Some(captures) = full_output_re.captures(trimmed) {\n full_output_command = Some(captures[\"command\"].to_string());\n continue;\n }\n\n if trimmed == \"_No output captured._\" {\n preview = None;\n continue;\n }\n\n if let Some(fenced) = trimmed\n .strip_prefix(\"```text\\n\")\n .and_then(|body| body.strip_suffix(\"\\n```\"))\n {\n preview = Some(fenced.to_string());\n }\n }\n\n Some(ParsedBackgroundTaskNotification {\n task_id: captures[\"task_id\"].to_string(),\n tool_name: captures[\"tool_name\"].to_string(),\n status: captures[\"status\"].to_string(),\n duration: captures[\"duration\"].to_string(),\n exit_label: captures[\"exit_label\"].to_string(),\n preview,\n full_output_command: full_output_command?,\n })\n}\n\npub fn background_task_status_notice(task: &BackgroundTaskCompleted) -> String {\n match task.status {\n BackgroundTaskStatus::Completed => {\n format!(\"Background task completed · {}\", task.tool_name)\n }\n BackgroundTaskStatus::Superseded => {\n format!(\"Background task superseded · {}\", task.tool_name)\n }\n BackgroundTaskStatus::Failed => match task.exit_code {\n Some(code) => format!(\n \"Background task failed · {} · exit {}\",\n task.tool_name, code\n ),\n None => format!(\"Background task failed · {}\", task.tool_name),\n },\n BackgroundTaskStatus::Running => format!(\"Background task running · {}\", task.tool_name),\n }\n}\n\n/// Role in conversation\n#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]\n#[serde(rename_all = \"lowercase\")]\npub enum Role {\n User,\n Assistant,\n}\n\n/// Plain-text tool output placeholder when execution was interrupted.\npub const TOOL_OUTPUT_MISSING_TEXT: &str =\n \"Tool output missing (session interrupted before tool execution completed)\";\n\n/// A message in the conversation\n#[derive(Debug, Clone, Serialize, Deserialize)]\npub struct Message {\n pub role: Role,\n pub content: Vec<ContentBlock>,\n #[serde(default, skip_serializing_if = \"Option::is_none\")]\n pub timestamp: Option<chrono::DateTime<Utc>>,\n #[serde(default, skip_serializing_if = \"Option::is_none\")]\n pub tool_duration_ms: Option<u64>,\n}\n\n/// Cache control metadata for prompt caching\n#[derive(Debug, Clone, Serialize, Deserialize)]\npub struct CacheControl {\n #[serde(rename = \"type\")]\n pub kind: String,\n #[serde(skip_serializing_if = \"Option::is_none\")]\n pub ttl: Option<String>,\n}\n\nimpl CacheControl {\n pub fn ephemeral(ttl: Option<String>) -> Self {\n Self {\n kind: \"ephemeral\".to_string(),\n ttl,\n }\n }\n}\n\n/// Content block within a message\n#[derive(Debug, Clone, Serialize, Deserialize)]\n#[serde(tag = \"type\", rename_all = \"snake_case\")]\npub enum ContentBlock {\n Text {\n text: String,\n #[serde(skip_serializing_if = \"Option::is_none\")]\n cache_control: Option<CacheControl>,\n },\n /// Hidden reasoning content used for providers that require it (not displayed)\n Reasoning {\n text: String,\n },\n ToolUse {\n id: String,\n name: String,\n input: serde_json::Value,\n },\n ToolResult {\n tool_use_id: String,\n content: String,\n #[serde(skip_serializing_if = \"Option::is_none\")]\n is_error: Option<bool>,\n },\n Image {\n media_type: String,\n data: String,\n },\n /// Hidden OpenAI Responses compaction item used to preserve native\n /// compaction state across turns/saves when jcode explicitly triggers it.\n OpenAICompaction {\n encrypted_content: String,\n },\n}\n\nimpl Message {\n pub fn user(text: &str) -> Self {\n Self {\n role: Role::User,\n content: vec![ContentBlock::Text {\n text: text.to_string(),\n cache_control: None,\n }],\n timestamp: Some(Utc::now()),\n tool_duration_ms: None,\n }\n }\n\n pub fn user_with_images(text: &str, images: Vec<(String, String)>) -> Self {\n let mut content: Vec<ContentBlock> = images\n .into_iter()\n .map(|(media_type, data)| ContentBlock::Image { media_type, data })\n .collect();\n content.push(ContentBlock::Text {\n text: text.to_string(),\n cache_control: None,\n });\n Self {\n role: Role::User,\n content,\n timestamp: Some(Utc::now()),\n tool_duration_ms: None,\n }\n }\n\n pub fn assistant_text(text: &str) -> Self {\n Self {\n role: Role::Assistant,\n content: vec![ContentBlock::Text {\n text: text.to_string(),\n cache_control: None,\n }],\n timestamp: Some(Utc::now()),\n tool_duration_ms: None,\n }\n }\n\n pub fn tool_result(tool_use_id: &str, content: &str, is_error: bool) -> Self {\n Self::tool_result_with_duration(tool_use_id, content, is_error, None)\n }\n\n pub fn tool_result_with_duration(\n tool_use_id: &str,\n content: &str,\n is_error: bool,\n tool_duration_ms: Option<u64>,\n ) -> Self {\n Self {\n role: Role::User,\n content: vec![ContentBlock::ToolResult {\n tool_use_id: tool_use_id.to_string(),\n content: content.to_string(),\n is_error: if is_error { Some(true) } else { None },\n }],\n timestamp: Some(Utc::now()),\n tool_duration_ms,\n }\n }\n\n /// Format a timestamp deterministically in UTC for injection into model-visible content.\n pub fn format_timestamp(ts: &chrono::DateTime<Utc>) -> String {\n ts.to_rfc3339_opts(chrono::SecondsFormat::Millis, true)\n }\n\n pub fn format_duration(duration_ms: u64) -> String {\n match duration_ms {\n 0..=999 => format!(\"{}ms\", duration_ms),\n 1_000..=9_999 => format!(\"{:.1}s\", duration_ms as f64 / 1000.0),\n 10_000..=59_999 => format!(\"{}s\", duration_ms / 1000),\n _ => {\n let total_seconds = duration_ms / 1000;\n let minutes = total_seconds / 60;\n let seconds = total_seconds % 60;\n if seconds == 0 {\n format!(\"{}m\", minutes)\n } else {\n format!(\"{}m {}s\", minutes, seconds)\n }\n }\n }\n }\n\n pub fn is_internal_system_reminder(&self) -> bool {\n self.content\n .iter()\n .find_map(|block| match block {\n ContentBlock::Text { text, .. } => Some(text.trim_start()),\n _ => None,\n })\n .is_some_and(|text| text.starts_with(\"<system-reminder>\"))\n }\n\n fn should_skip_timestamp_injection(&self) -> bool {\n self.is_internal_system_reminder()\n }\n\n fn tool_result_tag(&self, ts: &chrono::DateTime<Utc>) -> String {\n match self.tool_duration_ms {\n Some(duration_ms) => {\n let duration_ms_i64 = i64::try_from(duration_ms).unwrap_or(i64::MAX);\n let start_ts = ts\n .checked_sub_signed(chrono::Duration::milliseconds(duration_ms_i64))\n .unwrap_or(*ts);\n format!(\n \"[tool timing: start={} finish={} duration={}]\",\n Self::format_timestamp(&start_ts),\n Self::format_timestamp(ts),\n Self::format_duration(duration_ms)\n )\n }\n None => format!(\"[{}]\", Self::format_timestamp(ts)),\n }\n }\n\n /// Return a copy of messages with timestamps injected into user-role text content.\n /// Tool results get a stable UTC timing header prepended to content.\n /// User text messages get a stable UTC timestamp prepended to the first text block.\n pub fn with_timestamps(messages: &[Message]) -> Vec<Message> {\n messages\n .iter()\n .map(|msg| {\n let Some(ts) = msg.timestamp else {\n return msg.clone();\n };\n if msg.role != Role::User || msg.should_skip_timestamp_injection() {\n return msg.clone();\n }\n let text_tag = format!(\"[{}]\", Self::format_timestamp(&ts));\n let tool_result_tag = msg.tool_result_tag(&ts);\n let mut msg = msg.clone();\n let mut tagged = false;\n for block in &mut msg.content {\n match block {\n ContentBlock::Text { text, .. } if !tagged => {\n *text = format!(\"{} {}\", text_tag, text);\n tagged = true;\n }\n ContentBlock::ToolResult { content, .. } if !tagged => {\n *content = format!(\"{} {}\", tool_result_tag, content);\n tagged = true;\n }\n _ => {}\n }\n }\n msg\n })\n .collect()\n }\n}\n\nconst STABLE_HASH_SEED: u64 = 0xcbf29ce484222325;\nconst STABLE_HASH_PRIME: u64 = 0x100000001b3;\n\nfn stable_hash_bytes(bytes: &[u8]) -> u64 {\n let mut hash = STABLE_HASH_SEED;\n for byte in bytes {\n hash ^= *byte as u64;\n hash = hash.wrapping_mul(STABLE_HASH_PRIME);\n }\n hash\n}\n\npub(crate) fn extend_stable_hash(acc: u64, next: u64) -> u64 {\n stable_hash_bytes(&[acc.to_le_bytes().as_slice(), next.to_le_bytes().as_slice()].concat())\n}\n\npub(crate) fn stable_message_hash(message: &Message) -> u64 {\n match serde_json::to_vec(message) {\n Ok(bytes) => stable_hash_bytes(&bytes),\n Err(_) => stable_hash_bytes(format!(\"{:?}\", message).as_bytes()),\n }\n}\n\npub fn ends_with_fresh_user_turn(messages: &[Message]) -> bool {\n for msg in messages.iter().rev() {\n if msg.role != Role::User {\n return false;\n }\n\n if msg\n .content\n .iter()\n .any(|block| matches!(block, ContentBlock::ToolResult { .. }))\n {\n return false;\n }\n\n if msg.content.is_empty() {\n return false;\n }\n\n let mut saw_user_text = false;\n for block in &msg.content {\n match block {\n ContentBlock::Text { text, .. } => {\n let trimmed = text.trim();\n if !trimmed.is_empty() && !trimmed.starts_with(\"<system-reminder>\") {\n saw_user_text = true;\n }\n }\n _ => return false,\n }\n }\n\n if saw_user_text {\n return true;\n }\n\n if msg.is_internal_system_reminder() {\n continue;\n }\n\n return false;\n }\n\n false\n}\n\n/// Sanitize a tool ID so it matches the pattern `^[a-zA-Z0-9_-]+$`.\n///\n/// Different providers generate tool IDs in different formats. When switching\n/// from one provider to another mid-conversation, the historical tool IDs may\n/// contain characters that the new provider rejects (e.g., dots in Copilot IDs\n/// sent to Anthropic). This function replaces any invalid characters with\n/// underscores.\npub fn sanitize_tool_id(id: &str) -> String {\n if id.is_empty() {\n return \"unknown\".to_string();\n }\n let sanitized: String = id\n .chars()\n .map(|c| {\n if c.is_ascii_alphanumeric() || c == '_' || c == '-' {\n c\n } else {\n '_'\n }\n })\n .collect();\n if sanitized.is_empty() {\n \"unknown\".to_string()\n } else {\n sanitized\n }\n}\n\n/// Redact likely secrets from persisted tool output.\n///\n/// This is a best-effort safeguard for local session history files. It targets\n/// high-confidence token/key patterns and common `KEY=VALUE` assignments used by\n/// auth flows.\npub fn redact_secrets(text: &str) -> String {\n // Fast path to avoid regex work for most tool outputs.\n let lower = text.to_ascii_lowercase();\n\n if !text.contains(\"sk-\")\n && !text.contains(\"ghp_\")\n && !text.contains(\"github_pat_\")\n && !text.contains(\"AIza\")\n && !text.contains(\"ya29.\")\n && !text.contains(\"xox\")\n && !lower.contains(\"api_key\")\n && !lower.contains(\"token\")\n {\n return text.to_string();\n }\n\n static DIRECT_PATTERNS: OnceLock<Vec<Regex>> = OnceLock::new();\n static ASSIGNMENT_PATTERNS: OnceLock<Vec<Regex>> = OnceLock::new();\n\n let direct_patterns = DIRECT_PATTERNS.get_or_init(|| {\n vec![\n Regex::new(r\"sk-ant-(?:oat|ort)01-[A-Za-z0-9_-]{20,}\")\n .expect(\"valid Anthropic OAuth token regex\"),\n Regex::new(r\"sk-or-v1-[A-Za-z0-9_-]{20,}\").expect(\"valid OpenRouter key regex\"),\n Regex::new(r\"ghp_[A-Za-z0-9]{20,}\").expect(\"valid GitHub PAT regex\"),\n Regex::new(r\"github_pat_[A-Za-z0-9_]{20,}\")\n .expect(\"valid GitHub fine-grained PAT regex\"),\n Regex::new(r\"ya29\\.[A-Za-z0-9._-]{20,}\").expect(\"valid Google OAuth token regex\"),\n Regex::new(r\"AIza[0-9A-Za-z_-]{20,}\").expect(\"valid Google API key regex\"),\n Regex::new(r\"xox[baprs]-[A-Za-z0-9-]{10,}\").expect(\"valid Slack token regex\"),\n ]\n });\n\n let assignment_patterns = ASSIGNMENT_PATTERNS.get_or_init(|| {\n vec![\n Regex::new(r\"(?m)^\\s*(OPENROUTER_API_KEY\\s*=\\s*)[^\\r\\n]+\")\n .expect(\"valid OPENROUTER_API_KEY assignment regex\"),\n Regex::new(r\"(?m)^\\s*(OPENCODE_API_KEY\\s*=\\s*)[^\\r\\n]+\")\n .expect(\"valid OPENCODE_API_KEY assignment regex\"),\n Regex::new(r\"(?m)^\\s*(OPENCODE_GO_API_KEY\\s*=\\s*)[^\\r\\n]+\")\n .expect(\"valid OPENCODE_GO_API_KEY assignment regex\"),\n Regex::new(r\"(?m)^\\s*(ZHIPU_API_KEY\\s*=\\s*)[^\\r\\n]+\")\n .expect(\"valid ZHIPU_API_KEY assignment regex\"),\n Regex::new(r\"(?m)^\\s*(ZAI_API_KEY\\s*=\\s*)[^\\r\\n]+\")\n .expect(\"valid ZAI_API_KEY assignment regex\"),\n Regex::new(r\"(?m)^\\s*(302AI_API_KEY\\s*=\\s*)[^\\r\\n]+\")\n .expect(\"valid 302AI_API_KEY assignment regex\"),\n Regex::new(r\"(?m)^\\s*(BASETEN_API_KEY\\s*=\\s*)[^\\r\\n]+\")\n .expect(\"valid BASETEN_API_KEY assignment regex\"),\n Regex::new(r\"(?m)^\\s*(CORTECS_API_KEY\\s*=\\s*)[^\\r\\n]+\")\n .expect(\"valid CORTECS_API_KEY assignment regex\"),\n Regex::new(r\"(?m)^\\s*(DEEPSEEK_API_KEY\\s*=\\s*)[^\\r\\n]+\")\n .expect(\"valid DEEPSEEK_API_KEY assignment regex\"),\n Regex::new(r\"(?m)^\\s*(FIRMWARE_API_KEY\\s*=\\s*)[^\\r\\n]+\")\n .expect(\"valid FIRMWARE_API_KEY assignment regex\"),\n Regex::new(r\"(?m)^\\s*(HF_TOKEN\\s*=\\s*)[^\\r\\n]+\")\n .expect(\"valid HF_TOKEN assignment regex\"),\n Regex::new(r\"(?m)^\\s*(MOONSHOT_API_KEY\\s*=\\s*)[^\\r\\n]+\")\n .expect(\"valid MOONSHOT_API_KEY assignment regex\"),\n Regex::new(r\"(?m)^\\s*(NEBIUS_API_KEY\\s*=\\s*)[^\\r\\n]+\")\n .expect(\"valid NEBIUS_API_KEY assignment regex\"),\n Regex::new(r\"(?m)^\\s*(SCALEWAY_API_KEY\\s*=\\s*)[^\\r\\n]+\")\n .expect(\"valid SCALEWAY_API_KEY assignment regex\"),\n Regex::new(r\"(?m)^\\s*(STACKIT_API_KEY\\s*=\\s*)[^\\r\\n]+\")\n .expect(\"valid STACKIT_API_KEY assignment regex\"),\n Regex::new(r\"(?m)^\\s*(GROQ_API_KEY\\s*=\\s*)[^\\r\\n]+\")\n .expect(\"valid GROQ_API_KEY assignment regex\"),\n Regex::new(r\"(?m)^\\s*(MISTRAL_API_KEY\\s*=\\s*)[^\\r\\n]+\")\n .expect(\"valid MISTRAL_API_KEY assignment regex\"),\n Regex::new(r\"(?m)^\\s*(PERPLEXITY_API_KEY\\s*=\\s*)[^\\r\\n]+\")\n .expect(\"valid PERPLEXITY_API_KEY assignment regex\"),\n Regex::new(r\"(?m)^\\s*(TOGETHER_API_KEY\\s*=\\s*)[^\\r\\n]+\")\n .expect(\"valid TOGETHER_API_KEY assignment regex\"),\n Regex::new(r\"(?m)^\\s*(DEEPINFRA_API_KEY\\s*=\\s*)[^\\r\\n]+\")\n .expect(\"valid DEEPINFRA_API_KEY assignment regex\"),\n Regex::new(r\"(?m)^\\s*(XAI_API_KEY\\s*=\\s*)[^\\r\\n]+\")\n .expect(\"valid XAI_API_KEY assignment regex\"),\n Regex::new(r\"(?m)^\\s*(LMSTUDIO_API_KEY\\s*=\\s*)[^\\r\\n]+\")\n .expect(\"valid LMSTUDIO_API_KEY assignment regex\"),\n Regex::new(r\"(?m)^\\s*(OLLAMA_API_KEY\\s*=\\s*)[^\\r\\n]+\")\n .expect(\"valid OLLAMA_API_KEY assignment regex\"),\n Regex::new(r\"(?m)^\\s*(CHUTES_API_KEY\\s*=\\s*)[^\\r\\n]+\")\n .expect(\"valid CHUTES_API_KEY assignment regex\"),\n Regex::new(r\"(?m)^\\s*(CEREBRAS_API_KEY\\s*=\\s*)[^\\r\\n]+\")\n .expect(\"valid CEREBRAS_API_KEY assignment regex\"),\n Regex::new(r\"(?m)^\\s*(OPENAI_COMPAT_API_KEY\\s*=\\s*)[^\\r\\n]+\")\n .expect(\"valid OPENAI_COMPAT_API_KEY assignment regex\"),\n Regex::new(r\"(?m)^\\s*(ANTHROPIC_API_KEY\\s*=\\s*)[^\\r\\n]+\")\n .expect(\"valid ANTHROPIC_API_KEY assignment regex\"),\n Regex::new(r\"(?m)^\\s*(OPENAI_API_KEY\\s*=\\s*)[^\\r\\n]+\")\n .expect(\"valid OPENAI_API_KEY assignment regex\"),\n Regex::new(r\"(?m)^\\s*(AZURE_OPENAI_API_KEY\\s*=\\s*)[^\\r\\n]+\")\n .expect(\"valid AZURE_OPENAI_API_KEY assignment regex\"),\n Regex::new(r\"(?m)^\\s*(CURSOR_API_KEY\\s*=\\s*)[^\\r\\n]+\")\n .expect(\"valid CURSOR_API_KEY assignment regex\"),\n Regex::new(r\"(?m)^\\s*(GITHUB_TOKEN\\s*=\\s*)[^\\r\\n]+\")\n .expect(\"valid GITHUB_TOKEN assignment regex\"),\n ]\n });\n\n let mut redacted = text.to_string();\n let mut redacted_keys: HashSet<String> = [\n \"OPENROUTER_API_KEY\",\n \"OPENCODE_API_KEY\",\n \"OPENCODE_GO_API_KEY\",\n \"ZHIPU_API_KEY\",\n \"ZAI_API_KEY\",\n \"302AI_API_KEY\",\n \"BASETEN_API_KEY\",\n \"CORTECS_API_KEY\",\n \"DEEPSEEK_API_KEY\",\n \"FIRMWARE_API_KEY\",\n \"HF_TOKEN\",\n \"MOONSHOT_API_KEY\",\n \"NEBIUS_API_KEY\",\n \"SCALEWAY_API_KEY\",\n \"STACKIT_API_KEY\",\n \"GROQ_API_KEY\",\n \"MISTRAL_API_KEY\",\n \"PERPLEXITY_API_KEY\",\n \"TOGETHER_API_KEY\",\n \"DEEPINFRA_API_KEY\",\n \"XAI_API_KEY\",\n \"LMSTUDIO_API_KEY\",\n \"OLLAMA_API_KEY\",\n \"CHUTES_API_KEY\",\n \"CEREBRAS_API_KEY\",\n \"OPENAI_COMPAT_API_KEY\",\n \"ANTHROPIC_API_KEY\",\n \"OPENAI_API_KEY\",\n \"AZURE_OPENAI_API_KEY\",\n \"CURSOR_API_KEY\",\n \"GITHUB_TOKEN\",\n ]\n .iter()\n .map(|k| (*k).to_string())\n .collect();\n\n for re in direct_patterns {\n redacted = re.replace_all(&redacted, \"[REDACTED_SECRET]\").into_owned();\n }\n\n for re in assignment_patterns {\n redacted = re\n .replace_all(&redacted, \"${1}[REDACTED_SECRET]\")\n .into_owned();\n }\n\n // Also redact custom API key variable names configured at runtime.\n for source in [\n \"JCODE_OPENROUTER_API_KEY_NAME\",\n \"JCODE_OPENAI_COMPAT_API_KEY_NAME\",\n ] {\n let Some(key_name) = std::env::var(source)\n .ok()\n .map(|v| v.trim().to_string())\n .filter(|v| !v.is_empty())\n else {\n continue;\n };\n\n if !key_name\n .chars()\n .all(|c| c.is_ascii_uppercase() || c.is_ascii_digit() || c == '_')\n {\n continue;\n }\n if !redacted_keys.insert(key_name.clone()) {\n continue;\n }\n\n let pattern = format!(r\"(?m)^\\s*({}\\s*=\\s*)[^\\r\\n]+\", regex::escape(&key_name));\n if let Ok(re) = Regex::new(&pattern) {\n redacted = re\n .replace_all(&redacted, \"${1}[REDACTED_SECRET]\")\n .into_owned();\n }\n }\n\n redacted\n}\n\n/// Tool definition for the API\n#[derive(Debug, Clone, Serialize)]\npub struct ToolDefinition {\n pub name: String,\n pub description: String,\n pub input_schema: serde_json::Value,\n}\n\n/// A tool call from the model\n#[derive(Debug, Clone, Default, PartialEq, Eq, serde::Serialize, serde::Deserialize)]\npub struct ToolCall {\n #[serde(default)]\n pub id: String,\n #[serde(default)]\n pub name: String,\n #[serde(default)]\n pub input: serde_json::Value,\n #[serde(default, skip_serializing_if = \"Option::is_none\")]\n pub intent: Option<String>,\n}\n\n/// Connection phase for status bar transparency\n#[derive(Debug, Clone, PartialEq)]\npub enum ConnectionPhase {\n /// Refreshing OAuth token\n Authenticating,\n /// TCP + TLS connection to API\n Connecting,\n /// HTTP request sent, waiting for first response byte\n WaitingForResponse,\n /// First byte received, stream is active\n Streaming,\n /// Retrying after a transient error\n Retrying { attempt: u32, max: u32 },\n}\n\nimpl std::fmt::Display for ConnectionPhase {\n fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {\n match self {\n ConnectionPhase::Authenticating => write!(f, \"authenticating\"),\n ConnectionPhase::Connecting => write!(f, \"connecting\"),\n ConnectionPhase::WaitingForResponse => write!(f, \"waiting for response\"),\n ConnectionPhase::Streaming => write!(f, \"streaming\"),\n ConnectionPhase::Retrying { attempt, max } => {\n write!(f, \"retrying ({}/{})\", attempt, max)\n }\n }\n }\n}\n\n/// Streaming event from provider\n#[derive(Debug, Clone)]\npub enum StreamEvent {\n /// Text content delta\n TextDelta(String),\n /// Tool use started\n ToolUseStart { id: String, name: String },\n /// Tool input delta (JSON fragment)\n ToolInputDelta(String),\n /// Tool use complete\n ToolUseEnd,\n /// Tool result from provider (provider already executed the tool)\n ToolResult {\n tool_use_id: String,\n content: String,\n is_error: bool,\n },\n /// Extended thinking started\n ThinkingStart,\n /// Extended thinking delta (reasoning content)\n ThinkingDelta(String),\n /// Extended thinking ended\n ThinkingEnd,\n /// Extended thinking completed with duration\n ThinkingDone { duration_secs: f64 },\n /// Message complete (may have stop reason)\n MessageEnd { stop_reason: Option<String> },\n /// Token usage update\n TokenUsage {\n input_tokens: Option<u64>,\n output_tokens: Option<u64>,\n cache_read_input_tokens: Option<u64>,\n cache_creation_input_tokens: Option<u64>,\n },\n /// Active transport/connection type for this stream\n ConnectionType { connection: String },\n /// Connection phase update (for status bar transparency)\n ConnectionPhase { phase: ConnectionPhase },\n /// Provider-supplied human-readable transport detail for the status line\n StatusDetail { detail: String },\n /// Error occurred\n Error {\n message: String,\n /// Seconds until rate limit resets (if this is a rate limit error)\n retry_after_secs: Option<u64>,\n },\n /// Provider session ID (for conversation resume)\n SessionId(String),\n /// Compaction occurred (context was summarized)\n Compaction {\n trigger: String,\n pre_tokens: Option<u64>,\n /// Provider-native compaction artifact, if one was emitted.\n openai_encrypted_content: Option<String>,\n },\n /// Upstream provider info (e.g., which provider OpenRouter routed to)\n UpstreamProvider { provider: String },\n /// Native tool call from a provider bridge that needs execution by jcode\n NativeToolCall {\n request_id: String,\n tool_name: String,\n input: serde_json::Value,\n },\n}\n\n#[cfg(test)]\nmod tests {\n use super::*;\n\n #[test]\n fn sanitize_tool_id_alphanumeric_passthrough() {\n assert_eq!(\n sanitize_tool_id(\"toolu_01XFDUDYJgAACzvnptvVer6u\"),\n \"toolu_01XFDUDYJgAACzvnptvVer6u\"\n );\n assert_eq!(sanitize_tool_id(\"call_abc123\"), \"call_abc123\");\n assert_eq!(\n sanitize_tool_id(\"call_1234567890_9876543210\"),\n \"call_1234567890_9876543210\"\n );\n }\n\n #[test]\n fn sanitize_tool_id_hyphens_passthrough() {\n assert_eq!(sanitize_tool_id(\"call-abc-123\"), \"call-abc-123\");\n assert_eq!(\n sanitize_tool_id(\"tool_use-id_with-mixed\"),\n \"tool_use-id_with-mixed\"\n );\n }\n\n #[test]\n fn sanitize_tool_id_replaces_dots() {\n assert_eq!(\n sanitize_tool_id(\"chatcmpl-abc.def.ghi\"),\n \"chatcmpl-abc_def_ghi\"\n );\n assert_eq!(sanitize_tool_id(\"call.123\"), \"call_123\");\n }\n\n #[test]\n fn sanitize_tool_id_replaces_colons() {\n assert_eq!(sanitize_tool_id(\"call:123:456\"), \"call_123_456\");\n }\n\n #[test]\n fn sanitize_tool_id_replaces_special_chars() {\n assert_eq!(\n sanitize_tool_id(\"id@with#special$chars\"),\n \"id_with_special_chars\"\n );\n assert_eq!(sanitize_tool_id(\"id with spaces\"), \"id_with_spaces\");\n }\n\n #[test]\n fn sanitize_tool_id_empty_returns_unknown() {\n assert_eq!(sanitize_tool_id(\"\"), \"unknown\");\n }\n\n #[test]\n fn sanitize_tool_id_copilot_to_anthropic() {\n assert_eq!(\n sanitize_tool_id(\"chatcmpl-BF2xX.tool_call.0\"),\n \"chatcmpl-BF2xX_tool_call_0\"\n );\n }\n\n #[test]\n fn sanitize_tool_id_already_valid_unchanged() {\n let valid_ids = [\n \"toolu_01XFDUDYJgAACzvnptvVer6u\",\n \"call_abc123\",\n \"fallback_text_call_call_1234567890_9876543210\",\n \"tool_123\",\n \"a\",\n \"A\",\n \"0\",\n \"_\",\n \"-\",\n \"a-b_c\",\n ];\n for id in valid_ids {\n assert_eq!(sanitize_tool_id(id), id, \"ID '{}' should be unchanged\", id);\n }\n }\n\n #[test]\n fn redact_secrets_redacts_known_direct_token_formats() {\n let input = \"access=sk-ant-oat01-ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789\\nopenrouter=sk-or-v1-abcdefghijklmnopqrstuvwxyz0123456789\\ngithub=ghp_ABCDEFGHIJKLMNOPQRSTUVWXYZ0123\\n\";\n let out = redact_secrets(input);\n assert!(!out.contains(\"sk-ant-oat01-\"));\n assert!(!out.contains(\"sk-or-v1-\"));\n assert!(!out.contains(\"ghp_ABCDEFGHIJKLMNOPQRSTUVWXYZ0123\"));\n assert!(out.matches(\"[REDACTED_SECRET]\").count() >= 3);\n }\n\n #[test]\n fn redact_secrets_redacts_env_style_assignments() {\n let input = \"OPENROUTER_API_KEY=sk-or-v1-abc123abc123abc123abc123\\nOPENCODE_API_KEY=oc_test_secret\\nOPENCODE_GO_API_KEY=ocgo_test_secret\\nZAI_API_KEY=zai_secret\\nCHUTES_API_KEY=chutes_secret\\nCEREBRAS_API_KEY=cerebras_secret\\nOPENAI_COMPAT_API_KEY=compat_secret\\nCURSOR_API_KEY='my_cursor_secret_value'\\nOPENAI_API_KEY=sk-test-openai-example\\nAZURE_OPENAI_API_KEY=azure-openai-secret\\n\";\n let out = redact_secrets(input);\n assert!(out.contains(\"OPENROUTER_API_KEY=[REDACTED_SECRET]\"));\n assert!(out.contains(\"OPENCODE_API_KEY=[REDACTED_SECRET]\"));\n assert!(out.contains(\"OPENCODE_GO_API_KEY=[REDACTED_SECRET]\"));\n assert!(out.contains(\"ZAI_API_KEY=[REDACTED_SECRET]\"));\n assert!(out.contains(\"CHUTES_API_KEY=[REDACTED_SECRET]\"));\n assert!(out.contains(\"CEREBRAS_API_KEY=[REDACTED_SECRET]\"));\n assert!(out.contains(\"OPENAI_COMPAT_API_KEY=[REDACTED_SECRET]\"));\n assert!(out.contains(\"CURSOR_API_KEY=[REDACTED_SECRET]\"));\n assert!(out.contains(\"OPENAI_API_KEY=[REDACTED_SECRET]\"));\n assert!(out.contains(\"AZURE_OPENAI_API_KEY=[REDACTED_SECRET]\"));\n assert!(!out.contains(\"my_cursor_secret_value\"));\n }\n\n #[test]\n fn redact_secrets_redacts_runtime_key_assignment() {\n let key_var = \"JCODE_OPENAI_COMPAT_API_KEY_NAME\";\n let prev = std::env::var(key_var).ok();\n crate::env::set_var(key_var, \"GROQ_API_KEY\");\n\n let input = \"GROQ_API_KEY=my_secret_token_value\";\n let out = redact_secrets(input);\n assert_eq!(out, \"GROQ_API_KEY=[REDACTED_SECRET]\");\n\n if let Some(v) = prev {\n crate::env::set_var(key_var, v);\n } else {\n crate::env::remove_var(key_var);\n }\n }\n\n #[test]\n fn redact_secrets_redacts_mixed_case_token_assignments() {\n let input = \"my_token=ya29.ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789\";\n let out = redact_secrets(input);\n assert!(out.contains(\"[REDACTED_SECRET]\"));\n assert!(!out.contains(\"ya29.ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789\"));\n }\n\n #[test]\n fn redact_secrets_leaves_normal_output_unchanged() {\n let input = \"Found 5 files\\nNo auth errors\\nDone.\";\n assert_eq!(redact_secrets(input), input);\n }\n\n #[test]\n fn format_timestamp_is_stable_utc_rfc3339() {\n let ts = chrono::DateTime::parse_from_rfc3339(\"2025-03-15T02:24:13.250Z\")\n .unwrap()\n .with_timezone(&Utc);\n assert_eq!(Message::format_timestamp(&ts), \"2025-03-15T02:24:13.250Z\");\n }\n\n #[test]\n fn with_timestamps_prepends_utc_prefix_to_user_text() {\n let ts = chrono::DateTime::parse_from_rfc3339(\"2025-03-15T02:24:03Z\")\n .unwrap()\n .with_timezone(&Utc);\n let stamped = Message::with_timestamps(&[Message {\n role: Role::User,\n content: vec![ContentBlock::Text {\n text: \"hello\".to_string(),\n cache_control: None,\n }],\n timestamp: Some(ts),\n tool_duration_ms: None,\n }]);\n match &stamped[0].content[0] {\n ContentBlock::Text { text, .. } => {\n assert_eq!(text, \"[2025-03-15T02:24:03.000Z] hello\");\n }\n other => panic!(\"expected text block, got {other:?}\"),\n }\n }\n\n #[test]\n fn with_timestamps_adds_tool_timing_header_with_duration() {\n let ts = chrono::DateTime::parse_from_rfc3339(\"2025-03-15T02:24:13Z\")\n .unwrap()\n .with_timezone(&Utc);\n let stamped = Message::with_timestamps(&[Message {\n role: Role::User,\n content: vec![ContentBlock::ToolResult {\n tool_use_id: \"call_1\".to_string(),\n content: \"ok\".to_string(),\n is_error: None,\n }],\n timestamp: Some(ts),\n tool_duration_ms: Some(3_200),\n }]);\n match &stamped[0].content[0] {\n ContentBlock::ToolResult { content, .. } => {\n assert_eq!(\n content,\n \"[tool timing: start=2025-03-15T02:24:09.800Z finish=2025-03-15T02:24:13.000Z duration=3.2s] ok\"\n );\n }\n other => panic!(\"expected tool result block, got {other:?}\"),\n }\n }\n\n #[test]\n fn with_timestamps_skips_internal_system_reminders() {\n let ts = chrono::DateTime::parse_from_rfc3339(\"2025-03-15T02:24:13Z\")\n .unwrap()\n .with_timezone(&Utc);\n let original = Message {\n role: Role::User,\n content: vec![ContentBlock::Text {\n text: \"<system-reminder>\\ninternal\\n</system-reminder>\".to_string(),\n cache_control: None,\n }],\n timestamp: Some(ts),\n tool_duration_ms: None,\n };\n let stamped = Message::with_timestamps(&[original.clone()]);\n match &stamped[0].content[0] {\n ContentBlock::Text { text, .. } => {\n assert_eq!(text, \"<system-reminder>\\ninternal\\n</system-reminder>\");\n }\n other => panic!(\"expected text block, got {other:?}\"),\n }\n }\n\n #[test]\n fn ends_with_fresh_user_turn_accepts_plain_user_text() {\n let messages = vec![Message::user(\"hello\")];\n assert!(ends_with_fresh_user_turn(&messages));\n }\n\n #[test]\n fn ends_with_fresh_user_turn_rejects_trailing_tool_result() {\n let messages = vec![\n Message::user(\"hello\"),\n Message {\n role: Role::Assistant,\n content: vec![ContentBlock::ToolUse {\n id: \"call_1\".to_string(),\n name: \"bash\".to_string(),\n input: serde_json::json!({}),\n }],\n timestamp: Some(Utc::now()),\n tool_duration_ms: None,\n },\n Message::tool_result(\"call_1\", \"ok\", false),\n ];\n assert!(!ends_with_fresh_user_turn(&messages));\n }\n\n #[test]\n fn ends_with_fresh_user_turn_skips_internal_system_reminders() {\n let messages = vec![\n Message::user(\"hello\"),\n Message::user(\"<system-reminder>\\ninternal\\n</system-reminder>\"),\n ];\n assert!(ends_with_fresh_user_turn(&messages));\n }\n\n #[test]\n fn ends_with_fresh_user_turn_rejects_assistant_tail() {\n let messages = vec![\n Message::user(\"hello\"),\n Message::assistant_text(\"working on it\"),\n ];\n assert!(!ends_with_fresh_user_turn(&messages));\n }\n\n #[test]\n fn format_background_task_notification_markdown_uses_code_block_preview() {\n let rendered = format_background_task_notification_markdown(&BackgroundTaskCompleted {\n task_id: \"abc123\".to_string(),\n tool_name: \"bash\".to_string(),\n session_id: \"session\".to_string(),\n status: BackgroundTaskStatus::Completed,\n exit_code: Some(0),\n output_preview: \"[stderr] first line\\n[stdout] second line\\n\".to_string(),\n output_file: std::path::PathBuf::from(\"/tmp/output.log\"),\n duration_secs: 7.1,\n notify: true,\n wake: false,\n });\n\n assert!(\n rendered\n .contains(\"**Background task** `abc123` · `bash` · ✓ completed · 7.1s · exit 0\")\n );\n assert!(rendered.contains(\"```text\\n[stderr] first line\\n[stdout] second line\\n```\"));\n assert!(rendered.contains(\"_Full output:_ `bg action=\\\"output\\\" task_id=\\\"abc123\\\"`\"));\n }\n\n #[test]\n fn format_background_task_notification_markdown_handles_empty_preview() {\n let rendered = format_background_task_notification_markdown(&BackgroundTaskCompleted {\n task_id: \"abc123\".to_string(),\n tool_name: \"bash\".to_string(),\n session_id: \"session\".to_string(),\n status: BackgroundTaskStatus::Failed,\n exit_code: Some(9),\n output_preview: \"\\n\\n\".to_string(),\n output_file: std::path::PathBuf::from(\"/tmp/output.log\"),\n duration_secs: 1.0,\n notify: true,\n wake: false,\n });\n\n assert!(rendered.contains(\"✗ failed\"));\n assert!(rendered.contains(\"_No output captured._\"));\n }\n\n #[test]\n fn format_background_task_notification_markdown_renders_superseded_status() {\n let rendered = format_background_task_notification_markdown(&BackgroundTaskCompleted {\n task_id: \"abc123\".to_string(),\n tool_name: \"selfdev-build\".to_string(),\n session_id: \"session\".to_string(),\n status: BackgroundTaskStatus::Superseded,\n exit_code: Some(0),\n output_preview: \"Build completed, but source changed before activation\".to_string(),\n output_file: std::path::PathBuf::from(\"/tmp/output.log\"),\n duration_secs: 5.0,\n notify: true,\n wake: false,\n });\n\n assert!(rendered.contains(\"↻ superseded\"));\n assert!(rendered.contains(\"exit 0\"));\n assert!(rendered.contains(\"source changed before activation\"));\n }\n\n #[test]\n fn parse_background_task_notification_markdown_extracts_fields() {\n let rendered = format_background_task_notification_markdown(&BackgroundTaskCompleted {\n task_id: \"abc123\".to_string(),\n tool_name: \"bash\".to_string(),\n session_id: \"session\".to_string(),\n status: BackgroundTaskStatus::Completed,\n exit_code: Some(0),\n output_preview: \"[stderr] first line\\n[stdout] second line\\n\".to_string(),\n output_file: std::path::PathBuf::from(\"/tmp/output.log\"),\n duration_secs: 7.1,\n notify: true,\n wake: false,\n });\n\n let parsed = parse_background_task_notification_markdown(&rendered)\n .expect(\"background task notification should parse\");\n assert_eq!(parsed.task_id, \"abc123\");\n assert_eq!(parsed.tool_name, \"bash\");\n assert_eq!(parsed.status, \"✓ completed\");\n assert_eq!(parsed.duration, \"7.1s\");\n assert_eq!(parsed.exit_label, \"exit 0\");\n assert_eq!(\n parsed.preview.as_deref(),\n Some(\"[stderr] first line\\n[stdout] second line\")\n );\n assert_eq!(\n parsed.full_output_command,\n \"bg action=\\\"output\\\" task_id=\\\"abc123\\\"\"\n );\n }\n}\n"}, "0f162e5a436a7d6fd1f58b2ed65ee340e206a334": {"t": 1785757244.5845017, "n": 41588, "h": "79ebe96e39ce27ee", "c": "#![allow(dead_code)]\n\nuse crate::bus::{BackgroundTaskCompleted, BackgroundTaskStatus};\nuse chrono::Utc;\nuse regex::Regex;\nuse serde::{Deserialize, Serialize};\nuse std::collections::HashSet;\nuse std::sync::OnceLock;\n\n#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]\npub struct InputShellResult {\n pub command: String,\n #[serde(default, skip_serializing_if = \"Option::is_none\")]\n pub cwd: Option<String>,\n pub output: String,\n #[serde(default, skip_serializing_if = \"Option::is_none\")]\n pub exit_code: Option<i32>,\n pub duration_ms: u64,\n #[serde(default)]\n pub truncated: bool,\n #[serde(default)]\n pub failed_to_start: bool,\n}\n\nfn sanitize_fenced_block(text: &str) -> String {\n text.replace(\"```\", \"``\\u{200b}`\")\n}\n\npub fn format_input_shell_result_markdown(shell: &InputShellResult) -> String {\n let status = if shell.failed_to_start {\n \"✗ failed to start\".to_string()\n } else if shell.exit_code == Some(0) {\n \"✓ exit 0\".to_string()\n } else if let Some(code) = shell.exit_code {\n format!(\"✗ exit {}\", code)\n } else {\n \"✗ terminated\".to_string()\n };\n\n let mut meta = vec![status, Message::format_duration(shell.duration_ms)];\n if let Some(cwd) = shell.cwd.as_deref() {\n meta.push(format!(\"cwd `{}`\", cwd));\n }\n if shell.truncated {\n meta.push(\"truncated\".to_string());\n }\n\n let mut message = format!(\n \"**Shell command** · {}\\n\\n```bash\\n{}\\n```\",\n meta.join(\" · \"),\n sanitize_fenced_block(&shell.command)\n );\n\n if shell.output.trim().is_empty() {\n message.push_str(\"\\n\\n_No output._\");\n } else {\n message.push_str(&format!(\n \"\\n\\n```text\\n{}\\n```\",\n sanitize_fenced_block(shell.output.trim_end())\n ));\n }\n\n message\n}\n\npub fn input_shell_status_notice(shell: &InputShellResult) -> String {\n if shell.failed_to_start {\n \"Shell command failed to start\".to_string()\n } else if shell.exit_code == Some(0) {\n \"Shell command completed\".to_string()\n } else if let Some(code) = shell.exit_code {\n format!(\"Shell command failed (exit {})\", code)\n } else {\n \"Shell command terminated\".to_string()\n }\n}\n\nfn format_background_task_status(status: &BackgroundTaskStatus) -> &'static str {\n match status {\n BackgroundTaskStatus::Completed => \"✓ completed\",\n BackgroundTaskStatus::Superseded => \"↻ superseded\",\n BackgroundTaskStatus::Failed => \"✗ failed\",\n BackgroundTaskStatus::Running => \"running\",\n }\n}\n\nfn normalize_background_task_preview(preview: &str) -> Option<String> {\n let normalized = preview.replace(\"\\r\\n\", \"\\n\").replace('\\r', \"\\n\");\n let trimmed = normalized.trim_end();\n if trimmed.trim().is_empty() {\n None\n } else {\n Some(sanitize_fenced_block(trimmed))\n }\n}\n\npub fn format_background_task_notification_markdown(task: &BackgroundTaskCompleted) -> String {\n let exit_code = task\n .exit_code\n .map(|code| format!(\"exit {}\", code))\n .unwrap_or_else(|| \"exit n/a\".to_string());\n\n let mut message = format!(\n \"**Background task** `{}` · `{}` · {} · {:.1}s · {}\",\n task.task_id,\n task.tool_name,\n format_background_task_status(&task.status),\n task.duration_secs,\n exit_code,\n );\n\n if let Some(preview) = normalize_background_task_preview(&task.output_preview) {\n message.push_str(&format!(\"\\n\\n```text\\n{}\\n```\", preview));\n } else {\n message.push_str(\"\\n\\n_No output captured._\");\n }\n\n message.push_str(&format!(\n \"\\n\\n_Full output:_ `bg action=\\\"output\\\" task_id=\\\"{}\\\"`\",\n task.task_id\n ));\n\n message\n}\n\n#[derive(Debug, Clone, PartialEq, Eq)]\npub struct ParsedBackgroundTaskNotification {\n pub task_id: String,\n pub tool_name: String,\n pub status: String,\n pub duration: String,\n pub exit_label: String,\n pub preview: Option<String>,\n pub full_output_command: String,\n}\n\npub fn parse_background_task_notification_markdown(\n content: &str,\n) -> Option<ParsedBackgroundTaskNotification> {\n static HEADER_RE: OnceLock<Regex> = OnceLock::new();\n static FULL_OUTPUT_RE: OnceLock<Regex> = OnceLock::new();\n\n let header_re = HEADER_RE.get_or_init(|| {\n Regex::new(\n r\"^\\*\\*Background task\\*\\* `(?P<task_id>[^`]+)` · `(?P<tool_name>[^`]+)` · (?P<status>.+?) · (?P<duration>[0-9]+(?:\\.[0-9]+)?s) · (?P<exit_label>.+)$\",\n )\n .expect(\"valid background task header regex\")\n });\n let full_output_re = FULL_OUTPUT_RE.get_or_init(|| {\n Regex::new(r#\"^_Full output:_ `(?P<command>[^`]+)`$\"#)\n .expect(\"valid background task full output regex\")\n });\n\n let normalized = content.replace(\"\\r\\n\", \"\\n\").replace('\\r', \"\\n\");\n let mut sections = normalized.split(\"\\n\\n\");\n let header = sections.next()?.trim();\n let captures = header_re.captures(header)?;\n\n let mut preview: Option<String> = None;\n let mut full_output_command: Option<String> = None;\n\n for section in sections {\n let trimmed = section.trim();\n if trimmed.is_empty() {\n continue;\n }\n\n if let Some(captures) = full_output_re.captures(trimmed) {\n full_output_command = Some(captures[\"command\"].to_string());\n continue;\n }\n\n if trimmed == \"_No output captured._\" {\n preview = None;\n continue;\n }\n\n if let Some(fenced) = trimmed\n .strip_prefix(\"```text\\n\")\n .and_then(|body| body.strip_suffix(\"\\n```\"))\n {\n preview = Some(fenced.to_string());\n }\n }\n\n Some(ParsedBackgroundTaskNotification {\n task_id: captures[\"task_id\"].to_string(),\n tool_name: captures[\"tool_name\"].to_string(),\n status: captures[\"status\"].to_string(),\n duration: captures[\"duration\"].to_string(),\n exit_label: captures[\"exit_label\"].to_string(),\n preview,\n full_output_command: full_output_command?,\n })\n}\n\npub fn background_task_status_notice(task: &BackgroundTaskCompleted) -> String {\n match task.status {\n BackgroundTaskStatus::Completed => {\n format!(\"Background task completed · {}\", task.tool_name)\n }\n BackgroundTaskStatus::Superseded => {\n format!(\"Background task superseded · {}\", task.tool_name)\n }\n BackgroundTaskStatus::Failed => match task.exit_code {\n Some(code) => format!(\n \"Background task failed · {} · exit {}\",\n task.tool_name, code\n ),\n None => format!(\"Background task failed · {}\", task.tool_name),\n },\n BackgroundTaskStatus::Running => format!(\"Background task running · {}\", task.tool_name),\n }\n}\n\n/// Role in conversation\n#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]\n#[serde(rename_all = \"lowercase\")]\npub enum Role {\n User,\n Assistant,\n}\n\n/// Plain-text tool output placeholder when execution was interrupted.\npub const TOOL_OUTPUT_MISSING_TEXT: &str =\n \"Tool output missing (session interrupted before tool execution completed)\";\n\n/// A message in the conversation\n#[derive(Debug, Clone, Serialize, Deserialize)]\npub struct Message {\n pub role: Role,\n pub content: Vec<ContentBlock>,\n #[serde(default, skip_serializing_if = \"Option::is_none\")]\n pub timestamp: Option<chrono::DateTime<Utc>>,\n #[serde(default, skip_serializing_if = \"Option::is_none\")]\n pub tool_duration_ms: Option<u64>,\n}\n\n/// Cache control metadata for prompt caching\n#[derive(Debug, Clone, Serialize, Deserialize)]\npub struct CacheControl {\n #[serde(rename = \"type\")]\n pub kind: String,\n #[serde(skip_serializing_if = \"Option::is_none\")]\n pub ttl: Option<String>,\n}\n\nimpl CacheControl {\n pub fn ephemeral(ttl: Option<String>) -> Self {\n Self {\n kind: \"ephemeral\".to_string(),\n ttl,\n }\n }\n}\n\n/// Content block within a message\n#[derive(Debug, Clone, Serialize, Deserialize)]\n#[serde(tag = \"type\", rename_all = \"snake_case\")]\npub enum ContentBlock {\n Text {\n text: String,\n #[serde(skip_serializing_if = \"Option::is_none\")]\n cache_control: Option<CacheControl>,\n },\n /// Hidden reasoning content used for providers that require it (not displayed)\n Reasoning {\n text: String,\n },\n ToolUse {\n id: String,\n name: String,\n input: serde_json::Value,\n },\n ToolResult {\n tool_use_id: String,\n content: String,\n #[serde(skip_serializing_if = \"Option::is_none\")]\n is_error: Option<bool>,\n },\n Image {\n media_type: String,\n data: String,\n },\n /// Hidden OpenAI Responses compaction item used to preserve native\n /// compaction state across turns/saves when jcode explicitly triggers it.\n OpenAICompaction {\n encrypted_content: String,\n },\n}\n\nimpl Message {\n pub fn user(text: &str) -> Self {\n Self {\n role: Role::User,\n content: vec![ContentBlock::Text {\n text: text.to_string(),\n cache_control: None,\n }],\n timestamp: Some(Utc::now()),\n tool_duration_ms: None,\n }\n }\n\n pub fn user_with_images(text: &str, images: Vec<(String, String)>) -> Self {\n let mut content: Vec<ContentBlock> = images\n .into_iter()\n .map(|(media_type, data)| ContentBlock::Image { media_type, data })\n .collect();\n content.push(ContentBlock::Text {\n text: text.to_string(),\n cache_control: None,\n });\n Self {\n role: Role::User,\n content,\n timestamp: Some(Utc::now()),\n tool_duration_ms: None,\n }\n }\n\n pub fn assistant_text(text: &str) -> Self {\n Self {\n role: Role::Assistant,\n content: vec![ContentBlock::Text {\n text: text.to_string(),\n cache_control: None,\n }],\n timestamp: Some(Utc::now()),\n tool_duration_ms: None,\n }\n }\n\n pub fn tool_result(tool_use_id: &str, content: &str, is_error: bool) -> Self {\n Self::tool_result_with_duration(tool_use_id, content, is_error, None)\n }\n\n pub fn tool_result_with_duration(\n tool_use_id: &str,\n content: &str,\n is_error: bool,\n tool_duration_ms: Option<u64>,\n ) -> Self {\n Self {\n role: Role::User,\n content: vec![ContentBlock::ToolResult {\n tool_use_id: tool_use_id.to_string(),\n content: content.to_string(),\n is_error: if is_error { Some(true) } else { None },\n }],\n timestamp: Some(Utc::now()),\n tool_duration_ms,\n }\n }\n\n /// Format a timestamp deterministically in UTC for injection into model-visible content.\n pub fn format_timestamp(ts: &chrono::DateTime<Utc>) -> String {\n ts.to_rfc3339_opts(chrono::SecondsFormat::Millis, true)\n }\n\n pub fn format_duration(duration_ms: u64) -> String {\n match duration_ms {\n 0..=999 => format!(\"{}ms\", duration_ms),\n 1_000..=9_999 => format!(\"{:.1}s\", duration_ms as f64 / 1000.0),\n 10_000..=59_999 => format!(\"{}s\", duration_ms / 1000),\n _ => {\n let total_seconds = duration_ms / 1000;\n let minutes = total_seconds / 60;\n let seconds = total_seconds % 60;\n if seconds == 0 {\n format!(\"{}m\", minutes)\n } else {\n format!(\"{}m {}s\", minutes, seconds)\n }\n }\n }\n }\n\n pub fn is_internal_system_reminder(&self) -> bool {\n self.content\n .iter()\n .find_map(|block| match block {\n ContentBlock::Text { text, .. } => Some(text.trim_start()),\n _ => None,\n })\n .is_some_and(|text| text.starts_with(\"<system-reminder>\"))\n }\n\n fn should_skip_timestamp_injection(&self) -> bool {\n self.is_internal_system_reminder()\n }\n\n fn tool_result_tag(&self, ts: &chrono::DateTime<Utc>) -> String {\n match self.tool_duration_ms {\n Some(duration_ms) => {\n let duration_ms_i64 = i64::try_from(duration_ms).unwrap_or(i64::MAX);\n let start_ts = ts\n .checked_sub_signed(chrono::Duration::milliseconds(duration_ms_i64))\n .unwrap_or(*ts);\n format!(\n \"[tool timing: start={} finish={} duration={}]\",\n Self::format_timestamp(&start_ts),\n Self::format_timestamp(ts),\n Self::format_duration(duration_ms)\n )\n }\n None => format!(\"[{}]\", Self::format_timestamp(ts)),\n }\n }\n\n /// Return a copy of messages with timestamps injected into user-role text content.\n /// Tool results get a stable UTC timing header prepended to content.\n /// User text messages get a stable UTC timestamp prepended to the first text block.\n pub fn with_timestamps(messages: &[Message]) -> Vec<Message> {\n messages\n .iter()\n .map(|msg| {\n let Some(ts) = msg.timestamp else {\n return msg.clone();\n };\n if msg.role != Role::User || msg.should_skip_timestamp_injection() {\n return msg.clone();\n }\n let text_tag = format!(\"[{}]\", Self::format_timestamp(&ts));\n let tool_result_tag = msg.tool_result_tag(&ts);\n let mut msg = msg.clone();\n let mut tagged = false;\n for block in &mut msg.content {\n match block {\n ContentBlock::Text { text, .. } if !tagged => {\n *text = format!(\"{} {}\", text_tag, text);\n tagged = true;\n }\n ContentBlock::ToolResult { content, .. } if !tagged => {\n *content = format!(\"{} {}\", tool_result_tag, content);\n tagged = true;\n }\n _ => {}\n }\n }\n msg\n })\n .collect()\n }\n}\n\npub fn ends_with_fresh_user_turn(messages: &[Message]) -> bool {\n for msg in messages.iter().rev() {\n if msg.role != Role::User {\n return false;\n }\n\n if msg\n .content\n .iter()\n .any(|block| matches!(block, ContentBlock::ToolResult { .. }))\n {\n return false;\n }\n\n if msg.content.is_empty() {\n return false;\n }\n\n let mut saw_user_text = false;\n for block in &msg.content {\n match block {\n ContentBlock::Text { text, .. } => {\n let trimmed = text.trim();\n if !trimmed.is_empty() && !trimmed.starts_with(\"<system-reminder>\") {\n saw_user_text = true;\n }\n }\n _ => return false,\n }\n }\n\n if saw_user_text {\n return true;\n }\n\n if msg.is_internal_system_reminder() {\n continue;\n }\n\n return false;\n }\n\n false\n}\n\n/// Sanitize a tool ID so it matches the pattern `^[a-zA-Z0-9_-]+$`.\n///\n/// Different providers generate tool IDs in different formats. When switching\n/// from one provider to another mid-conversation, the historical tool IDs may\n/// contain characters that the new provider rejects (e.g., dots in Copilot IDs\n/// sent to Anthropic). This function replaces any invalid characters with\n/// underscores.\npub fn sanitize_tool_id(id: &str) -> String {\n if id.is_empty() {\n return \"unknown\".to_string();\n }\n let sanitized: String = id\n .chars()\n .map(|c| {\n if c.is_ascii_alphanumeric() || c == '_' || c == '-' {\n c\n } else {\n '_'\n }\n })\n .collect();\n if sanitized.is_empty() {\n \"unknown\".to_string()\n } else {\n sanitized\n }\n}\n\n/// Redact likely secrets from persisted tool output.\n///\n/// This is a best-effort safeguard for local session history files. It targets\n/// high-confidence token/key patterns and common `KEY=VALUE` assignments used by\n/// auth flows.\npub fn redact_secrets(text: &str) -> String {\n // Fast path to avoid regex work for most tool outputs.\n let lower = text.to_ascii_lowercase();\n\n if !text.contains(\"sk-\")\n && !text.contains(\"ghp_\")\n && !text.contains(\"github_pat_\")\n && !text.contains(\"AIza\")\n && !text.contains(\"ya29.\")\n && !text.contains(\"xox\")\n && !lower.contains(\"api_key\")\n && !lower.contains(\"token\")\n {\n return text.to_string();\n }\n\n static DIRECT_PATTERNS: OnceLock<Vec<Regex>> = OnceLock::new();\n static ASSIGNMENT_PATTERNS: OnceLock<Vec<Regex>> = OnceLock::new();\n\n let direct_patterns = DIRECT_PATTERNS.get_or_init(|| {\n vec![\n Regex::new(r\"sk-ant-(?:oat|ort)01-[A-Za-z0-9_-]{20,}\")\n .expect(\"valid Anthropic OAuth token regex\"),\n Regex::new(r\"sk-or-v1-[A-Za-z0-9_-]{20,}\").expect(\"valid OpenRouter key regex\"),\n Regex::new(r\"ghp_[A-Za-z0-9]{20,}\").expect(\"valid GitHub PAT regex\"),\n Regex::new(r\"github_pat_[A-Za-z0-9_]{20,}\")\n .expect(\"valid GitHub fine-grained PAT regex\"),\n Regex::new(r\"ya29\\.[A-Za-z0-9._-]{20,}\").expect(\"valid Google OAuth token regex\"),\n Regex::new(r\"AIza[0-9A-Za-z_-]{20,}\").expect(\"valid Google API key regex\"),\n Regex::new(r\"xox[baprs]-[A-Za-z0-9-]{10,}\").expect(\"valid Slack token regex\"),\n ]\n });\n\n let assignment_patterns = ASSIGNMENT_PATTERNS.get_or_init(|| {\n vec![\n Regex::new(r\"(?m)^\\s*(OPENROUTER_API_KEY\\s*=\\s*)[^\\r\\n]+\")\n .expect(\"valid OPENROUTER_API_KEY assignment regex\"),\n Regex::new(r\"(?m)^\\s*(OPENCODE_API_KEY\\s*=\\s*)[^\\r\\n]+\")\n .expect(\"valid OPENCODE_API_KEY assignment regex\"),\n Regex::new(r\"(?m)^\\s*(OPENCODE_GO_API_KEY\\s*=\\s*)[^\\r\\n]+\")\n .expect(\"valid OPENCODE_GO_API_KEY assignment regex\"),\n Regex::new(r\"(?m)^\\s*(ZHIPU_API_KEY\\s*=\\s*)[^\\r\\n]+\")\n .expect(\"valid ZHIPU_API_KEY assignment regex\"),\n Regex::new(r\"(?m)^\\s*(ZAI_API_KEY\\s*=\\s*)[^\\r\\n]+\")\n .expect(\"valid ZAI_API_KEY assignment regex\"),\n Regex::new(r\"(?m)^\\s*(302AI_API_KEY\\s*=\\s*)[^\\r\\n]+\")\n .expect(\"valid 302AI_API_KEY assignment regex\"),\n Regex::new(r\"(?m)^\\s*(BASETEN_API_KEY\\s*=\\s*)[^\\r\\n]+\")\n .expect(\"valid BASETEN_API_KEY assignment regex\"),\n Regex::new(r\"(?m)^\\s*(CORTECS_API_KEY\\s*=\\s*)[^\\r\\n]+\")\n .expect(\"valid CORTECS_API_KEY assignment regex\"),\n Regex::new(r\"(?m)^\\s*(DEEPSEEK_API_KEY\\s*=\\s*)[^\\r\\n]+\")\n .expect(\"valid DEEPSEEK_API_KEY assignment regex\"),\n Regex::new(r\"(?m)^\\s*(FIRMWARE_API_KEY\\s*=\\s*)[^\\r\\n]+\")\n .expect(\"valid FIRMWARE_API_KEY assignment regex\"),\n Regex::new(r\"(?m)^\\s*(HF_TOKEN\\s*=\\s*)[^\\r\\n]+\")\n .expect(\"valid HF_TOKEN assignment regex\"),\n Regex::new(r\"(?m)^\\s*(MOONSHOT_API_KEY\\s*=\\s*)[^\\r\\n]+\")\n .expect(\"valid MOONSHOT_API_KEY assignment regex\"),\n Regex::new(r\"(?m)^\\s*(NEBIUS_API_KEY\\s*=\\s*)[^\\r\\n]+\")\n .expect(\"valid NEBIUS_API_KEY assignment regex\"),\n Regex::new(r\"(?m)^\\s*(SCALEWAY_API_KEY\\s*=\\s*)[^\\r\\n]+\")\n .expect(\"valid SCALEWAY_API_KEY assignment regex\"),\n Regex::new(r\"(?m)^\\s*(STACKIT_API_KEY\\s*=\\s*)[^\\r\\n]+\")\n .expect(\"valid STACKIT_API_KEY assignment regex\"),\n Regex::new(r\"(?m)^\\s*(GROQ_API_KEY\\s*=\\s*)[^\\r\\n]+\")\n .expect(\"valid GROQ_API_KEY assignment regex\"),\n Regex::new(r\"(?m)^\\s*(MISTRAL_API_KEY\\s*=\\s*)[^\\r\\n]+\")\n .expect(\"valid MISTRAL_API_KEY assignment regex\"),\n Regex::new(r\"(?m)^\\s*(PERPLEXITY_API_KEY\\s*=\\s*)[^\\r\\n]+\")\n .expect(\"valid PERPLEXITY_API_KEY assignment regex\"),\n Regex::new(r\"(?m)^\\s*(TOGETHER_API_KEY\\s*=\\s*)[^\\r\\n]+\")\n .expect(\"valid TOGETHER_API_KEY assignment regex\"),\n Regex::new(r\"(?m)^\\s*(DEEPINFRA_API_KEY\\s*=\\s*)[^\\r\\n]+\")\n .expect(\"valid DEEPINFRA_API_KEY assignment regex\"),\n Regex::new(r\"(?m)^\\s*(XAI_API_KEY\\s*=\\s*)[^\\r\\n]+\")\n .expect(\"valid XAI_API_KEY assignment regex\"),\n Regex::new(r\"(?m)^\\s*(LMSTUDIO_API_KEY\\s*=\\s*)[^\\r\\n]+\")\n .expect(\"valid LMSTUDIO_API_KEY assignment regex\"),\n Regex::new(r\"(?m)^\\s*(OLLAMA_API_KEY\\s*=\\s*)[^\\r\\n]+\")\n .expect(\"valid OLLAMA_API_KEY assignment regex\"),\n Regex::new(r\"(?m)^\\s*(CHUTES_API_KEY\\s*=\\s*)[^\\r\\n]+\")\n .expect(\"valid CHUTES_API_KEY assignment regex\"),\n Regex::new(r\"(?m)^\\s*(CEREBRAS_API_KEY\\s*=\\s*)[^\\r\\n]+\")\n .expect(\"valid CEREBRAS_API_KEY assignment regex\"),\n Regex::new(r\"(?m)^\\s*(OPENAI_COMPAT_API_KEY\\s*=\\s*)[^\\r\\n]+\")\n .expect(\"valid OPENAI_COMPAT_API_KEY assignment regex\"),\n Regex::new(r\"(?m)^\\s*(ANTHROPIC_API_KEY\\s*=\\s*)[^\\r\\n]+\")\n .expect(\"valid ANTHROPIC_API_KEY assignment regex\"),\n Regex::new(r\"(?m)^\\s*(OPENAI_API_KEY\\s*=\\s*)[^\\r\\n]+\")\n .expect(\"valid OPENAI_API_KEY assignment regex\"),\n Regex::new(r\"(?m)^\\s*(AZURE_OPENAI_API_KEY\\s*=\\s*)[^\\r\\n]+\")\n .expect(\"valid AZURE_OPENAI_API_KEY assignment regex\"),\n Regex::new(r\"(?m)^\\s*(CURSOR_API_KEY\\s*=\\s*)[^\\r\\n]+\")\n .expect(\"valid CURSOR_API_KEY assignment regex\"),\n Regex::new(r\"(?m)^\\s*(GITHUB_TOKEN\\s*=\\s*)[^\\r\\n]+\")\n .expect(\"valid GITHUB_TOKEN assignment regex\"),\n ]\n });\n\n let mut redacted = text.to_string();\n let mut redacted_keys: HashSet<String> = [\n \"OPENROUTER_API_KEY\",\n \"OPENCODE_API_KEY\",\n \"OPENCODE_GO_API_KEY\",\n \"ZHIPU_API_KEY\",\n \"ZAI_API_KEY\",\n \"302AI_API_KEY\",\n \"BASETEN_API_KEY\",\n \"CORTECS_API_KEY\",\n \"DEEPSEEK_API_KEY\",\n \"FIRMWARE_API_KEY\",\n \"HF_TOKEN\",\n \"MOONSHOT_API_KEY\",\n \"NEBIUS_API_KEY\",\n \"SCALEWAY_API_KEY\",\n \"STACKIT_API_KEY\",\n \"GROQ_API_KEY\",\n \"MISTRAL_API_KEY\",\n \"PERPLEXITY_API_KEY\",\n \"TOGETHER_API_KEY\",\n \"DEEPINFRA_API_KEY\",\n \"XAI_API_KEY\",\n \"LMSTUDIO_API_KEY\",\n \"OLLAMA_API_KEY\",\n \"CHUTES_API_KEY\",\n \"CEREBRAS_API_KEY\",\n \"OPENAI_COMPAT_API_KEY\",\n \"ANTHROPIC_API_KEY\",\n \"OPENAI_API_KEY\",\n \"AZURE_OPENAI_API_KEY\",\n \"CURSOR_API_KEY\",\n \"GITHUB_TOKEN\",\n ]\n .iter()\n .map(|k| (*k).to_string())\n .collect();\n\n for re in direct_patterns {\n redacted = re.replace_all(&redacted, \"[REDACTED_SECRET]\").into_owned();\n }\n\n for re in assignment_patterns {\n redacted = re\n .replace_all(&redacted, \"${1}[REDACTED_SECRET]\")\n .into_owned();\n }\n\n // Also redact custom API key variable names configured at runtime.\n for source in [\n \"JCODE_OPENROUTER_API_KEY_NAME\",\n \"JCODE_OPENAI_COMPAT_API_KEY_NAME\",\n ] {\n let Some(key_name) = std::env::var(source)\n .ok()\n .map(|v| v.trim().to_string())\n .filter(|v| !v.is_empty())\n else {\n continue;\n };\n\n if !key_name\n .chars()\n .all(|c| c.is_ascii_uppercase() || c.is_ascii_digit() || c == '_')\n {\n continue;\n }\n if !redacted_keys.insert(key_name.clone()) {\n continue;\n }\n\n let pattern = format!(r\"(?m)^\\s*({}\\s*=\\s*)[^\\r\\n]+\", regex::escape(&key_name));\n if let Ok(re) = Regex::new(&pattern) {\n redacted = re\n .replace_all(&redacted, \"${1}[REDACTED_SECRET]\")\n .into_owned();\n }\n }\n\n redacted\n}\n\n/// Tool definition for the API\n#[derive(Debug, Clone, Serialize)]\npub struct ToolDefinition {\n pub name: String,\n pub description: String,\n pub input_schema: serde_json::Value,\n}\n\n/// A tool call from the model\n#[derive(Debug, Clone, Default, PartialEq, Eq, serde::Serialize, serde::Deserialize)]\npub struct ToolCall {\n #[serde(default)]\n pub id: String,\n #[serde(default)]\n pub name: String,\n #[serde(default)]\n pub input: serde_json::Value,\n #[serde(default, skip_serializing_if = \"Option::is_none\")]\n pub intent: Option<String>,\n}\n\n/// Connection phase for status bar transparency\n#[derive(Debug, Clone, PartialEq)]\npub enum ConnectionPhase {\n /// Refreshing OAuth token\n Authenticating,\n /// TCP + TLS connection to API\n Connecting,\n /// HTTP request sent, waiting for first response byte\n WaitingForResponse,\n /// First byte received, stream is active\n Streaming,\n /// Retrying after a transient error\n Retrying { attempt: u32, max: u32 },\n}\n\nimpl std::fmt::Display for ConnectionPhase {\n fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {\n match self {\n ConnectionPhase::Authenticating => write!(f, \"authenticating\"),\n ConnectionPhase::Connecting => write!(f, \"connecting\"),\n ConnectionPhase::WaitingForResponse => write!(f, \"waiting for response\"),\n ConnectionPhase::Streaming => write!(f, \"streaming\"),\n ConnectionPhase::Retrying { attempt, max } => {\n write!(f, \"retrying ({}/{})\", attempt, max)\n }\n }\n }\n}\n\n/// Streaming event from provider\n#[derive(Debug, Clone)]\npub enum StreamEvent {\n /// Text content delta\n TextDelta(String),\n /// Tool use started\n ToolUseStart { id: String, name: String },\n /// Tool input delta (JSON fragment)\n ToolInputDelta(String),\n /// Tool use complete\n ToolUseEnd,\n /// Tool result from provider (provider already executed the tool)\n ToolResult {\n tool_use_id: String,\n content: String,\n is_error: bool,\n },\n /// Extended thinking started\n ThinkingStart,\n /// Extended thinking delta (reasoning content)\n ThinkingDelta(String),\n /// Extended thinking ended\n ThinkingEnd,\n /// Extended thinking completed with duration\n ThinkingDone { duration_secs: f64 },\n /// Message complete (may have stop reason)\n MessageEnd { stop_reason: Option<String> },\n /// Token usage update\n TokenUsage {\n input_tokens: Option<u64>,\n output_tokens: Option<u64>,\n cache_read_input_tokens: Option<u64>,\n cache_creation_input_tokens: Option<u64>,\n },\n /// Active transport/connection type for this stream\n ConnectionType { connection: String },\n /// Connection phase update (for status bar transparency)\n ConnectionPhase { phase: ConnectionPhase },\n /// Provider-supplied human-readable transport detail for the status line\n StatusDetail { detail: String },\n /// Error occurred\n Error {\n message: String,\n /// Seconds until rate limit resets (if this is a rate limit error)\n retry_after_secs: Option<u64>,\n },\n /// Provider session ID (for conversation resume)\n SessionId(String),\n /// Compaction occurred (context was summarized)\n Compaction {\n trigger: String,\n pre_tokens: Option<u64>,\n /// Provider-native compaction artifact, if one was emitted.\n openai_encrypted_content: Option<String>,\n },\n /// Upstream provider info (e.g., which provider OpenRouter routed to)\n UpstreamProvider { provider: String },\n /// Native tool call from a provider bridge that needs execution by jcode\n NativeToolCall {\n request_id: String,\n tool_name: String,\n input: serde_json::Value,\n },\n}\n\n#[cfg(test)]\nmod tests {\n use super::*;\n\n #[test]\n fn sanitize_tool_id_alphanumeric_passthrough() {\n assert_eq!(\n sanitize_tool_id(\"toolu_01XFDUDYJgAACzvnptvVer6u\"),\n \"toolu_01XFDUDYJgAACzvnptvVer6u\"\n );\n assert_eq!(sanitize_tool_id(\"call_abc123\"), \"call_abc123\");\n assert_eq!(\n sanitize_tool_id(\"call_1234567890_9876543210\"),\n \"call_1234567890_9876543210\"\n );\n }\n\n #[test]\n fn sanitize_tool_id_hyphens_passthrough() {\n assert_eq!(sanitize_tool_id(\"call-abc-123\"), \"call-abc-123\");\n assert_eq!(\n sanitize_tool_id(\"tool_use-id_with-mixed\"),\n \"tool_use-id_with-mixed\"\n );\n }\n\n #[test]\n fn sanitize_tool_id_replaces_dots() {\n assert_eq!(\n sanitize_tool_id(\"chatcmpl-abc.def.ghi\"),\n \"chatcmpl-abc_def_ghi\"\n );\n assert_eq!(sanitize_tool_id(\"call.123\"), \"call_123\");\n }\n\n #[test]\n fn sanitize_tool_id_replaces_colons() {\n assert_eq!(sanitize_tool_id(\"call:123:456\"), \"call_123_456\");\n }\n\n #[test]\n fn sanitize_tool_id_replaces_special_chars() {\n assert_eq!(\n sanitize_tool_id(\"id@with#special$chars\"),\n \"id_with_special_chars\"\n );\n assert_eq!(sanitize_tool_id(\"id with spaces\"), \"id_with_spaces\");\n }\n\n #[test]\n fn sanitize_tool_id_empty_returns_unknown() {\n assert_eq!(sanitize_tool_id(\"\"), \"unknown\");\n }\n\n #[test]\n fn sanitize_tool_id_copilot_to_anthropic() {\n assert_eq!(\n sanitize_tool_id(\"chatcmpl-BF2xX.tool_call.0\"),\n \"chatcmpl-BF2xX_tool_call_0\"\n );\n }\n\n #[test]\n fn sanitize_tool_id_already_valid_unchanged() {\n let valid_ids = [\n \"toolu_01XFDUDYJgAACzvnptvVer6u\",\n \"call_abc123\",\n \"fallback_text_call_call_1234567890_9876543210\",\n \"tool_123\",\n \"a\",\n \"A\",\n \"0\",\n \"_\",\n \"-\",\n \"a-b_c\",\n ];\n for id in valid_ids {\n assert_eq!(sanitize_tool_id(id), id, \"ID '{}' should be unchanged\", id);\n }\n }\n\n #[test]\n fn redact_secrets_redacts_known_direct_token_formats() {\n let input = \"access=sk-ant-oat01-ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789\\nopenrouter=sk-or-v1-abcdefghijklmnopqrstuvwxyz0123456789\\ngithub=ghp_ABCDEFGHIJKLMNOPQRSTUVWXYZ0123\\n\";\n let out = redact_secrets(input);\n assert!(!out.contains(\"sk-ant-oat01-\"));\n assert!(!out.contains(\"sk-or-v1-\"));\n assert!(!out.contains(\"ghp_ABCDEFGHIJKLMNOPQRSTUVWXYZ0123\"));\n assert!(out.matches(\"[REDACTED_SECRET]\").count() >= 3);\n }\n\n #[test]\n fn redact_secrets_redacts_env_style_assignments() {\n let input = \"OPENROUTER_API_KEY=sk-or-v1-abc123abc123abc123abc123\\nOPENCODE_API_KEY=oc_test_secret\\nOPENCODE_GO_API_KEY=ocgo_test_secret\\nZAI_API_KEY=zai_secret\\nCHUTES_API_KEY=chutes_secret\\nCEREBRAS_API_KEY=cerebras_secret\\nOPENAI_COMPAT_API_KEY=compat_secret\\nCURSOR_API_KEY='my_cursor_secret_value'\\nOPENAI_API_KEY=sk-test-openai-example\\nAZURE_OPENAI_API_KEY=azure-openai-secret\\n\";\n let out = redact_secrets(input);\n assert!(out.contains(\"OPENROUTER_API_KEY=[REDACTED_SECRET]\"));\n assert!(out.contains(\"OPENCODE_API_KEY=[REDACTED_SECRET]\"));\n assert!(out.contains(\"OPENCODE_GO_API_KEY=[REDACTED_SECRET]\"));\n assert!(out.contains(\"ZAI_API_KEY=[REDACTED_SECRET]\"));\n assert!(out.contains(\"CHUTES_API_KEY=[REDACTED_SECRET]\"));\n assert!(out.contains(\"CEREBRAS_API_KEY=[REDACTED_SECRET]\"));\n assert!(out.contains(\"OPENAI_COMPAT_API_KEY=[REDACTED_SECRET]\"));\n assert!(out.contains(\"CURSOR_API_KEY=[REDACTED_SECRET]\"));\n assert!(out.contains(\"OPENAI_API_KEY=[REDACTED_SECRET]\"));\n assert!(out.contains(\"AZURE_OPENAI_API_KEY=[REDACTED_SECRET]\"));\n assert!(!out.contains(\"my_cursor_secret_value\"));\n }\n\n #[test]\n fn redact_secrets_redacts_runtime_key_assignment() {\n let key_var = \"JCODE_OPENAI_COMPAT_API_KEY_NAME\";\n let prev = std::env::var(key_var).ok();\n crate::env::set_var(key_var, \"GROQ_API_KEY\");\n\n let input = \"GROQ_API_KEY=my_secret_token_value\";\n let out = redact_secrets(input);\n assert_eq!(out, \"GROQ_API_KEY=[REDACTED_SECRET]\");\n\n if let Some(v) = prev {\n crate::env::set_var(key_var, v);\n } else {\n crate::env::remove_var(key_var);\n }\n }\n\n #[test]\n fn redact_secrets_redacts_mixed_case_token_assignments() {\n let input = \"my_token=ya29.ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789\";\n let out = redact_secrets(input);\n assert!(out.contains(\"[REDACTED_SECRET]\"));\n assert!(!out.contains(\"ya29.ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789\"));\n }\n\n #[test]\n fn redact_secrets_leaves_normal_output_unchanged() {\n let input = \"Found 5 files\\nNo auth errors\\nDone.\";\n assert_eq!(redact_secrets(input), input);\n }\n\n #[test]\n fn format_timestamp_is_stable_utc_rfc3339() {\n let ts = chrono::DateTime::parse_from_rfc3339(\"2025-03-15T02:24:13.250Z\")\n .unwrap()\n .with_timezone(&Utc);\n assert_eq!(Message::format_timestamp(&ts), \"2025-03-15T02:24:13.250Z\");\n }\n\n #[test]\n fn with_timestamps_prepends_utc_prefix_to_user_text() {\n let ts = chrono::DateTime::parse_from_rfc3339(\"2025-03-15T02:24:03Z\")\n .unwrap()\n .with_timezone(&Utc);\n let stamped = Message::with_timestamps(&[Message {\n role: Role::User,\n content: vec![ContentBlock::Text {\n text: \"hello\".to_string(),\n cache_control: None,\n }],\n timestamp: Some(ts),\n tool_duration_ms: None,\n }]);\n match &stamped[0].content[0] {\n ContentBlock::Text { text, .. } => {\n assert_eq!(text, \"[2025-03-15T02:24:03.000Z] hello\");\n }\n other => panic!(\"expected text block, got {other:?}\"),\n }\n }\n\n #[test]\n fn with_timestamps_adds_tool_timing_header_with_duration() {\n let ts = chrono::DateTime::parse_from_rfc3339(\"2025-03-15T02:24:13Z\")\n .unwrap()\n .with_timezone(&Utc);\n let stamped = Message::with_timestamps(&[Message {\n role: Role::User,\n content: vec![ContentBlock::ToolResult {\n tool_use_id: \"call_1\".to_string(),\n content: \"ok\".to_string(),\n is_error: None,\n }],\n timestamp: Some(ts),\n tool_duration_ms: Some(3_200),\n }]);\n match &stamped[0].content[0] {\n ContentBlock::ToolResult { content, .. } => {\n assert_eq!(\n content,\n \"[tool timing: start=2025-03-15T02:24:09.800Z finish=2025-03-15T02:24:13.000Z duration=3.2s] ok\"\n );\n }\n other => panic!(\"expected tool result block, got {other:?}\"),\n }\n }\n\n #[test]\n fn with_timestamps_skips_internal_system_reminders() {\n let ts = chrono::DateTime::parse_from_rfc3339(\"2025-03-15T02:24:13Z\")\n .unwrap()\n .with_timezone(&Utc);\n let original = Message {\n role: Role::User,\n content: vec![ContentBlock::Text {\n text: \"<system-reminder>\\ninternal\\n</system-reminder>\".to_string(),\n cache_control: None,\n }],\n timestamp: Some(ts),\n tool_duration_ms: None,\n };\n let stamped = Message::with_timestamps(&[original.clone()]);\n match &stamped[0].content[0] {\n ContentBlock::Text { text, .. } => {\n assert_eq!(text, \"<system-reminder>\\ninternal\\n</system-reminder>\");\n }\n other => panic!(\"expected text block, got {other:?}\"),\n }\n }\n\n #[test]\n fn ends_with_fresh_user_turn_accepts_plain_user_text() {\n let messages = vec![Message::user(\"hello\")];\n assert!(ends_with_fresh_user_turn(&messages));\n }\n\n #[test]\n fn ends_with_fresh_user_turn_rejects_trailing_tool_result() {\n let messages = vec![\n Message::user(\"hello\"),\n Message {\n role: Role::Assistant,\n content: vec![ContentBlock::ToolUse {\n id: \"call_1\".to_string(),\n name: \"bash\".to_string(),\n input: serde_json::json!({}),\n }],\n timestamp: Some(Utc::now()),\n tool_duration_ms: None,\n },\n Message::tool_result(\"call_1\", \"ok\", false),\n ];\n assert!(!ends_with_fresh_user_turn(&messages));\n }\n\n #[test]\n fn ends_with_fresh_user_turn_skips_internal_system_reminders() {\n let messages = vec![\n Message::user(\"hello\"),\n Message::user(\"<system-reminder>\\ninternal\\n</system-reminder>\"),\n ];\n assert!(ends_with_fresh_user_turn(&messages));\n }\n\n #[test]\n fn ends_with_fresh_user_turn_rejects_assistant_tail() {\n let messages = vec![\n Message::user(\"hello\"),\n Message::assistant_text(\"working on it\"),\n ];\n assert!(!ends_with_fresh_user_turn(&messages));\n }\n\n #[test]\n fn format_background_task_notification_markdown_uses_code_block_preview() {\n let rendered = format_background_task_notification_markdown(&BackgroundTaskCompleted {\n task_id: \"abc123\".to_string(),\n tool_name: \"bash\".to_string(),\n session_id: \"session\".to_string(),\n status: BackgroundTaskStatus::Completed,\n exit_code: Some(0),\n output_preview: \"[stderr] first line\\n[stdout] second line\\n\".to_string(),\n output_file: std::path::PathBuf::from(\"/tmp/output.log\"),\n duration_secs: 7.1,\n notify: true,\n wake: false,\n });\n\n assert!(\n rendered\n .contains(\"**Background task** `abc123` · `bash` · ✓ completed · 7.1s · exit 0\")\n );\n assert!(rendered.contains(\"```text\\n[stderr] first line\\n[stdout] second line\\n```\"));\n assert!(rendered.contains(\"_Full output:_ `bg action=\\\"output\\\" task_id=\\\"abc123\\\"`\"));\n }\n\n #[test]\n fn format_background_task_notification_markdown_handles_empty_preview() {\n let rendered = format_background_task_notification_markdown(&BackgroundTaskCompleted {\n task_id: \"abc123\".to_string(),\n tool_name: \"bash\".to_string(),\n session_id: \"session\".to_string(),\n status: BackgroundTaskStatus::Failed,\n exit_code: Some(9),\n output_preview: \"\\n\\n\".to_string(),\n output_file: std::path::PathBuf::from(\"/tmp/output.log\"),\n duration_secs: 1.0,\n notify: true,\n wake: false,\n });\n\n assert!(rendered.contains(\"✗ failed\"));\n assert!(rendered.contains(\"_No output captured._\"));\n }\n\n #[test]\n fn format_background_task_notification_markdown_renders_superseded_status() {\n let rendered = format_background_task_notification_markdown(&BackgroundTaskCompleted {\n task_id: \"abc123\".to_string(),\n tool_name: \"selfdev-build\".to_string(),\n session_id: \"session\".to_string(),\n status: BackgroundTaskStatus::Superseded,\n exit_code: Some(0),\n output_preview: \"Build completed, but source changed before activation\".to_string(),\n output_file: std::path::PathBuf::from(\"/tmp/output.log\"),\n duration_secs: 5.0,\n notify: true,\n wake: false,\n });\n\n assert!(rendered.contains(\"↻ superseded\"));\n assert!(rendered.contains(\"exit 0\"));\n assert!(rendered.contains(\"source changed before activation\"));\n }\n\n #[test]\n fn parse_background_task_notification_markdown_extracts_fields() {\n let rendered = format_background_task_notification_markdown(&BackgroundTaskCompleted {\n task_id: \"abc123\".to_string(),\n tool_name: \"bash\".to_string(),\n session_id: \"session\".to_string(),\n status: BackgroundTaskStatus::Completed,\n exit_code: Some(0),\n output_preview: \"[stderr] first line\\n[stdout] second line\\n\".to_string(),\n output_file: std::path::PathBuf::from(\"/tmp/output.log\"),\n duration_secs: 7.1,\n notify: true,\n wake: false,\n });\n\n let parsed = parse_background_task_notification_markdown(&rendered)\n .expect(\"background task notification should parse\");\n assert_eq!(parsed.task_id, \"abc123\");\n assert_eq!(parsed.tool_name, \"bash\");\n assert_eq!(parsed.status, \"✓ completed\");\n assert_eq!(parsed.duration, \"7.1s\");\n assert_eq!(parsed.exit_label, \"exit 0\");\n assert_eq!(\n parsed.preview.as_deref(),\n Some(\"[stderr] first line\\n[stdout] second line\")\n );\n assert_eq!(\n parsed.full_output_command,\n \"bg action=\\\"output\\\" task_id=\\\"abc123\\\"\"\n );\n }\n}\n"}, "3e0b11139b4b938d594eaab4e8fe0c05de2fc339": {"t": 1785757246.0462618, "n": 39031, "h": "530a994b93023fd4", "c": "#![allow(dead_code)]\n\nuse crate::bus::{BackgroundTaskCompleted, BackgroundTaskStatus};\nuse chrono::Utc;\nuse regex::Regex;\nuse serde::{Deserialize, Serialize};\nuse std::collections::HashSet;\nuse std::sync::OnceLock;\n\n#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]\npub struct InputShellResult {\n pub command: String,\n #[serde(default, skip_serializing_if = \"Option::is_none\")]\n pub cwd: Option<String>,\n pub output: String,\n #[serde(default, skip_serializing_if = \"Option::is_none\")]\n pub exit_code: Option<i32>,\n pub duration_ms: u64,\n #[serde(default)]\n pub truncated: bool,\n #[serde(default)]\n pub failed_to_start: bool,\n}\n\nfn sanitize_fenced_block(text: &str) -> String {\n text.replace(\"```\", \"``\\u{200b}`\")\n}\n\npub fn format_input_shell_result_markdown(shell: &InputShellResult) -> String {\n let status = if shell.failed_to_start {\n \"✗ failed to start\".to_string()\n } else if shell.exit_code == Some(0) {\n \"✓ exit 0\".to_string()\n } else if let Some(code) = shell.exit_code {\n format!(\"✗ exit {}\", code)\n } else {\n \"✗ terminated\".to_string()\n };\n\n let mut meta = vec![status, Message::format_duration(shell.duration_ms)];\n if let Some(cwd) = shell.cwd.as_deref() {\n meta.push(format!(\"cwd `{}`\", cwd));\n }\n if shell.truncated {\n meta.push(\"truncated\".to_string());\n }\n\n let mut message = format!(\n \"**Shell command** · {}\\n\\n```bash\\n{}\\n```\",\n meta.join(\" · \"),\n sanitize_fenced_block(&shell.command)\n );\n\n if shell.output.trim().is_empty() {\n message.push_str(\"\\n\\n_No output._\");\n } else {\n message.push_str(&format!(\n \"\\n\\n```text\\n{}\\n```\",\n sanitize_fenced_block(shell.output.trim_end())\n ));\n }\n\n message\n}\n\npub fn input_shell_status_notice(shell: &InputShellResult) -> String {\n if shell.failed_to_start {\n \"Shell command failed to start\".to_string()\n } else if shell.exit_code == Some(0) {\n \"Shell command completed\".to_string()\n } else if let Some(code) = shell.exit_code {\n format!(\"Shell command failed (exit {})\", code)\n } else {\n \"Shell command terminated\".to_string()\n }\n}\n\nfn format_background_task_status(status: &BackgroundTaskStatus) -> &'static str {\n match status {\n BackgroundTaskStatus::Completed => \"✓ completed\",\n BackgroundTaskStatus::Superseded => \"↻ superseded\",\n BackgroundTaskStatus::Failed => \"✗ failed\",\n BackgroundTaskStatus::Running => \"running\",\n }\n}\n\nfn normalize_background_task_preview(preview: &str) -> Option<String> {\n let normalized = preview.replace(\"\\r\\n\", \"\\n\").replace('\\r', \"\\n\");\n let trimmed = normalized.trim_end();\n if trimmed.trim().is_empty() {\n None\n } else {\n Some(sanitize_fenced_block(trimmed))\n }\n}\n\npub fn format_background_task_notification_markdown(task: &BackgroundTaskCompleted) -> String {\n let exit_code = task\n .exit_code\n .map(|code| format!(\"exit {}\", code))\n .unwrap_or_else(|| \"exit n/a\".to_string());\n\n let mut message = format!(\n \"**Background task** `{}` · `{}` · {} · {:.1}s · {}\",\n task.task_id,\n task.tool_name,\n format_background_task_status(&task.status),\n task.duration_secs,\n exit_code,\n );\n\n if let Some(preview) = normalize_background_task_preview(&task.output_preview) {\n message.push_str(&format!(\"\\n\\n```text\\n{}\\n```\", preview));\n } else {\n message.push_str(\"\\n\\n_No output captured._\");\n }\n\n message.push_str(&format!(\n \"\\n\\n_Full output:_ `bg action=\\\"output\\\" task_id=\\\"{}\\\"`\",\n task.task_id\n ));\n\n message\n}\n\n#[derive(Debug, Clone, PartialEq, Eq)]\npub struct ParsedBackgroundTaskNotification {\n pub task_id: String,\n pub tool_name: String,\n pub status: String,\n pub duration: String,\n pub exit_label: String,\n pub preview: Option<String>,\n pub full_output_command: String,\n}\n\npub fn parse_background_task_notification_markdown(\n content: &str,\n) -> Option<ParsedBackgroundTaskNotification> {\n static HEADER_RE: OnceLock<Regex> = OnceLock::new();\n static FULL_OUTPUT_RE: OnceLock<Regex> = OnceLock::new();\n\n let header_re = HEADER_RE.get_or_init(|| {\n Regex::new(\n r\"^\\*\\*Background task\\*\\* `(?P<task_id>[^`]+)` · `(?P<tool_name>[^`]+)` · (?P<status>.+?) · (?P<duration>[0-9]+(?:\\.[0-9]+)?s) · (?P<exit_label>.+)$\",\n )\n .expect(\"valid background task header regex\")\n });\n let full_output_re = FULL_OUTPUT_RE.get_or_init(|| {\n Regex::new(r#\"^_Full output:_ `(?P<command>[^`]+)`$\"#)\n .expect(\"valid background task full output regex\")\n });\n\n let normalized = content.replace(\"\\r\\n\", \"\\n\").replace('\\r', \"\\n\");\n let mut sections = normalized.split(\"\\n\\n\");\n let header = sections.next()?.trim();\n let captures = header_re.captures(header)?;\n\n let mut preview: Option<String> = None;\n let mut full_output_command: Option<String> = None;\n\n for section in sections {\n let trimmed = section.trim();\n if trimmed.is_empty() {\n continue;\n }\n\n if let Some(captures) = full_output_re.captures(trimmed) {\n full_output_command = Some(captures[\"command\"].to_string());\n continue;\n }\n\n if trimmed == \"_No output captured._\" {\n preview = None;\n continue;\n }\n\n if let Some(fenced) = trimmed\n .strip_prefix(\"```text\\n\")\n .and_then(|body| body.strip_suffix(\"\\n```\"))\n {\n preview = Some(fenced.to_string());\n }\n }\n\n Some(ParsedBackgroundTaskNotification {\n task_id: captures[\"task_id\"].to_string(),\n tool_name: captures[\"tool_name\"].to_string(),\n status: captures[\"status\"].to_string(),\n duration: captures[\"duration\"].to_string(),\n exit_label: captures[\"exit_label\"].to_string(),\n preview,\n full_output_command: full_output_command?,\n })\n}\n\npub fn background_task_status_notice(task: &BackgroundTaskCompleted) -> String {\n match task.status {\n BackgroundTaskStatus::Completed => {\n format!(\"Background task completed · {}\", task.tool_name)\n }\n BackgroundTaskStatus::Superseded => {\n format!(\"Background task superseded · {}\", task.tool_name)\n }\n BackgroundTaskStatus::Failed => match task.exit_code {\n Some(code) => format!(\n \"Background task failed · {} · exit {}\",\n task.tool_name, code\n ),\n None => format!(\"Background task failed · {}\", task.tool_name),\n },\n BackgroundTaskStatus::Running => format!(\"Background task running · {}\", task.tool_name),\n }\n}\n\n/// Role in conversation\n#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]\n#[serde(rename_all = \"lowercase\")]\npub enum Role {\n User,\n Assistant,\n}\n\n/// Plain-text tool output placeholder when execution was interrupted.\npub const TOOL_OUTPUT_MISSING_TEXT: &str =\n \"Tool output missing (session interrupted before tool execution completed)\";\n\n/// A message in the conversation\n#[derive(Debug, Clone, Serialize, Deserialize)]\npub struct Message {\n pub role: Role,\n pub content: Vec<ContentBlock>,\n #[serde(default, skip_serializing_if = \"Option::is_none\")]\n pub timestamp: Option<chrono::DateTime<Utc>>,\n #[serde(default, skip_serializing_if = \"Option::is_none\")]\n pub tool_duration_ms: Option<u64>,\n}\n\n/// Cache control metadata for prompt caching\n#[derive(Debug, Clone, Serialize, Deserialize)]\npub struct CacheControl {\n #[serde(rename = \"type\")]\n pub kind: String,\n #[serde(skip_serializing_if = \"Option::is_none\")]\n pub ttl: Option<String>,\n}\n\nimpl CacheControl {\n pub fn ephemeral(ttl: Option<String>) -> Self {\n Self {\n kind: \"ephemeral\".to_string(),\n ttl,\n }\n }\n}\n\n/// Content block within a message\n#[derive(Debug, Clone, Serialize, Deserialize)]\n#[serde(tag = \"type\", rename_all = \"snake_case\")]\npub enum ContentBlock {\n Text {\n text: String,\n #[serde(skip_serializing_if = \"Option::is_none\")]\n cache_control: Option<CacheControl>,\n },\n /// Hidden reasoning content used for providers that require it (not displayed)\n Reasoning {\n text: String,\n },\n ToolUse {\n id: String,\n name: String,\n input: serde_json::Value,\n },\n ToolResult {\n tool_use_id: String,\n content: String,\n #[serde(skip_serializing_if = \"Option::is_none\")]\n is_error: Option<bool>,\n },\n Image {\n media_type: String,\n data: String,\n },\n /// Hidden OpenAI Responses compaction item used to preserve native\n /// compaction state across turns/saves when jcode explicitly triggers it.\n OpenAICompaction {\n encrypted_content: String,\n },\n}\n\nimpl Message {\n pub fn user(text: &str) -> Self {\n Self {\n role: Role::User,\n content: vec![ContentBlock::Text {\n text: text.to_string(),\n cache_control: None,\n }],\n timestamp: Some(Utc::now()),\n tool_duration_ms: None,\n }\n }\n\n pub fn user_with_images(text: &str, images: Vec<(String, String)>) -> Self {\n let mut content: Vec<ContentBlock> = images\n .into_iter()\n .map(|(media_type, data)| ContentBlock::Image { media_type, data })\n .collect();\n content.push(ContentBlock::Text {\n text: text.to_string(),\n cache_control: None,\n });\n Self {\n role: Role::User,\n content,\n timestamp: Some(Utc::now()),\n tool_duration_ms: None,\n }\n }\n\n pub fn assistant_text(text: &str) -> Self {\n Self {\n role: Role::Assistant,\n content: vec![ContentBlock::Text {\n text: text.to_string(),\n cache_control: None,\n }],\n timestamp: Some(Utc::now()),\n tool_duration_ms: None,\n }\n }\n\n pub fn tool_result(tool_use_id: &str, content: &str, is_error: bool) -> Self {\n Self::tool_result_with_duration(tool_use_id, content, is_error, None)\n }\n\n pub fn tool_result_with_duration(\n tool_use_id: &str,\n content: &str,\n is_error: bool,\n tool_duration_ms: Option<u64>,\n ) -> Self {\n Self {\n role: Role::User,\n content: vec![ContentBlock::ToolResult {\n tool_use_id: tool_use_id.to_string(),\n content: content.to_string(),\n is_error: if is_error { Some(true) } else { None },\n }],\n timestamp: Some(Utc::now()),\n tool_duration_ms,\n }\n }\n\n /// Format a timestamp deterministically in UTC for injection into model-visible content.\n pub fn format_timestamp(ts: &chrono::DateTime<Utc>) -> String {\n ts.to_rfc3339_opts(chrono::SecondsFormat::Millis, true)\n }\n\n pub fn format_duration(duration_ms: u64) -> String {\n match duration_ms {\n 0..=999 => format!(\"{}ms\", duration_ms),\n 1_000..=9_999 => format!(\"{:.1}s\", duration_ms as f64 / 1000.0),\n 10_000..=59_999 => format!(\"{}s\", duration_ms / 1000),\n _ => {\n let total_seconds = duration_ms / 1000;\n let minutes = total_seconds / 60;\n let seconds = total_seconds % 60;\n if seconds == 0 {\n format!(\"{}m\", minutes)\n } else {\n format!(\"{}m {}s\", minutes, seconds)\n }\n }\n }\n }\n\n fn should_skip_timestamp_injection(&self) -> bool {\n self.content\n .iter()\n .find_map(|block| match block {\n ContentBlock::Text { text, .. } => Some(text.trim_start()),\n _ => None,\n })\n .is_some_and(|text| text.starts_with(\"<system-reminder>\"))\n }\n\n fn tool_result_tag(&self, ts: &chrono::DateTime<Utc>) -> String {\n match self.tool_duration_ms {\n Some(duration_ms) => {\n let duration_ms_i64 = i64::try_from(duration_ms).unwrap_or(i64::MAX);\n let start_ts = ts\n .checked_sub_signed(chrono::Duration::milliseconds(duration_ms_i64))\n .unwrap_or(*ts);\n format!(\n \"[tool timing: start={} finish={} duration={}]\",\n Self::format_timestamp(&start_ts),\n Self::format_timestamp(ts),\n Self::format_duration(duration_ms)\n )\n }\n None => format!(\"[{}]\", Self::format_timestamp(ts)),\n }\n }\n\n /// Return a copy of messages with timestamps injected into user-role text content.\n /// Tool results get a stable UTC timing header prepended to content.\n /// User text messages get a stable UTC timestamp prepended to the first text block.\n pub fn with_timestamps(messages: &[Message]) -> Vec<Message> {\n messages\n .iter()\n .map(|msg| {\n let Some(ts) = msg.timestamp else {\n return msg.clone();\n };\n if msg.role != Role::User || msg.should_skip_timestamp_injection() {\n return msg.clone();\n }\n let text_tag = format!(\"[{}]\", Self::format_timestamp(&ts));\n let tool_result_tag = msg.tool_result_tag(&ts);\n let mut msg = msg.clone();\n let mut tagged = false;\n for block in &mut msg.content {\n match block {\n ContentBlock::Text { text, .. } if !tagged => {\n *text = format!(\"{} {}\", text_tag, text);\n tagged = true;\n }\n ContentBlock::ToolResult { content, .. } if !tagged => {\n *content = format!(\"{} {}\", tool_result_tag, content);\n tagged = true;\n }\n _ => {}\n }\n }\n msg\n })\n .collect()\n }\n}\n\n/// Sanitize a tool ID so it matches the pattern `^[a-zA-Z0-9_-]+$`.\n///\n/// Different providers generate tool IDs in different formats. When switching\n/// from one provider to another mid-conversation, the historical tool IDs may\n/// contain characters that the new provider rejects (e.g., dots in Copilot IDs\n/// sent to Anthropic). This function replaces any invalid characters with\n/// underscores.\npub fn sanitize_tool_id(id: &str) -> String {\n if id.is_empty() {\n return \"unknown\".to_string();\n }\n let sanitized: String = id\n .chars()\n .map(|c| {\n if c.is_ascii_alphanumeric() || c == '_' || c == '-' {\n c\n } else {\n '_'\n }\n })\n .collect();\n if sanitized.is_empty() {\n \"unknown\".to_string()\n } else {\n sanitized\n }\n}\n\n/// Redact likely secrets from persisted tool output.\n///\n/// This is a best-effort safeguard for local session history files. It targets\n/// high-confidence token/key patterns and common `KEY=VALUE` assignments used by\n/// auth flows.\npub fn redact_secrets(text: &str) -> String {\n // Fast path to avoid regex work for most tool outputs.\n let lower = text.to_ascii_lowercase();\n\n if !text.contains(\"sk-\")\n && !text.contains(\"ghp_\")\n && !text.contains(\"github_pat_\")\n && !text.contains(\"AIza\")\n && !text.contains(\"ya29.\")\n && !text.contains(\"xox\")\n && !lower.contains(\"api_key\")\n && !lower.contains(\"token\")\n {\n return text.to_string();\n }\n\n static DIRECT_PATTERNS: OnceLock<Vec<Regex>> = OnceLock::new();\n static ASSIGNMENT_PATTERNS: OnceLock<Vec<Regex>> = OnceLock::new();\n\n let direct_patterns = DIRECT_PATTERNS.get_or_init(|| {\n vec![\n Regex::new(r\"sk-ant-(?:oat|ort)01-[A-Za-z0-9_-]{20,}\")\n .expect(\"valid Anthropic OAuth token regex\"),\n Regex::new(r\"sk-or-v1-[A-Za-z0-9_-]{20,}\").expect(\"valid OpenRouter key regex\"),\n Regex::new(r\"ghp_[A-Za-z0-9]{20,}\").expect(\"valid GitHub PAT regex\"),\n Regex::new(r\"github_pat_[A-Za-z0-9_]{20,}\")\n .expect(\"valid GitHub fine-grained PAT regex\"),\n Regex::new(r\"ya29\\.[A-Za-z0-9._-]{20,}\").expect(\"valid Google OAuth token regex\"),\n Regex::new(r\"AIza[0-9A-Za-z_-]{20,}\").expect(\"valid Google API key regex\"),\n Regex::new(r\"xox[baprs]-[A-Za-z0-9-]{10,}\").expect(\"valid Slack token regex\"),\n ]\n });\n\n let assignment_patterns = ASSIGNMENT_PATTERNS.get_or_init(|| {\n vec![\n Regex::new(r\"(?m)^\\s*(OPENROUTER_API_KEY\\s*=\\s*)[^\\r\\n]+\")\n .expect(\"valid OPENROUTER_API_KEY assignment regex\"),\n Regex::new(r\"(?m)^\\s*(OPENCODE_API_KEY\\s*=\\s*)[^\\r\\n]+\")\n .expect(\"valid OPENCODE_API_KEY assignment regex\"),\n Regex::new(r\"(?m)^\\s*(OPENCODE_GO_API_KEY\\s*=\\s*)[^\\r\\n]+\")\n .expect(\"valid OPENCODE_GO_API_KEY assignment regex\"),\n Regex::new(r\"(?m)^\\s*(ZHIPU_API_KEY\\s*=\\s*)[^\\r\\n]+\")\n .expect(\"valid ZHIPU_API_KEY assignment regex\"),\n Regex::new(r\"(?m)^\\s*(ZAI_API_KEY\\s*=\\s*)[^\\r\\n]+\")\n .expect(\"valid ZAI_API_KEY assignment regex\"),\n Regex::new(r\"(?m)^\\s*(302AI_API_KEY\\s*=\\s*)[^\\r\\n]+\")\n .expect(\"valid 302AI_API_KEY assignment regex\"),\n Regex::new(r\"(?m)^\\s*(BASETEN_API_KEY\\s*=\\s*)[^\\r\\n]+\")\n .expect(\"valid BASETEN_API_KEY assignment regex\"),\n Regex::new(r\"(?m)^\\s*(CORTECS_API_KEY\\s*=\\s*)[^\\r\\n]+\")\n .expect(\"valid CORTECS_API_KEY assignment regex\"),\n Regex::new(r\"(?m)^\\s*(DEEPSEEK_API_KEY\\s*=\\s*)[^\\r\\n]+\")\n .expect(\"valid DEEPSEEK_API_KEY assignment regex\"),\n Regex::new(r\"(?m)^\\s*(FIRMWARE_API_KEY\\s*=\\s*)[^\\r\\n]+\")\n .expect(\"valid FIRMWARE_API_KEY assignment regex\"),\n Regex::new(r\"(?m)^\\s*(HF_TOKEN\\s*=\\s*)[^\\r\\n]+\")\n .expect(\"valid HF_TOKEN assignment regex\"),\n Regex::new(r\"(?m)^\\s*(MOONSHOT_API_KEY\\s*=\\s*)[^\\r\\n]+\")\n .expect(\"valid MOONSHOT_API_KEY assignment regex\"),\n Regex::new(r\"(?m)^\\s*(NEBIUS_API_KEY\\s*=\\s*)[^\\r\\n]+\")\n .expect(\"valid NEBIUS_API_KEY assignment regex\"),\n Regex::new(r\"(?m)^\\s*(SCALEWAY_API_KEY\\s*=\\s*)[^\\r\\n]+\")\n .expect(\"valid SCALEWAY_API_KEY assignment regex\"),\n Regex::new(r\"(?m)^\\s*(STACKIT_API_KEY\\s*=\\s*)[^\\r\\n]+\")\n .expect(\"valid STACKIT_API_KEY assignment regex\"),\n Regex::new(r\"(?m)^\\s*(GROQ_API_KEY\\s*=\\s*)[^\\r\\n]+\")\n .expect(\"valid GROQ_API_KEY assignment regex\"),\n Regex::new(r\"(?m)^\\s*(MISTRAL_API_KEY\\s*=\\s*)[^\\r\\n]+\")\n .expect(\"valid MISTRAL_API_KEY assignment regex\"),\n Regex::new(r\"(?m)^\\s*(PERPLEXITY_API_KEY\\s*=\\s*)[^\\r\\n]+\")\n .expect(\"valid PERPLEXITY_API_KEY assignment regex\"),\n Regex::new(r\"(?m)^\\s*(TOGETHER_API_KEY\\s*=\\s*)[^\\r\\n]+\")\n .expect(\"valid TOGETHER_API_KEY assignment regex\"),\n Regex::new(r\"(?m)^\\s*(DEEPINFRA_API_KEY\\s*=\\s*)[^\\r\\n]+\")\n .expect(\"valid DEEPINFRA_API_KEY assignment regex\"),\n Regex::new(r\"(?m)^\\s*(XAI_API_KEY\\s*=\\s*)[^\\r\\n]+\")\n .expect(\"valid XAI_API_KEY assignment regex\"),\n Regex::new(r\"(?m)^\\s*(LMSTUDIO_API_KEY\\s*=\\s*)[^\\r\\n]+\")\n .expect(\"valid LMSTUDIO_API_KEY assignment regex\"),\n Regex::new(r\"(?m)^\\s*(OLLAMA_API_KEY\\s*=\\s*)[^\\r\\n]+\")\n .expect(\"valid OLLAMA_API_KEY assignment regex\"),\n Regex::new(r\"(?m)^\\s*(CHUTES_API_KEY\\s*=\\s*)[^\\r\\n]+\")\n .expect(\"valid CHUTES_API_KEY assignment regex\"),\n Regex::new(r\"(?m)^\\s*(CEREBRAS_API_KEY\\s*=\\s*)[^\\r\\n]+\")\n .expect(\"valid CEREBRAS_API_KEY assignment regex\"),\n Regex::new(r\"(?m)^\\s*(OPENAI_COMPAT_API_KEY\\s*=\\s*)[^\\r\\n]+\")\n .expect(\"valid OPENAI_COMPAT_API_KEY assignment regex\"),\n Regex::new(r\"(?m)^\\s*(ANTHROPIC_API_KEY\\s*=\\s*)[^\\r\\n]+\")\n .expect(\"valid ANTHROPIC_API_KEY assignment regex\"),\n Regex::new(r\"(?m)^\\s*(OPENAI_API_KEY\\s*=\\s*)[^\\r\\n]+\")\n .expect(\"valid OPENAI_API_KEY assignment regex\"),\n Regex::new(r\"(?m)^\\s*(AZURE_OPENAI_API_KEY\\s*=\\s*)[^\\r\\n]+\")\n .expect(\"valid AZURE_OPENAI_API_KEY assignment regex\"),\n Regex::new(r\"(?m)^\\s*(CURSOR_API_KEY\\s*=\\s*)[^\\r\\n]+\")\n .expect(\"valid CURSOR_API_KEY assignment regex\"),\n Regex::new(r\"(?m)^\\s*(GITHUB_TOKEN\\s*=\\s*)[^\\r\\n]+\")\n .expect(\"valid GITHUB_TOKEN assignment regex\"),\n ]\n });\n\n let mut redacted = text.to_string();\n let mut redacted_keys: HashSet<String> = [\n \"OPENROUTER_API_KEY\",\n \"OPENCODE_API_KEY\",\n \"OPENCODE_GO_API_KEY\",\n \"ZHIPU_API_KEY\",\n \"ZAI_API_KEY\",\n \"302AI_API_KEY\",\n \"BASETEN_API_KEY\",\n \"CORTECS_API_KEY\",\n \"DEEPSEEK_API_KEY\",\n \"FIRMWARE_API_KEY\",\n \"HF_TOKEN\",\n \"MOONSHOT_API_KEY\",\n \"NEBIUS_API_KEY\",\n \"SCALEWAY_API_KEY\",\n \"STACKIT_API_KEY\",\n \"GROQ_API_KEY\",\n \"MISTRAL_API_KEY\",\n \"PERPLEXITY_API_KEY\",\n \"TOGETHER_API_KEY\",\n \"DEEPINFRA_API_KEY\",\n \"XAI_API_KEY\",\n \"LMSTUDIO_API_KEY\",\n \"OLLAMA_API_KEY\",\n \"CHUTES_API_KEY\",\n \"CEREBRAS_API_KEY\",\n \"OPENAI_COMPAT_API_KEY\",\n \"ANTHROPIC_API_KEY\",\n \"OPENAI_API_KEY\",\n \"AZURE_OPENAI_API_KEY\",\n \"CURSOR_API_KEY\",\n \"GITHUB_TOKEN\",\n ]\n .iter()\n .map(|k| (*k).to_string())\n .collect();\n\n for re in direct_patterns {\n redacted = re.replace_all(&redacted, \"[REDACTED_SECRET]\").into_owned();\n }\n\n for re in assignment_patterns {\n redacted = re\n .replace_all(&redacted, \"${1}[REDACTED_SECRET]\")\n .into_owned();\n }\n\n // Also redact custom API key variable names configured at runtime.\n for source in [\n \"JCODE_OPENROUTER_API_KEY_NAME\",\n \"JCODE_OPENAI_COMPAT_API_KEY_NAME\",\n ] {\n let Some(key_name) = std::env::var(source)\n .ok()\n .map(|v| v.trim().to_string())\n .filter(|v| !v.is_empty())\n else {\n continue;\n };\n\n if !key_name\n .chars()\n .all(|c| c.is_ascii_uppercase() || c.is_ascii_digit() || c == '_')\n {\n continue;\n }\n if !redacted_keys.insert(key_name.clone()) {\n continue;\n }\n\n let pattern = format!(r\"(?m)^\\s*({}\\s*=\\s*)[^\\r\\n]+\", regex::escape(&key_name));\n if let Ok(re) = Regex::new(&pattern) {\n redacted = re\n .replace_all(&redacted, \"${1}[REDACTED_SECRET]\")\n .into_owned();\n }\n }\n\n redacted\n}\n\n/// Tool definition for the API\n#[derive(Debug, Clone, Serialize)]\npub struct ToolDefinition {\n pub name: String,\n pub description: String,\n pub input_schema: serde_json::Value,\n}\n\n/// A tool call from the model\n#[derive(Debug, Clone, Default, PartialEq, Eq, serde::Serialize, serde::Deserialize)]\npub struct ToolCall {\n #[serde(default)]\n pub id: String,\n #[serde(default)]\n pub name: String,\n #[serde(default)]\n pub input: serde_json::Value,\n #[serde(default, skip_serializing_if = \"Option::is_none\")]\n pub intent: Option<String>,\n}\n\n/// Connection phase for status bar transparency\n#[derive(Debug, Clone, PartialEq)]\npub enum ConnectionPhase {\n /// Refreshing OAuth token\n Authenticating,\n /// TCP + TLS connection to API\n Connecting,\n /// HTTP request sent, waiting for first response byte\n WaitingForResponse,\n /// First byte received, stream is active\n Streaming,\n /// Retrying after a transient error\n Retrying { attempt: u32, max: u32 },\n}\n\nimpl std::fmt::Display for ConnectionPhase {\n fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {\n match self {\n ConnectionPhase::Authenticating => write!(f, \"authenticating\"),\n ConnectionPhase::Connecting => write!(f, \"connecting\"),\n ConnectionPhase::WaitingForResponse => write!(f, \"waiting for response\"),\n ConnectionPhase::Streaming => write!(f, \"streaming\"),\n ConnectionPhase::Retrying { attempt, max } => {\n write!(f, \"retrying ({}/{})\", attempt, max)\n }\n }\n }\n}\n\n/// Streaming event from provider\n#[derive(Debug, Clone)]\npub enum StreamEvent {\n /// Text content delta\n TextDelta(String),\n /// Tool use started\n ToolUseStart { id: String, name: String },\n /// Tool input delta (JSON fragment)\n ToolInputDelta(String),\n /// Tool use complete\n ToolUseEnd,\n /// Tool result from provider (provider already executed the tool)\n ToolResult {\n tool_use_id: String,\n content: String,\n is_error: bool,\n },\n /// Extended thinking started\n ThinkingStart,\n /// Extended thinking delta (reasoning content)\n ThinkingDelta(String),\n /// Extended thinking ended\n ThinkingEnd,\n /// Extended thinking completed with duration\n ThinkingDone { duration_secs: f64 },\n /// Message complete (may have stop reason)\n MessageEnd { stop_reason: Option<String> },\n /// Token usage update\n TokenUsage {\n input_tokens: Option<u64>,\n output_tokens: Option<u64>,\n cache_read_input_tokens: Option<u64>,\n cache_creation_input_tokens: Option<u64>,\n },\n /// Active transport/connection type for this stream\n ConnectionType { connection: String },\n /// Connection phase update (for status bar transparency)\n ConnectionPhase { phase: ConnectionPhase },\n /// Provider-supplied human-readable transport detail for the status line\n StatusDetail { detail: String },\n /// Error occurred\n Error {\n message: String,\n /// Seconds until rate limit resets (if this is a rate limit error)\n retry_after_secs: Option<u64>,\n },\n /// Provider session ID (for conversation resume)\n SessionId(String),\n /// Compaction occurred (context was summarized)\n Compaction {\n trigger: String,\n pre_tokens: Option<u64>,\n /// Provider-native compaction artifact, if one was emitted.\n openai_encrypted_content: Option<String>,\n },\n /// Upstream provider info (e.g., which provider OpenRouter routed to)\n UpstreamProvider { provider: String },\n /// Native tool call from a provider bridge that needs execution by jcode\n NativeToolCall {\n request_id: String,\n tool_name: String,\n input: serde_json::Value,\n },\n}\n\n#[cfg(test)]\nmod tests {\n use super::*;\n\n #[test]\n fn sanitize_tool_id_alphanumeric_passthrough() {\n assert_eq!(\n sanitize_tool_id(\"toolu_01XFDUDYJgAACzvnptvVer6u\"),\n \"toolu_01XFDUDYJgAACzvnptvVer6u\"\n );\n assert_eq!(sanitize_tool_id(\"call_abc123\"), \"call_abc123\");\n assert_eq!(\n sanitize_tool_id(\"call_1234567890_9876543210\"),\n \"call_1234567890_9876543210\"\n );\n }\n\n #[test]\n fn sanitize_tool_id_hyphens_passthrough() {\n assert_eq!(sanitize_tool_id(\"call-abc-123\"), \"call-abc-123\");\n assert_eq!(\n sanitize_tool_id(\"tool_use-id_with-mixed\"),\n \"tool_use-id_with-mixed\"\n );\n }\n\n #[test]\n fn sanitize_tool_id_replaces_dots() {\n assert_eq!(\n sanitize_tool_id(\"chatcmpl-abc.def.ghi\"),\n \"chatcmpl-abc_def_ghi\"\n );\n assert_eq!(sanitize_tool_id(\"call.123\"), \"call_123\");\n }\n\n #[test]\n fn sanitize_tool_id_replaces_colons() {\n assert_eq!(sanitize_tool_id(\"call:123:456\"), \"call_123_456\");\n }\n\n #[test]\n fn sanitize_tool_id_replaces_special_chars() {\n assert_eq!(\n sanitize_tool_id(\"id@with#special$chars\"),\n \"id_with_special_chars\"\n );\n assert_eq!(sanitize_tool_id(\"id with spaces\"), \"id_with_spaces\");\n }\n\n #[test]\n fn sanitize_tool_id_empty_returns_unknown() {\n assert_eq!(sanitize_tool_id(\"\"), \"unknown\");\n }\n\n #[test]\n fn sanitize_tool_id_copilot_to_anthropic() {\n assert_eq!(\n sanitize_tool_id(\"chatcmpl-BF2xX.tool_call.0\"),\n \"chatcmpl-BF2xX_tool_call_0\"\n );\n }\n\n #[test]\n fn sanitize_tool_id_already_valid_unchanged() {\n let valid_ids = [\n \"toolu_01XFDUDYJgAACzvnptvVer6u\",\n \"call_abc123\",\n \"fallback_text_call_call_1234567890_9876543210\",\n \"tool_123\",\n \"a\",\n \"A\",\n \"0\",\n \"_\",\n \"-\",\n \"a-b_c\",\n ];\n for id in valid_ids {\n assert_eq!(sanitize_tool_id(id), id, \"ID '{}' should be unchanged\", id);\n }\n }\n\n #[test]\n fn redact_secrets_redacts_known_direct_token_formats() {\n let input = \"access=sk-ant-oat01-ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789\\nopenrouter=sk-or-v1-abcdefghijklmnopqrstuvwxyz0123456789\\ngithub=ghp_ABCDEFGHIJKLMNOPQRSTUVWXYZ0123\\n\";\n let out = redact_secrets(input);\n assert!(!out.contains(\"sk-ant-oat01-\"));\n assert!(!out.contains(\"sk-or-v1-\"));\n assert!(!out.contains(\"ghp_ABCDEFGHIJKLMNOPQRSTUVWXYZ0123\"));\n assert!(out.matches(\"[REDACTED_SECRET]\").count() >= 3);\n }\n\n #[test]\n fn redact_secrets_redacts_env_style_assignments() {\n let input = \"OPENROUTER_API_KEY=sk-or-v1-abc123abc123abc123abc123\\nOPENCODE_API_KEY=oc_test_secret\\nOPENCODE_GO_API_KEY=ocgo_test_secret\\nZAI_API_KEY=zai_secret\\nCHUTES_API_KEY=chutes_secret\\nCEREBRAS_API_KEY=cerebras_secret\\nOPENAI_COMPAT_API_KEY=compat_secret\\nCURSOR_API_KEY='my_cursor_secret_value'\\nOPENAI_API_KEY=sk-test-openai-example\\nAZURE_OPENAI_API_KEY=azure-openai-secret\\n\";\n let out = redact_secrets(input);\n assert!(out.contains(\"OPENROUTER_API_KEY=[REDACTED_SECRET]\"));\n assert!(out.contains(\"OPENCODE_API_KEY=[REDACTED_SECRET]\"));\n assert!(out.contains(\"OPENCODE_GO_API_KEY=[REDACTED_SECRET]\"));\n assert!(out.contains(\"ZAI_API_KEY=[REDACTED_SECRET]\"));\n assert!(out.contains(\"CHUTES_API_KEY=[REDACTED_SECRET]\"));\n assert!(out.contains(\"CEREBRAS_API_KEY=[REDACTED_SECRET]\"));\n assert!(out.contains(\"OPENAI_COMPAT_API_KEY=[REDACTED_SECRET]\"));\n assert!(out.contains(\"CURSOR_API_KEY=[REDACTED_SECRET]\"));\n assert!(out.contains(\"OPENAI_API_KEY=[REDACTED_SECRET]\"));\n assert!(out.contains(\"AZURE_OPENAI_API_KEY=[REDACTED_SECRET]\"));\n assert!(!out.contains(\"my_cursor_secret_value\"));\n }\n\n #[test]\n fn redact_secrets_redacts_runtime_key_assignment() {\n let key_var = \"JCODE_OPENAI_COMPAT_API_KEY_NAME\";\n let prev = std::env::var(key_var).ok();\n crate::env::set_var(key_var, \"GROQ_API_KEY\");\n\n let input = \"GROQ_API_KEY=my_secret_token_value\";\n let out = redact_secrets(input);\n assert_eq!(out, \"GROQ_API_KEY=[REDACTED_SECRET]\");\n\n if let Some(v) = prev {\n crate::env::set_var(key_var, v);\n } else {\n crate::env::remove_var(key_var);\n }\n }\n\n #[test]\n fn redact_secrets_redacts_mixed_case_token_assignments() {\n let input = \"my_token=ya29.ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789\";\n let out = redact_secrets(input);\n assert!(out.contains(\"[REDACTED_SECRET]\"));\n assert!(!out.contains(\"ya29.ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789\"));\n }\n\n #[test]\n fn redact_secrets_leaves_normal_output_unchanged() {\n let input = \"Found 5 files\\nNo auth errors\\nDone.\";\n assert_eq!(redact_secrets(input), input);\n }\n\n #[test]\n fn format_timestamp_is_stable_utc_rfc3339() {\n let ts = chrono::DateTime::parse_from_rfc3339(\"2025-03-15T02:24:13.250Z\")\n .unwrap()\n .with_timezone(&Utc);\n assert_eq!(Message::format_timestamp(&ts), \"2025-03-15T02:24:13.250Z\");\n }\n\n #[test]\n fn with_timestamps_prepends_utc_prefix_to_user_text() {\n let ts = chrono::DateTime::parse_from_rfc3339(\"2025-03-15T02:24:03Z\")\n .unwrap()\n .with_timezone(&Utc);\n let stamped = Message::with_timestamps(&[Message {\n role: Role::User,\n content: vec![ContentBlock::Text {\n text: \"hello\".to_string(),\n cache_control: None,\n }],\n timestamp: Some(ts),\n tool_duration_ms: None,\n }]);\n match &stamped[0].content[0] {\n ContentBlock::Text { text, .. } => {\n assert_eq!(text, \"[2025-03-15T02:24:03.000Z] hello\");\n }\n other => panic!(\"expected text block, got {other:?}\"),\n }\n }\n\n #[test]\n fn with_timestamps_adds_tool_timing_header_with_duration() {\n let ts = chrono::DateTime::parse_from_rfc3339(\"2025-03-15T02:24:13Z\")\n .unwrap()\n .with_timezone(&Utc);\n let stamped = Message::with_timestamps(&[Message {\n role: Role::User,\n content: vec![ContentBlock::ToolResult {\n tool_use_id: \"call_1\".to_string(),\n content: \"ok\".to_string(),\n is_error: None,\n }],\n timestamp: Some(ts),\n tool_duration_ms: Some(3_200),\n }]);\n match &stamped[0].content[0] {\n ContentBlock::ToolResult { content, .. } => {\n assert_eq!(\n content,\n \"[tool timing: start=2025-03-15T02:24:09.800Z finish=2025-03-15T02:24:13.000Z duration=3.2s] ok\"\n );\n }\n other => panic!(\"expected tool result block, got {other:?}\"),\n }\n }\n\n #[test]\n fn with_timestamps_skips_internal_system_reminders() {\n let ts = chrono::DateTime::parse_from_rfc3339(\"2025-03-15T02:24:13Z\")\n .unwrap()\n .with_timezone(&Utc);\n let original = Message {\n role: Role::User,\n content: vec![ContentBlock::Text {\n text: \"<system-reminder>\\ninternal\\n</system-reminder>\".to_string(),\n cache_control: None,\n }],\n timestamp: Some(ts),\n tool_duration_ms: None,\n };\n let stamped = Message::with_timestamps(&[original.clone()]);\n match &stamped[0].content[0] {\n ContentBlock::Text { text, .. } => {\n assert_eq!(text, \"<system-reminder>\\ninternal\\n</system-reminder>\");\n }\n other => panic!(\"expected text block, got {other:?}\"),\n }\n }\n\n #[test]\n fn format_background_task_notification_markdown_uses_code_block_preview() {\n let rendered = format_background_task_notification_markdown(&BackgroundTaskCompleted {\n task_id: \"abc123\".to_string(),\n tool_name: \"bash\".to_string(),\n session_id: \"session\".to_string(),\n status: BackgroundTaskStatus::Completed,\n exit_code: Some(0),\n output_preview: \"[stderr] first line\\n[stdout] second line\\n\".to_string(),\n output_file: std::path::PathBuf::from(\"/tmp/output.log\"),\n duration_secs: 7.1,\n notify: true,\n wake: false,\n });\n\n assert!(\n rendered\n .contains(\"**Background task** `abc123` · `bash` · ✓ completed · 7.1s · exit 0\")\n );\n assert!(rendered.contains(\"```text\\n[stderr] first line\\n[stdout] second line\\n```\"));\n assert!(rendered.contains(\"_Full output:_ `bg action=\\\"output\\\" task_id=\\\"abc123\\\"`\"));\n }\n\n #[test]\n fn format_background_task_notification_markdown_handles_empty_preview() {\n let rendered = format_background_task_notification_markdown(&BackgroundTaskCompleted {\n task_id: \"abc123\".to_string(),\n tool_name: \"bash\".to_string(),\n session_id: \"session\".to_string(),\n status: BackgroundTaskStatus::Failed,\n exit_code: Some(9),\n output_preview: \"\\n\\n\".to_string(),\n output_file: std::path::PathBuf::from(\"/tmp/output.log\"),\n duration_secs: 1.0,\n notify: true,\n wake: false,\n });\n\n assert!(rendered.contains(\"✗ failed\"));\n assert!(rendered.contains(\"_No output captured._\"));\n }\n\n #[test]\n fn format_background_task_notification_markdown_renders_superseded_status() {\n let rendered = format_background_task_notification_markdown(&BackgroundTaskCompleted {\n task_id: \"abc123\".to_string(),\n tool_name: \"selfdev-build\".to_string(),\n session_id: \"session\".to_string(),\n status: BackgroundTaskStatus::Superseded,\n exit_code: Some(0),\n output_preview: \"Build completed, but source changed before activation\".to_string(),\n output_file: std::path::PathBuf::from(\"/tmp/output.log\"),\n duration_secs: 5.0,\n notify: true,\n wake: false,\n });\n\n assert!(rendered.contains(\"↻ superseded\"));\n assert!(rendered.contains(\"exit 0\"));\n assert!(rendered.contains(\"source changed before activation\"));\n }\n\n #[test]\n fn parse_background_task_notification_markdown_extracts_fields() {\n let rendered = format_background_task_notification_markdown(&BackgroundTaskCompleted {\n task_id: \"abc123\".to_string(),\n tool_name: \"bash\".to_string(),\n session_id: \"session\".to_string(),\n status: BackgroundTaskStatus::Completed,\n exit_code: Some(0),\n output_preview: \"[stderr] first line\\n[stdout] second line\\n\".to_string(),\n output_file: std::path::PathBuf::from(\"/tmp/output.log\"),\n duration_secs: 7.1,\n notify: true,\n wake: false,\n });\n\n let parsed = parse_background_task_notification_markdown(&rendered)\n .expect(\"background task notification should parse\");\n assert_eq!(parsed.task_id, \"abc123\");\n assert_eq!(parsed.tool_name, \"bash\");\n assert_eq!(parsed.status, \"✓ completed\");\n assert_eq!(parsed.duration, \"7.1s\");\n assert_eq!(parsed.exit_label, \"exit 0\");\n assert_eq!(\n parsed.preview.as_deref(),\n Some(\"[stderr] first line\\n[stdout] second line\")\n );\n assert_eq!(\n parsed.full_output_command,\n \"bg action=\\\"output\\\" task_id=\\\"abc123\\\"\"\n );\n }\n}\n"}, "b353cc6362cbf2d640a63a219071ffa27c25e960": {"t": 1785757247.6604867, "n": 37987, "h": "37164d0aff747213", "c": "#![allow(dead_code)]\n\nuse crate::bus::{BackgroundTaskCompleted, BackgroundTaskStatus};\nuse chrono::Utc;\nuse regex::Regex;\nuse serde::{Deserialize, Serialize};\nuse std::collections::HashSet;\nuse std::sync::OnceLock;\n\n#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]\npub struct InputShellResult {\n pub command: String,\n #[serde(default, skip_serializing_if = \"Option::is_none\")]\n pub cwd: Option<String>,\n pub output: String,\n #[serde(default, skip_serializing_if = \"Option::is_none\")]\n pub exit_code: Option<i32>,\n pub duration_ms: u64,\n #[serde(default)]\n pub truncated: bool,\n #[serde(default)]\n pub failed_to_start: bool,\n}\n\nfn sanitize_fenced_block(text: &str) -> String {\n text.replace(\"```\", \"``\\u{200b}`\")\n}\n\npub fn format_input_shell_result_markdown(shell: &InputShellResult) -> String {\n let status = if shell.failed_to_start {\n \"✗ failed to start\".to_string()\n } else if shell.exit_code == Some(0) {\n \"✓ exit 0\".to_string()\n } else if let Some(code) = shell.exit_code {\n format!(\"✗ exit {}\", code)\n } else {\n \"✗ terminated\".to_string()\n };\n\n let mut meta = vec![status, Message::format_duration(shell.duration_ms)];\n if let Some(cwd) = shell.cwd.as_deref() {\n meta.push(format!(\"cwd `{}`\", cwd));\n }\n if shell.truncated {\n meta.push(\"truncated\".to_string());\n }\n\n let mut message = format!(\n \"**Shell command** · {}\\n\\n```bash\\n{}\\n```\",\n meta.join(\" · \"),\n sanitize_fenced_block(&shell.command)\n );\n\n if shell.output.trim().is_empty() {\n message.push_str(\"\\n\\n_No output._\");\n } else {\n message.push_str(&format!(\n \"\\n\\n```text\\n{}\\n```\",\n sanitize_fenced_block(shell.output.trim_end())\n ));\n }\n\n message\n}\n\npub fn input_shell_status_notice(shell: &InputShellResult) -> String {\n if shell.failed_to_start {\n \"Shell command failed to start\".to_string()\n } else if shell.exit_code == Some(0) {\n \"Shell command completed\".to_string()\n } else if let Some(code) = shell.exit_code {\n format!(\"Shell command failed (exit {})\", code)\n } else {\n \"Shell command terminated\".to_string()\n }\n}\n\nfn format_background_task_status(status: &BackgroundTaskStatus) -> &'static str {\n match status {\n BackgroundTaskStatus::Completed => \"✓ completed\",\n BackgroundTaskStatus::Failed => \"✗ failed\",\n BackgroundTaskStatus::Running => \"running\",\n }\n}\n\nfn normalize_background_task_preview(preview: &str) -> Option<String> {\n let normalized = preview.replace(\"\\r\\n\", \"\\n\").replace('\\r', \"\\n\");\n let trimmed = normalized.trim_end();\n if trimmed.trim().is_empty() {\n None\n } else {\n Some(sanitize_fenced_block(trimmed))\n }\n}\n\npub fn format_background_task_notification_markdown(task: &BackgroundTaskCompleted) -> String {\n let exit_code = task\n .exit_code\n .map(|code| format!(\"exit {}\", code))\n .unwrap_or_else(|| \"exit n/a\".to_string());\n\n let mut message = format!(\n \"**Background task** `{}` · `{}` · {} · {:.1}s · {}\",\n task.task_id,\n task.tool_name,\n format_background_task_status(&task.status),\n task.duration_secs,\n exit_code,\n );\n\n if let Some(preview) = normalize_background_task_preview(&task.output_preview) {\n message.push_str(&format!(\"\\n\\n```text\\n{}\\n```\", preview));\n } else {\n message.push_str(\"\\n\\n_No output captured._\");\n }\n\n message.push_str(&format!(\n \"\\n\\n_Full output:_ `bg action=\\\"output\\\" task_id=\\\"{}\\\"`\",\n task.task_id\n ));\n\n message\n}\n\n#[derive(Debug, Clone, PartialEq, Eq)]\npub struct ParsedBackgroundTaskNotification {\n pub task_id: String,\n pub tool_name: String,\n pub status: String,\n pub duration: String,\n pub exit_label: String,\n pub preview: Option<String>,\n pub full_output_command: String,\n}\n\npub fn parse_background_task_notification_markdown(\n content: &str,\n) -> Option<ParsedBackgroundTaskNotification> {\n static HEADER_RE: OnceLock<Regex> = OnceLock::new();\n static FULL_OUTPUT_RE: OnceLock<Regex> = OnceLock::new();\n\n let header_re = HEADER_RE.get_or_init(|| {\n Regex::new(\n r\"^\\*\\*Background task\\*\\* `(?P<task_id>[^`]+)` · `(?P<tool_name>[^`]+)` · (?P<status>.+?) · (?P<duration>[0-9]+(?:\\.[0-9]+)?s) · (?P<exit_label>.+)$\",\n )\n .expect(\"valid background task header regex\")\n });\n let full_output_re = FULL_OUTPUT_RE.get_or_init(|| {\n Regex::new(r#\"^_Full output:_ `(?P<command>[^`]+)`$\"#)\n .expect(\"valid background task full output regex\")\n });\n\n let normalized = content.replace(\"\\r\\n\", \"\\n\").replace('\\r', \"\\n\");\n let mut sections = normalized.split(\"\\n\\n\");\n let header = sections.next()?.trim();\n let captures = header_re.captures(header)?;\n\n let mut preview: Option<String> = None;\n let mut full_output_command: Option<String> = None;\n\n for section in sections {\n let trimmed = section.trim();\n if trimmed.is_empty() {\n continue;\n }\n\n if let Some(captures) = full_output_re.captures(trimmed) {\n full_output_command = Some(captures[\"command\"].to_string());\n continue;\n }\n\n if trimmed == \"_No output captured._\" {\n preview = None;\n continue;\n }\n\n if let Some(fenced) = trimmed\n .strip_prefix(\"```text\\n\")\n .and_then(|body| body.strip_suffix(\"\\n```\"))\n {\n preview = Some(fenced.to_string());\n }\n }\n\n Some(ParsedBackgroundTaskNotification {\n task_id: captures[\"task_id\"].to_string(),\n tool_name: captures[\"tool_name\"].to_string(),\n status: captures[\"status\"].to_string(),\n duration: captures[\"duration\"].to_string(),\n exit_label: captures[\"exit_label\"].to_string(),\n preview,\n full_output_command: full_output_command?,\n })\n}\n\npub fn background_task_status_notice(task: &BackgroundTaskCompleted) -> String {\n match task.status {\n BackgroundTaskStatus::Completed => {\n format!(\"Background task completed · {}\", task.tool_name)\n }\n BackgroundTaskStatus::Failed => match task.exit_code {\n Some(code) => format!(\n \"Background task failed · {} · exit {}\",\n task.tool_name, code\n ),\n None => format!(\"Background task failed · {}\", task.tool_name),\n },\n BackgroundTaskStatus::Running => format!(\"Background task running · {}\", task.tool_name),\n }\n}\n\n/// Role in conversation\n#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]\n#[serde(rename_all = \"lowercase\")]\npub enum Role {\n User,\n Assistant,\n}\n\n/// Plain-text tool output placeholder when execution was interrupted.\npub const TOOL_OUTPUT_MISSING_TEXT: &str =\n \"Tool output missing (session interrupted before tool execution completed)\";\n\n/// A message in the conversation\n#[derive(Debug, Clone, Serialize, Deserialize)]\npub struct Message {\n pub role: Role,\n pub content: Vec<ContentBlock>,\n #[serde(default, skip_serializing_if = \"Option::is_none\")]\n pub timestamp: Option<chrono::DateTime<Utc>>,\n #[serde(default, skip_serializing_if = \"Option::is_none\")]\n pub tool_duration_ms: Option<u64>,\n}\n\n/// Cache control metadata for prompt caching\n#[derive(Debug, Clone, Serialize, Deserialize)]\npub struct CacheControl {\n #[serde(rename = \"type\")]\n pub kind: String,\n #[serde(skip_serializing_if = \"Option::is_none\")]\n pub ttl: Option<String>,\n}\n\nimpl CacheControl {\n pub fn ephemeral(ttl: Option<String>) -> Self {\n Self {\n kind: \"ephemeral\".to_string(),\n ttl,\n }\n }\n}\n\n/// Content block within a message\n#[derive(Debug, Clone, Serialize, Deserialize)]\n#[serde(tag = \"type\", rename_all = \"snake_case\")]\npub enum ContentBlock {\n Text {\n text: String,\n #[serde(skip_serializing_if = \"Option::is_none\")]\n cache_control: Option<CacheControl>,\n },\n /// Hidden reasoning content used for providers that require it (not displayed)\n Reasoning {\n text: String,\n },\n ToolUse {\n id: String,\n name: String,\n input: serde_json::Value,\n },\n ToolResult {\n tool_use_id: String,\n content: String,\n #[serde(skip_serializing_if = \"Option::is_none\")]\n is_error: Option<bool>,\n },\n Image {\n media_type: String,\n data: String,\n },\n /// Hidden OpenAI Responses compaction item used to preserve native\n /// compaction state across turns/saves when jcode explicitly triggers it.\n OpenAICompaction {\n encrypted_content: String,\n },\n}\n\nimpl Message {\n pub fn user(text: &str) -> Self {\n Self {\n role: Role::User,\n content: vec![ContentBlock::Text {\n text: text.to_string(),\n cache_control: None,\n }],\n timestamp: Some(Utc::now()),\n tool_duration_ms: None,\n }\n }\n\n pub fn user_with_images(text: &str, images: Vec<(String, String)>) -> Self {\n let mut content: Vec<ContentBlock> = images\n .into_iter()\n .map(|(media_type, data)| ContentBlock::Image { media_type, data })\n .collect();\n content.push(ContentBlock::Text {\n text: text.to_string(),\n cache_control: None,\n });\n Self {\n role: Role::User,\n content,\n timestamp: Some(Utc::now()),\n tool_duration_ms: None,\n }\n }\n\n pub fn assistant_text(text: &str) -> Self {\n Self {\n role: Role::Assistant,\n content: vec![ContentBlock::Text {\n text: text.to_string(),\n cache_control: None,\n }],\n timestamp: Some(Utc::now()),\n tool_duration_ms: None,\n }\n }\n\n pub fn tool_result(tool_use_id: &str, content: &str, is_error: bool) -> Self {\n Self::tool_result_with_duration(tool_use_id, content, is_error, None)\n }\n\n pub fn tool_result_with_duration(\n tool_use_id: &str,\n content: &str,\n is_error: bool,\n tool_duration_ms: Option<u64>,\n ) -> Self {\n Self {\n role: Role::User,\n content: vec![ContentBlock::ToolResult {\n tool_use_id: tool_use_id.to_string(),\n content: content.to_string(),\n is_error: if is_error { Some(true) } else { None },\n }],\n timestamp: Some(Utc::now()),\n tool_duration_ms,\n }\n }\n\n /// Format a timestamp deterministically in UTC for injection into model-visible content.\n pub fn format_timestamp(ts: &chrono::DateTime<Utc>) -> String {\n ts.to_rfc3339_opts(chrono::SecondsFormat::Millis, true)\n }\n\n pub fn format_duration(duration_ms: u64) -> String {\n match duration_ms {\n 0..=999 => format!(\"{}ms\", duration_ms),\n 1_000..=9_999 => format!(\"{:.1}s\", duration_ms as f64 / 1000.0),\n 10_000..=59_999 => format!(\"{}s\", duration_ms / 1000),\n _ => {\n let total_seconds = duration_ms / 1000;\n let minutes = total_seconds / 60;\n let seconds = total_seconds % 60;\n if seconds == 0 {\n format!(\"{}m\", minutes)\n } else {\n format!(\"{}m {}s\", minutes, seconds)\n }\n }\n }\n }\n\n fn should_skip_timestamp_injection(&self) -> bool {\n self.content\n .iter()\n .find_map(|block| match block {\n ContentBlock::Text { text, .. } => Some(text.trim_start()),\n _ => None,\n })\n .is_some_and(|text| text.starts_with(\"<system-reminder>\"))\n }\n\n fn tool_result_tag(&self, ts: &chrono::DateTime<Utc>) -> String {\n match self.tool_duration_ms {\n Some(duration_ms) => {\n let duration_ms_i64 = i64::try_from(duration_ms).unwrap_or(i64::MAX);\n let start_ts = ts\n .checked_sub_signed(chrono::Duration::milliseconds(duration_ms_i64))\n .unwrap_or(*ts);\n format!(\n \"[tool timing: start={} finish={} duration={}]\",\n Self::format_timestamp(&start_ts),\n Self::format_timestamp(ts),\n Self::format_duration(duration_ms)\n )\n }\n None => format!(\"[{}]\", Self::format_timestamp(ts)),\n }\n }\n\n /// Return a copy of messages with timestamps injected into user-role text content.\n /// Tool results get a stable UTC timing header prepended to content.\n /// User text messages get a stable UTC timestamp prepended to the first text block.\n pub fn with_timestamps(messages: &[Message]) -> Vec<Message> {\n messages\n .iter()\n .map(|msg| {\n let Some(ts) = msg.timestamp else {\n return msg.clone();\n };\n if msg.role != Role::User || msg.should_skip_timestamp_injection() {\n return msg.clone();\n }\n let text_tag = format!(\"[{}]\", Self::format_timestamp(&ts));\n let tool_result_tag = msg.tool_result_tag(&ts);\n let mut msg = msg.clone();\n let mut tagged = false;\n for block in &mut msg.content {\n match block {\n ContentBlock::Text { text, .. } if !tagged => {\n *text = format!(\"{} {}\", text_tag, text);\n tagged = true;\n }\n ContentBlock::ToolResult { content, .. } if !tagged => {\n *content = format!(\"{} {}\", tool_result_tag, content);\n tagged = true;\n }\n _ => {}\n }\n }\n msg\n })\n .collect()\n }\n}\n\n/// Sanitize a tool ID so it matches the pattern `^[a-zA-Z0-9_-]+$`.\n///\n/// Different providers generate tool IDs in different formats. When switching\n/// from one provider to another mid-conversation, the historical tool IDs may\n/// contain characters that the new provider rejects (e.g., dots in Copilot IDs\n/// sent to Anthropic). This function replaces any invalid characters with\n/// underscores.\npub fn sanitize_tool_id(id: &str) -> String {\n if id.is_empty() {\n return \"unknown\".to_string();\n }\n let sanitized: String = id\n .chars()\n .map(|c| {\n if c.is_ascii_alphanumeric() || c == '_' || c == '-' {\n c\n } else {\n '_'\n }\n })\n .collect();\n if sanitized.is_empty() {\n \"unknown\".to_string()\n } else {\n sanitized\n }\n}\n\n/// Redact likely secrets from persisted tool output.\n///\n/// This is a best-effort safeguard for local session history files. It targets\n/// high-confidence token/key patterns and common `KEY=VALUE` assignments used by\n/// auth flows.\npub fn redact_secrets(text: &str) -> String {\n // Fast path to avoid regex work for most tool outputs.\n let lower = text.to_ascii_lowercase();\n\n if !text.contains(\"sk-\")\n && !text.contains(\"ghp_\")\n && !text.contains(\"github_pat_\")\n && !text.contains(\"AIza\")\n && !text.contains(\"ya29.\")\n && !text.contains(\"xox\")\n && !lower.contains(\"api_key\")\n && !lower.contains(\"token\")\n {\n return text.to_string();\n }\n\n static DIRECT_PATTERNS: OnceLock<Vec<Regex>> = OnceLock::new();\n static ASSIGNMENT_PATTERNS: OnceLock<Vec<Regex>> = OnceLock::new();\n\n let direct_patterns = DIRECT_PATTERNS.get_or_init(|| {\n vec![\n Regex::new(r\"sk-ant-(?:oat|ort)01-[A-Za-z0-9_-]{20,}\")\n .expect(\"valid Anthropic OAuth token regex\"),\n Regex::new(r\"sk-or-v1-[A-Za-z0-9_-]{20,}\").expect(\"valid OpenRouter key regex\"),\n Regex::new(r\"ghp_[A-Za-z0-9]{20,}\").expect(\"valid GitHub PAT regex\"),\n Regex::new(r\"github_pat_[A-Za-z0-9_]{20,}\")\n .expect(\"valid GitHub fine-grained PAT regex\"),\n Regex::new(r\"ya29\\.[A-Za-z0-9._-]{20,}\").expect(\"valid Google OAuth token regex\"),\n Regex::new(r\"AIza[0-9A-Za-z_-]{20,}\").expect(\"valid Google API key regex\"),\n Regex::new(r\"xox[baprs]-[A-Za-z0-9-]{10,}\").expect(\"valid Slack token regex\"),\n ]\n });\n\n let assignment_patterns = ASSIGNMENT_PATTERNS.get_or_init(|| {\n vec![\n Regex::new(r\"(?m)^\\s*(OPENROUTER_API_KEY\\s*=\\s*)[^\\r\\n]+\")\n .expect(\"valid OPENROUTER_API_KEY assignment regex\"),\n Regex::new(r\"(?m)^\\s*(OPENCODE_API_KEY\\s*=\\s*)[^\\r\\n]+\")\n .expect(\"valid OPENCODE_API_KEY assignment regex\"),\n Regex::new(r\"(?m)^\\s*(OPENCODE_GO_API_KEY\\s*=\\s*)[^\\r\\n]+\")\n .expect(\"valid OPENCODE_GO_API_KEY assignment regex\"),\n Regex::new(r\"(?m)^\\s*(ZHIPU_API_KEY\\s*=\\s*)[^\\r\\n]+\")\n .expect(\"valid ZHIPU_API_KEY assignment regex\"),\n Regex::new(r\"(?m)^\\s*(ZAI_API_KEY\\s*=\\s*)[^\\r\\n]+\")\n .expect(\"valid ZAI_API_KEY assignment regex\"),\n Regex::new(r\"(?m)^\\s*(302AI_API_KEY\\s*=\\s*)[^\\r\\n]+\")\n .expect(\"valid 302AI_API_KEY assignment regex\"),\n Regex::new(r\"(?m)^\\s*(BASETEN_API_KEY\\s*=\\s*)[^\\r\\n]+\")\n .expect(\"valid BASETEN_API_KEY assignment regex\"),\n Regex::new(r\"(?m)^\\s*(CORTECS_API_KEY\\s*=\\s*)[^\\r\\n]+\")\n .expect(\"valid CORTECS_API_KEY assignment regex\"),\n Regex::new(r\"(?m)^\\s*(DEEPSEEK_API_KEY\\s*=\\s*)[^\\r\\n]+\")\n .expect(\"valid DEEPSEEK_API_KEY assignment regex\"),\n Regex::new(r\"(?m)^\\s*(FIRMWARE_API_KEY\\s*=\\s*)[^\\r\\n]+\")\n .expect(\"valid FIRMWARE_API_KEY assignment regex\"),\n Regex::new(r\"(?m)^\\s*(HF_TOKEN\\s*=\\s*)[^\\r\\n]+\")\n .expect(\"valid HF_TOKEN assignment regex\"),\n Regex::new(r\"(?m)^\\s*(MOONSHOT_API_KEY\\s*=\\s*)[^\\r\\n]+\")\n .expect(\"valid MOONSHOT_API_KEY assignment regex\"),\n Regex::new(r\"(?m)^\\s*(NEBIUS_API_KEY\\s*=\\s*)[^\\r\\n]+\")\n .expect(\"valid NEBIUS_API_KEY assignment regex\"),\n Regex::new(r\"(?m)^\\s*(SCALEWAY_API_KEY\\s*=\\s*)[^\\r\\n]+\")\n .expect(\"valid SCALEWAY_API_KEY assignment regex\"),\n Regex::new(r\"(?m)^\\s*(STACKIT_API_KEY\\s*=\\s*)[^\\r\\n]+\")\n .expect(\"valid STACKIT_API_KEY assignment regex\"),\n Regex::new(r\"(?m)^\\s*(GROQ_API_KEY\\s*=\\s*)[^\\r\\n]+\")\n .expect(\"valid GROQ_API_KEY assignment regex\"),\n Regex::new(r\"(?m)^\\s*(MISTRAL_API_KEY\\s*=\\s*)[^\\r\\n]+\")\n .expect(\"valid MISTRAL_API_KEY assignment regex\"),\n Regex::new(r\"(?m)^\\s*(PERPLEXITY_API_KEY\\s*=\\s*)[^\\r\\n]+\")\n .expect(\"valid PERPLEXITY_API_KEY assignment regex\"),\n Regex::new(r\"(?m)^\\s*(TOGETHER_API_KEY\\s*=\\s*)[^\\r\\n]+\")\n .expect(\"valid TOGETHER_API_KEY assignment regex\"),\n Regex::new(r\"(?m)^\\s*(DEEPINFRA_API_KEY\\s*=\\s*)[^\\r\\n]+\")\n .expect(\"valid DEEPINFRA_API_KEY assignment regex\"),\n Regex::new(r\"(?m)^\\s*(XAI_API_KEY\\s*=\\s*)[^\\r\\n]+\")\n .expect(\"valid XAI_API_KEY assignment regex\"),\n Regex::new(r\"(?m)^\\s*(LMSTUDIO_API_KEY\\s*=\\s*)[^\\r\\n]+\")\n .expect(\"valid LMSTUDIO_API_KEY assignment regex\"),\n Regex::new(r\"(?m)^\\s*(OLLAMA_API_KEY\\s*=\\s*)[^\\r\\n]+\")\n .expect(\"valid OLLAMA_API_KEY assignment regex\"),\n Regex::new(r\"(?m)^\\s*(CHUTES_API_KEY\\s*=\\s*)[^\\r\\n]+\")\n .expect(\"valid CHUTES_API_KEY assignment regex\"),\n Regex::new(r\"(?m)^\\s*(CEREBRAS_API_KEY\\s*=\\s*)[^\\r\\n]+\")\n .expect(\"valid CEREBRAS_API_KEY assignment regex\"),\n Regex::new(r\"(?m)^\\s*(OPENAI_COMPAT_API_KEY\\s*=\\s*)[^\\r\\n]+\")\n .expect(\"valid OPENAI_COMPAT_API_KEY assignment regex\"),\n Regex::new(r\"(?m)^\\s*(ANTHROPIC_API_KEY\\s*=\\s*)[^\\r\\n]+\")\n .expect(\"valid ANTHROPIC_API_KEY assignment regex\"),\n Regex::new(r\"(?m)^\\s*(OPENAI_API_KEY\\s*=\\s*)[^\\r\\n]+\")\n .expect(\"valid OPENAI_API_KEY assignment regex\"),\n Regex::new(r\"(?m)^\\s*(AZURE_OPENAI_API_KEY\\s*=\\s*)[^\\r\\n]+\")\n .expect(\"valid AZURE_OPENAI_API_KEY assignment regex\"),\n Regex::new(r\"(?m)^\\s*(CURSOR_API_KEY\\s*=\\s*)[^\\r\\n]+\")\n .expect(\"valid CURSOR_API_KEY assignment regex\"),\n Regex::new(r\"(?m)^\\s*(GITHUB_TOKEN\\s*=\\s*)[^\\r\\n]+\")\n .expect(\"valid GITHUB_TOKEN assignment regex\"),\n ]\n });\n\n let mut redacted = text.to_string();\n let mut redacted_keys: HashSet<String> = [\n \"OPENROUTER_API_KEY\",\n \"OPENCODE_API_KEY\",\n \"OPENCODE_GO_API_KEY\",\n \"ZHIPU_API_KEY\",\n \"ZAI_API_KEY\",\n \"302AI_API_KEY\",\n \"BASETEN_API_KEY\",\n \"CORTECS_API_KEY\",\n \"DEEPSEEK_API_KEY\",\n \"FIRMWARE_API_KEY\",\n \"HF_TOKEN\",\n \"MOONSHOT_API_KEY\",\n \"NEBIUS_API_KEY\",\n \"SCALEWAY_API_KEY\",\n \"STACKIT_API_KEY\",\n \"GROQ_API_KEY\",\n \"MISTRAL_API_KEY\",\n \"PERPLEXITY_API_KEY\",\n \"TOGETHER_API_KEY\",\n \"DEEPINFRA_API_KEY\",\n \"XAI_API_KEY\",\n \"LMSTUDIO_API_KEY\",\n \"OLLAMA_API_KEY\",\n \"CHUTES_API_KEY\",\n \"CEREBRAS_API_KEY\",\n \"OPENAI_COMPAT_API_KEY\",\n \"ANTHROPIC_API_KEY\",\n \"OPENAI_API_KEY\",\n \"AZURE_OPENAI_API_KEY\",\n \"CURSOR_API_KEY\",\n \"GITHUB_TOKEN\",\n ]\n .iter()\n .map(|k| (*k).to_string())\n .collect();\n\n for re in direct_patterns {\n redacted = re.replace_all(&redacted, \"[REDACTED_SECRET]\").into_owned();\n }\n\n for re in assignment_patterns {\n redacted = re\n .replace_all(&redacted, \"${1}[REDACTED_SECRET]\")\n .into_owned();\n }\n\n // Also redact custom API key variable names configured at runtime.\n for source in [\n \"JCODE_OPENROUTER_API_KEY_NAME\",\n \"JCODE_OPENAI_COMPAT_API_KEY_NAME\",\n ] {\n let Some(key_name) = std::env::var(source)\n .ok()\n .map(|v| v.trim().to_string())\n .filter(|v| !v.is_empty())\n else {\n continue;\n };\n\n if !key_name\n .chars()\n .all(|c| c.is_ascii_uppercase() || c.is_ascii_digit() || c == '_')\n {\n continue;\n }\n if !redacted_keys.insert(key_name.clone()) {\n continue;\n }\n\n let pattern = format!(r\"(?m)^\\s*({}\\s*=\\s*)[^\\r\\n]+\", regex::escape(&key_name));\n if let Ok(re) = Regex::new(&pattern) {\n redacted = re\n .replace_all(&redacted, \"${1}[REDACTED_SECRET]\")\n .into_owned();\n }\n }\n\n redacted\n}\n\n/// Tool definition for the API\n#[derive(Debug, Clone, Serialize)]\npub struct ToolDefinition {\n pub name: String,\n pub description: String,\n pub input_schema: serde_json::Value,\n}\n\n/// A tool call from the model\n#[derive(Debug, Clone, Default, PartialEq, Eq, serde::Serialize, serde::Deserialize)]\npub struct ToolCall {\n #[serde(default)]\n pub id: String,\n #[serde(default)]\n pub name: String,\n #[serde(default)]\n pub input: serde_json::Value,\n #[serde(default, skip_serializing_if = \"Option::is_none\")]\n pub intent: Option<String>,\n}\n\n/// Connection phase for status bar transparency\n#[derive(Debug, Clone, PartialEq)]\npub enum ConnectionPhase {\n /// Refreshing OAuth token\n Authenticating,\n /// TCP + TLS connection to API\n Connecting,\n /// HTTP request sent, waiting for first response byte\n WaitingForResponse,\n /// First byte received, stream is active\n Streaming,\n /// Retrying after a transient error\n Retrying { attempt: u32, max: u32 },\n}\n\nimpl std::fmt::Display for ConnectionPhase {\n fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {\n match self {\n ConnectionPhase::Authenticating => write!(f, \"authenticating\"),\n ConnectionPhase::Connecting => write!(f, \"connecting\"),\n ConnectionPhase::WaitingForResponse => write!(f, \"waiting for response\"),\n ConnectionPhase::Streaming => write!(f, \"streaming\"),\n ConnectionPhase::Retrying { attempt, max } => {\n write!(f, \"retrying ({}/{})\", attempt, max)\n }\n }\n }\n}\n\n/// Streaming event from provider\n#[derive(Debug, Clone)]\npub enum StreamEvent {\n /// Text content delta\n TextDelta(String),\n /// Tool use started\n ToolUseStart { id: String, name: String },\n /// Tool input delta (JSON fragment)\n ToolInputDelta(String),\n /// Tool use complete\n ToolUseEnd,\n /// Tool result from provider (provider already executed the tool)\n ToolResult {\n tool_use_id: String,\n content: String,\n is_error: bool,\n },\n /// Extended thinking started\n ThinkingStart,\n /// Extended thinking delta (reasoning content)\n ThinkingDelta(String),\n /// Extended thinking ended\n ThinkingEnd,\n /// Extended thinking completed with duration\n ThinkingDone { duration_secs: f64 },\n /// Message complete (may have stop reason)\n MessageEnd { stop_reason: Option<String> },\n /// Token usage update\n TokenUsage {\n input_tokens: Option<u64>,\n output_tokens: Option<u64>,\n cache_read_input_tokens: Option<u64>,\n cache_creation_input_tokens: Option<u64>,\n },\n /// Active transport/connection type for this stream\n ConnectionType { connection: String },\n /// Connection phase update (for status bar transparency)\n ConnectionPhase { phase: ConnectionPhase },\n /// Provider-supplied human-readable transport detail for the status line\n StatusDetail { detail: String },\n /// Error occurred\n Error {\n message: String,\n /// Seconds until rate limit resets (if this is a rate limit error)\n retry_after_secs: Option<u64>,\n },\n /// Provider session ID (for conversation resume)\n SessionId(String),\n /// Compaction occurred (context was summarized)\n Compaction {\n trigger: String,\n pre_tokens: Option<u64>,\n /// Provider-native compaction artifact, if one was emitted.\n openai_encrypted_content: Option<String>,\n },\n /// Upstream provider info (e.g., which provider OpenRouter routed to)\n UpstreamProvider { provider: String },\n /// Native tool call from a provider bridge that needs execution by jcode\n NativeToolCall {\n request_id: String,\n tool_name: String,\n input: serde_json::Value,\n },\n}\n\n#[cfg(test)]\nmod tests {\n use super::*;\n\n #[test]\n fn sanitize_tool_id_alphanumeric_passthrough() {\n assert_eq!(\n sanitize_tool_id(\"toolu_01XFDUDYJgAACzvnptvVer6u\"),\n \"toolu_01XFDUDYJgAACzvnptvVer6u\"\n );\n assert_eq!(sanitize_tool_id(\"call_abc123\"), \"call_abc123\");\n assert_eq!(\n sanitize_tool_id(\"call_1234567890_9876543210\"),\n \"call_1234567890_9876543210\"\n );\n }\n\n #[test]\n fn sanitize_tool_id_hyphens_passthrough() {\n assert_eq!(sanitize_tool_id(\"call-abc-123\"), \"call-abc-123\");\n assert_eq!(\n sanitize_tool_id(\"tool_use-id_with-mixed\"),\n \"tool_use-id_with-mixed\"\n );\n }\n\n #[test]\n fn sanitize_tool_id_replaces_dots() {\n assert_eq!(\n sanitize_tool_id(\"chatcmpl-abc.def.ghi\"),\n \"chatcmpl-abc_def_ghi\"\n );\n assert_eq!(sanitize_tool_id(\"call.123\"), \"call_123\");\n }\n\n #[test]\n fn sanitize_tool_id_replaces_colons() {\n assert_eq!(sanitize_tool_id(\"call:123:456\"), \"call_123_456\");\n }\n\n #[test]\n fn sanitize_tool_id_replaces_special_chars() {\n assert_eq!(\n sanitize_tool_id(\"id@with#special$chars\"),\n \"id_with_special_chars\"\n );\n assert_eq!(sanitize_tool_id(\"id with spaces\"), \"id_with_spaces\");\n }\n\n #[test]\n fn sanitize_tool_id_empty_returns_unknown() {\n assert_eq!(sanitize_tool_id(\"\"), \"unknown\");\n }\n\n #[test]\n fn sanitize_tool_id_copilot_to_anthropic() {\n assert_eq!(\n sanitize_tool_id(\"chatcmpl-BF2xX.tool_call.0\"),\n \"chatcmpl-BF2xX_tool_call_0\"\n );\n }\n\n #[test]\n fn sanitize_tool_id_already_valid_unchanged() {\n let valid_ids = [\n \"toolu_01XFDUDYJgAACzvnptvVer6u\",\n \"call_abc123\",\n \"fallback_text_call_call_1234567890_9876543210\",\n \"tool_123\",\n \"a\",\n \"A\",\n \"0\",\n \"_\",\n \"-\",\n \"a-b_c\",\n ];\n for id in valid_ids {\n assert_eq!(sanitize_tool_id(id), id, \"ID '{}' should be unchanged\", id);\n }\n }\n\n #[test]\n fn redact_secrets_redacts_known_direct_token_formats() {\n let input = \"access=sk-ant-oat01-ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789\\nopenrouter=sk-or-v1-abcdefghijklmnopqrstuvwxyz0123456789\\ngithub=ghp_ABCDEFGHIJKLMNOPQRSTUVWXYZ0123\\n\";\n let out = redact_secrets(input);\n assert!(!out.contains(\"sk-ant-oat01-\"));\n assert!(!out.contains(\"sk-or-v1-\"));\n assert!(!out.contains(\"ghp_ABCDEFGHIJKLMNOPQRSTUVWXYZ0123\"));\n assert!(out.matches(\"[REDACTED_SECRET]\").count() >= 3);\n }\n\n #[test]\n fn redact_secrets_redacts_env_style_assignments() {\n let input = \"OPENROUTER_API_KEY=sk-or-v1-abc123abc123abc123abc123\\nOPENCODE_API_KEY=oc_test_secret\\nOPENCODE_GO_API_KEY=ocgo_test_secret\\nZAI_API_KEY=zai_secret\\nCHUTES_API_KEY=chutes_secret\\nCEREBRAS_API_KEY=cerebras_secret\\nOPENAI_COMPAT_API_KEY=compat_secret\\nCURSOR_API_KEY='my_cursor_secret_value'\\nOPENAI_API_KEY=sk-test-openai-example\\nAZURE_OPENAI_API_KEY=azure-openai-secret\\n\";\n let out = redact_secrets(input);\n assert!(out.contains(\"OPENROUTER_API_KEY=[REDACTED_SECRET]\"));\n assert!(out.contains(\"OPENCODE_API_KEY=[REDACTED_SECRET]\"));\n assert!(out.contains(\"OPENCODE_GO_API_KEY=[REDACTED_SECRET]\"));\n assert!(out.contains(\"ZAI_API_KEY=[REDACTED_SECRET]\"));\n assert!(out.contains(\"CHUTES_API_KEY=[REDACTED_SECRET]\"));\n assert!(out.contains(\"CEREBRAS_API_KEY=[REDACTED_SECRET]\"));\n assert!(out.contains(\"OPENAI_COMPAT_API_KEY=[REDACTED_SECRET]\"));\n assert!(out.contains(\"CURSOR_API_KEY=[REDACTED_SECRET]\"));\n assert!(out.contains(\"OPENAI_API_KEY=[REDACTED_SECRET]\"));\n assert!(out.contains(\"AZURE_OPENAI_API_KEY=[REDACTED_SECRET]\"));\n assert!(!out.contains(\"my_cursor_secret_value\"));\n }\n\n #[test]\n fn redact_secrets_redacts_runtime_key_assignment() {\n let key_var = \"JCODE_OPENAI_COMPAT_API_KEY_NAME\";\n let prev = std::env::var(key_var).ok();\n crate::env::set_var(key_var, \"GROQ_API_KEY\");\n\n let input = \"GROQ_API_KEY=my_secret_token_value\";\n let out = redact_secrets(input);\n assert_eq!(out, \"GROQ_API_KEY=[REDACTED_SECRET]\");\n\n if let Some(v) = prev {\n crate::env::set_var(key_var, v);\n } else {\n crate::env::remove_var(key_var);\n }\n }\n\n #[test]\n fn redact_secrets_redacts_mixed_case_token_assignments() {\n let input = \"my_token=ya29.ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789\";\n let out = redact_secrets(input);\n assert!(out.contains(\"[REDACTED_SECRET]\"));\n assert!(!out.contains(\"ya29.ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789\"));\n }\n\n #[test]\n fn redact_secrets_leaves_normal_output_unchanged() {\n let input = \"Found 5 files\\nNo auth errors\\nDone.\";\n assert_eq!(redact_secrets(input), input);\n }\n\n #[test]\n fn format_timestamp_is_stable_utc_rfc3339() {\n let ts = chrono::DateTime::parse_from_rfc3339(\"2025-03-15T02:24:13.250Z\")\n .unwrap()\n .with_timezone(&Utc);\n assert_eq!(Message::format_timestamp(&ts), \"2025-03-15T02:24:13.250Z\");\n }\n\n #[test]\n fn with_timestamps_prepends_utc_prefix_to_user_text() {\n let ts = chrono::DateTime::parse_from_rfc3339(\"2025-03-15T02:24:03Z\")\n .unwrap()\n .with_timezone(&Utc);\n let stamped = Message::with_timestamps(&[Message {\n role: Role::User,\n content: vec![ContentBlock::Text {\n text: \"hello\".to_string(),\n cache_control: None,\n }],\n timestamp: Some(ts),\n tool_duration_ms: None,\n }]);\n match &stamped[0].content[0] {\n ContentBlock::Text { text, .. } => {\n assert_eq!(text, \"[2025-03-15T02:24:03.000Z] hello\");\n }\n other => panic!(\"expected text block, got {other:?}\"),\n }\n }\n\n #[test]\n fn with_timestamps_adds_tool_timing_header_with_duration() {\n let ts = chrono::DateTime::parse_from_rfc3339(\"2025-03-15T02:24:13Z\")\n .unwrap()\n .with_timezone(&Utc);\n let stamped = Message::with_timestamps(&[Message {\n role: Role::User,\n content: vec![ContentBlock::ToolResult {\n tool_use_id: \"call_1\".to_string(),\n content: \"ok\".to_string(),\n is_error: None,\n }],\n timestamp: Some(ts),\n tool_duration_ms: Some(3_200),\n }]);\n match &stamped[0].content[0] {\n ContentBlock::ToolResult { content, .. } => {\n assert_eq!(\n content,\n \"[tool timing: start=2025-03-15T02:24:09.800Z finish=2025-03-15T02:24:13.000Z duration=3.2s] ok\"\n );\n }\n other => panic!(\"expected tool result block, got {other:?}\"),\n }\n }\n\n #[test]\n fn with_timestamps_skips_internal_system_reminders() {\n let ts = chrono::DateTime::parse_from_rfc3339(\"2025-03-15T02:24:13Z\")\n .unwrap()\n .with_timezone(&Utc);\n let original = Message {\n role: Role::User,\n content: vec![ContentBlock::Text {\n text: \"<system-reminder>\\ninternal\\n</system-reminder>\".to_string(),\n cache_control: None,\n }],\n timestamp: Some(ts),\n tool_duration_ms: None,\n };\n let stamped = Message::with_timestamps(&[original.clone()]);\n match &stamped[0].content[0] {\n ContentBlock::Text { text, .. } => {\n assert_eq!(text, \"<system-reminder>\\ninternal\\n</system-reminder>\");\n }\n other => panic!(\"expected text block, got {other:?}\"),\n }\n }\n\n #[test]\n fn format_background_task_notification_markdown_uses_code_block_preview() {\n let rendered = format_background_task_notification_markdown(&BackgroundTaskCompleted {\n task_id: \"abc123\".to_string(),\n tool_name: \"bash\".to_string(),\n session_id: \"session\".to_string(),\n status: BackgroundTaskStatus::Completed,\n exit_code: Some(0),\n output_preview: \"[stderr] first line\\n[stdout] second line\\n\".to_string(),\n output_file: std::path::PathBuf::from(\"/tmp/output.log\"),\n duration_secs: 7.1,\n notify: true,\n wake: false,\n });\n\n assert!(\n rendered\n .contains(\"**Background task** `abc123` · `bash` · ✓ completed · 7.1s · exit 0\")\n );\n assert!(rendered.contains(\"```text\\n[stderr] first line\\n[stdout] second line\\n```\"));\n assert!(rendered.contains(\"_Full output:_ `bg action=\\\"output\\\" task_id=\\\"abc123\\\"`\"));\n }\n\n #[test]\n fn format_background_task_notification_markdown_handles_empty_preview() {\n let rendered = format_background_task_notification_markdown(&BackgroundTaskCompleted {\n task_id: \"abc123\".to_string(),\n tool_name: \"bash\".to_string(),\n session_id: \"session\".to_string(),\n status: BackgroundTaskStatus::Failed,\n exit_code: Some(9),\n output_preview: \"\\n\\n\".to_string(),\n output_file: std::path::PathBuf::from(\"/tmp/output.log\"),\n duration_secs: 1.0,\n notify: true,\n wake: false,\n });\n\n assert!(rendered.contains(\"✗ failed\"));\n assert!(rendered.contains(\"_No output captured._\"));\n }\n\n #[test]\n fn parse_background_task_notification_markdown_extracts_fields() {\n let rendered = format_background_task_notification_markdown(&BackgroundTaskCompleted {\n task_id: \"abc123\".to_string(),\n tool_name: \"bash\".to_string(),\n session_id: \"session\".to_string(),\n status: BackgroundTaskStatus::Completed,\n exit_code: Some(0),\n output_preview: \"[stderr] first line\\n[stdout] second line\\n\".to_string(),\n output_file: std::path::PathBuf::from(\"/tmp/output.log\"),\n duration_secs: 7.1,\n notify: true,\n wake: false,\n });\n\n let parsed = parse_background_task_notification_markdown(&rendered)\n .expect(\"background task notification should parse\");\n assert_eq!(parsed.task_id, \"abc123\");\n assert_eq!(parsed.tool_name, \"bash\");\n assert_eq!(parsed.status, \"✓ completed\");\n assert_eq!(parsed.duration, \"7.1s\");\n assert_eq!(parsed.exit_label, \"exit 0\");\n assert_eq!(\n parsed.preview.as_deref(),\n Some(\"[stderr] first line\\n[stdout] second line\")\n );\n assert_eq!(\n parsed.full_output_command,\n \"bg action=\\\"output\\\" task_id=\\\"abc123\\\"\"\n );\n }\n}\n"}, "b9a90a73ccf6161346a67076312c167867702fa2": {"t": 1785757249.3953333, "n": 37912, "h": "2ea2a7d30eda94af", "c": "#![allow(dead_code)]\n\nuse crate::bus::{BackgroundTaskCompleted, BackgroundTaskStatus};\nuse chrono::Utc;\nuse regex::Regex;\nuse serde::{Deserialize, Serialize};\nuse std::collections::HashSet;\nuse std::sync::OnceLock;\n\n#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]\npub struct InputShellResult {\n pub command: String,\n #[serde(default, skip_serializing_if = \"Option::is_none\")]\n pub cwd: Option<String>,\n pub output: String,\n #[serde(default, skip_serializing_if = \"Option::is_none\")]\n pub exit_code: Option<i32>,\n pub duration_ms: u64,\n #[serde(default)]\n pub truncated: bool,\n #[serde(default)]\n pub failed_to_start: bool,\n}\n\nfn sanitize_fenced_block(text: &str) -> String {\n text.replace(\"```\", \"``\\u{200b}`\")\n}\n\npub fn format_input_shell_result_markdown(shell: &InputShellResult) -> String {\n let status = if shell.failed_to_start {\n \"✗ failed to start\".to_string()\n } else if shell.exit_code == Some(0) {\n \"✓ exit 0\".to_string()\n } else if let Some(code) = shell.exit_code {\n format!(\"✗ exit {}\", code)\n } else {\n \"✗ terminated\".to_string()\n };\n\n let mut meta = vec![status, Message::format_duration(shell.duration_ms)];\n if let Some(cwd) = shell.cwd.as_deref() {\n meta.push(format!(\"cwd `{}`\", cwd));\n }\n if shell.truncated {\n meta.push(\"truncated\".to_string());\n }\n\n let mut message = format!(\n \"**Shell command** · {}\\n\\n```bash\\n{}\\n```\",\n meta.join(\" · \"),\n sanitize_fenced_block(&shell.command)\n );\n\n if shell.output.trim().is_empty() {\n message.push_str(\"\\n\\n_No output._\");\n } else {\n message.push_str(&format!(\n \"\\n\\n```text\\n{}\\n```\",\n sanitize_fenced_block(shell.output.trim_end())\n ));\n }\n\n message\n}\n\npub fn input_shell_status_notice(shell: &InputShellResult) -> String {\n if shell.failed_to_start {\n \"Shell command failed to start\".to_string()\n } else if shell.exit_code == Some(0) {\n \"Shell command completed\".to_string()\n } else if let Some(code) = shell.exit_code {\n format!(\"Shell command failed (exit {})\", code)\n } else {\n \"Shell command terminated\".to_string()\n }\n}\n\nfn format_background_task_status(status: &BackgroundTaskStatus) -> &'static str {\n match status {\n BackgroundTaskStatus::Completed => \"✓ completed\",\n BackgroundTaskStatus::Failed => \"✗ failed\",\n BackgroundTaskStatus::Running => \"running\",\n }\n}\n\nfn normalize_background_task_preview(preview: &str) -> Option<String> {\n let normalized = preview.replace(\"\\r\\n\", \"\\n\").replace('\\r', \"\\n\");\n let trimmed = normalized.trim_end();\n if trimmed.trim().is_empty() {\n None\n } else {\n Some(sanitize_fenced_block(trimmed))\n }\n}\n\npub fn format_background_task_notification_markdown(task: &BackgroundTaskCompleted) -> String {\n let exit_code = task\n .exit_code\n .map(|code| format!(\"exit {}\", code))\n .unwrap_or_else(|| \"exit n/a\".to_string());\n\n let mut message = format!(\n \"**Background task** `{}` · `{}` · {} · {:.1}s · {}\",\n task.task_id,\n task.tool_name,\n format_background_task_status(&task.status),\n task.duration_secs,\n exit_code,\n );\n\n if let Some(preview) = normalize_background_task_preview(&task.output_preview) {\n message.push_str(&format!(\"\\n\\n```text\\n{}\\n```\", preview));\n } else {\n message.push_str(\"\\n\\n_No output captured._\");\n }\n\n message.push_str(&format!(\n \"\\n\\n_Full output:_ `bg action=\\\"output\\\" task_id=\\\"{}\\\"`\",\n task.task_id\n ));\n\n message\n}\n\n#[derive(Debug, Clone, PartialEq, Eq)]\npub struct ParsedBackgroundTaskNotification {\n pub task_id: String,\n pub tool_name: String,\n pub status: String,\n pub duration: String,\n pub exit_label: String,\n pub preview: Option<String>,\n pub full_output_command: String,\n}\n\npub fn parse_background_task_notification_markdown(\n content: &str,\n) -> Option<ParsedBackgroundTaskNotification> {\n static HEADER_RE: OnceLock<Regex> = OnceLock::new();\n static FULL_OUTPUT_RE: OnceLock<Regex> = OnceLock::new();\n\n let header_re = HEADER_RE.get_or_init(|| {\n Regex::new(\n r\"^\\*\\*Background task\\*\\* `(?P<task_id>[^`]+)` · `(?P<tool_name>[^`]+)` · (?P<status>.+?) · (?P<duration>[0-9]+(?:\\.[0-9]+)?s) · (?P<exit_label>.+)$\",\n )\n .expect(\"valid background task header regex\")\n });\n let full_output_re = FULL_OUTPUT_RE.get_or_init(|| {\n Regex::new(r#\"^_Full output:_ `(?P<command>[^`]+)`$\"#)\n .expect(\"valid background task full output regex\")\n });\n\n let normalized = content.replace(\"\\r\\n\", \"\\n\").replace('\\r', \"\\n\");\n let mut sections = normalized.split(\"\\n\\n\");\n let header = sections.next()?.trim();\n let captures = header_re.captures(header)?;\n\n let mut preview: Option<String> = None;\n let mut full_output_command: Option<String> = None;\n\n for section in sections {\n let trimmed = section.trim();\n if trimmed.is_empty() {\n continue;\n }\n\n if let Some(captures) = full_output_re.captures(trimmed) {\n full_output_command = Some(captures[\"command\"].to_string());\n continue;\n }\n\n if trimmed == \"_No output captured._\" {\n preview = None;\n continue;\n }\n\n if let Some(fenced) = trimmed\n .strip_prefix(\"```text\\n\")\n .and_then(|body| body.strip_suffix(\"\\n```\"))\n {\n preview = Some(fenced.to_string());\n }\n }\n\n Some(ParsedBackgroundTaskNotification {\n task_id: captures[\"task_id\"].to_string(),\n tool_name: captures[\"tool_name\"].to_string(),\n status: captures[\"status\"].to_string(),\n duration: captures[\"duration\"].to_string(),\n exit_label: captures[\"exit_label\"].to_string(),\n preview,\n full_output_command: full_output_command?,\n })\n}\n\npub fn background_task_status_notice(task: &BackgroundTaskCompleted) -> String {\n match task.status {\n BackgroundTaskStatus::Completed => {\n format!(\"Background task completed · {}\", task.tool_name)\n }\n BackgroundTaskStatus::Failed => match task.exit_code {\n Some(code) => format!(\n \"Background task failed · {} · exit {}\",\n task.tool_name, code\n ),\n None => format!(\"Background task failed · {}\", task.tool_name),\n },\n BackgroundTaskStatus::Running => format!(\"Background task running · {}\", task.tool_name),\n }\n}\n\n/// Role in conversation\n#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]\n#[serde(rename_all = \"lowercase\")]\npub enum Role {\n User,\n Assistant,\n}\n\n/// Plain-text tool output placeholder when execution was interrupted.\npub const TOOL_OUTPUT_MISSING_TEXT: &str =\n \"Tool output missing (session interrupted before tool execution completed)\";\n\n/// A message in the conversation\n#[derive(Debug, Clone, Serialize, Deserialize)]\npub struct Message {\n pub role: Role,\n pub content: Vec<ContentBlock>,\n #[serde(default, skip_serializing_if = \"Option::is_none\")]\n pub timestamp: Option<chrono::DateTime<Utc>>,\n #[serde(default, skip_serializing_if = \"Option::is_none\")]\n pub tool_duration_ms: Option<u64>,\n}\n\n/// Cache control metadata for prompt caching\n#[derive(Debug, Clone, Serialize, Deserialize)]\npub struct CacheControl {\n #[serde(rename = \"type\")]\n pub kind: String,\n #[serde(skip_serializing_if = \"Option::is_none\")]\n pub ttl: Option<String>,\n}\n\nimpl CacheControl {\n pub fn ephemeral(ttl: Option<String>) -> Self {\n Self {\n kind: \"ephemeral\".to_string(),\n ttl,\n }\n }\n}\n\n/// Content block within a message\n#[derive(Debug, Clone, Serialize, Deserialize)]\n#[serde(tag = \"type\", rename_all = \"snake_case\")]\npub enum ContentBlock {\n Text {\n text: String,\n #[serde(skip_serializing_if = \"Option::is_none\")]\n cache_control: Option<CacheControl>,\n },\n /// Hidden reasoning content used for providers that require it (not displayed)\n Reasoning {\n text: String,\n },\n ToolUse {\n id: String,\n name: String,\n input: serde_json::Value,\n },\n ToolResult {\n tool_use_id: String,\n content: String,\n #[serde(skip_serializing_if = \"Option::is_none\")]\n is_error: Option<bool>,\n },\n Image {\n media_type: String,\n data: String,\n },\n /// Hidden OpenAI Responses compaction item used to preserve native\n /// compaction state across turns/saves when jcode explicitly triggers it.\n OpenAICompaction {\n encrypted_content: String,\n },\n}\n\nimpl Message {\n pub fn user(text: &str) -> Self {\n Self {\n role: Role::User,\n content: vec![ContentBlock::Text {\n text: text.to_string(),\n cache_control: None,\n }],\n timestamp: Some(Utc::now()),\n tool_duration_ms: None,\n }\n }\n\n pub fn user_with_images(text: &str, images: Vec<(String, String)>) -> Self {\n let mut content: Vec<ContentBlock> = images\n .into_iter()\n .map(|(media_type, data)| ContentBlock::Image { media_type, data })\n .collect();\n content.push(ContentBlock::Text {\n text: text.to_string(),\n cache_control: None,\n });\n Self {\n role: Role::User,\n content,\n timestamp: Some(Utc::now()),\n tool_duration_ms: None,\n }\n }\n\n pub fn assistant_text(text: &str) -> Self {\n Self {\n role: Role::Assistant,\n content: vec![ContentBlock::Text {\n text: text.to_string(),\n cache_control: None,\n }],\n timestamp: Some(Utc::now()),\n tool_duration_ms: None,\n }\n }\n\n pub fn tool_result(tool_use_id: &str, content: &str, is_error: bool) -> Self {\n Self::tool_result_with_duration(tool_use_id, content, is_error, None)\n }\n\n pub fn tool_result_with_duration(\n tool_use_id: &str,\n content: &str,\n is_error: bool,\n tool_duration_ms: Option<u64>,\n ) -> Self {\n Self {\n role: Role::User,\n content: vec![ContentBlock::ToolResult {\n tool_use_id: tool_use_id.to_string(),\n content: content.to_string(),\n is_error: if is_error { Some(true) } else { None },\n }],\n timestamp: Some(Utc::now()),\n tool_duration_ms,\n }\n }\n\n /// Format a timestamp deterministically in UTC for injection into model-visible content.\n pub fn format_timestamp(ts: &chrono::DateTime<Utc>) -> String {\n ts.to_rfc3339_opts(chrono::SecondsFormat::Millis, true)\n }\n\n pub fn format_duration(duration_ms: u64) -> String {\n match duration_ms {\n 0..=999 => format!(\"{}ms\", duration_ms),\n 1_000..=9_999 => format!(\"{:.1}s\", duration_ms as f64 / 1000.0),\n 10_000..=59_999 => format!(\"{}s\", duration_ms / 1000),\n _ => {\n let total_seconds = duration_ms / 1000;\n let minutes = total_seconds / 60;\n let seconds = total_seconds % 60;\n if seconds == 0 {\n format!(\"{}m\", minutes)\n } else {\n format!(\"{}m {}s\", minutes, seconds)\n }\n }\n }\n }\n\n fn should_skip_timestamp_injection(&self) -> bool {\n self.content\n .iter()\n .find_map(|block| match block {\n ContentBlock::Text { text, .. } => Some(text.trim_start()),\n _ => None,\n })\n .is_some_and(|text| text.starts_with(\"<system-reminder>\"))\n }\n\n fn tool_result_tag(&self, ts: &chrono::DateTime<Utc>) -> String {\n match self.tool_duration_ms {\n Some(duration_ms) => {\n let duration_ms_i64 = i64::try_from(duration_ms).unwrap_or(i64::MAX);\n let start_ts = ts\n .checked_sub_signed(chrono::Duration::milliseconds(duration_ms_i64))\n .unwrap_or(*ts);\n format!(\n \"[tool timing: start={} finish={} duration={}]\",\n Self::format_timestamp(&start_ts),\n Self::format_timestamp(ts),\n Self::format_duration(duration_ms)\n )\n }\n None => format!(\"[{}]\", Self::format_timestamp(ts)),\n }\n }\n\n /// Return a copy of messages with timestamps injected into user-role text content.\n /// Tool results get a stable UTC timing header prepended to content.\n /// User text messages get a stable UTC timestamp prepended to the first text block.\n pub fn with_timestamps(messages: &[Message]) -> Vec<Message> {\n messages\n .iter()\n .map(|msg| {\n let Some(ts) = msg.timestamp else {\n return msg.clone();\n };\n if msg.role != Role::User || msg.should_skip_timestamp_injection() {\n return msg.clone();\n }\n let text_tag = format!(\"[{}]\", Self::format_timestamp(&ts));\n let tool_result_tag = msg.tool_result_tag(&ts);\n let mut msg = msg.clone();\n let mut tagged = false;\n for block in &mut msg.content {\n match block {\n ContentBlock::Text { text, .. } if !tagged => {\n *text = format!(\"{} {}\", text_tag, text);\n tagged = true;\n }\n ContentBlock::ToolResult { content, .. } if !tagged => {\n *content = format!(\"{} {}\", tool_result_tag, content);\n tagged = true;\n }\n _ => {}\n }\n }\n msg\n })\n .collect()\n }\n}\n\n/// Sanitize a tool ID so it matches the pattern `^[a-zA-Z0-9_-]+$`.\n///\n/// Different providers generate tool IDs in different formats. When switching\n/// from one provider to another mid-conversation, the historical tool IDs may\n/// contain characters that the new provider rejects (e.g., dots in Copilot IDs\n/// sent to Anthropic). This function replaces any invalid characters with\n/// underscores.\npub fn sanitize_tool_id(id: &str) -> String {\n if id.is_empty() {\n return \"unknown\".to_string();\n }\n let sanitized: String = id\n .chars()\n .map(|c| {\n if c.is_ascii_alphanumeric() || c == '_' || c == '-' {\n c\n } else {\n '_'\n }\n })\n .collect();\n if sanitized.is_empty() {\n \"unknown\".to_string()\n } else {\n sanitized\n }\n}\n\n/// Redact likely secrets from persisted tool output.\n///\n/// This is a best-effort safeguard for local session history files. It targets\n/// high-confidence token/key patterns and common `KEY=VALUE` assignments used by\n/// auth flows.\npub fn redact_secrets(text: &str) -> String {\n // Fast path to avoid regex work for most tool outputs.\n let lower = text.to_ascii_lowercase();\n\n if !text.contains(\"sk-\")\n && !text.contains(\"ghp_\")\n && !text.contains(\"github_pat_\")\n && !text.contains(\"AIza\")\n && !text.contains(\"ya29.\")\n && !text.contains(\"xox\")\n && !lower.contains(\"api_key\")\n && !lower.contains(\"token\")\n {\n return text.to_string();\n }\n\n static DIRECT_PATTERNS: OnceLock<Vec<Regex>> = OnceLock::new();\n static ASSIGNMENT_PATTERNS: OnceLock<Vec<Regex>> = OnceLock::new();\n\n let direct_patterns = DIRECT_PATTERNS.get_or_init(|| {\n vec![\n Regex::new(r\"sk-ant-(?:oat|ort)01-[A-Za-z0-9_-]{20,}\")\n .expect(\"valid Anthropic OAuth token regex\"),\n Regex::new(r\"sk-or-v1-[A-Za-z0-9_-]{20,}\").expect(\"valid OpenRouter key regex\"),\n Regex::new(r\"ghp_[A-Za-z0-9]{20,}\").expect(\"valid GitHub PAT regex\"),\n Regex::new(r\"github_pat_[A-Za-z0-9_]{20,}\")\n .expect(\"valid GitHub fine-grained PAT regex\"),\n Regex::new(r\"ya29\\.[A-Za-z0-9._-]{20,}\").expect(\"valid Google OAuth token regex\"),\n Regex::new(r\"AIza[0-9A-Za-z_-]{20,}\").expect(\"valid Google API key regex\"),\n Regex::new(r\"xox[baprs]-[A-Za-z0-9-]{10,}\").expect(\"valid Slack token regex\"),\n ]\n });\n\n let assignment_patterns = ASSIGNMENT_PATTERNS.get_or_init(|| {\n vec![\n Regex::new(r\"(?m)^\\s*(OPENROUTER_API_KEY\\s*=\\s*)[^\\r\\n]+\")\n .expect(\"valid OPENROUTER_API_KEY assignment regex\"),\n Regex::new(r\"(?m)^\\s*(OPENCODE_API_KEY\\s*=\\s*)[^\\r\\n]+\")\n .expect(\"valid OPENCODE_API_KEY assignment regex\"),\n Regex::new(r\"(?m)^\\s*(OPENCODE_GO_API_KEY\\s*=\\s*)[^\\r\\n]+\")\n .expect(\"valid OPENCODE_GO_API_KEY assignment regex\"),\n Regex::new(r\"(?m)^\\s*(ZHIPU_API_KEY\\s*=\\s*)[^\\r\\n]+\")\n .expect(\"valid ZHIPU_API_KEY assignment regex\"),\n Regex::new(r\"(?m)^\\s*(ZAI_API_KEY\\s*=\\s*)[^\\r\\n]+\")\n .expect(\"valid ZAI_API_KEY assignment regex\"),\n Regex::new(r\"(?m)^\\s*(302AI_API_KEY\\s*=\\s*)[^\\r\\n]+\")\n .expect(\"valid 302AI_API_KEY assignment regex\"),\n Regex::new(r\"(?m)^\\s*(BASETEN_API_KEY\\s*=\\s*)[^\\r\\n]+\")\n .expect(\"valid BASETEN_API_KEY assignment regex\"),\n Regex::new(r\"(?m)^\\s*(CORTECS_API_KEY\\s*=\\s*)[^\\r\\n]+\")\n .expect(\"valid CORTECS_API_KEY assignment regex\"),\n Regex::new(r\"(?m)^\\s*(DEEPSEEK_API_KEY\\s*=\\s*)[^\\r\\n]+\")\n .expect(\"valid DEEPSEEK_API_KEY assignment regex\"),\n Regex::new(r\"(?m)^\\s*(FIRMWARE_API_KEY\\s*=\\s*)[^\\r\\n]+\")\n .expect(\"valid FIRMWARE_API_KEY assignment regex\"),\n Regex::new(r\"(?m)^\\s*(HF_TOKEN\\s*=\\s*)[^\\r\\n]+\")\n .expect(\"valid HF_TOKEN assignment regex\"),\n Regex::new(r\"(?m)^\\s*(MOONSHOT_API_KEY\\s*=\\s*)[^\\r\\n]+\")\n .expect(\"valid MOONSHOT_API_KEY assignment regex\"),\n Regex::new(r\"(?m)^\\s*(NEBIUS_API_KEY\\s*=\\s*)[^\\r\\n]+\")\n .expect(\"valid NEBIUS_API_KEY assignment regex\"),\n Regex::new(r\"(?m)^\\s*(SCALEWAY_API_KEY\\s*=\\s*)[^\\r\\n]+\")\n .expect(\"valid SCALEWAY_API_KEY assignment regex\"),\n Regex::new(r\"(?m)^\\s*(STACKIT_API_KEY\\s*=\\s*)[^\\r\\n]+\")\n .expect(\"valid STACKIT_API_KEY assignment regex\"),\n Regex::new(r\"(?m)^\\s*(GROQ_API_KEY\\s*=\\s*)[^\\r\\n]+\")\n .expect(\"valid GROQ_API_KEY assignment regex\"),\n Regex::new(r\"(?m)^\\s*(MISTRAL_API_KEY\\s*=\\s*)[^\\r\\n]+\")\n .expect(\"valid MISTRAL_API_KEY assignment regex\"),\n Regex::new(r\"(?m)^\\s*(PERPLEXITY_API_KEY\\s*=\\s*)[^\\r\\n]+\")\n .expect(\"valid PERPLEXITY_API_KEY assignment regex\"),\n Regex::new(r\"(?m)^\\s*(TOGETHER_API_KEY\\s*=\\s*)[^\\r\\n]+\")\n .expect(\"valid TOGETHER_API_KEY assignment regex\"),\n Regex::new(r\"(?m)^\\s*(DEEPINFRA_API_KEY\\s*=\\s*)[^\\r\\n]+\")\n .expect(\"valid DEEPINFRA_API_KEY assignment regex\"),\n Regex::new(r\"(?m)^\\s*(XAI_API_KEY\\s*=\\s*)[^\\r\\n]+\")\n .expect(\"valid XAI_API_KEY assignment regex\"),\n Regex::new(r\"(?m)^\\s*(LMSTUDIO_API_KEY\\s*=\\s*)[^\\r\\n]+\")\n .expect(\"valid LMSTUDIO_API_KEY assignment regex\"),\n Regex::new(r\"(?m)^\\s*(OLLAMA_API_KEY\\s*=\\s*)[^\\r\\n]+\")\n .expect(\"valid OLLAMA_API_KEY assignment regex\"),\n Regex::new(r\"(?m)^\\s*(CHUTES_API_KEY\\s*=\\s*)[^\\r\\n]+\")\n .expect(\"valid CHUTES_API_KEY assignment regex\"),\n Regex::new(r\"(?m)^\\s*(CEREBRAS_API_KEY\\s*=\\s*)[^\\r\\n]+\")\n .expect(\"valid CEREBRAS_API_KEY assignment regex\"),\n Regex::new(r\"(?m)^\\s*(OPENAI_COMPAT_API_KEY\\s*=\\s*)[^\\r\\n]+\")\n .expect(\"valid OPENAI_COMPAT_API_KEY assignment regex\"),\n Regex::new(r\"(?m)^\\s*(ANTHROPIC_API_KEY\\s*=\\s*)[^\\r\\n]+\")\n .expect(\"valid ANTHROPIC_API_KEY assignment regex\"),\n Regex::new(r\"(?m)^\\s*(OPENAI_API_KEY\\s*=\\s*)[^\\r\\n]+\")\n .expect(\"valid OPENAI_API_KEY assignment regex\"),\n Regex::new(r\"(?m)^\\s*(AZURE_OPENAI_API_KEY\\s*=\\s*)[^\\r\\n]+\")\n .expect(\"valid AZURE_OPENAI_API_KEY assignment regex\"),\n Regex::new(r\"(?m)^\\s*(CURSOR_API_KEY\\s*=\\s*)[^\\r\\n]+\")\n .expect(\"valid CURSOR_API_KEY assignment regex\"),\n Regex::new(r\"(?m)^\\s*(GITHUB_TOKEN\\s*=\\s*)[^\\r\\n]+\")\n .expect(\"valid GITHUB_TOKEN assignment regex\"),\n ]\n });\n\n let mut redacted = text.to_string();\n let mut redacted_keys: HashSet<String> = [\n \"OPENROUTER_API_KEY\",\n \"OPENCODE_API_KEY\",\n \"OPENCODE_GO_API_KEY\",\n \"ZHIPU_API_KEY\",\n \"ZAI_API_KEY\",\n \"302AI_API_KEY\",\n \"BASETEN_API_KEY\",\n \"CORTECS_API_KEY\",\n \"DEEPSEEK_API_KEY\",\n \"FIRMWARE_API_KEY\",\n \"HF_TOKEN\",\n \"MOONSHOT_API_KEY\",\n \"NEBIUS_API_KEY\",\n \"SCALEWAY_API_KEY\",\n \"STACKIT_API_KEY\",\n \"GROQ_API_KEY\",\n \"MISTRAL_API_KEY\",\n \"PERPLEXITY_API_KEY\",\n \"TOGETHER_API_KEY\",\n \"DEEPINFRA_API_KEY\",\n \"XAI_API_KEY\",\n \"LMSTUDIO_API_KEY\",\n \"OLLAMA_API_KEY\",\n \"CHUTES_API_KEY\",\n \"CEREBRAS_API_KEY\",\n \"OPENAI_COMPAT_API_KEY\",\n \"ANTHROPIC_API_KEY\",\n \"OPENAI_API_KEY\",\n \"AZURE_OPENAI_API_KEY\",\n \"CURSOR_API_KEY\",\n \"GITHUB_TOKEN\",\n ]\n .iter()\n .map(|k| (*k).to_string())\n .collect();\n\n for re in direct_patterns {\n redacted = re.replace_all(&redacted, \"[REDACTED_SECRET]\").into_owned();\n }\n\n for re in assignment_patterns {\n redacted = re\n .replace_all(&redacted, \"${1}[REDACTED_SECRET]\")\n .into_owned();\n }\n\n // Also redact custom API key variable names configured at runtime.\n for source in [\n \"JCODE_OPENROUTER_API_KEY_NAME\",\n \"JCODE_OPENAI_COMPAT_API_KEY_NAME\",\n ] {\n let Some(key_name) = std::env::var(source)\n .ok()\n .map(|v| v.trim().to_string())\n .filter(|v| !v.is_empty())\n else {\n continue;\n };\n\n if !key_name\n .chars()\n .all(|c| c.is_ascii_uppercase() || c.is_ascii_digit() || c == '_')\n {\n continue;\n }\n if !redacted_keys.insert(key_name.clone()) {\n continue;\n }\n\n let pattern = format!(r\"(?m)^\\s*({}\\s*=\\s*)[^\\r\\n]+\", regex::escape(&key_name));\n if let Ok(re) = Regex::new(&pattern) {\n redacted = re\n .replace_all(&redacted, \"${1}[REDACTED_SECRET]\")\n .into_owned();\n }\n }\n\n redacted\n}\n\n/// Tool definition for the API\n#[derive(Debug, Clone, Serialize)]\npub struct ToolDefinition {\n pub name: String,\n pub description: String,\n pub input_schema: serde_json::Value,\n}\n\n/// A tool call from the model\n#[derive(Debug, Clone, Default, PartialEq, Eq, serde::Serialize, serde::Deserialize)]\npub struct ToolCall {\n #[serde(default)]\n pub id: String,\n #[serde(default)]\n pub name: String,\n #[serde(default)]\n pub input: serde_json::Value,\n #[serde(default, skip_serializing_if = \"Option::is_none\")]\n pub intent: Option<String>,\n}\n\n/// Connection phase for status bar transparency\n#[derive(Debug, Clone, PartialEq)]\npub enum ConnectionPhase {\n /// Refreshing OAuth token\n Authenticating,\n /// TCP + TLS connection to API\n Connecting,\n /// HTTP request sent, waiting for first response byte\n WaitingForResponse,\n /// First byte received, stream is active\n Streaming,\n /// Retrying after a transient error\n Retrying { attempt: u32, max: u32 },\n}\n\nimpl std::fmt::Display for ConnectionPhase {\n fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {\n match self {\n ConnectionPhase::Authenticating => write!(f, \"authenticating\"),\n ConnectionPhase::Connecting => write!(f, \"connecting\"),\n ConnectionPhase::WaitingForResponse => write!(f, \"waiting for response\"),\n ConnectionPhase::Streaming => write!(f, \"streaming\"),\n ConnectionPhase::Retrying { attempt, max } => {\n write!(f, \"retrying ({}/{})\", attempt, max)\n }\n }\n }\n}\n\n/// Streaming event from provider\n#[derive(Debug, Clone)]\npub enum StreamEvent {\n /// Text content delta\n TextDelta(String),\n /// Tool use started\n ToolUseStart { id: String, name: String },\n /// Tool input delta (JSON fragment)\n ToolInputDelta(String),\n /// Tool use complete\n ToolUseEnd,\n /// Tool result from provider (provider already executed the tool)\n ToolResult {\n tool_use_id: String,\n content: String,\n is_error: bool,\n },\n /// Extended thinking started\n ThinkingStart,\n /// Extended thinking delta (reasoning content)\n ThinkingDelta(String),\n /// Extended thinking ended\n ThinkingEnd,\n /// Extended thinking completed with duration\n ThinkingDone { duration_secs: f64 },\n /// Message complete (may have stop reason)\n MessageEnd { stop_reason: Option<String> },\n /// Token usage update\n TokenUsage {\n input_tokens: Option<u64>,\n output_tokens: Option<u64>,\n cache_read_input_tokens: Option<u64>,\n cache_creation_input_tokens: Option<u64>,\n },\n /// Active transport/connection type for this stream\n ConnectionType { connection: String },\n /// Connection phase update (for status bar transparency)\n ConnectionPhase { phase: ConnectionPhase },\n /// Provider-supplied human-readable transport detail for the status line\n StatusDetail { detail: String },\n /// Error occurred\n Error {\n message: String,\n /// Seconds until rate limit resets (if this is a rate limit error)\n retry_after_secs: Option<u64>,\n },\n /// Provider session ID (for conversation resume)\n SessionId(String),\n /// Compaction occurred (context was summarized)\n Compaction {\n trigger: String,\n pre_tokens: Option<u64>,\n /// Provider-native compaction artifact, if one was emitted.\n openai_encrypted_content: Option<String>,\n },\n /// Upstream provider info (e.g., which provider OpenRouter routed to)\n UpstreamProvider { provider: String },\n /// Native tool call from a provider bridge that needs execution by jcode\n NativeToolCall {\n request_id: String,\n tool_name: String,\n input: serde_json::Value,\n },\n}\n\n#[cfg(test)]\nmod tests {\n use super::*;\n\n #[test]\n fn sanitize_tool_id_alphanumeric_passthrough() {\n assert_eq!(\n sanitize_tool_id(\"toolu_01XFDUDYJgAACzvnptvVer6u\"),\n \"toolu_01XFDUDYJgAACzvnptvVer6u\"\n );\n assert_eq!(sanitize_tool_id(\"call_abc123\"), \"call_abc123\");\n assert_eq!(\n sanitize_tool_id(\"call_1234567890_9876543210\"),\n \"call_1234567890_9876543210\"\n );\n }\n\n #[test]\n fn sanitize_tool_id_hyphens_passthrough() {\n assert_eq!(sanitize_tool_id(\"call-abc-123\"), \"call-abc-123\");\n assert_eq!(\n sanitize_tool_id(\"tool_use-id_with-mixed\"),\n \"tool_use-id_with-mixed\"\n );\n }\n\n #[test]\n fn sanitize_tool_id_replaces_dots() {\n assert_eq!(\n sanitize_tool_id(\"chatcmpl-abc.def.ghi\"),\n \"chatcmpl-abc_def_ghi\"\n );\n assert_eq!(sanitize_tool_id(\"call.123\"), \"call_123\");\n }\n\n #[test]\n fn sanitize_tool_id_replaces_colons() {\n assert_eq!(sanitize_tool_id(\"call:123:456\"), \"call_123_456\");\n }\n\n #[test]\n fn sanitize_tool_id_replaces_special_chars() {\n assert_eq!(\n sanitize_tool_id(\"id@with#special$chars\"),\n \"id_with_special_chars\"\n );\n assert_eq!(sanitize_tool_id(\"id with spaces\"), \"id_with_spaces\");\n }\n\n #[test]\n fn sanitize_tool_id_empty_returns_unknown() {\n assert_eq!(sanitize_tool_id(\"\"), \"unknown\");\n }\n\n #[test]\n fn sanitize_tool_id_copilot_to_anthropic() {\n assert_eq!(\n sanitize_tool_id(\"chatcmpl-BF2xX.tool_call.0\"),\n \"chatcmpl-BF2xX_tool_call_0\"\n );\n }\n\n #[test]\n fn sanitize_tool_id_already_valid_unchanged() {\n let valid_ids = [\n \"toolu_01XFDUDYJgAACzvnptvVer6u\",\n \"call_abc123\",\n \"fallback_text_call_call_1234567890_9876543210\",\n \"tool_123\",\n \"a\",\n \"A\",\n \"0\",\n \"_\",\n \"-\",\n \"a-b_c\",\n ];\n for id in valid_ids {\n assert_eq!(sanitize_tool_id(id), id, \"ID '{}' should be unchanged\", id);\n }\n }\n\n #[test]\n fn redact_secrets_redacts_known_direct_token_formats() {\n let input = \"access=sk-ant-oat01-ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789\\nopenrouter=sk-or-v1-abcdefghijklmnopqrstuvwxyz0123456789\\ngithub=ghp_ABCDEFGHIJKLMNOPQRSTUVWXYZ0123\\n\";\n let out = redact_secrets(input);\n assert!(!out.contains(\"sk-ant-oat01-\"));\n assert!(!out.contains(\"sk-or-v1-\"));\n assert!(!out.contains(\"ghp_ABCDEFGHIJKLMNOPQRSTUVWXYZ0123\"));\n assert!(out.matches(\"[REDACTED_SECRET]\").count() >= 3);\n }\n\n #[test]\n fn redact_secrets_redacts_env_style_assignments() {\n let input = \"OPENROUTER_API_KEY=sk-or-v1-abc123abc123abc123abc123\\nOPENCODE_API_KEY=oc_test_secret\\nOPENCODE_GO_API_KEY=ocgo_test_secret\\nZAI_API_KEY=zai_secret\\nCHUTES_API_KEY=chutes_secret\\nCEREBRAS_API_KEY=cerebras_secret\\nOPENAI_COMPAT_API_KEY=compat_secret\\nCURSOR_API_KEY='my_cursor_secret_value'\\nOPENAI_API_KEY=sk-test-openai-example\\nAZURE_OPENAI_API_KEY=azure-openai-secret\\n\";\n let out = redact_secrets(input);\n assert!(out.contains(\"OPENROUTER_API_KEY=[REDACTED_SECRET]\"));\n assert!(out.contains(\"OPENCODE_API_KEY=[REDACTED_SECRET]\"));\n assert!(out.contains(\"OPENCODE_GO_API_KEY=[REDACTED_SECRET]\"));\n assert!(out.contains(\"ZAI_API_KEY=[REDACTED_SECRET]\"));\n assert!(out.contains(\"CHUTES_API_KEY=[REDACTED_SECRET]\"));\n assert!(out.contains(\"CEREBRAS_API_KEY=[REDACTED_SECRET]\"));\n assert!(out.contains(\"OPENAI_COMPAT_API_KEY=[REDACTED_SECRET]\"));\n assert!(out.contains(\"CURSOR_API_KEY=[REDACTED_SECRET]\"));\n assert!(out.contains(\"OPENAI_API_KEY=[REDACTED_SECRET]\"));\n assert!(out.contains(\"AZURE_OPENAI_API_KEY=[REDACTED_SECRET]\"));\n assert!(!out.contains(\"my_cursor_secret_value\"));\n }\n\n #[test]\n fn redact_secrets_redacts_runtime_key_assignment() {\n let key_var = \"JCODE_OPENAI_COMPAT_API_KEY_NAME\";\n let prev = std::env::var(key_var).ok();\n crate::env::set_var(key_var, \"GROQ_API_KEY\");\n\n let input = \"GROQ_API_KEY=my_secret_token_value\";\n let out = redact_secrets(input);\n assert_eq!(out, \"GROQ_API_KEY=[REDACTED_SECRET]\");\n\n if let Some(v) = prev {\n crate::env::set_var(key_var, v);\n } else {\n crate::env::remove_var(key_var);\n }\n }\n\n #[test]\n fn redact_secrets_redacts_mixed_case_token_assignments() {\n let input = \"my_token=ya29.ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789\";\n let out = redact_secrets(input);\n assert!(out.contains(\"[REDACTED_SECRET]\"));\n assert!(!out.contains(\"ya29.ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789\"));\n }\n\n #[test]\n fn redact_secrets_leaves_normal_output_unchanged() {\n let input = \"Found 5 files\\nNo auth errors\\nDone.\";\n assert_eq!(redact_secrets(input), input);\n }\n\n #[test]\n fn format_timestamp_is_stable_utc_rfc3339() {\n let ts = chrono::DateTime::parse_from_rfc3339(\"2025-03-15T02:24:13.250Z\")\n .unwrap()\n .with_timezone(&Utc);\n assert_eq!(Message::format_timestamp(&ts), \"2025-03-15T02:24:13.250Z\");\n }\n\n #[test]\n fn with_timestamps_prepends_utc_prefix_to_user_text() {\n let ts = chrono::DateTime::parse_from_rfc3339(\"2025-03-15T02:24:03Z\")\n .unwrap()\n .with_timezone(&Utc);\n let stamped = Message::with_timestamps(&[Message {\n role: Role::User,\n content: vec![ContentBlock::Text {\n text: \"hello\".to_string(),\n cache_control: None,\n }],\n timestamp: Some(ts),\n tool_duration_ms: None,\n }]);\n match &stamped[0].content[0] {\n ContentBlock::Text { text, .. } => {\n assert_eq!(text, \"[2025-03-15T02:24:03.000Z] hello\");\n }\n other => panic!(\"expected text block, got {other:?}\"),\n }\n }\n\n #[test]\n fn with_timestamps_adds_tool_timing_header_with_duration() {\n let ts = chrono::DateTime::parse_from_rfc3339(\"2025-03-15T02:24:13Z\")\n .unwrap()\n .with_timezone(&Utc);\n let stamped = Message::with_timestamps(&[Message {\n role: Role::User,\n content: vec![ContentBlock::ToolResult {\n tool_use_id: \"call_1\".to_string(),\n content: \"ok\".to_string(),\n is_error: None,\n }],\n timestamp: Some(ts),\n tool_duration_ms: Some(3_200),\n }]);\n match &stamped[0].content[0] {\n ContentBlock::ToolResult { content, .. } => {\n assert_eq!(\n content,\n \"[tool timing: start=2025-03-15T02:24:09.800Z finish=2025-03-15T02:24:13.000Z duration=3.2s] ok\"\n );\n }\n other => panic!(\"expected tool result block, got {other:?}\"),\n }\n }\n\n #[test]\n fn with_timestamps_skips_internal_system_reminders() {\n let ts = chrono::DateTime::parse_from_rfc3339(\"2025-03-15T02:24:13Z\")\n .unwrap()\n .with_timezone(&Utc);\n let original = Message {\n role: Role::User,\n content: vec![ContentBlock::Text {\n text: \"<system-reminder>\\ninternal\\n</system-reminder>\".to_string(),\n cache_control: None,\n }],\n timestamp: Some(ts),\n tool_duration_ms: None,\n };\n let stamped = Message::with_timestamps(&[original.clone()]);\n match &stamped[0].content[0] {\n ContentBlock::Text { text, .. } => {\n assert_eq!(text, \"<system-reminder>\\ninternal\\n</system-reminder>\");\n }\n other => panic!(\"expected text block, got {other:?}\"),\n }\n }\n\n #[test]\n fn format_background_task_notification_markdown_uses_code_block_preview() {\n let rendered = format_background_task_notification_markdown(&BackgroundTaskCompleted {\n task_id: \"abc123\".to_string(),\n tool_name: \"bash\".to_string(),\n session_id: \"session\".to_string(),\n status: BackgroundTaskStatus::Completed,\n exit_code: Some(0),\n output_preview: \"[stderr] first line\\n[stdout] second line\\n\".to_string(),\n output_file: std::path::PathBuf::from(\"/tmp/output.log\"),\n duration_secs: 7.1,\n notify: true,\n });\n\n assert!(\n rendered\n .contains(\"**Background task** `abc123` · `bash` · ✓ completed · 7.1s · exit 0\")\n );\n assert!(rendered.contains(\"```text\\n[stderr] first line\\n[stdout] second line\\n```\"));\n assert!(rendered.contains(\"_Full output:_ `bg action=\\\"output\\\" task_id=\\\"abc123\\\"`\"));\n }\n\n #[test]\n fn format_background_task_notification_markdown_handles_empty_preview() {\n let rendered = format_background_task_notification_markdown(&BackgroundTaskCompleted {\n task_id: \"abc123\".to_string(),\n tool_name: \"bash\".to_string(),\n session_id: \"session\".to_string(),\n status: BackgroundTaskStatus::Failed,\n exit_code: Some(9),\n output_preview: \"\\n\\n\".to_string(),\n output_file: std::path::PathBuf::from(\"/tmp/output.log\"),\n duration_secs: 1.0,\n notify: true,\n });\n\n assert!(rendered.contains(\"✗ failed\"));\n assert!(rendered.contains(\"_No output captured._\"));\n }\n\n #[test]\n fn parse_background_task_notification_markdown_extracts_fields() {\n let rendered = format_background_task_notification_markdown(&BackgroundTaskCompleted {\n task_id: \"abc123\".to_string(),\n tool_name: \"bash\".to_string(),\n session_id: \"session\".to_string(),\n status: BackgroundTaskStatus::Completed,\n exit_code: Some(0),\n output_preview: \"[stderr] first line\\n[stdout] second line\\n\".to_string(),\n output_file: std::path::PathBuf::from(\"/tmp/output.log\"),\n duration_secs: 7.1,\n notify: true,\n });\n\n let parsed = parse_background_task_notification_markdown(&rendered)\n .expect(\"background task notification should parse\");\n assert_eq!(parsed.task_id, \"abc123\");\n assert_eq!(parsed.tool_name, \"bash\");\n assert_eq!(parsed.status, \"✓ completed\");\n assert_eq!(parsed.duration, \"7.1s\");\n assert_eq!(parsed.exit_label, \"exit 0\");\n assert_eq!(\n parsed.preview.as_deref(),\n Some(\"[stderr] first line\\n[stdout] second line\")\n );\n assert_eq!(\n parsed.full_output_command,\n \"bg action=\\\"output\\\" task_id=\\\"abc123\\\"\"\n );\n }\n}\n"}, "49480e38e332f558a09703ec8f89b8820168447c": {"t": 1785757250.9267042, "n": 37797, "h": "01b77538c0ee9a7c", "c": "#![allow(dead_code)]\n\nuse crate::bus::{BackgroundTaskCompleted, BackgroundTaskStatus};\nuse chrono::Utc;\nuse regex::Regex;\nuse serde::{Deserialize, Serialize};\nuse std::collections::HashSet;\nuse std::sync::OnceLock;\n\n#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]\npub struct InputShellResult {\n pub command: String,\n #[serde(default, skip_serializing_if = \"Option::is_none\")]\n pub cwd: Option<String>,\n pub output: String,\n #[serde(default, skip_serializing_if = \"Option::is_none\")]\n pub exit_code: Option<i32>,\n pub duration_ms: u64,\n #[serde(default)]\n pub truncated: bool,\n #[serde(default)]\n pub failed_to_start: bool,\n}\n\nfn sanitize_fenced_block(text: &str) -> String {\n text.replace(\"```\", \"``\\u{200b}`\")\n}\n\npub fn format_input_shell_result_markdown(shell: &InputShellResult) -> String {\n let status = if shell.failed_to_start {\n \"✗ failed to start\".to_string()\n } else if shell.exit_code == Some(0) {\n \"✓ exit 0\".to_string()\n } else if let Some(code) = shell.exit_code {\n format!(\"✗ exit {}\", code)\n } else {\n \"✗ terminated\".to_string()\n };\n\n let mut meta = vec![status, Message::format_duration(shell.duration_ms)];\n if let Some(cwd) = shell.cwd.as_deref() {\n meta.push(format!(\"cwd `{}`\", cwd));\n }\n if shell.truncated {\n meta.push(\"truncated\".to_string());\n }\n\n let mut message = format!(\n \"**Shell command** · {}\\n\\n```bash\\n{}\\n```\",\n meta.join(\" · \"),\n sanitize_fenced_block(&shell.command)\n );\n\n if shell.output.trim().is_empty() {\n message.push_str(\"\\n\\n_No output._\");\n } else {\n message.push_str(&format!(\n \"\\n\\n```text\\n{}\\n```\",\n sanitize_fenced_block(shell.output.trim_end())\n ));\n }\n\n message\n}\n\npub fn input_shell_status_notice(shell: &InputShellResult) -> String {\n if shell.failed_to_start {\n \"Shell command failed to start\".to_string()\n } else if shell.exit_code == Some(0) {\n \"Shell command completed\".to_string()\n } else if let Some(code) = shell.exit_code {\n format!(\"Shell command failed (exit {})\", code)\n } else {\n \"Shell command terminated\".to_string()\n }\n}\n\nfn format_background_task_status(status: &BackgroundTaskStatus) -> &'static str {\n match status {\n BackgroundTaskStatus::Completed => \"✓ completed\",\n BackgroundTaskStatus::Failed => \"✗ failed\",\n BackgroundTaskStatus::Running => \"running\",\n }\n}\n\nfn normalize_background_task_preview(preview: &str) -> Option<String> {\n let normalized = preview.replace(\"\\r\\n\", \"\\n\").replace('\\r', \"\\n\");\n let trimmed = normalized.trim_end();\n if trimmed.trim().is_empty() {\n None\n } else {\n Some(sanitize_fenced_block(trimmed))\n }\n}\n\npub fn format_background_task_notification_markdown(task: &BackgroundTaskCompleted) -> String {\n let exit_code = task\n .exit_code\n .map(|code| format!(\"exit {}\", code))\n .unwrap_or_else(|| \"exit n/a\".to_string());\n\n let mut message = format!(\n \"**Background task** `{}` · `{}` · {} · {:.1}s · {}\",\n task.task_id,\n task.tool_name,\n format_background_task_status(&task.status),\n task.duration_secs,\n exit_code,\n );\n\n if let Some(preview) = normalize_background_task_preview(&task.output_preview) {\n message.push_str(&format!(\"\\n\\n```text\\n{}\\n```\", preview));\n } else {\n message.push_str(\"\\n\\n_No output captured._\");\n }\n\n message.push_str(&format!(\n \"\\n\\n_Full output:_ `bg action=\\\"output\\\" task_id=\\\"{}\\\"`\",\n task.task_id\n ));\n\n message\n}\n\n#[derive(Debug, Clone, PartialEq, Eq)]\npub struct ParsedBackgroundTaskNotification {\n pub task_id: String,\n pub tool_name: String,\n pub status: String,\n pub duration: String,\n pub exit_label: String,\n pub preview: Option<String>,\n pub full_output_command: String,\n}\n\npub fn parse_background_task_notification_markdown(\n content: &str,\n) -> Option<ParsedBackgroundTaskNotification> {\n static HEADER_RE: OnceLock<Regex> = OnceLock::new();\n static FULL_OUTPUT_RE: OnceLock<Regex> = OnceLock::new();\n\n let header_re = HEADER_RE.get_or_init(|| {\n Regex::new(\n r\"^\\*\\*Background task\\*\\* `(?P<task_id>[^`]+)` · `(?P<tool_name>[^`]+)` · (?P<status>.+?) · (?P<duration>[0-9]+(?:\\.[0-9]+)?s) · (?P<exit_label>.+)$\",\n )\n .expect(\"valid background task header regex\")\n });\n let full_output_re = FULL_OUTPUT_RE.get_or_init(|| {\n Regex::new(r#\"^_Full output:_ `(?P<command>[^`]+)`$\"#)\n .expect(\"valid background task full output regex\")\n });\n\n let normalized = content.replace(\"\\r\\n\", \"\\n\").replace('\\r', \"\\n\");\n let mut sections = normalized.split(\"\\n\\n\");\n let header = sections.next()?.trim();\n let captures = header_re.captures(header)?;\n\n let mut preview: Option<String> = None;\n let mut full_output_command: Option<String> = None;\n\n for section in sections {\n let trimmed = section.trim();\n if trimmed.is_empty() {\n continue;\n }\n\n if let Some(captures) = full_output_re.captures(trimmed) {\n full_output_command = Some(captures[\"command\"].to_string());\n continue;\n }\n\n if trimmed == \"_No output captured._\" {\n preview = None;\n continue;\n }\n\n if let Some(fenced) = trimmed\n .strip_prefix(\"```text\\n\")\n .and_then(|body| body.strip_suffix(\"\\n```\"))\n {\n preview = Some(fenced.to_string());\n }\n }\n\n Some(ParsedBackgroundTaskNotification {\n task_id: captures[\"task_id\"].to_string(),\n tool_name: captures[\"tool_name\"].to_string(),\n status: captures[\"status\"].to_string(),\n duration: captures[\"duration\"].to_string(),\n exit_label: captures[\"exit_label\"].to_string(),\n preview,\n full_output_command: full_output_command?,\n })\n}\n\npub fn background_task_status_notice(task: &BackgroundTaskCompleted) -> String {\n match task.status {\n BackgroundTaskStatus::Completed => {\n format!(\"Background task completed · {}\", task.tool_name)\n }\n BackgroundTaskStatus::Failed => match task.exit_code {\n Some(code) => format!(\n \"Background task failed · {} · exit {}\",\n task.tool_name, code\n ),\n None => format!(\"Background task failed · {}\", task.tool_name),\n },\n BackgroundTaskStatus::Running => format!(\"Background task running · {}\", task.tool_name),\n }\n}\n\n/// Role in conversation\n#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]\n#[serde(rename_all = \"lowercase\")]\npub enum Role {\n User,\n Assistant,\n}\n\n/// Plain-text tool output placeholder when execution was interrupted.\npub const TOOL_OUTPUT_MISSING_TEXT: &str =\n \"Tool output missing (session interrupted before tool execution completed)\";\n\n/// A message in the conversation\n#[derive(Debug, Clone, Serialize, Deserialize)]\npub struct Message {\n pub role: Role,\n pub content: Vec<ContentBlock>,\n #[serde(default, skip_serializing_if = \"Option::is_none\")]\n pub timestamp: Option<chrono::DateTime<Utc>>,\n #[serde(default, skip_serializing_if = \"Option::is_none\")]\n pub tool_duration_ms: Option<u64>,\n}\n\n/// Cache control metadata for prompt caching\n#[derive(Debug, Clone, Serialize, Deserialize)]\npub struct CacheControl {\n #[serde(rename = \"type\")]\n pub kind: String,\n #[serde(skip_serializing_if = \"Option::is_none\")]\n pub ttl: Option<String>,\n}\n\nimpl CacheControl {\n pub fn ephemeral(ttl: Option<String>) -> Self {\n Self {\n kind: \"ephemeral\".to_string(),\n ttl,\n }\n }\n}\n\n/// Content block within a message\n#[derive(Debug, Clone, Serialize, Deserialize)]\n#[serde(tag = \"type\", rename_all = \"snake_case\")]\npub enum ContentBlock {\n Text {\n text: String,\n #[serde(skip_serializing_if = \"Option::is_none\")]\n cache_control: Option<CacheControl>,\n },\n /// Hidden reasoning content used for providers that require it (not displayed)\n Reasoning {\n text: String,\n },\n ToolUse {\n id: String,\n name: String,\n input: serde_json::Value,\n },\n ToolResult {\n tool_use_id: String,\n content: String,\n #[serde(skip_serializing_if = \"Option::is_none\")]\n is_error: Option<bool>,\n },\n Image {\n media_type: String,\n data: String,\n },\n /// Hidden OpenAI Responses compaction item used to preserve native\n /// compaction state across turns/saves when jcode explicitly triggers it.\n OpenAICompaction {\n encrypted_content: String,\n },\n}\n\nimpl Message {\n pub fn user(text: &str) -> Self {\n Self {\n role: Role::User,\n content: vec![ContentBlock::Text {\n text: text.to_string(),\n cache_control: None,\n }],\n timestamp: Some(Utc::now()),\n tool_duration_ms: None,\n }\n }\n\n pub fn user_with_images(text: &str, images: Vec<(String, String)>) -> Self {\n let mut content: Vec<ContentBlock> = images\n .into_iter()\n .map(|(media_type, data)| ContentBlock::Image { media_type, data })\n .collect();\n content.push(ContentBlock::Text {\n text: text.to_string(),\n cache_control: None,\n });\n Self {\n role: Role::User,\n content,\n timestamp: Some(Utc::now()),\n tool_duration_ms: None,\n }\n }\n\n pub fn assistant_text(text: &str) -> Self {\n Self {\n role: Role::Assistant,\n content: vec![ContentBlock::Text {\n text: text.to_string(),\n cache_control: None,\n }],\n timestamp: Some(Utc::now()),\n tool_duration_ms: None,\n }\n }\n\n pub fn tool_result(tool_use_id: &str, content: &str, is_error: bool) -> Self {\n Self::tool_result_with_duration(tool_use_id, content, is_error, None)\n }\n\n pub fn tool_result_with_duration(\n tool_use_id: &str,\n content: &str,\n is_error: bool,\n tool_duration_ms: Option<u64>,\n ) -> Self {\n Self {\n role: Role::User,\n content: vec![ContentBlock::ToolResult {\n tool_use_id: tool_use_id.to_string(),\n content: content.to_string(),\n is_error: if is_error { Some(true) } else { None },\n }],\n timestamp: Some(Utc::now()),\n tool_duration_ms,\n }\n }\n\n /// Format a timestamp deterministically in UTC for injection into model-visible content.\n pub fn format_timestamp(ts: &chrono::DateTime<Utc>) -> String {\n ts.to_rfc3339_opts(chrono::SecondsFormat::Millis, true)\n }\n\n pub fn format_duration(duration_ms: u64) -> String {\n match duration_ms {\n 0..=999 => format!(\"{}ms\", duration_ms),\n 1_000..=9_999 => format!(\"{:.1}s\", duration_ms as f64 / 1000.0),\n 10_000..=59_999 => format!(\"{}s\", duration_ms / 1000),\n _ => {\n let total_seconds = duration_ms / 1000;\n let minutes = total_seconds / 60;\n let seconds = total_seconds % 60;\n if seconds == 0 {\n format!(\"{}m\", minutes)\n } else {\n format!(\"{}m {}s\", minutes, seconds)\n }\n }\n }\n }\n\n fn should_skip_timestamp_injection(&self) -> bool {\n self.content\n .iter()\n .find_map(|block| match block {\n ContentBlock::Text { text, .. } => Some(text.trim_start()),\n _ => None,\n })\n .is_some_and(|text| text.starts_with(\"<system-reminder>\"))\n }\n\n fn tool_result_tag(&self, ts: &chrono::DateTime<Utc>) -> String {\n match self.tool_duration_ms {\n Some(duration_ms) => {\n let duration_ms_i64 = i64::try_from(duration_ms).unwrap_or(i64::MAX);\n let start_ts = ts\n .checked_sub_signed(chrono::Duration::milliseconds(duration_ms_i64))\n .unwrap_or(*ts);\n format!(\n \"[tool timing: start={} finish={} duration={}]\",\n Self::format_timestamp(&start_ts),\n Self::format_timestamp(ts),\n Self::format_duration(duration_ms)\n )\n }\n None => format!(\"[{}]\", Self::format_timestamp(ts)),\n }\n }\n\n /// Return a copy of messages with timestamps injected into user-role text content.\n /// Tool results get a stable UTC timing header prepended to content.\n /// User text messages get a stable UTC timestamp prepended to the first text block.\n pub fn with_timestamps(messages: &[Message]) -> Vec<Message> {\n messages\n .iter()\n .map(|msg| {\n let Some(ts) = msg.timestamp else {\n return msg.clone();\n };\n if msg.role != Role::User || msg.should_skip_timestamp_injection() {\n return msg.clone();\n }\n let text_tag = format!(\"[{}]\", Self::format_timestamp(&ts));\n let tool_result_tag = msg.tool_result_tag(&ts);\n let mut msg = msg.clone();\n let mut tagged = false;\n for block in &mut msg.content {\n match block {\n ContentBlock::Text { text, .. } if !tagged => {\n *text = format!(\"{} {}\", text_tag, text);\n tagged = true;\n }\n ContentBlock::ToolResult { content, .. } if !tagged => {\n *content = format!(\"{} {}\", tool_result_tag, content);\n tagged = true;\n }\n _ => {}\n }\n }\n msg\n })\n .collect()\n }\n}\n\n/// Sanitize a tool ID so it matches the pattern `^[a-zA-Z0-9_-]+$`.\n///\n/// Different providers generate tool IDs in different formats. When switching\n/// from one provider to another mid-conversation, the historical tool IDs may\n/// contain characters that the new provider rejects (e.g., dots in Copilot IDs\n/// sent to Anthropic). This function replaces any invalid characters with\n/// underscores.\npub fn sanitize_tool_id(id: &str) -> String {\n if id.is_empty() {\n return \"unknown\".to_string();\n }\n let sanitized: String = id\n .chars()\n .map(|c| {\n if c.is_ascii_alphanumeric() || c == '_' || c == '-' {\n c\n } else {\n '_'\n }\n })\n .collect();\n if sanitized.is_empty() {\n \"unknown\".to_string()\n } else {\n sanitized\n }\n}\n\n/// Redact likely secrets from persisted tool output.\n///\n/// This is a best-effort safeguard for local session history files. It targets\n/// high-confidence token/key patterns and common `KEY=VALUE` assignments used by\n/// auth flows.\npub fn redact_secrets(text: &str) -> String {\n // Fast path to avoid regex work for most tool outputs.\n let lower = text.to_ascii_lowercase();\n\n if !text.contains(\"sk-\")\n && !text.contains(\"ghp_\")\n && !text.contains(\"github_pat_\")\n && !text.contains(\"AIza\")\n && !text.contains(\"ya29.\")\n && !text.contains(\"xox\")\n && !lower.contains(\"api_key\")\n && !lower.contains(\"token\")\n {\n return text.to_string();\n }\n\n static DIRECT_PATTERNS: OnceLock<Vec<Regex>> = OnceLock::new();\n static ASSIGNMENT_PATTERNS: OnceLock<Vec<Regex>> = OnceLock::new();\n\n let direct_patterns = DIRECT_PATTERNS.get_or_init(|| {\n vec![\n Regex::new(r\"sk-ant-(?:oat|ort)01-[A-Za-z0-9_-]{20,}\")\n .expect(\"valid Anthropic OAuth token regex\"),\n Regex::new(r\"sk-or-v1-[A-Za-z0-9_-]{20,}\").expect(\"valid OpenRouter key regex\"),\n Regex::new(r\"ghp_[A-Za-z0-9]{20,}\").expect(\"valid GitHub PAT regex\"),\n Regex::new(r\"github_pat_[A-Za-z0-9_]{20,}\")\n .expect(\"valid GitHub fine-grained PAT regex\"),\n Regex::new(r\"ya29\\.[A-Za-z0-9._-]{20,}\").expect(\"valid Google OAuth token regex\"),\n Regex::new(r\"AIza[0-9A-Za-z_-]{20,}\").expect(\"valid Google API key regex\"),\n Regex::new(r\"xox[baprs]-[A-Za-z0-9-]{10,}\").expect(\"valid Slack token regex\"),\n ]\n });\n\n let assignment_patterns = ASSIGNMENT_PATTERNS.get_or_init(|| {\n vec![\n Regex::new(r\"(?m)^\\s*(OPENROUTER_API_KEY\\s*=\\s*)[^\\r\\n]+\")\n .expect(\"valid OPENROUTER_API_KEY assignment regex\"),\n Regex::new(r\"(?m)^\\s*(OPENCODE_API_KEY\\s*=\\s*)[^\\r\\n]+\")\n .expect(\"valid OPENCODE_API_KEY assignment regex\"),\n Regex::new(r\"(?m)^\\s*(OPENCODE_GO_API_KEY\\s*=\\s*)[^\\r\\n]+\")\n .expect(\"valid OPENCODE_GO_API_KEY assignment regex\"),\n Regex::new(r\"(?m)^\\s*(ZHIPU_API_KEY\\s*=\\s*)[^\\r\\n]+\")\n .expect(\"valid ZHIPU_API_KEY assignment regex\"),\n Regex::new(r\"(?m)^\\s*(ZAI_API_KEY\\s*=\\s*)[^\\r\\n]+\")\n .expect(\"valid ZAI_API_KEY assignment regex\"),\n Regex::new(r\"(?m)^\\s*(302AI_API_KEY\\s*=\\s*)[^\\r\\n]+\")\n .expect(\"valid 302AI_API_KEY assignment regex\"),\n Regex::new(r\"(?m)^\\s*(BASETEN_API_KEY\\s*=\\s*)[^\\r\\n]+\")\n .expect(\"valid BASETEN_API_KEY assignment regex\"),\n Regex::new(r\"(?m)^\\s*(CORTECS_API_KEY\\s*=\\s*)[^\\r\\n]+\")\n .expect(\"valid CORTECS_API_KEY assignment regex\"),\n Regex::new(r\"(?m)^\\s*(DEEPSEEK_API_KEY\\s*=\\s*)[^\\r\\n]+\")\n .expect(\"valid DEEPSEEK_API_KEY assignment regex\"),\n Regex::new(r\"(?m)^\\s*(FIRMWARE_API_KEY\\s*=\\s*)[^\\r\\n]+\")\n .expect(\"valid FIRMWARE_API_KEY assignment regex\"),\n Regex::new(r\"(?m)^\\s*(HF_TOKEN\\s*=\\s*)[^\\r\\n]+\")\n .expect(\"valid HF_TOKEN assignment regex\"),\n Regex::new(r\"(?m)^\\s*(MOONSHOT_API_KEY\\s*=\\s*)[^\\r\\n]+\")\n .expect(\"valid MOONSHOT_API_KEY assignment regex\"),\n Regex::new(r\"(?m)^\\s*(NEBIUS_API_KEY\\s*=\\s*)[^\\r\\n]+\")\n .expect(\"valid NEBIUS_API_KEY assignment regex\"),\n Regex::new(r\"(?m)^\\s*(SCALEWAY_API_KEY\\s*=\\s*)[^\\r\\n]+\")\n .expect(\"valid SCALEWAY_API_KEY assignment regex\"),\n Regex::new(r\"(?m)^\\s*(STACKIT_API_KEY\\s*=\\s*)[^\\r\\n]+\")\n .expect(\"valid STACKIT_API_KEY assignment regex\"),\n Regex::new(r\"(?m)^\\s*(GROQ_API_KEY\\s*=\\s*)[^\\r\\n]+\")\n .expect(\"valid GROQ_API_KEY assignment regex\"),\n Regex::new(r\"(?m)^\\s*(MISTRAL_API_KEY\\s*=\\s*)[^\\r\\n]+\")\n .expect(\"valid MISTRAL_API_KEY assignment regex\"),\n Regex::new(r\"(?m)^\\s*(PERPLEXITY_API_KEY\\s*=\\s*)[^\\r\\n]+\")\n .expect(\"valid PERPLEXITY_API_KEY assignment regex\"),\n Regex::new(r\"(?m)^\\s*(TOGETHER_API_KEY\\s*=\\s*)[^\\r\\n]+\")\n .expect(\"valid TOGETHER_API_KEY assignment regex\"),\n Regex::new(r\"(?m)^\\s*(DEEPINFRA_API_KEY\\s*=\\s*)[^\\r\\n]+\")\n .expect(\"valid DEEPINFRA_API_KEY assignment regex\"),\n Regex::new(r\"(?m)^\\s*(XAI_API_KEY\\s*=\\s*)[^\\r\\n]+\")\n .expect(\"valid XAI_API_KEY assignment regex\"),\n Regex::new(r\"(?m)^\\s*(LMSTUDIO_API_KEY\\s*=\\s*)[^\\r\\n]+\")\n .expect(\"valid LMSTUDIO_API_KEY assignment regex\"),\n Regex::new(r\"(?m)^\\s*(OLLAMA_API_KEY\\s*=\\s*)[^\\r\\n]+\")\n .expect(\"valid OLLAMA_API_KEY assignment regex\"),\n Regex::new(r\"(?m)^\\s*(CHUTES_API_KEY\\s*=\\s*)[^\\r\\n]+\")\n .expect(\"valid CHUTES_API_KEY assignment regex\"),\n Regex::new(r\"(?m)^\\s*(CEREBRAS_API_KEY\\s*=\\s*)[^\\r\\n]+\")\n .expect(\"valid CEREBRAS_API_KEY assignment regex\"),\n Regex::new(r\"(?m)^\\s*(OPENAI_COMPAT_API_KEY\\s*=\\s*)[^\\r\\n]+\")\n .expect(\"valid OPENAI_COMPAT_API_KEY assignment regex\"),\n Regex::new(r\"(?m)^\\s*(ANTHROPIC_API_KEY\\s*=\\s*)[^\\r\\n]+\")\n .expect(\"valid ANTHROPIC_API_KEY assignment regex\"),\n Regex::new(r\"(?m)^\\s*(OPENAI_API_KEY\\s*=\\s*)[^\\r\\n]+\")\n .expect(\"valid OPENAI_API_KEY assignment regex\"),\n Regex::new(r\"(?m)^\\s*(AZURE_OPENAI_API_KEY\\s*=\\s*)[^\\r\\n]+\")\n .expect(\"valid AZURE_OPENAI_API_KEY assignment regex\"),\n Regex::new(r\"(?m)^\\s*(CURSOR_API_KEY\\s*=\\s*)[^\\r\\n]+\")\n .expect(\"valid CURSOR_API_KEY assignment regex\"),\n Regex::new(r\"(?m)^\\s*(GITHUB_TOKEN\\s*=\\s*)[^\\r\\n]+\")\n .expect(\"valid GITHUB_TOKEN assignment regex\"),\n ]\n });\n\n let mut redacted = text.to_string();\n let mut redacted_keys: HashSet<String> = [\n \"OPENROUTER_API_KEY\",\n \"OPENCODE_API_KEY\",\n \"OPENCODE_GO_API_KEY\",\n \"ZHIPU_API_KEY\",\n \"ZAI_API_KEY\",\n \"302AI_API_KEY\",\n \"BASETEN_API_KEY\",\n \"CORTECS_API_KEY\",\n \"DEEPSEEK_API_KEY\",\n \"FIRMWARE_API_KEY\",\n \"HF_TOKEN\",\n \"MOONSHOT_API_KEY\",\n \"NEBIUS_API_KEY\",\n \"SCALEWAY_API_KEY\",\n \"STACKIT_API_KEY\",\n \"GROQ_API_KEY\",\n \"MISTRAL_API_KEY\",\n \"PERPLEXITY_API_KEY\",\n \"TOGETHER_API_KEY\",\n \"DEEPINFRA_API_KEY\",\n \"XAI_API_KEY\",\n \"LMSTUDIO_API_KEY\",\n \"OLLAMA_API_KEY\",\n \"CHUTES_API_KEY\",\n \"CEREBRAS_API_KEY\",\n \"OPENAI_COMPAT_API_KEY\",\n \"ANTHROPIC_API_KEY\",\n \"OPENAI_API_KEY\",\n \"AZURE_OPENAI_API_KEY\",\n \"CURSOR_API_KEY\",\n \"GITHUB_TOKEN\",\n ]\n .iter()\n .map(|k| (*k).to_string())\n .collect();\n\n for re in direct_patterns {\n redacted = re.replace_all(&redacted, \"[REDACTED_SECRET]\").into_owned();\n }\n\n for re in assignment_patterns {\n redacted = re\n .replace_all(&redacted, \"${1}[REDACTED_SECRET]\")\n .into_owned();\n }\n\n // Also redact custom API key variable names configured at runtime.\n for source in [\n \"JCODE_OPENROUTER_API_KEY_NAME\",\n \"JCODE_OPENAI_COMPAT_API_KEY_NAME\",\n ] {\n let Some(key_name) = std::env::var(source)\n .ok()\n .map(|v| v.trim().to_string())\n .filter(|v| !v.is_empty())\n else {\n continue;\n };\n\n if !key_name\n .chars()\n .all(|c| c.is_ascii_uppercase() || c.is_ascii_digit() || c == '_')\n {\n continue;\n }\n if !redacted_keys.insert(key_name.clone()) {\n continue;\n }\n\n let pattern = format!(r\"(?m)^\\s*({}\\s*=\\s*)[^\\r\\n]+\", regex::escape(&key_name));\n if let Ok(re) = Regex::new(&pattern) {\n redacted = re\n .replace_all(&redacted, \"${1}[REDACTED_SECRET]\")\n .into_owned();\n }\n }\n\n redacted\n}\n\n/// Tool definition for the API\n#[derive(Debug, Clone, Serialize)]\npub struct ToolDefinition {\n pub name: String,\n pub description: String,\n pub input_schema: serde_json::Value,\n}\n\n/// A tool call from the model\n#[derive(Debug, Clone, Default, PartialEq, Eq, serde::Serialize, serde::Deserialize)]\npub struct ToolCall {\n #[serde(default)]\n pub id: String,\n #[serde(default)]\n pub name: String,\n #[serde(default)]\n pub input: serde_json::Value,\n #[serde(default, skip_serializing_if = \"Option::is_none\")]\n pub intent: Option<String>,\n}\n\n/// Connection phase for status bar transparency\n#[derive(Debug, Clone, PartialEq)]\npub enum ConnectionPhase {\n /// Refreshing OAuth token\n Authenticating,\n /// TCP + TLS connection to API\n Connecting,\n /// HTTP request sent, waiting for first response byte\n WaitingForResponse,\n /// First byte received, stream is active\n Streaming,\n /// Retrying after a transient error\n Retrying { attempt: u32, max: u32 },\n}\n\nimpl std::fmt::Display for ConnectionPhase {\n fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {\n match self {\n ConnectionPhase::Authenticating => write!(f, \"authenticating\"),\n ConnectionPhase::Connecting => write!(f, \"connecting\"),\n ConnectionPhase::WaitingForResponse => write!(f, \"waiting for response\"),\n ConnectionPhase::Streaming => write!(f, \"streaming\"),\n ConnectionPhase::Retrying { attempt, max } => {\n write!(f, \"retrying ({}/{})\", attempt, max)\n }\n }\n }\n}\n\n/// Streaming event from provider\n#[derive(Debug, Clone)]\npub enum StreamEvent {\n /// Text content delta\n TextDelta(String),\n /// Tool use started\n ToolUseStart { id: String, name: String },\n /// Tool input delta (JSON fragment)\n ToolInputDelta(String),\n /// Tool use complete\n ToolUseEnd,\n /// Tool result from provider (provider already executed the tool)\n ToolResult {\n tool_use_id: String,\n content: String,\n is_error: bool,\n },\n /// Extended thinking started\n ThinkingStart,\n /// Extended thinking delta (reasoning content)\n ThinkingDelta(String),\n /// Extended thinking ended\n ThinkingEnd,\n /// Extended thinking completed with duration\n ThinkingDone { duration_secs: f64 },\n /// Message complete (may have stop reason)\n MessageEnd { stop_reason: Option<String> },\n /// Token usage update\n TokenUsage {\n input_tokens: Option<u64>,\n output_tokens: Option<u64>,\n cache_read_input_tokens: Option<u64>,\n cache_creation_input_tokens: Option<u64>,\n },\n /// Active transport/connection type for this stream\n ConnectionType { connection: String },\n /// Connection phase update (for status bar transparency)\n ConnectionPhase { phase: ConnectionPhase },\n /// Error occurred\n Error {\n message: String,\n /// Seconds until rate limit resets (if this is a rate limit error)\n retry_after_secs: Option<u64>,\n },\n /// Provider session ID (for conversation resume)\n SessionId(String),\n /// Compaction occurred (context was summarized)\n Compaction {\n trigger: String,\n pre_tokens: Option<u64>,\n /// Provider-native compaction artifact, if one was emitted.\n openai_encrypted_content: Option<String>,\n },\n /// Upstream provider info (e.g., which provider OpenRouter routed to)\n UpstreamProvider { provider: String },\n /// Native tool call from a provider bridge that needs execution by jcode\n NativeToolCall {\n request_id: String,\n tool_name: String,\n input: serde_json::Value,\n },\n}\n\n#[cfg(test)]\nmod tests {\n use super::*;\n\n #[test]\n fn sanitize_tool_id_alphanumeric_passthrough() {\n assert_eq!(\n sanitize_tool_id(\"toolu_01XFDUDYJgAACzvnptvVer6u\"),\n \"toolu_01XFDUDYJgAACzvnptvVer6u\"\n );\n assert_eq!(sanitize_tool_id(\"call_abc123\"), \"call_abc123\");\n assert_eq!(\n sanitize_tool_id(\"call_1234567890_9876543210\"),\n \"call_1234567890_9876543210\"\n );\n }\n\n #[test]\n fn sanitize_tool_id_hyphens_passthrough() {\n assert_eq!(sanitize_tool_id(\"call-abc-123\"), \"call-abc-123\");\n assert_eq!(\n sanitize_tool_id(\"tool_use-id_with-mixed\"),\n \"tool_use-id_with-mixed\"\n );\n }\n\n #[test]\n fn sanitize_tool_id_replaces_dots() {\n assert_eq!(\n sanitize_tool_id(\"chatcmpl-abc.def.ghi\"),\n \"chatcmpl-abc_def_ghi\"\n );\n assert_eq!(sanitize_tool_id(\"call.123\"), \"call_123\");\n }\n\n #[test]\n fn sanitize_tool_id_replaces_colons() {\n assert_eq!(sanitize_tool_id(\"call:123:456\"), \"call_123_456\");\n }\n\n #[test]\n fn sanitize_tool_id_replaces_special_chars() {\n assert_eq!(\n sanitize_tool_id(\"id@with#special$chars\"),\n \"id_with_special_chars\"\n );\n assert_eq!(sanitize_tool_id(\"id with spaces\"), \"id_with_spaces\");\n }\n\n #[test]\n fn sanitize_tool_id_empty_returns_unknown() {\n assert_eq!(sanitize_tool_id(\"\"), \"unknown\");\n }\n\n #[test]\n fn sanitize_tool_id_copilot_to_anthropic() {\n assert_eq!(\n sanitize_tool_id(\"chatcmpl-BF2xX.tool_call.0\"),\n \"chatcmpl-BF2xX_tool_call_0\"\n );\n }\n\n #[test]\n fn sanitize_tool_id_already_valid_unchanged() {\n let valid_ids = [\n \"toolu_01XFDUDYJgAACzvnptvVer6u\",\n \"call_abc123\",\n \"fallback_text_call_call_1234567890_9876543210\",\n \"tool_123\",\n \"a\",\n \"A\",\n \"0\",\n \"_\",\n \"-\",\n \"a-b_c\",\n ];\n for id in valid_ids {\n assert_eq!(sanitize_tool_id(id), id, \"ID '{}' should be unchanged\", id);\n }\n }\n\n #[test]\n fn redact_secrets_redacts_known_direct_token_formats() {\n let input = \"access=sk-ant-oat01-ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789\\nopenrouter=sk-or-v1-abcdefghijklmnopqrstuvwxyz0123456789\\ngithub=ghp_ABCDEFGHIJKLMNOPQRSTUVWXYZ0123\\n\";\n let out = redact_secrets(input);\n assert!(!out.contains(\"sk-ant-oat01-\"));\n assert!(!out.contains(\"sk-or-v1-\"));\n assert!(!out.contains(\"ghp_ABCDEFGHIJKLMNOPQRSTUVWXYZ0123\"));\n assert!(out.matches(\"[REDACTED_SECRET]\").count() >= 3);\n }\n\n #[test]\n fn redact_secrets_redacts_env_style_assignments() {\n let input = \"OPENROUTER_API_KEY=sk-or-v1-abc123abc123abc123abc123\\nOPENCODE_API_KEY=oc_test_secret\\nOPENCODE_GO_API_KEY=ocgo_test_secret\\nZAI_API_KEY=zai_secret\\nCHUTES_API_KEY=chutes_secret\\nCEREBRAS_API_KEY=cerebras_secret\\nOPENAI_COMPAT_API_KEY=compat_secret\\nCURSOR_API_KEY='my_cursor_secret_value'\\nOPENAI_API_KEY=sk-test-openai-example\\nAZURE_OPENAI_API_KEY=azure-openai-secret\\n\";\n let out = redact_secrets(input);\n assert!(out.contains(\"OPENROUTER_API_KEY=[REDACTED_SECRET]\"));\n assert!(out.contains(\"OPENCODE_API_KEY=[REDACTED_SECRET]\"));\n assert!(out.contains(\"OPENCODE_GO_API_KEY=[REDACTED_SECRET]\"));\n assert!(out.contains(\"ZAI_API_KEY=[REDACTED_SECRET]\"));\n assert!(out.contains(\"CHUTES_API_KEY=[REDACTED_SECRET]\"));\n assert!(out.contains(\"CEREBRAS_API_KEY=[REDACTED_SECRET]\"));\n assert!(out.contains(\"OPENAI_COMPAT_API_KEY=[REDACTED_SECRET]\"));\n assert!(out.contains(\"CURSOR_API_KEY=[REDACTED_SECRET]\"));\n assert!(out.contains(\"OPENAI_API_KEY=[REDACTED_SECRET]\"));\n assert!(out.contains(\"AZURE_OPENAI_API_KEY=[REDACTED_SECRET]\"));\n assert!(!out.contains(\"my_cursor_secret_value\"));\n }\n\n #[test]\n fn redact_secrets_redacts_runtime_key_assignment() {\n let key_var = \"JCODE_OPENAI_COMPAT_API_KEY_NAME\";\n let prev = std::env::var(key_var).ok();\n crate::env::set_var(key_var, \"GROQ_API_KEY\");\n\n let input = \"GROQ_API_KEY=my_secret_token_value\";\n let out = redact_secrets(input);\n assert_eq!(out, \"GROQ_API_KEY=[REDACTED_SECRET]\");\n\n if let Some(v) = prev {\n crate::env::set_var(key_var, v);\n } else {\n crate::env::remove_var(key_var);\n }\n }\n\n #[test]\n fn redact_secrets_redacts_mixed_case_token_assignments() {\n let input = \"my_token=ya29.ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789\";\n let out = redact_secrets(input);\n assert!(out.contains(\"[REDACTED_SECRET]\"));\n assert!(!out.contains(\"ya29.ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789\"));\n }\n\n #[test]\n fn redact_secrets_leaves_normal_output_unchanged() {\n let input = \"Found 5 files\\nNo auth errors\\nDone.\";\n assert_eq!(redact_secrets(input), input);\n }\n\n #[test]\n fn format_timestamp_is_stable_utc_rfc3339() {\n let ts = chrono::DateTime::parse_from_rfc3339(\"2025-03-15T02:24:13.250Z\")\n .unwrap()\n .with_timezone(&Utc);\n assert_eq!(Message::format_timestamp(&ts), \"2025-03-15T02:24:13.250Z\");\n }\n\n #[test]\n fn with_timestamps_prepends_utc_prefix_to_user_text() {\n let ts = chrono::DateTime::parse_from_rfc3339(\"2025-03-15T02:24:03Z\")\n .unwrap()\n .with_timezone(&Utc);\n let stamped = Message::with_timestamps(&[Message {\n role: Role::User,\n content: vec![ContentBlock::Text {\n text: \"hello\".to_string(),\n cache_control: None,\n }],\n timestamp: Some(ts),\n tool_duration_ms: None,\n }]);\n match &stamped[0].content[0] {\n ContentBlock::Text { text, .. } => {\n assert_eq!(text, \"[2025-03-15T02:24:03.000Z] hello\");\n }\n other => panic!(\"expected text block, got {other:?}\"),\n }\n }\n\n #[test]\n fn with_timestamps_adds_tool_timing_header_with_duration() {\n let ts = chrono::DateTime::parse_from_rfc3339(\"2025-03-15T02:24:13Z\")\n .unwrap()\n .with_timezone(&Utc);\n let stamped = Message::with_timestamps(&[Message {\n role: Role::User,\n content: vec![ContentBlock::ToolResult {\n tool_use_id: \"call_1\".to_string(),\n content: \"ok\".to_string(),\n is_error: None,\n }],\n timestamp: Some(ts),\n tool_duration_ms: Some(3_200),\n }]);\n match &stamped[0].content[0] {\n ContentBlock::ToolResult { content, .. } => {\n assert_eq!(\n content,\n \"[tool timing: start=2025-03-15T02:24:09.800Z finish=2025-03-15T02:24:13.000Z duration=3.2s] ok\"\n );\n }\n other => panic!(\"expected tool result block, got {other:?}\"),\n }\n }\n\n #[test]\n fn with_timestamps_skips_internal_system_reminders() {\n let ts = chrono::DateTime::parse_from_rfc3339(\"2025-03-15T02:24:13Z\")\n .unwrap()\n .with_timezone(&Utc);\n let original = Message {\n role: Role::User,\n content: vec![ContentBlock::Text {\n text: \"<system-reminder>\\ninternal\\n</system-reminder>\".to_string(),\n cache_control: None,\n }],\n timestamp: Some(ts),\n tool_duration_ms: None,\n };\n let stamped = Message::with_timestamps(&[original.clone()]);\n match &stamped[0].content[0] {\n ContentBlock::Text { text, .. } => {\n assert_eq!(text, \"<system-reminder>\\ninternal\\n</system-reminder>\");\n }\n other => panic!(\"expected text block, got {other:?}\"),\n }\n }\n\n #[test]\n fn format_background_task_notification_markdown_uses_code_block_preview() {\n let rendered = format_background_task_notification_markdown(&BackgroundTaskCompleted {\n task_id: \"abc123\".to_string(),\n tool_name: \"bash\".to_string(),\n session_id: \"session\".to_string(),\n status: BackgroundTaskStatus::Completed,\n exit_code: Some(0),\n output_preview: \"[stderr] first line\\n[stdout] second line\\n\".to_string(),\n output_file: std::path::PathBuf::from(\"/tmp/output.log\"),\n duration_secs: 7.1,\n notify: true,\n });\n\n assert!(\n rendered\n .contains(\"**Background task** `abc123` · `bash` · ✓ completed · 7.1s · exit 0\")\n );\n assert!(rendered.contains(\"```text\\n[stderr] first line\\n[stdout] second line\\n```\"));\n assert!(rendered.contains(\"_Full output:_ `bg action=\\\"output\\\" task_id=\\\"abc123\\\"`\"));\n }\n\n #[test]\n fn format_background_task_notification_markdown_handles_empty_preview() {\n let rendered = format_background_task_notification_markdown(&BackgroundTaskCompleted {\n task_id: \"abc123\".to_string(),\n tool_name: \"bash\".to_string(),\n session_id: \"session\".to_string(),\n status: BackgroundTaskStatus::Failed,\n exit_code: Some(9),\n output_preview: \"\\n\\n\".to_string(),\n output_file: std::path::PathBuf::from(\"/tmp/output.log\"),\n duration_secs: 1.0,\n notify: true,\n });\n\n assert!(rendered.contains(\"✗ failed\"));\n assert!(rendered.contains(\"_No output captured._\"));\n }\n\n #[test]\n fn parse_background_task_notification_markdown_extracts_fields() {\n let rendered = format_background_task_notification_markdown(&BackgroundTaskCompleted {\n task_id: \"abc123\".to_string(),\n tool_name: \"bash\".to_string(),\n session_id: \"session\".to_string(),\n status: BackgroundTaskStatus::Completed,\n exit_code: Some(0),\n output_preview: \"[stderr] first line\\n[stdout] second line\\n\".to_string(),\n output_file: std::path::PathBuf::from(\"/tmp/output.log\"),\n duration_secs: 7.1,\n notify: true,\n });\n\n let parsed = parse_background_task_notification_markdown(&rendered)\n .expect(\"background task notification should parse\");\n assert_eq!(parsed.task_id, \"abc123\");\n assert_eq!(parsed.tool_name, \"bash\");\n assert_eq!(parsed.status, \"✓ completed\");\n assert_eq!(parsed.duration, \"7.1s\");\n assert_eq!(parsed.exit_label, \"exit 0\");\n assert_eq!(\n parsed.preview.as_deref(),\n Some(\"[stderr] first line\\n[stdout] second line\")\n );\n assert_eq!(\n parsed.full_output_command,\n \"bg action=\\\"output\\\" task_id=\\\"abc123\\\"\"\n );\n }\n}\n"}, "110c9631f05ea5ab0c767467962338c87bf3bc53": {"t": 1785757252.5497403, "n": 33866, "h": "ab5c6d19322edf61", "c": "#![allow(dead_code)]\n\nuse crate::bus::{BackgroundTaskCompleted, BackgroundTaskStatus};\nuse chrono::Utc;\nuse regex::Regex;\nuse serde::{Deserialize, Serialize};\nuse std::collections::HashSet;\nuse std::sync::OnceLock;\n\n#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]\npub struct InputShellResult {\n pub command: String,\n #[serde(default, skip_serializing_if = \"Option::is_none\")]\n pub cwd: Option<String>,\n pub output: String,\n #[serde(default, skip_serializing_if = \"Option::is_none\")]\n pub exit_code: Option<i32>,\n pub duration_ms: u64,\n #[serde(default)]\n pub truncated: bool,\n #[serde(default)]\n pub failed_to_start: bool,\n}\n\nfn sanitize_fenced_block(text: &str) -> String {\n text.replace(\"```\", \"``\\u{200b}`\")\n}\n\npub fn format_input_shell_result_markdown(shell: &InputShellResult) -> String {\n let status = if shell.failed_to_start {\n \"✗ failed to start\".to_string()\n } else if shell.exit_code == Some(0) {\n \"✓ exit 0\".to_string()\n } else if let Some(code) = shell.exit_code {\n format!(\"✗ exit {}\", code)\n } else {\n \"✗ terminated\".to_string()\n };\n\n let mut meta = vec![status, Message::format_duration(shell.duration_ms)];\n if let Some(cwd) = shell.cwd.as_deref() {\n meta.push(format!(\"cwd `{}`\", cwd));\n }\n if shell.truncated {\n meta.push(\"truncated\".to_string());\n }\n\n let mut message = format!(\n \"**Shell command** · {}\\n\\n```bash\\n{}\\n```\",\n meta.join(\" · \"),\n sanitize_fenced_block(&shell.command)\n );\n\n if shell.output.trim().is_empty() {\n message.push_str(\"\\n\\n_No output._\");\n } else {\n message.push_str(&format!(\n \"\\n\\n```text\\n{}\\n```\",\n sanitize_fenced_block(shell.output.trim_end())\n ));\n }\n\n message\n}\n\npub fn input_shell_status_notice(shell: &InputShellResult) -> String {\n if shell.failed_to_start {\n \"Shell command failed to start\".to_string()\n } else if shell.exit_code == Some(0) {\n \"Shell command completed\".to_string()\n } else if let Some(code) = shell.exit_code {\n format!(\"Shell command failed (exit {})\", code)\n } else {\n \"Shell command terminated\".to_string()\n }\n}\n\nfn format_background_task_status(status: &BackgroundTaskStatus) -> &'static str {\n match status {\n BackgroundTaskStatus::Completed => \"✓ completed\",\n BackgroundTaskStatus::Failed => \"✗ failed\",\n BackgroundTaskStatus::Running => \"running\",\n }\n}\n\nfn normalize_background_task_preview(preview: &str) -> Option<String> {\n let normalized = preview.replace(\"\\r\\n\", \"\\n\").replace('\\r', \"\\n\");\n let trimmed = normalized.trim_end();\n if trimmed.trim().is_empty() {\n None\n } else {\n Some(sanitize_fenced_block(trimmed))\n }\n}\n\npub fn format_background_task_notification_markdown(task: &BackgroundTaskCompleted) -> String {\n let exit_code = task\n .exit_code\n .map(|code| format!(\"exit {}\", code))\n .unwrap_or_else(|| \"exit n/a\".to_string());\n\n let mut message = format!(\n \"**Background task** `{}` · `{}` · {} · {:.1}s · {}\",\n task.task_id,\n task.tool_name,\n format_background_task_status(&task.status),\n task.duration_secs,\n exit_code,\n );\n\n if let Some(preview) = normalize_background_task_preview(&task.output_preview) {\n message.push_str(&format!(\"\\n\\n```text\\n{}\\n```\", preview));\n } else {\n message.push_str(\"\\n\\n_No output captured._\");\n }\n\n message.push_str(&format!(\n \"\\n\\n_Full output:_ `bg action=\\\"output\\\" task_id=\\\"{}\\\"`\",\n task.task_id\n ));\n\n message\n}\n\npub fn background_task_status_notice(task: &BackgroundTaskCompleted) -> String {\n match task.status {\n BackgroundTaskStatus::Completed => {\n format!(\"Background task completed · {}\", task.tool_name)\n }\n BackgroundTaskStatus::Failed => match task.exit_code {\n Some(code) => format!(\n \"Background task failed · {} · exit {}\",\n task.tool_name, code\n ),\n None => format!(\"Background task failed · {}\", task.tool_name),\n },\n BackgroundTaskStatus::Running => format!(\"Background task running · {}\", task.tool_name),\n }\n}\n\n/// Role in conversation\n#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]\n#[serde(rename_all = \"lowercase\")]\npub enum Role {\n User,\n Assistant,\n}\n\n/// Plain-text tool output placeholder when execution was interrupted.\npub const TOOL_OUTPUT_MISSING_TEXT: &str =\n \"Tool output missing (session interrupted before tool execution completed)\";\n\n/// A message in the conversation\n#[derive(Debug, Clone, Serialize, Deserialize)]\npub struct Message {\n pub role: Role,\n pub content: Vec<ContentBlock>,\n #[serde(default, skip_serializing_if = \"Option::is_none\")]\n pub timestamp: Option<chrono::DateTime<Utc>>,\n #[serde(default, skip_serializing_if = \"Option::is_none\")]\n pub tool_duration_ms: Option<u64>,\n}\n\n/// Cache control metadata for prompt caching\n#[derive(Debug, Clone, Serialize, Deserialize)]\npub struct CacheControl {\n #[serde(rename = \"type\")]\n pub kind: String,\n #[serde(skip_serializing_if = \"Option::is_none\")]\n pub ttl: Option<String>,\n}\n\nimpl CacheControl {\n pub fn ephemeral(ttl: Option<String>) -> Self {\n Self {\n kind: \"ephemeral\".to_string(),\n ttl,\n }\n }\n}\n\n/// Content block within a message\n#[derive(Debug, Clone, Serialize, Deserialize)]\n#[serde(tag = \"type\", rename_all = \"snake_case\")]\npub enum ContentBlock {\n Text {\n text: String,\n #[serde(skip_serializing_if = \"Option::is_none\")]\n cache_control: Option<CacheControl>,\n },\n /// Hidden reasoning content used for providers that require it (not displayed)\n Reasoning {\n text: String,\n },\n ToolUse {\n id: String,\n name: String,\n input: serde_json::Value,\n },\n ToolResult {\n tool_use_id: String,\n content: String,\n #[serde(skip_serializing_if = \"Option::is_none\")]\n is_error: Option<bool>,\n },\n Image {\n media_type: String,\n data: String,\n },\n /// Hidden OpenAI Responses compaction item used to preserve native\n /// compaction state across turns/saves when jcode explicitly triggers it.\n OpenAICompaction {\n encrypted_content: String,\n },\n}\n\nimpl Message {\n pub fn user(text: &str) -> Self {\n Self {\n role: Role::User,\n content: vec![ContentBlock::Text {\n text: text.to_string(),\n cache_control: None,\n }],\n timestamp: Some(Utc::now()),\n tool_duration_ms: None,\n }\n }\n\n pub fn user_with_images(text: &str, images: Vec<(String, String)>) -> Self {\n let mut content: Vec<ContentBlock> = images\n .into_iter()\n .map(|(media_type, data)| ContentBlock::Image { media_type, data })\n .collect();\n content.push(ContentBlock::Text {\n text: text.to_string(),\n cache_control: None,\n });\n Self {\n role: Role::User,\n content,\n timestamp: Some(Utc::now()),\n tool_duration_ms: None,\n }\n }\n\n pub fn assistant_text(text: &str) -> Self {\n Self {\n role: Role::Assistant,\n content: vec![ContentBlock::Text {\n text: text.to_string(),\n cache_control: None,\n }],\n timestamp: Some(Utc::now()),\n tool_duration_ms: None,\n }\n }\n\n pub fn tool_result(tool_use_id: &str, content: &str, is_error: bool) -> Self {\n Self::tool_result_with_duration(tool_use_id, content, is_error, None)\n }\n\n pub fn tool_result_with_duration(\n tool_use_id: &str,\n content: &str,\n is_error: bool,\n tool_duration_ms: Option<u64>,\n ) -> Self {\n Self {\n role: Role::User,\n content: vec![ContentBlock::ToolResult {\n tool_use_id: tool_use_id.to_string(),\n content: content.to_string(),\n is_error: if is_error { Some(true) } else { None },\n }],\n timestamp: Some(Utc::now()),\n tool_duration_ms,\n }\n }\n\n /// Format a timestamp deterministically in UTC for injection into model-visible content.\n pub fn format_timestamp(ts: &chrono::DateTime<Utc>) -> String {\n ts.to_rfc3339_opts(chrono::SecondsFormat::Millis, true)\n }\n\n pub fn format_duration(duration_ms: u64) -> String {\n match duration_ms {\n 0..=999 => format!(\"{}ms\", duration_ms),\n 1_000..=9_999 => format!(\"{:.1}s\", duration_ms as f64 / 1000.0),\n 10_000..=59_999 => format!(\"{}s\", duration_ms / 1000),\n _ => {\n let total_seconds = duration_ms / 1000;\n let minutes = total_seconds / 60;\n let seconds = total_seconds % 60;\n if seconds == 0 {\n format!(\"{}m\", minutes)\n } else {\n format!(\"{}m {}s\", minutes, seconds)\n }\n }\n }\n }\n\n fn should_skip_timestamp_injection(&self) -> bool {\n self.content\n .iter()\n .find_map(|block| match block {\n ContentBlock::Text { text, .. } => Some(text.trim_start()),\n _ => None,\n })\n .is_some_and(|text| text.starts_with(\"<system-reminder>\"))\n }\n\n fn tool_result_tag(&self, ts: &chrono::DateTime<Utc>) -> String {\n match self.tool_duration_ms {\n Some(duration_ms) => {\n let duration_ms_i64 = i64::try_from(duration_ms).unwrap_or(i64::MAX);\n let start_ts = ts\n .checked_sub_signed(chrono::Duration::milliseconds(duration_ms_i64))\n .unwrap_or(*ts);\n format!(\n \"[tool timing: start={} finish={} duration={}]\",\n Self::format_timestamp(&start_ts),\n Self::format_timestamp(ts),\n Self::format_duration(duration_ms)\n )\n }\n None => format!(\"[{}]\", Self::format_timestamp(ts)),\n }\n }\n\n /// Return a copy of messages with timestamps injected into user-role text content.\n /// Tool results get a stable UTC timing header prepended to content.\n /// User text messages get a stable UTC timestamp prepended to the first text block.\n pub fn with_timestamps(messages: &[Message]) -> Vec<Message> {\n messages\n .iter()\n .map(|msg| {\n let Some(ts) = msg.timestamp else {\n return msg.clone();\n };\n if msg.role != Role::User || msg.should_skip_timestamp_injection() {\n return msg.clone();\n }\n let text_tag = format!(\"[{}]\", Self::format_timestamp(&ts));\n let tool_result_tag = msg.tool_result_tag(&ts);\n let mut msg = msg.clone();\n let mut tagged = false;\n for block in &mut msg.content {\n match block {\n ContentBlock::Text { text, .. } if !tagged => {\n *text = format!(\"{} {}\", text_tag, text);\n tagged = true;\n }\n ContentBlock::ToolResult { content, .. } if !tagged => {\n *content = format!(\"{} {}\", tool_result_tag, content);\n tagged = true;\n }\n _ => {}\n }\n }\n msg\n })\n .collect()\n }\n}\n\n/// Sanitize a tool ID so it matches the pattern `^[a-zA-Z0-9_-]+$`.\n///\n/// Different providers generate tool IDs in different formats. When switching\n/// from one provider to another mid-conversation, the historical tool IDs may\n/// contain characters that the new provider rejects (e.g., dots in Copilot IDs\n/// sent to Anthropic). This function replaces any invalid characters with\n/// underscores.\npub fn sanitize_tool_id(id: &str) -> String {\n if id.is_empty() {\n return \"unknown\".to_string();\n }\n let sanitized: String = id\n .chars()\n .map(|c| {\n if c.is_ascii_alphanumeric() || c == '_' || c == '-' {\n c\n } else {\n '_'\n }\n })\n .collect();\n if sanitized.is_empty() {\n \"unknown\".to_string()\n } else {\n sanitized\n }\n}\n\n/// Redact likely secrets from persisted tool output.\n///\n/// This is a best-effort safeguard for local session history files. It targets\n/// high-confidence token/key patterns and common `KEY=VALUE` assignments used by\n/// auth flows.\npub fn redact_secrets(text: &str) -> String {\n // Fast path to avoid regex work for most tool outputs.\n let lower = text.to_ascii_lowercase();\n\n if !text.contains(\"sk-\")\n && !text.contains(\"ghp_\")\n && !text.contains(\"github_pat_\")\n && !text.contains(\"AIza\")\n && !text.contains(\"ya29.\")\n && !text.contains(\"xox\")\n && !lower.contains(\"api_key\")\n && !lower.contains(\"token\")\n {\n return text.to_string();\n }\n\n static DIRECT_PATTERNS: OnceLock<Vec<Regex>> = OnceLock::new();\n static ASSIGNMENT_PATTERNS: OnceLock<Vec<Regex>> = OnceLock::new();\n\n let direct_patterns = DIRECT_PATTERNS.get_or_init(|| {\n vec![\n Regex::new(r\"sk-ant-(?:oat|ort)01-[A-Za-z0-9_-]{20,}\")\n .expect(\"valid Anthropic OAuth token regex\"),\n Regex::new(r\"sk-or-v1-[A-Za-z0-9_-]{20,}\").expect(\"valid OpenRouter key regex\"),\n Regex::new(r\"ghp_[A-Za-z0-9]{20,}\").expect(\"valid GitHub PAT regex\"),\n Regex::new(r\"github_pat_[A-Za-z0-9_]{20,}\")\n .expect(\"valid GitHub fine-grained PAT regex\"),\n Regex::new(r\"ya29\\.[A-Za-z0-9._-]{20,}\").expect(\"valid Google OAuth token regex\"),\n Regex::new(r\"AIza[0-9A-Za-z_-]{20,}\").expect(\"valid Google API key regex\"),\n Regex::new(r\"xox[baprs]-[A-Za-z0-9-]{10,}\").expect(\"valid Slack token regex\"),\n ]\n });\n\n let assignment_patterns = ASSIGNMENT_PATTERNS.get_or_init(|| {\n vec![\n Regex::new(r\"(?m)^\\s*(OPENROUTER_API_KEY\\s*=\\s*)[^\\r\\n]+\")\n .expect(\"valid OPENROUTER_API_KEY assignment regex\"),\n Regex::new(r\"(?m)^\\s*(OPENCODE_API_KEY\\s*=\\s*)[^\\r\\n]+\")\n .expect(\"valid OPENCODE_API_KEY assignment regex\"),\n Regex::new(r\"(?m)^\\s*(OPENCODE_GO_API_KEY\\s*=\\s*)[^\\r\\n]+\")\n .expect(\"valid OPENCODE_GO_API_KEY assignment regex\"),\n Regex::new(r\"(?m)^\\s*(ZHIPU_API_KEY\\s*=\\s*)[^\\r\\n]+\")\n .expect(\"valid ZHIPU_API_KEY assignment regex\"),\n Regex::new(r\"(?m)^\\s*(ZAI_API_KEY\\s*=\\s*)[^\\r\\n]+\")\n .expect(\"valid ZAI_API_KEY assignment regex\"),\n Regex::new(r\"(?m)^\\s*(302AI_API_KEY\\s*=\\s*)[^\\r\\n]+\")\n .expect(\"valid 302AI_API_KEY assignment regex\"),\n Regex::new(r\"(?m)^\\s*(BASETEN_API_KEY\\s*=\\s*)[^\\r\\n]+\")\n .expect(\"valid BASETEN_API_KEY assignment regex\"),\n Regex::new(r\"(?m)^\\s*(CORTECS_API_KEY\\s*=\\s*)[^\\r\\n]+\")\n .expect(\"valid CORTECS_API_KEY assignment regex\"),\n Regex::new(r\"(?m)^\\s*(DEEPSEEK_API_KEY\\s*=\\s*)[^\\r\\n]+\")\n .expect(\"valid DEEPSEEK_API_KEY assignment regex\"),\n Regex::new(r\"(?m)^\\s*(FIRMWARE_API_KEY\\s*=\\s*)[^\\r\\n]+\")\n .expect(\"valid FIRMWARE_API_KEY assignment regex\"),\n Regex::new(r\"(?m)^\\s*(HF_TOKEN\\s*=\\s*)[^\\r\\n]+\")\n .expect(\"valid HF_TOKEN assignment regex\"),\n Regex::new(r\"(?m)^\\s*(MOONSHOT_API_KEY\\s*=\\s*)[^\\r\\n]+\")\n .expect(\"valid MOONSHOT_API_KEY assignment regex\"),\n Regex::new(r\"(?m)^\\s*(NEBIUS_API_KEY\\s*=\\s*)[^\\r\\n]+\")\n .expect(\"valid NEBIUS_API_KEY assignment regex\"),\n Regex::new(r\"(?m)^\\s*(SCALEWAY_API_KEY\\s*=\\s*)[^\\r\\n]+\")\n .expect(\"valid SCALEWAY_API_KEY assignment regex\"),\n Regex::new(r\"(?m)^\\s*(STACKIT_API_KEY\\s*=\\s*)[^\\r\\n]+\")\n .expect(\"valid STACKIT_API_KEY assignment regex\"),\n Regex::new(r\"(?m)^\\s*(GROQ_API_KEY\\s*=\\s*)[^\\r\\n]+\")\n .expect(\"valid GROQ_API_KEY assignment regex\"),\n Regex::new(r\"(?m)^\\s*(MISTRAL_API_KEY\\s*=\\s*)[^\\r\\n]+\")\n .expect(\"valid MISTRAL_API_KEY assignment regex\"),\n Regex::new(r\"(?m)^\\s*(PERPLEXITY_API_KEY\\s*=\\s*)[^\\r\\n]+\")\n .expect(\"valid PERPLEXITY_API_KEY assignment regex\"),\n Regex::new(r\"(?m)^\\s*(TOGETHER_API_KEY\\s*=\\s*)[^\\r\\n]+\")\n .expect(\"valid TOGETHER_API_KEY assignment regex\"),\n Regex::new(r\"(?m)^\\s*(DEEPINFRA_API_KEY\\s*=\\s*)[^\\r\\n]+\")\n .expect(\"valid DEEPINFRA_API_KEY assignment regex\"),\n Regex::new(r\"(?m)^\\s*(XAI_API_KEY\\s*=\\s*)[^\\r\\n]+\")\n .expect(\"valid XAI_API_KEY assignment regex\"),\n Regex::new(r\"(?m)^\\s*(CHUTES_API_KEY\\s*=\\s*)[^\\r\\n]+\")\n .expect(\"valid CHUTES_API_KEY assignment regex\"),\n Regex::new(r\"(?m)^\\s*(CEREBRAS_API_KEY\\s*=\\s*)[^\\r\\n]+\")\n .expect(\"valid CEREBRAS_API_KEY assignment regex\"),\n Regex::new(r\"(?m)^\\s*(OPENAI_COMPAT_API_KEY\\s*=\\s*)[^\\r\\n]+\")\n .expect(\"valid OPENAI_COMPAT_API_KEY assignment regex\"),\n Regex::new(r\"(?m)^\\s*(ANTHROPIC_API_KEY\\s*=\\s*)[^\\r\\n]+\")\n .expect(\"valid ANTHROPIC_API_KEY assignment regex\"),\n Regex::new(r\"(?m)^\\s*(OPENAI_API_KEY\\s*=\\s*)[^\\r\\n]+\")\n .expect(\"valid OPENAI_API_KEY assignment regex\"),\n Regex::new(r\"(?m)^\\s*(AZURE_OPENAI_API_KEY\\s*=\\s*)[^\\r\\n]+\")\n .expect(\"valid AZURE_OPENAI_API_KEY assignment regex\"),\n Regex::new(r\"(?m)^\\s*(CURSOR_API_KEY\\s*=\\s*)[^\\r\\n]+\")\n .expect(\"valid CURSOR_API_KEY assignment regex\"),\n Regex::new(r\"(?m)^\\s*(GITHUB_TOKEN\\s*=\\s*)[^\\r\\n]+\")\n .expect(\"valid GITHUB_TOKEN assignment regex\"),\n ]\n });\n\n let mut redacted = text.to_string();\n let mut redacted_keys: HashSet<String> = [\n \"OPENROUTER_API_KEY\",\n \"OPENCODE_API_KEY\",\n \"OPENCODE_GO_API_KEY\",\n \"ZHIPU_API_KEY\",\n \"ZAI_API_KEY\",\n \"302AI_API_KEY\",\n \"BASETEN_API_KEY\",\n \"CORTECS_API_KEY\",\n \"DEEPSEEK_API_KEY\",\n \"FIRMWARE_API_KEY\",\n \"HF_TOKEN\",\n \"MOONSHOT_API_KEY\",\n \"NEBIUS_API_KEY\",\n \"SCALEWAY_API_KEY\",\n \"STACKIT_API_KEY\",\n \"GROQ_API_KEY\",\n \"MISTRAL_API_KEY\",\n \"PERPLEXITY_API_KEY\",\n \"TOGETHER_API_KEY\",\n \"DEEPINFRA_API_KEY\",\n \"XAI_API_KEY\",\n \"CHUTES_API_KEY\",\n \"CEREBRAS_API_KEY\",\n \"OPENAI_COMPAT_API_KEY\",\n \"ANTHROPIC_API_KEY\",\n \"OPENAI_API_KEY\",\n \"AZURE_OPENAI_API_KEY\",\n \"CURSOR_API_KEY\",\n \"GITHUB_TOKEN\",\n ]\n .iter()\n .map(|k| (*k).to_string())\n .collect();\n\n for re in direct_patterns {\n redacted = re.replace_all(&redacted, \"[REDACTED_SECRET]\").into_owned();\n }\n\n for re in assignment_patterns {\n redacted = re\n .replace_all(&redacted, \"${1}[REDACTED_SECRET]\")\n .into_owned();\n }\n\n // Also redact custom API key variable names configured at runtime.\n for source in [\n \"JCODE_OPENROUTER_API_KEY_NAME\",\n \"JCODE_OPENAI_COMPAT_API_KEY_NAME\",\n ] {\n let Some(key_name) = std::env::var(source)\n .ok()\n .map(|v| v.trim().to_string())\n .filter(|v| !v.is_empty())\n else {\n continue;\n };\n\n if !key_name\n .chars()\n .all(|c| c.is_ascii_uppercase() || c.is_ascii_digit() || c == '_')\n {\n continue;\n }\n if !redacted_keys.insert(key_name.clone()) {\n continue;\n }\n\n let pattern = format!(r\"(?m)^\\s*({}\\s*=\\s*)[^\\r\\n]+\", regex::escape(&key_name));\n if let Ok(re) = Regex::new(&pattern) {\n redacted = re\n .replace_all(&redacted, \"${1}[REDACTED_SECRET]\")\n .into_owned();\n }\n }\n\n redacted\n}\n\n/// Tool definition for the API\n#[derive(Debug, Clone, Serialize)]\npub struct ToolDefinition {\n pub name: String,\n pub description: String,\n pub input_schema: serde_json::Value,\n}\n\n/// A tool call from the model\n#[derive(Debug, Clone, Default, PartialEq, Eq, serde::Serialize, serde::Deserialize)]\npub struct ToolCall {\n #[serde(default)]\n pub id: String,\n #[serde(default)]\n pub name: String,\n #[serde(default)]\n pub input: serde_json::Value,\n #[serde(default, skip_serializing_if = \"Option::is_none\")]\n pub intent: Option<String>,\n}\n\n/// Connection phase for status bar transparency\n#[derive(Debug, Clone, PartialEq)]\npub enum ConnectionPhase {\n /// Refreshing OAuth token\n Authenticating,\n /// TCP + TLS connection to API\n Connecting,\n /// HTTP request sent, waiting for first response byte\n WaitingForResponse,\n /// First byte received, stream is active\n Streaming,\n /// Retrying after a transient error\n Retrying { attempt: u32, max: u32 },\n}\n\nimpl std::fmt::Display for ConnectionPhase {\n fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {\n match self {\n ConnectionPhase::Authenticating => write!(f, \"authenticating\"),\n ConnectionPhase::Connecting => write!(f, \"connecting\"),\n ConnectionPhase::WaitingForResponse => write!(f, \"waiting for response\"),\n ConnectionPhase::Streaming => write!(f, \"streaming\"),\n ConnectionPhase::Retrying { attempt, max } => {\n write!(f, \"retrying ({}/{})\", attempt, max)\n }\n }\n }\n}\n\n/// Streaming event from provider\n#[derive(Debug, Clone)]\npub enum StreamEvent {\n /// Text content delta\n TextDelta(String),\n /// Tool use started\n ToolUseStart { id: String, name: String },\n /// Tool input delta (JSON fragment)\n ToolInputDelta(String),\n /// Tool use complete\n ToolUseEnd,\n /// Tool result from provider (provider already executed the tool)\n ToolResult {\n tool_use_id: String,\n content: String,\n is_error: bool,\n },\n /// Extended thinking started\n ThinkingStart,\n /// Extended thinking delta (reasoning content)\n ThinkingDelta(String),\n /// Extended thinking ended\n ThinkingEnd,\n /// Extended thinking completed with duration\n ThinkingDone { duration_secs: f64 },\n /// Message complete (may have stop reason)\n MessageEnd { stop_reason: Option<String> },\n /// Token usage update\n TokenUsage {\n input_tokens: Option<u64>,\n output_tokens: Option<u64>,\n cache_read_input_tokens: Option<u64>,\n cache_creation_input_tokens: Option<u64>,\n },\n /// Active transport/connection type for this stream\n ConnectionType { connection: String },\n /// Connection phase update (for status bar transparency)\n ConnectionPhase { phase: ConnectionPhase },\n /// Error occurred\n Error {\n message: String,\n /// Seconds until rate limit resets (if this is a rate limit error)\n retry_after_secs: Option<u64>,\n },\n /// Provider session ID (for conversation resume)\n SessionId(String),\n /// Compaction occurred (context was summarized)\n Compaction {\n trigger: String,\n pre_tokens: Option<u64>,\n /// Provider-native compaction artifact, if one was emitted.\n openai_encrypted_content: Option<String>,\n },\n /// Upstream provider info (e.g., which provider OpenRouter routed to)\n UpstreamProvider { provider: String },\n /// Native tool call from a provider bridge that needs execution by jcode\n NativeToolCall {\n request_id: String,\n tool_name: String,\n input: serde_json::Value,\n },\n}\n\n#[cfg(test)]\nmod tests {\n use super::*;\n\n #[test]\n fn sanitize_tool_id_alphanumeric_passthrough() {\n assert_eq!(\n sanitize_tool_id(\"toolu_01XFDUDYJgAACzvnptvVer6u\"),\n \"toolu_01XFDUDYJgAACzvnptvVer6u\"\n );\n assert_eq!(sanitize_tool_id(\"call_abc123\"), \"call_abc123\");\n assert_eq!(\n sanitize_tool_id(\"call_1234567890_9876543210\"),\n \"call_1234567890_9876543210\"\n );\n }\n\n #[test]\n fn sanitize_tool_id_hyphens_passthrough() {\n assert_eq!(sanitize_tool_id(\"call-abc-123\"), \"call-abc-123\");\n assert_eq!(\n sanitize_tool_id(\"tool_use-id_with-mixed\"),\n \"tool_use-id_with-mixed\"\n );\n }\n\n #[test]\n fn sanitize_tool_id_replaces_dots() {\n assert_eq!(\n sanitize_tool_id(\"chatcmpl-abc.def.ghi\"),\n \"chatcmpl-abc_def_ghi\"\n );\n assert_eq!(sanitize_tool_id(\"call.123\"), \"call_123\");\n }\n\n #[test]\n fn sanitize_tool_id_replaces_colons() {\n assert_eq!(sanitize_tool_id(\"call:123:456\"), \"call_123_456\");\n }\n\n #[test]\n fn sanitize_tool_id_replaces_special_chars() {\n assert_eq!(\n sanitize_tool_id(\"id@with#special$chars\"),\n \"id_with_special_chars\"\n );\n assert_eq!(sanitize_tool_id(\"id with spaces\"), \"id_with_spaces\");\n }\n\n #[test]\n fn sanitize_tool_id_empty_returns_unknown() {\n assert_eq!(sanitize_tool_id(\"\"), \"unknown\");\n }\n\n #[test]\n fn sanitize_tool_id_copilot_to_anthropic() {\n assert_eq!(\n sanitize_tool_id(\"chatcmpl-BF2xX.tool_call.0\"),\n \"chatcmpl-BF2xX_tool_call_0\"\n );\n }\n\n #[test]\n fn sanitize_tool_id_already_valid_unchanged() {\n let valid_ids = [\n \"toolu_01XFDUDYJgAACzvnptvVer6u\",\n \"call_abc123\",\n \"fallback_text_call_call_1234567890_9876543210\",\n \"tool_123\",\n \"a\",\n \"A\",\n \"0\",\n \"_\",\n \"-\",\n \"a-b_c\",\n ];\n for id in valid_ids {\n assert_eq!(sanitize_tool_id(id), id, \"ID '{}' should be unchanged\", id);\n }\n }\n\n #[test]\n fn redact_secrets_redacts_known_direct_token_formats() {\n let input = \"access=sk-ant-oat01-ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789\\nopenrouter=sk-or-v1-abcdefghijklmnopqrstuvwxyz0123456789\\ngithub=ghp_ABCDEFGHIJKLMNOPQRSTUVWXYZ0123\\n\";\n let out = redact_secrets(input);\n assert!(!out.contains(\"sk-ant-oat01-\"));\n assert!(!out.contains(\"sk-or-v1-\"));\n assert!(!out.contains(\"ghp_ABCDEFGHIJKLMNOPQRSTUVWXYZ0123\"));\n assert!(out.matches(\"[REDACTED_SECRET]\").count() >= 3);\n }\n\n #[test]\n fn redact_secrets_redacts_env_style_assignments() {\n let input = \"OPENROUTER_API_KEY=sk-or-v1-abc123abc123abc123abc123\\nOPENCODE_API_KEY=oc_test_secret\\nOPENCODE_GO_API_KEY=ocgo_test_secret\\nZAI_API_KEY=zai_secret\\nCHUTES_API_KEY=chutes_secret\\nCEREBRAS_API_KEY=cerebras_secret\\nOPENAI_COMPAT_API_KEY=compat_secret\\nCURSOR_API_KEY='my_cursor_secret_value'\\nOPENAI_API_KEY=sk-test-openai-example\\nAZURE_OPENAI_API_KEY=azure-openai-secret\\n\";\n let out = redact_secrets(input);\n assert!(out.contains(\"OPENROUTER_API_KEY=[REDACTED_SECRET]\"));\n assert!(out.contains(\"OPENCODE_API_KEY=[REDACTED_SECRET]\"));\n assert!(out.contains(\"OPENCODE_GO_API_KEY=[REDACTED_SECRET]\"));\n assert!(out.contains(\"ZAI_API_KEY=[REDACTED_SECRET]\"));\n assert!(out.contains(\"CHUTES_API_KEY=[REDACTED_SECRET]\"));\n assert!(out.contains(\"CEREBRAS_API_KEY=[REDACTED_SECRET]\"));\n assert!(out.contains(\"OPENAI_COMPAT_API_KEY=[REDACTED_SECRET]\"));\n assert!(out.contains(\"CURSOR_API_KEY=[REDACTED_SECRET]\"));\n assert!(out.contains(\"OPENAI_API_KEY=[REDACTED_SECRET]\"));\n assert!(out.contains(\"AZURE_OPENAI_API_KEY=[REDACTED_SECRET]\"));\n assert!(!out.contains(\"my_cursor_secret_value\"));\n }\n\n #[test]\n fn redact_secrets_redacts_runtime_key_assignment() {\n let key_var = \"JCODE_OPENAI_COMPAT_API_KEY_NAME\";\n let prev = std::env::var(key_var).ok();\n crate::env::set_var(key_var, \"GROQ_API_KEY\");\n\n let input = \"GROQ_API_KEY=my_secret_token_value\";\n let out = redact_secrets(input);\n assert_eq!(out, \"GROQ_API_KEY=[REDACTED_SECRET]\");\n\n if let Some(v) = prev {\n crate::env::set_var(key_var, v);\n } else {\n crate::env::remove_var(key_var);\n }\n }\n\n #[test]\n fn redact_secrets_redacts_mixed_case_token_assignments() {\n let input = \"my_token=ya29.ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789\";\n let out = redact_secrets(input);\n assert!(out.contains(\"[REDACTED_SECRET]\"));\n assert!(!out.contains(\"ya29.ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789\"));\n }\n\n #[test]\n fn redact_secrets_leaves_normal_output_unchanged() {\n let input = \"Found 5 files\\nNo auth errors\\nDone.\";\n assert_eq!(redact_secrets(input), input);\n }\n\n #[test]\n fn format_timestamp_is_stable_utc_rfc3339() {\n let ts = chrono::DateTime::parse_from_rfc3339(\"2025-03-15T02:24:13.250Z\")\n .unwrap()\n .with_timezone(&Utc);\n assert_eq!(Message::format_timestamp(&ts), \"2025-03-15T02:24:13.250Z\");\n }\n\n #[test]\n fn with_timestamps_prepends_utc_prefix_to_user_text() {\n let ts = chrono::DateTime::parse_from_rfc3339(\"2025-03-15T02:24:03Z\")\n .unwrap()\n .with_timezone(&Utc);\n let stamped = Message::with_timestamps(&[Message {\n role: Role::User,\n content: vec![ContentBlock::Text {\n text: \"hello\".to_string(),\n cache_control: None,\n }],\n timestamp: Some(ts),\n tool_duration_ms: None,\n }]);\n match &stamped[0].content[0] {\n ContentBlock::Text { text, .. } => {\n assert_eq!(text, \"[2025-03-15T02:24:03.000Z] hello\");\n }\n other => panic!(\"expected text block, got {other:?}\"),\n }\n }\n\n #[test]\n fn with_timestamps_adds_tool_timing_header_with_duration() {\n let ts = chrono::DateTime::parse_from_rfc3339(\"2025-03-15T02:24:13Z\")\n .unwrap()\n .with_timezone(&Utc);\n let stamped = Message::with_timestamps(&[Message {\n role: Role::User,\n content: vec![ContentBlock::ToolResult {\n tool_use_id: \"call_1\".to_string(),\n content: \"ok\".to_string(),\n is_error: None,\n }],\n timestamp: Some(ts),\n tool_duration_ms: Some(3_200),\n }]);\n match &stamped[0].content[0] {\n ContentBlock::ToolResult { content, .. } => {\n assert_eq!(\n content,\n \"[tool timing: start=2025-03-15T02:24:09.800Z finish=2025-03-15T02:24:13.000Z duration=3.2s] ok\"\n );\n }\n other => panic!(\"expected tool result block, got {other:?}\"),\n }\n }\n\n #[test]\n fn with_timestamps_skips_internal_system_reminders() {\n let ts = chrono::DateTime::parse_from_rfc3339(\"2025-03-15T02:24:13Z\")\n .unwrap()\n .with_timezone(&Utc);\n let original = Message {\n role: Role::User,\n content: vec![ContentBlock::Text {\n text: \"<system-reminder>\\ninternal\\n</system-reminder>\".to_string(),\n cache_control: None,\n }],\n timestamp: Some(ts),\n tool_duration_ms: None,\n };\n let stamped = Message::with_timestamps(&[original.clone()]);\n match &stamped[0].content[0] {\n ContentBlock::Text { text, .. } => {\n assert_eq!(text, \"<system-reminder>\\ninternal\\n</system-reminder>\");\n }\n other => panic!(\"expected text block, got {other:?}\"),\n }\n }\n\n #[test]\n fn format_background_task_notification_markdown_uses_code_block_preview() {\n let rendered = format_background_task_notification_markdown(&BackgroundTaskCompleted {\n task_id: \"abc123\".to_string(),\n tool_name: \"bash\".to_string(),\n session_id: \"session\".to_string(),\n status: BackgroundTaskStatus::Completed,\n exit_code: Some(0),\n output_preview: \"[stderr] first line\\n[stdout] second line\\n\".to_string(),\n output_file: std::path::PathBuf::from(\"/tmp/output.log\"),\n duration_secs: 7.1,\n notify: true,\n });\n\n assert!(\n rendered\n .contains(\"**Background task** `abc123` · `bash` · ✓ completed · 7.1s · exit 0\")\n );\n assert!(rendered.contains(\"```text\\n[stderr] first line\\n[stdout] second line\\n```\"));\n assert!(rendered.contains(\"_Full output:_ `bg action=\\\"output\\\" task_id=\\\"abc123\\\"`\"));\n }\n\n #[test]\n fn format_background_task_notification_markdown_handles_empty_preview() {\n let rendered = format_background_task_notification_markdown(&BackgroundTaskCompleted {\n task_id: \"abc123\".to_string(),\n tool_name: \"bash\".to_string(),\n session_id: \"session\".to_string(),\n status: BackgroundTaskStatus::Failed,\n exit_code: Some(9),\n output_preview: \"\\n\\n\".to_string(),\n output_file: std::path::PathBuf::from(\"/tmp/output.log\"),\n duration_secs: 1.0,\n notify: true,\n });\n\n assert!(rendered.contains(\"✗ failed\"));\n assert!(rendered.contains(\"_No output captured._\"));\n }\n}\n"}}}}