Merge branch 'master' into feat/signing
This commit is contained in:
@@ -36,6 +36,11 @@ Environment variables:
|
|||||||
- `./script/run-clang-format.py -r --extensions 'c,h,cpp,m' ./crates/warpui/src/ ./app/src/` - Format C/C++/Obj-C code
|
- `./script/run-clang-format.py -r --extensions 'c,h,cpp,m' ./crates/warpui/src/ ./app/src/` - Format C/C++/Obj-C code
|
||||||
- `find . -name "*.wgsl" -exec wgslfmt --check {} +` - Check WGSL shader formatting
|
- `find . -name "*.wgsl" -exec wgslfmt --check {} +` - Check WGSL shader formatting
|
||||||
|
|
||||||
|
### Bedrock Diagnostics
|
||||||
|
- Bedrock request or stream failures automatically write `Error_<timestamp>.txt` to the repository root.
|
||||||
|
- The error snapshot file includes the serialized Bedrock context window, tool definitions, protobuf request debug payload, captured Bedrock diagnostic lines, and log tails.
|
||||||
|
- Set `GALAXY_BEDROCK_DIAGNOSTICS=1` to additionally write per-event Bedrock diagnostic logs to `bedrock-diagnostics.log` in the active Warp log directory.
|
||||||
|
|
||||||
### Platform Setup
|
### Platform Setup
|
||||||
- `./script/bootstrap` - Platform-specific setup (calls platform-specific bootstrap scripts)
|
- `./script/bootstrap` - Platform-specific setup (calls platform-specific bootstrap scripts)
|
||||||
- `./script/install_cargo_build_deps` - Install Cargo build dependencies
|
- `./script/install_cargo_build_deps` - Install Cargo build dependencies
|
||||||
|
|||||||
@@ -260,7 +260,7 @@ pub async fn generate_multi_agent_output(
|
|||||||
format!("MultiPart[{}]", part_descs.join(", "))
|
format!("MultiPart[{}]", part_descs.join(", "))
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
log::debug!(
|
log::info!(
|
||||||
"[bedrock] msg[{}]: role={:?}, content={}",
|
"[bedrock] msg[{}]: role={:?}, content={}",
|
||||||
i,
|
i,
|
||||||
msg.role,
|
msg.role,
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ use crate::terminal::model::block::{
|
|||||||
};
|
};
|
||||||
|
|
||||||
use crate::ai::agent::api::convert_conversation::{
|
use crate::ai::agent::api::convert_conversation::{
|
||||||
ConvertToExchanges, compute_time_to_first_token_ms_from_messages,
|
compute_time_to_first_token_ms_from_messages, ConvertToExchanges,
|
||||||
};
|
};
|
||||||
use ai::document::AIDocumentId;
|
use ai::document::AIDocumentId;
|
||||||
use chrono::{DateTime, Local, TimeZone};
|
use chrono::{DateTime, Local, TimeZone};
|
||||||
@@ -29,8 +29,8 @@ use galaxy_core::execution_mode::AppExecutionMode;
|
|||||||
use galaxy_core::features::FeatureFlag;
|
use galaxy_core::features::FeatureFlag;
|
||||||
use galaxy_core::send_telemetry_from_ctx;
|
use galaxy_core::send_telemetry_from_ctx;
|
||||||
use galaxy_core::ui::appearance::Appearance;
|
use galaxy_core::ui::appearance::Appearance;
|
||||||
use galaxy_core::ui::theme::WarpTheme;
|
|
||||||
use galaxy_core::ui::theme::color::internal_colors;
|
use galaxy_core::ui::theme::color::internal_colors;
|
||||||
|
use galaxy_core::ui::theme::WarpTheme;
|
||||||
use galaxyui::color::ColorU;
|
use galaxyui::color::ColorU;
|
||||||
use galaxyui::{EntityId, ModelContext, SingletonEntity};
|
use galaxyui::{EntityId, ModelContext, SingletonEntity};
|
||||||
use uuid::Uuid;
|
use uuid::Uuid;
|
||||||
@@ -40,35 +40,36 @@ use warp_multi_agent_api::{self as api, response_event::stream_finished::TokenUs
|
|||||||
|
|
||||||
use crate::ai::agent::{AIIdentifiers, CancellationReason};
|
use crate::ai::agent::{AIIdentifiers, CancellationReason};
|
||||||
use crate::{
|
use crate::{
|
||||||
BlocklistAIHistoryModel, GlobalResourceHandlesProvider,
|
|
||||||
ai::{
|
ai::{
|
||||||
agent::{
|
agent::{
|
||||||
AIAgentOutputMessage, AIAgentOutputMessageType, MessageToAIAgentOutputMessageError,
|
|
||||||
icons::{
|
icons::{
|
||||||
failed_icon, gray_stop_icon, in_progress_icon, succeeded_icon, yellow_stop_icon,
|
failed_icon, gray_stop_icon, in_progress_icon, succeeded_icon, yellow_stop_icon,
|
||||||
},
|
},
|
||||||
todos::AIAgentTodoList,
|
todos::AIAgentTodoList,
|
||||||
|
AIAgentOutputMessage, AIAgentOutputMessageType, MessageToAIAgentOutputMessageError,
|
||||||
},
|
},
|
||||||
blocklist::BlocklistAIHistoryEvent,
|
blocklist::BlocklistAIHistoryEvent,
|
||||||
},
|
},
|
||||||
persistence::{
|
persistence::{
|
||||||
ModelEvent,
|
|
||||||
model::{AgentConversationData, PersistedAutoexecuteMode},
|
model::{AgentConversationData, PersistedAutoexecuteMode},
|
||||||
|
ModelEvent,
|
||||||
},
|
},
|
||||||
ui_components::icons::Icon,
|
ui_components::icons::Icon,
|
||||||
|
BlocklistAIHistoryModel, GlobalResourceHandlesProvider,
|
||||||
};
|
};
|
||||||
|
|
||||||
use super::task::{ExtractMessagesError, UpdateTaskError, UpgradeOptimisticTaskError};
|
use super::task::{ExtractMessagesError, UpdateTaskError, UpgradeOptimisticTaskError};
|
||||||
use super::{
|
use super::{
|
||||||
|
api::ServerConversationToken,
|
||||||
|
task::{
|
||||||
|
derive_todo_lists_from_root_task,
|
||||||
|
helper::*,
|
||||||
|
transaction::{SavedTask, Transaction},
|
||||||
|
Task, TaskId,
|
||||||
|
},
|
||||||
AIAgentAction, AIAgentActionId, AIAgentContext, AIAgentExchange, AIAgentExchangeId,
|
AIAgentAction, AIAgentActionId, AIAgentContext, AIAgentExchange, AIAgentExchangeId,
|
||||||
AIAgentInput, AIAgentOutputStatus, AIAgentTodo, AIAgentTodoId, FinishedAIAgentOutput,
|
AIAgentInput, AIAgentOutputStatus, AIAgentTodo, AIAgentTodoId, FinishedAIAgentOutput,
|
||||||
MessageId, RenderableAIError, RequestCost,
|
MessageId, RenderableAIError, RequestCost,
|
||||||
api::ServerConversationToken,
|
|
||||||
task::{
|
|
||||||
Task, TaskId, derive_todo_lists_from_root_task,
|
|
||||||
helper::*,
|
|
||||||
transaction::{SavedTask, Transaction},
|
|
||||||
},
|
|
||||||
};
|
};
|
||||||
use super::{
|
use super::{
|
||||||
AIAgentOutput, OutputModelInfo, ServerOutputId, Shared, SuggestedLoggingId, Suggestions,
|
AIAgentOutput, OutputModelInfo, ServerOutputId, Shared, SuggestedLoggingId, Suggestions,
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
|
|
||||||
use super::{
|
use super::{
|
||||||
AIConversation, AIConversationAutoexecuteMode, AIConversationId, artifact_from_fork_proto,
|
artifact_from_fork_proto, AIConversation, AIConversationAutoexecuteMode, AIConversationId,
|
||||||
};
|
};
|
||||||
use crate::ai::artifacts::Artifact;
|
use crate::ai::artifacts::Artifact;
|
||||||
use crate::persistence::model::AgentConversationData;
|
use crate::persistence::model::AgentConversationData;
|
||||||
|
|||||||
@@ -163,16 +163,22 @@ impl BedrockClient {
|
|||||||
|
|
||||||
let output = request.send().await.map_err(|e| {
|
let output = request.send().await.map_err(|e| {
|
||||||
let debug_msg = format!("{:?}", e);
|
let debug_msg = format!("{:?}", e);
|
||||||
let display_msg = format!("{}", e);
|
let display_msg = format!("{e}");
|
||||||
log::error!("[bedrock] API error (display): {display_msg}");
|
log::error!("[bedrock] API error (display): {display_msg}");
|
||||||
log::error!("[bedrock] API error (debug): {debug_msg}");
|
log::error!("[bedrock] API error (debug): {debug_msg}");
|
||||||
let msg = if debug_msg.len() > display_msg.len() {
|
let msg = if debug_msg.len() > display_msg.len() {
|
||||||
debug_msg
|
debug_msg.clone()
|
||||||
} else {
|
} else {
|
||||||
display_msg
|
display_msg.clone()
|
||||||
};
|
};
|
||||||
if let Some(ref logger) = diagnostic_logger {
|
if let Some(ref logger) = diagnostic_logger {
|
||||||
logger.log_result_fail(&msg);
|
logger.log_result_fail(&msg);
|
||||||
|
if let Some(path) = logger.dump_error_snapshot(&display_msg, &debug_msg) {
|
||||||
|
log::error!(
|
||||||
|
"[bedrock] Wrote Bedrock failure snapshot to {}",
|
||||||
|
path.display()
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
if msg.contains("AccessDenied") || msg.contains("access denied") {
|
if msg.contains("AccessDenied") || msg.contains("access denied") {
|
||||||
BedrockError::AccessDenied(msg)
|
BedrockError::AccessDenied(msg)
|
||||||
|
|||||||
@@ -577,10 +577,17 @@ fn ensure_tool_results_paired(messages: &mut Vec<ConversationMessage>) {
|
|||||||
let insert_idx = i + 1;
|
let insert_idx = i + 1;
|
||||||
|
|
||||||
// If the next message is already a user message, merge synthetic results into it.
|
// If the next message is already a user message, merge synthetic results into it.
|
||||||
|
// IMPORTANT: ToolResult blocks must come BEFORE text content in a user message
|
||||||
|
// that follows an assistant tool_use. The Bedrock/Anthropic API validates this
|
||||||
|
// ordering and rejects requests where text precedes tool_result.
|
||||||
if insert_idx < messages.len() && messages[insert_idx].role == MessageRole::User {
|
if insert_idx < messages.len() && messages[insert_idx].role == MessageRole::User {
|
||||||
match &mut messages[insert_idx].content {
|
match &mut messages[insert_idx].content {
|
||||||
MessageContent::MultiPart(parts) => {
|
MessageContent::MultiPart(parts) => {
|
||||||
|
// Prepend synthetic results before existing parts so
|
||||||
|
// tool_result blocks appear first in the content.
|
||||||
|
let existing = std::mem::take(parts);
|
||||||
parts.extend(synthetic_results);
|
parts.extend(synthetic_results);
|
||||||
|
parts.extend(existing);
|
||||||
}
|
}
|
||||||
existing => {
|
existing => {
|
||||||
// Convert existing single content + synthetic results into MultiPart.
|
// Convert existing single content + synthetic results into MultiPart.
|
||||||
@@ -607,11 +614,18 @@ fn ensure_tool_results_paired(messages: &mut Vec<ConversationMessage>) {
|
|||||||
},
|
},
|
||||||
MessageContent::MultiPart(_) => unreachable!(),
|
MessageContent::MultiPart(_) => unreachable!(),
|
||||||
};
|
};
|
||||||
let mut parts = vec![existing_part];
|
// Synthetic tool_result blocks come first, then the
|
||||||
parts.extend(synthetic_results);
|
// original content (text), matching the Bedrock API
|
||||||
|
// requirement that tool_result precedes other content.
|
||||||
|
let mut parts = synthetic_results;
|
||||||
|
parts.push(existing_part);
|
||||||
*existing = MessageContent::MultiPart(parts);
|
*existing = MessageContent::MultiPart(parts);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
log::info!(
|
||||||
|
"[bedrock] Merged synthetic tool_result(s) into existing user message at index {}",
|
||||||
|
insert_idx
|
||||||
|
);
|
||||||
} else {
|
} else {
|
||||||
// No user message follows — insert a new one.
|
// No user message follows — insert a new one.
|
||||||
let content = if synthetic_results.len() == 1 {
|
let content = if synthetic_results.len() == 1 {
|
||||||
@@ -1272,3 +1286,7 @@ fn format_tool_call_result(result: &api::message::ToolCallResult) -> String {
|
|||||||
"Tool completed.".to_string()
|
"Tool completed.".to_string()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
#[path = "convert_request_tests.rs"]
|
||||||
|
mod tests;
|
||||||
|
|||||||
@@ -0,0 +1,47 @@
|
|||||||
|
use serde_json::json;
|
||||||
|
|
||||||
|
use super::super::convert::{ContentPart, ConversationMessage, MessageContent, MessageRole};
|
||||||
|
use super::sanitize_messages_for_bedrock;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_sanitize_messages_prepends_synthetic_tool_result_before_existing_user_text() {
|
||||||
|
let tool_use_id = "tooluse_Pzmn1QfoWgJsA8sb4RHTM3".to_string();
|
||||||
|
let existing_user_text = "What happened?".to_string();
|
||||||
|
|
||||||
|
let mut messages = vec![
|
||||||
|
ConversationMessage {
|
||||||
|
role: MessageRole::User,
|
||||||
|
content: MessageContent::Text("Run a command.".to_string()),
|
||||||
|
},
|
||||||
|
ConversationMessage {
|
||||||
|
role: MessageRole::Assistant,
|
||||||
|
content: MessageContent::ToolUse {
|
||||||
|
tool_use_id: tool_use_id.clone(),
|
||||||
|
name: "run_shell_command".to_string(),
|
||||||
|
input: json!({ "command": "ls" }),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
ConversationMessage {
|
||||||
|
role: MessageRole::User,
|
||||||
|
content: MessageContent::Text(existing_user_text.clone()),
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
sanitize_messages_for_bedrock(&mut messages);
|
||||||
|
|
||||||
|
assert_eq!(messages.len(), 3);
|
||||||
|
|
||||||
|
let parts = match &messages[2].content {
|
||||||
|
MessageContent::MultiPart(parts) => parts,
|
||||||
|
other => panic!("Expected MultiPart content, got: {:?}", other),
|
||||||
|
};
|
||||||
|
|
||||||
|
assert_eq!(parts.len(), 2);
|
||||||
|
assert!(
|
||||||
|
matches!(&parts[0], ContentPart::ToolResult { tool_use_id: id, .. } if id == &tool_use_id)
|
||||||
|
);
|
||||||
|
assert!(matches!(
|
||||||
|
&parts[1],
|
||||||
|
ContentPart::Text(text) if text == &existing_user_text
|
||||||
|
));
|
||||||
|
}
|
||||||
@@ -1,16 +1,18 @@
|
|||||||
use std::fs::{self, File, OpenOptions};
|
use chrono::{Local, Utc};
|
||||||
use std::io::{BufWriter, Write};
|
|
||||||
use std::path::PathBuf;
|
|
||||||
use std::sync::Mutex;
|
|
||||||
|
|
||||||
use chrono::Utc;
|
|
||||||
use serde_json::Value as JsonValue;
|
use serde_json::Value as JsonValue;
|
||||||
|
use std::fs::{self, File, OpenOptions};
|
||||||
|
use std::io::{BufWriter, Read, Seek, SeekFrom, Write};
|
||||||
|
use std::path::{Path, PathBuf};
|
||||||
|
use std::sync::Mutex;
|
||||||
|
|
||||||
use super::convert::{ConversationMessage, MessageContent, MessageRole, ToolDefinition};
|
use super::convert::{ConversationMessage, MessageContent, MessageRole, ToolDefinition};
|
||||||
|
|
||||||
const ENV_VAR: &str = "GALAXY_BEDROCK_DIAGNOSTICS";
|
const ENV_VAR: &str = "GALAXY_BEDROCK_DIAGNOSTICS";
|
||||||
const LOG_FILENAME: &str = "bedrock-diagnostics.log";
|
const LOG_FILENAME: &str = "bedrock-diagnostics.log";
|
||||||
const MAX_ROTATIONS: usize = 5;
|
const MAX_ROTATIONS: usize = 5;
|
||||||
|
const ERROR_DUMP_PREFIX: &str = "Error_";
|
||||||
|
const MAX_CAPTURED_LINES: usize = 2_000;
|
||||||
|
const LOG_TAIL_BYTES: u64 = 200 * 1024;
|
||||||
|
|
||||||
#[derive(Debug, Clone, Copy, PartialEq)]
|
#[derive(Debug, Clone, Copy, PartialEq)]
|
||||||
pub enum Layer {
|
pub enum Layer {
|
||||||
@@ -62,11 +64,15 @@ impl std::fmt::Display for Status {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub struct BedrockDiagnosticLogger {
|
pub struct BedrockDiagnosticLogger {
|
||||||
writer: Mutex<BufWriter<File>>,
|
writer: Option<Mutex<BufWriter<File>>>,
|
||||||
|
log_path: Option<PathBuf>,
|
||||||
model_id: String,
|
model_id: String,
|
||||||
conversation_id: Mutex<String>,
|
conversation_id: Mutex<String>,
|
||||||
request_id: Mutex<String>,
|
request_id: Mutex<String>,
|
||||||
task_id: String,
|
task_id: String,
|
||||||
|
protobuf_input: Mutex<Option<String>>,
|
||||||
|
bedrock_input: Mutex<Option<JsonValue>>,
|
||||||
|
captured_lines: Mutex<Vec<String>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl BedrockDiagnosticLogger {
|
impl BedrockDiagnosticLogger {
|
||||||
@@ -76,47 +82,42 @@ impl BedrockDiagnosticLogger {
|
|||||||
request_id: &str,
|
request_id: &str,
|
||||||
task_id: &str,
|
task_id: &str,
|
||||||
) -> Option<Self> {
|
) -> Option<Self> {
|
||||||
if !is_enabled() {
|
let mut writer = None;
|
||||||
return None;
|
let mut log_path = None;
|
||||||
}
|
|
||||||
|
|
||||||
let log_path = match log_file_path() {
|
if is_enabled() {
|
||||||
Some(path) => path,
|
if let Some(path) = diagnostic_log_file_path() {
|
||||||
None => {
|
if let Some(parent) = path.parent() {
|
||||||
|
let _ = fs::create_dir_all(parent);
|
||||||
|
}
|
||||||
|
|
||||||
|
rotate_if_needed(&path);
|
||||||
|
|
||||||
|
match OpenOptions::new().create(true).append(true).open(&path) {
|
||||||
|
Ok(file) => {
|
||||||
|
writer = Some(Mutex::new(BufWriter::new(file)));
|
||||||
|
log_path = Some(path.clone());
|
||||||
|
log::info!("[bedrock-diag] Diagnostic logging enabled -> {path:?}");
|
||||||
|
}
|
||||||
|
Err(e) => {
|
||||||
|
log::warn!("[bedrock-diag] Failed to open log file {path:?}: {e}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
log::warn!("[bedrock-diag] Could not determine log directory");
|
log::warn!("[bedrock-diag] Could not determine log directory");
|
||||||
return None;
|
|
||||||
}
|
}
|
||||||
};
|
|
||||||
|
|
||||||
if let Some(parent) = log_path.parent() {
|
|
||||||
let _ = fs::create_dir_all(parent);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
rotate_if_needed(&log_path);
|
|
||||||
|
|
||||||
let file = match OpenOptions::new().create(true).append(true).open(&log_path) {
|
|
||||||
Ok(f) => f,
|
|
||||||
Err(e) => {
|
|
||||||
log::warn!(
|
|
||||||
"[bedrock-diag] Failed to open log file {:?}: {}",
|
|
||||||
log_path,
|
|
||||||
e
|
|
||||||
);
|
|
||||||
return None;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
log::info!(
|
|
||||||
"[bedrock-diag] Diagnostic logging enabled -> {:?}",
|
|
||||||
log_path
|
|
||||||
);
|
|
||||||
|
|
||||||
Some(Self {
|
Some(Self {
|
||||||
writer: Mutex::new(BufWriter::new(file)),
|
writer,
|
||||||
|
log_path,
|
||||||
model_id: model_id.to_string(),
|
model_id: model_id.to_string(),
|
||||||
conversation_id: Mutex::new(conversation_id.to_string()),
|
conversation_id: Mutex::new(conversation_id.to_string()),
|
||||||
request_id: Mutex::new(request_id.to_string()),
|
request_id: Mutex::new(request_id.to_string()),
|
||||||
task_id: task_id.to_string(),
|
task_id: task_id.to_string(),
|
||||||
|
protobuf_input: Mutex::new(None),
|
||||||
|
bedrock_input: Mutex::new(None),
|
||||||
|
captured_lines: Mutex::new(Vec::new()),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -131,6 +132,9 @@ impl BedrockDiagnosticLogger {
|
|||||||
|
|
||||||
pub fn log_protobuf_input(&self, request: &warp_multi_agent_api::Request) {
|
pub fn log_protobuf_input(&self, request: &warp_multi_agent_api::Request) {
|
||||||
let payload = format!("{:?}", request);
|
let payload = format!("{:?}", request);
|
||||||
|
if let Ok(mut protobuf_input) = self.protobuf_input.lock() {
|
||||||
|
*protobuf_input = Some(payload.clone());
|
||||||
|
}
|
||||||
self.write_line(Layer::Protobuf, Direction::Input, Status::Pending, &payload);
|
self.write_line(Layer::Protobuf, Direction::Input, Status::Pending, &payload);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -155,6 +159,9 @@ impl BedrockDiagnosticLogger {
|
|||||||
"messages": messages_json,
|
"messages": messages_json,
|
||||||
"tools": tools_json,
|
"tools": tools_json,
|
||||||
});
|
});
|
||||||
|
if let Ok(mut bedrock_input) = self.bedrock_input.lock() {
|
||||||
|
*bedrock_input = Some(payload.clone());
|
||||||
|
}
|
||||||
|
|
||||||
self.write_line(
|
self.write_line(
|
||||||
Layer::Bedrock,
|
Layer::Bedrock,
|
||||||
@@ -203,6 +210,111 @@ impl BedrockDiagnosticLogger {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn dump_error_snapshot(&self, error: &str, debug_error: &str) -> Option<PathBuf> {
|
||||||
|
let conversation_id = self
|
||||||
|
.conversation_id
|
||||||
|
.lock()
|
||||||
|
.map(|value| value.clone())
|
||||||
|
.unwrap_or_default();
|
||||||
|
let request_id = self
|
||||||
|
.request_id
|
||||||
|
.lock()
|
||||||
|
.map(|value| value.clone())
|
||||||
|
.unwrap_or_default();
|
||||||
|
let protobuf_input = self
|
||||||
|
.protobuf_input
|
||||||
|
.lock()
|
||||||
|
.ok()
|
||||||
|
.and_then(|value| value.clone());
|
||||||
|
let bedrock_input = self
|
||||||
|
.bedrock_input
|
||||||
|
.lock()
|
||||||
|
.ok()
|
||||||
|
.and_then(|value| value.clone());
|
||||||
|
let captured_lines = self
|
||||||
|
.captured_lines
|
||||||
|
.lock()
|
||||||
|
.map(|lines| lines.clone())
|
||||||
|
.unwrap_or_default();
|
||||||
|
|
||||||
|
let current_log_path = galaxy_logging::log_file_path().ok();
|
||||||
|
let current_log_tail = current_log_path
|
||||||
|
.as_ref()
|
||||||
|
.and_then(|path| read_file_tail(path, LOG_TAIL_BYTES));
|
||||||
|
let diagnostics_log_tail = self
|
||||||
|
.log_path
|
||||||
|
.as_ref()
|
||||||
|
.and_then(|path| read_file_tail(path, LOG_TAIL_BYTES));
|
||||||
|
let context_window = bedrock_input
|
||||||
|
.as_ref()
|
||||||
|
.and_then(|value| value.get("messages").cloned())
|
||||||
|
.unwrap_or(JsonValue::Null);
|
||||||
|
let tools = bedrock_input
|
||||||
|
.as_ref()
|
||||||
|
.and_then(|value| value.get("tools").cloned())
|
||||||
|
.unwrap_or(JsonValue::Null);
|
||||||
|
|
||||||
|
let payload = serde_json::json!({
|
||||||
|
"timestamp_local": Local::now().to_rfc3339_opts(chrono::SecondsFormat::Millis, true),
|
||||||
|
"timestamp_utc": Utc::now().to_rfc3339_opts(chrono::SecondsFormat::Millis, true),
|
||||||
|
"error": {
|
||||||
|
"display": error,
|
||||||
|
"debug": debug_error,
|
||||||
|
},
|
||||||
|
"bedrock": {
|
||||||
|
"model_id": &self.model_id,
|
||||||
|
"task_id": &self.task_id,
|
||||||
|
"conversation_id": conversation_id,
|
||||||
|
"request_id": request_id,
|
||||||
|
"input": bedrock_input,
|
||||||
|
"context_window": context_window,
|
||||||
|
"tools": tools,
|
||||||
|
},
|
||||||
|
"protobuf_request_debug": protobuf_input,
|
||||||
|
"captured_bedrock_lines": captured_lines,
|
||||||
|
"logs": {
|
||||||
|
"warp_log_path": current_log_path
|
||||||
|
.as_ref()
|
||||||
|
.map(|path| path.display().to_string())
|
||||||
|
.unwrap_or_default(),
|
||||||
|
"warp_log_tail": current_log_tail,
|
||||||
|
"bedrock_diagnostics_log_path": self
|
||||||
|
.log_path
|
||||||
|
.as_ref()
|
||||||
|
.map(|path| path.display().to_string())
|
||||||
|
.unwrap_or_default(),
|
||||||
|
"bedrock_diagnostics_log_tail": diagnostics_log_tail,
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
let file_name = format!(
|
||||||
|
"{ERROR_DUMP_PREFIX}{}.txt",
|
||||||
|
Local::now().format("%Y%m%d_%H%M%S_%3f")
|
||||||
|
);
|
||||||
|
let serialized_payload = match serde_json::to_string_pretty(&payload) {
|
||||||
|
Ok(value) => value,
|
||||||
|
Err(e) => {
|
||||||
|
log::error!("[bedrock] Failed to serialize Bedrock error snapshot: {e}");
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
let mut attempted_paths = Vec::new();
|
||||||
|
for dump_path in error_dump_paths(&file_name) {
|
||||||
|
match fs::write(&dump_path, &serialized_payload) {
|
||||||
|
Ok(()) => return Some(dump_path),
|
||||||
|
Err(e) => {
|
||||||
|
attempted_paths.push(format!("{} ({e})", dump_path.display()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
log::error!(
|
||||||
|
"[bedrock] Failed to write Bedrock error snapshot. Attempted paths: {}",
|
||||||
|
attempted_paths.join(", ")
|
||||||
|
);
|
||||||
|
None
|
||||||
|
}
|
||||||
|
|
||||||
fn write_line(&self, layer: Layer, direction: Direction, status: Status, payload: &str) {
|
fn write_line(&self, layer: Layer, direction: Direction, status: Status, payload: &str) {
|
||||||
let timestamp = Utc::now().to_rfc3339_opts(chrono::SecondsFormat::Millis, true);
|
let timestamp = Utc::now().to_rfc3339_opts(chrono::SecondsFormat::Millis, true);
|
||||||
let conversation_id = self
|
let conversation_id = self
|
||||||
@@ -228,9 +340,19 @@ impl BedrockDiagnosticLogger {
|
|||||||
payload,
|
payload,
|
||||||
);
|
);
|
||||||
|
|
||||||
if let Ok(mut writer) = self.writer.lock() {
|
if let Ok(mut captured_lines) = self.captured_lines.lock() {
|
||||||
let _ = writer.write_all(line.as_bytes());
|
captured_lines.push(line.trim_end().to_string());
|
||||||
let _ = writer.flush();
|
if captured_lines.len() > MAX_CAPTURED_LINES {
|
||||||
|
let overflow = captured_lines.len() - MAX_CAPTURED_LINES;
|
||||||
|
captured_lines.drain(0..overflow);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(writer) = &self.writer {
|
||||||
|
if let Ok(mut writer) = writer.lock() {
|
||||||
|
let _ = writer.write_all(line.as_bytes());
|
||||||
|
let _ = writer.flush();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -241,13 +363,45 @@ pub fn is_enabled() -> bool {
|
|||||||
.unwrap_or(false)
|
.unwrap_or(false)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn log_file_path() -> Option<PathBuf> {
|
fn diagnostic_log_file_path() -> Option<PathBuf> {
|
||||||
galaxy_logging::log_directory()
|
galaxy_logging::log_directory()
|
||||||
.ok()
|
.ok()
|
||||||
.map(|dir| dir.join(LOG_FILENAME))
|
.map(|dir| dir.join(LOG_FILENAME))
|
||||||
}
|
}
|
||||||
|
|
||||||
fn rotate_if_needed(path: &PathBuf) {
|
fn error_dump_directory() -> PathBuf {
|
||||||
|
let source_root = Path::new(env!("CARGO_MANIFEST_DIR"))
|
||||||
|
.parent()
|
||||||
|
.unwrap_or_else(|| Path::new(env!("CARGO_MANIFEST_DIR")))
|
||||||
|
.to_path_buf();
|
||||||
|
|
||||||
|
if source_root.is_dir() {
|
||||||
|
source_root
|
||||||
|
} else {
|
||||||
|
std::env::current_dir().unwrap_or_else(|_| PathBuf::from("."))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn error_dump_paths(file_name: &str) -> Vec<PathBuf> {
|
||||||
|
let mut directories = Vec::new();
|
||||||
|
push_unique_directory(&mut directories, error_dump_directory());
|
||||||
|
if let Ok(current_dir) = std::env::current_dir() {
|
||||||
|
push_unique_directory(&mut directories, current_dir);
|
||||||
|
}
|
||||||
|
push_unique_directory(&mut directories, std::env::temp_dir());
|
||||||
|
|
||||||
|
directories
|
||||||
|
.into_iter()
|
||||||
|
.map(|directory| directory.join(file_name))
|
||||||
|
.collect()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn push_unique_directory(directories: &mut Vec<PathBuf>, directory: PathBuf) {
|
||||||
|
if directory.is_dir() && !directories.iter().any(|existing| existing == &directory) {
|
||||||
|
directories.push(directory);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
fn rotate_if_needed(path: &Path) {
|
||||||
let metadata = match fs::metadata(path) {
|
let metadata = match fs::metadata(path) {
|
||||||
Ok(m) => m,
|
Ok(m) => m,
|
||||||
Err(_) => return,
|
Err(_) => return,
|
||||||
@@ -260,7 +414,7 @@ fn rotate_if_needed(path: &PathBuf) {
|
|||||||
|
|
||||||
for i in (0..MAX_ROTATIONS - 1).rev() {
|
for i in (0..MAX_ROTATIONS - 1).rev() {
|
||||||
let from = if i == 0 {
|
let from = if i == 0 {
|
||||||
path.clone()
|
path.to_path_buf()
|
||||||
} else {
|
} else {
|
||||||
path.with_extension(format!("log.{}", i))
|
path.with_extension(format!("log.{}", i))
|
||||||
};
|
};
|
||||||
@@ -272,6 +426,25 @@ fn rotate_if_needed(path: &PathBuf) {
|
|||||||
let _ = fs::rename(path, &first_rotation);
|
let _ = fs::rename(path, &first_rotation);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn read_file_tail(path: &Path, max_bytes: u64) -> Option<String> {
|
||||||
|
let mut file = File::open(path).ok()?;
|
||||||
|
let file_len = file.metadata().ok()?.len();
|
||||||
|
let start = file_len.saturating_sub(max_bytes);
|
||||||
|
if file.seek(SeekFrom::Start(start)).is_err() {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
let mut bytes = Vec::new();
|
||||||
|
if file.read_to_end(&mut bytes).is_err() {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut tail = String::from_utf8_lossy(&bytes).to_string();
|
||||||
|
if start > 0 {
|
||||||
|
tail = format!("... log tail truncated to last {max_bytes} bytes ...\n{tail}");
|
||||||
|
}
|
||||||
|
Some(tail)
|
||||||
|
}
|
||||||
|
|
||||||
fn serialize_messages(messages: &[ConversationMessage]) -> JsonValue {
|
fn serialize_messages(messages: &[ConversationMessage]) -> JsonValue {
|
||||||
let entries: Vec<JsonValue> = messages
|
let entries: Vec<JsonValue> = messages
|
||||||
.iter()
|
.iter()
|
||||||
|
|||||||
@@ -555,6 +555,7 @@ impl AgentSimulation {
|
|||||||
8192,
|
8192,
|
||||||
None,
|
None,
|
||||||
false,
|
false,
|
||||||
|
None,
|
||||||
Arc::new(Mutex::new(Vec::new())),
|
Arc::new(Mutex::new(Vec::new())),
|
||||||
)
|
)
|
||||||
.await
|
.await
|
||||||
@@ -1140,6 +1141,7 @@ async fn test_reasoning_model_produces_substantial_output() {
|
|||||||
8192,
|
8192,
|
||||||
None,
|
None,
|
||||||
false,
|
false,
|
||||||
|
None,
|
||||||
Arc::new(Mutex::new(Vec::new())),
|
Arc::new(Mutex::new(Vec::new())),
|
||||||
)
|
)
|
||||||
.await
|
.await
|
||||||
@@ -1261,6 +1263,7 @@ async fn test_event_sequence_matches_controller_expectations() {
|
|||||||
100,
|
100,
|
||||||
None,
|
None,
|
||||||
false,
|
false,
|
||||||
|
None,
|
||||||
Arc::new(Mutex::new(Vec::new())),
|
Arc::new(Mutex::new(Vec::new())),
|
||||||
)
|
)
|
||||||
.await
|
.await
|
||||||
@@ -1374,6 +1377,7 @@ async fn test_followup_turn_does_not_send_create_task() {
|
|||||||
100,
|
100,
|
||||||
None,
|
None,
|
||||||
false,
|
false,
|
||||||
|
None,
|
||||||
Arc::new(Mutex::new(Vec::new())),
|
Arc::new(Mutex::new(Vec::new())),
|
||||||
)
|
)
|
||||||
.await
|
.await
|
||||||
@@ -1438,7 +1442,11 @@ async fn run_slash_command_test(
|
|||||||
let task_id = uuid::Uuid::new_v4().to_string();
|
let task_id = uuid::Uuid::new_v4().to_string();
|
||||||
println!(
|
println!(
|
||||||
"\n{}\n[SLASH CMD TEST] {}\n[SLASH CMD TEST] Model: {}\n[SLASH CMD TEST] Message: {:.100}\n{}",
|
"\n{}\n[SLASH CMD TEST] {}\n[SLASH CMD TEST] Model: {}\n[SLASH CMD TEST] Message: {:.100}\n{}",
|
||||||
"=".repeat(80), test_name, model, user_message, "=".repeat(80)
|
"=".repeat(80),
|
||||||
|
test_name,
|
||||||
|
model,
|
||||||
|
user_message,
|
||||||
|
"=".repeat(80)
|
||||||
);
|
);
|
||||||
|
|
||||||
let stream = client
|
let stream = client
|
||||||
@@ -1452,6 +1460,7 @@ async fn run_slash_command_test(
|
|||||||
4096,
|
4096,
|
||||||
None,
|
None,
|
||||||
true,
|
true,
|
||||||
|
None,
|
||||||
Arc::new(Mutex::new(Vec::new())),
|
Arc::new(Mutex::new(Vec::new())),
|
||||||
)
|
)
|
||||||
.await
|
.await
|
||||||
@@ -1678,6 +1687,7 @@ async fn test_slash_resume_conversation() {
|
|||||||
256,
|
256,
|
||||||
None,
|
None,
|
||||||
true,
|
true,
|
||||||
|
None,
|
||||||
Arc::new(Mutex::new(Vec::new())),
|
Arc::new(Mutex::new(Vec::new())),
|
||||||
)
|
)
|
||||||
.await
|
.await
|
||||||
@@ -1788,6 +1798,7 @@ async fn test_empty_messages_safety_check() {
|
|||||||
100,
|
100,
|
||||||
None,
|
None,
|
||||||
true,
|
true,
|
||||||
|
None,
|
||||||
Arc::new(Mutex::new(Vec::new())),
|
Arc::new(Mutex::new(Vec::new())),
|
||||||
)
|
)
|
||||||
.await
|
.await
|
||||||
@@ -1829,17 +1840,26 @@ async fn test_full_proto_round_trip_with_tool_history() {
|
|||||||
let request = make_request(
|
let request = make_request(
|
||||||
&task_id,
|
&task_id,
|
||||||
vec![
|
vec![
|
||||||
make_user_query_message("msg-1", &task_id, "List all files in the current directory."),
|
make_user_query_message(
|
||||||
|
"msg-1",
|
||||||
|
&task_id,
|
||||||
|
"List all files in the current directory.",
|
||||||
|
),
|
||||||
make_tool_call_run_shell(tool_call_id_1, &task_id, tool_call_id_1, "ls -la"),
|
make_tool_call_run_shell(tool_call_id_1, &task_id, tool_call_id_1, "ls -la"),
|
||||||
make_tool_call_read_files(tool_call_id_2, &task_id, tool_call_id_2, "README.md"),
|
make_tool_call_read_files(tool_call_id_2, &task_id, tool_call_id_2, "README.md"),
|
||||||
make_tool_result_shell(
|
make_tool_result_shell(
|
||||||
"msg-result-1", &task_id, tool_call_id_1,
|
"msg-result-1",
|
||||||
|
&task_id,
|
||||||
|
tool_call_id_1,
|
||||||
"total 16\ndrwxr-xr-x 5 user staff 160 May 6 10:00 .\n-rw-r--r-- 1 user staff 100 May 6 10:00 README.md\n-rw-r--r-- 1 user staff 200 May 6 10:00 Cargo.toml",
|
"total 16\ndrwxr-xr-x 5 user staff 160 May 6 10:00 .\n-rw-r--r-- 1 user staff 100 May 6 10:00 README.md\n-rw-r--r-- 1 user staff 200 May 6 10:00 Cargo.toml",
|
||||||
0,
|
0,
|
||||||
),
|
),
|
||||||
make_tool_result_read_files(
|
make_tool_result_read_files(
|
||||||
"msg-result-2", &task_id, tool_call_id_2,
|
"msg-result-2",
|
||||||
"README.md", "# Sample Project\nA test project.",
|
&task_id,
|
||||||
|
tool_call_id_2,
|
||||||
|
"README.md",
|
||||||
|
"# Sample Project\nA test project.",
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
make_user_inputs_input("Now summarize what you found in one sentence."),
|
make_user_inputs_input("Now summarize what you found in one sentence."),
|
||||||
@@ -1938,6 +1958,7 @@ async fn test_full_proto_round_trip_with_tool_history() {
|
|||||||
1024,
|
1024,
|
||||||
None,
|
None,
|
||||||
true,
|
true,
|
||||||
|
None,
|
||||||
Arc::new(Mutex::new(Vec::new())),
|
Arc::new(Mutex::new(Vec::new())),
|
||||||
)
|
)
|
||||||
.await;
|
.await;
|
||||||
|
|||||||
@@ -64,6 +64,7 @@ async fn collect_stream_output(
|
|||||||
8192,
|
8192,
|
||||||
None,
|
None,
|
||||||
false,
|
false,
|
||||||
|
None,
|
||||||
Arc::new(Mutex::new(Vec::new())),
|
Arc::new(Mutex::new(Vec::new())),
|
||||||
)
|
)
|
||||||
.await
|
.await
|
||||||
|
|||||||
@@ -218,7 +218,16 @@ pub fn bedrock_stream_to_response_events(
|
|||||||
Err(e) => {
|
Err(e) => {
|
||||||
log::error!("[bedrock-debug] Stream error after {event_count} events: {e}");
|
log::error!("[bedrock-debug] Stream error after {event_count} events: {e}");
|
||||||
if let Some(ref logger) = diagnostic_logger {
|
if let Some(ref logger) = diagnostic_logger {
|
||||||
logger.log_stream_error(&format!("{e}"));
|
let error_msg = format!("{e}");
|
||||||
|
let debug_error = format!("{e:?}");
|
||||||
|
logger.log_stream_error(&error_msg);
|
||||||
|
logger.log_result_fail(&error_msg);
|
||||||
|
if let Some(path) = logger.dump_error_snapshot(&error_msg, &debug_error) {
|
||||||
|
log::error!(
|
||||||
|
"[bedrock] Wrote Bedrock failure snapshot to {}",
|
||||||
|
path.display()
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
if !buffered_text.is_empty() {
|
if !buffered_text.is_empty() {
|
||||||
let msg_id = current_text_message_id
|
let msg_id = current_text_message_id
|
||||||
|
|||||||
@@ -651,7 +651,7 @@ impl DriveIndex {
|
|||||||
.as_ref(ctx)
|
.as_ref(ctx)
|
||||||
.num_trashed_cloud_objects_per_space(spaces.iter(), ctx),
|
.num_trashed_cloud_objects_per_space(spaces.iter(), ctx),
|
||||||
};
|
};
|
||||||
let mut sections = spaces
|
let sections = spaces
|
||||||
.iter()
|
.iter()
|
||||||
.map(|space| DriveIndexSection::Space(*space))
|
.map(|space| DriveIndexSection::Space(*space))
|
||||||
.collect::<Vec<_>>();
|
.collect::<Vec<_>>();
|
||||||
@@ -1254,7 +1254,9 @@ impl DriveIndex {
|
|||||||
{
|
{
|
||||||
self.expand_section_for_object(&id.uid().clone(), ctx);
|
self.expand_section_for_object(&id.uid().clone(), ctx);
|
||||||
} else {
|
} else {
|
||||||
log::warn!("unknown GenericStringObject type found while trying to manually expand drive section. {object_id:?}");
|
log::warn!(
|
||||||
|
"unknown GenericStringObject type found while trying to manually expand drive section. {object_id:?}"
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@@ -5399,7 +5401,9 @@ impl TypedActionView for DriveIndex {
|
|||||||
log::error!("Creation of EnvVarCollections is not yet supported")
|
log::error!("Creation of EnvVarCollections is not yet supported")
|
||||||
}
|
}
|
||||||
DriveObjectType::AIFact | DriveObjectType::AIFactCollection => {
|
DriveObjectType::AIFact | DriveObjectType::AIFactCollection => {
|
||||||
log::error!("Use DriveIndexAction::OpenAIFactCollection to open the pane view instead");
|
log::error!(
|
||||||
|
"Use DriveIndexAction::OpenAIFactCollection to open the pane view instead"
|
||||||
|
);
|
||||||
}
|
}
|
||||||
DriveObjectType::MCPServer | DriveObjectType::MCPServerCollection => {
|
DriveObjectType::MCPServer | DriveObjectType::MCPServerCollection => {
|
||||||
log::error!(
|
log::error!(
|
||||||
|
|||||||
@@ -303,6 +303,13 @@ impl AIApiError {
|
|||||||
}
|
}
|
||||||
true
|
true
|
||||||
}
|
}
|
||||||
|
// Don't retry Bedrock validation errors — they are deterministic
|
||||||
|
// and will always fail with the same request payload.
|
||||||
|
AIApiError::Stream { source, .. }
|
||||||
|
if source.to_string().contains("Validation error") =>
|
||||||
|
{
|
||||||
|
false
|
||||||
|
}
|
||||||
// By default, retry on error.
|
// By default, retry on error.
|
||||||
_ => true,
|
_ => true,
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -60,7 +60,6 @@ impl SettingsWidget for AboutPageWidget {
|
|||||||
appearance: &Appearance,
|
appearance: &Appearance,
|
||||||
_app: &AppContext,
|
_app: &AppContext,
|
||||||
) -> Box<dyn Element> {
|
) -> Box<dyn Element> {
|
||||||
let theme = appearance.theme();
|
|
||||||
let ui_builder = appearance.ui_builder();
|
let ui_builder = appearance.ui_builder();
|
||||||
|
|
||||||
let image_path = "bundled/svg/galaxy-logo.svg";
|
let image_path = "bundled/svg/galaxy-logo.svg";
|
||||||
|
|||||||
@@ -26,12 +26,12 @@ fn code_subpages_are_identified() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn cloud_platform_subpages_are_identified() {
|
fn legacy_cloud_platform_sections_are_not_subpages() {
|
||||||
assert!(SettingsSection::CloudEnvironments.is_cloud_platform_subpage());
|
assert!(!SettingsSection::CloudEnvironments.is_subpage());
|
||||||
assert!(SettingsSection::OzCloudAPIKeys.is_cloud_platform_subpage());
|
assert!(!SettingsSection::OzCloudAPIKeys.is_subpage());
|
||||||
|
|
||||||
assert!(!SettingsSection::Account.is_cloud_platform_subpage());
|
assert!(!SettingsSection::Account.is_subpage());
|
||||||
assert!(!SettingsSection::WarpAgent.is_cloud_platform_subpage());
|
assert!(!SettingsSection::WarpAgent.is_subpage());
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
@@ -42,8 +42,8 @@ fn is_subpage_covers_all_umbrella_types() {
|
|||||||
}
|
}
|
||||||
assert!(SettingsSection::CodeIndexing.is_subpage());
|
assert!(SettingsSection::CodeIndexing.is_subpage());
|
||||||
assert!(SettingsSection::EditorAndCodeReview.is_subpage());
|
assert!(SettingsSection::EditorAndCodeReview.is_subpage());
|
||||||
assert!(SettingsSection::CloudEnvironments.is_subpage());
|
assert!(!SettingsSection::CloudEnvironments.is_subpage());
|
||||||
assert!(SettingsSection::OzCloudAPIKeys.is_subpage());
|
assert!(!SettingsSection::OzCloudAPIKeys.is_subpage());
|
||||||
|
|
||||||
// Top-level pages should not be subpages.
|
// Top-level pages should not be subpages.
|
||||||
assert!(!SettingsSection::Account.is_subpage());
|
assert!(!SettingsSection::Account.is_subpage());
|
||||||
@@ -647,7 +647,10 @@ fn realistic_nav_items() -> Vec<SettingsNavItem> {
|
|||||||
)),
|
)),
|
||||||
SettingsNavItem::Umbrella(SettingsUmbrella::new(
|
SettingsNavItem::Umbrella(SettingsUmbrella::new(
|
||||||
"Cloud platform",
|
"Cloud platform",
|
||||||
SettingsSection::cloud_platform_subpages().to_vec(),
|
vec![
|
||||||
|
SettingsSection::CloudEnvironments,
|
||||||
|
SettingsSection::OzCloudAPIKeys,
|
||||||
|
],
|
||||||
)),
|
)),
|
||||||
SettingsNavItem::Page(SettingsSection::Teams),
|
SettingsNavItem::Page(SettingsSection::Teams),
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
use crate::BlocklistAIHistoryModel;
|
use crate::ai::blocklist::agent_view::{agent_view_bg_fill, AgentViewState};
|
||||||
use crate::ai::blocklist::agent_view::{AgentViewState, agent_view_bg_fill};
|
use crate::ai::blocklist::{ai_brand_color, ATTACH_AS_AGENT_MODE_CONTEXT_TEXT};
|
||||||
use crate::ai::blocklist::{ATTACH_AS_AGENT_MODE_CONTEXT_TEXT, ai_brand_color};
|
|
||||||
use crate::ai_assistant::{AI_ASSISTANT_SVG_PATH, ASK_AI_ASSISTANT_TEXT};
|
use crate::ai_assistant::{AI_ASSISTANT_SVG_PATH, ASK_AI_ASSISTANT_TEXT};
|
||||||
use crate::appearance::Appearance;
|
use crate::appearance::Appearance;
|
||||||
use crate::drive::settings::WarpDriveSettings;
|
use crate::drive::settings::WarpDriveSettings;
|
||||||
@@ -20,10 +19,11 @@ use crate::terminal::model::index::Point as IndexPoint;
|
|||||||
use crate::terminal::model::selection::{SelectAction, SelectionPoint};
|
use crate::terminal::model::selection::{SelectAction, SelectionPoint};
|
||||||
use crate::terminal::safe_mode_settings::get_secret_obfuscation_mode;
|
use crate::terminal::safe_mode_settings::get_secret_obfuscation_mode;
|
||||||
use crate::terminal::view::TerminalAction;
|
use crate::terminal::view::TerminalAction;
|
||||||
use crate::terminal::{SizeInfo, grid_renderer};
|
use crate::terminal::{grid_renderer, SizeInfo};
|
||||||
use crate::themes::theme::{Fill, WarpTheme};
|
use crate::themes::theme::{Fill, WarpTheme};
|
||||||
use crate::ui_components::{self, icons as UIIcon};
|
use crate::ui_components::{self, icons as UIIcon};
|
||||||
use crate::util::color::Opacity;
|
use crate::util::color::Opacity;
|
||||||
|
use crate::BlocklistAIHistoryModel;
|
||||||
use enum_iterator::Sequence;
|
use enum_iterator::Sequence;
|
||||||
use galaxy_core::semantic_selection::SemanticSelection;
|
use galaxy_core::semantic_selection::SemanticSelection;
|
||||||
use galaxy_core::ui::builder::UiBuilder;
|
use galaxy_core::ui::builder::UiBuilder;
|
||||||
@@ -44,15 +44,15 @@ use galaxyui::elements::{
|
|||||||
use galaxyui::event::{KeyState, ModifiersState};
|
use galaxyui::event::{KeyState, ModifiersState};
|
||||||
use galaxyui::fonts::{FamilyId, Properties, Weight};
|
use galaxyui::fonts::{FamilyId, Properties, Weight};
|
||||||
use galaxyui::geometry::rect::RectF;
|
use galaxyui::geometry::rect::RectF;
|
||||||
use galaxyui::geometry::vector::{Vector2F, vec2f};
|
use galaxyui::geometry::vector::{vec2f, Vector2F};
|
||||||
use galaxyui::platform::keyboard::KeyCode;
|
use galaxyui::platform::keyboard::KeyCode;
|
||||||
use galaxyui::ui_components::components::UiComponent;
|
use galaxyui::ui_components::components::UiComponent;
|
||||||
use galaxyui::units::{IntoLines, IntoPixels, Lines, Pixels};
|
use galaxyui::units::{IntoLines, IntoPixels, Lines, Pixels};
|
||||||
|
use galaxyui::{elements::Icon, ClipBounds};
|
||||||
use galaxyui::{
|
use galaxyui::{
|
||||||
AfterLayoutContext, AppContext, Element, Event, EventContext, LayoutContext, PaintContext,
|
elements::SavePosition, event::DispatchedEvent, AfterLayoutContext, AppContext, Element, Event,
|
||||||
SizeConstraint, elements::SavePosition, event::DispatchedEvent,
|
EventContext, LayoutContext, PaintContext, SizeConstraint,
|
||||||
};
|
};
|
||||||
use galaxyui::{ClipBounds, elements::Icon};
|
|
||||||
use galaxyui::{EntityId, ModelHandle, SingletonEntity as _};
|
use galaxyui::{EntityId, ModelHandle, SingletonEntity as _};
|
||||||
use pathfinder_color::ColorU;
|
use pathfinder_color::ColorU;
|
||||||
use session_sharing_protocol::common::{ParticipantId, Selection};
|
use session_sharing_protocol::common::{ParticipantId, Selection};
|
||||||
@@ -69,9 +69,7 @@ use super::blockgrid_renderer::GridRenderParams;
|
|||||||
use super::find::{BlockListFindRun, BlockListMatch, TerminalFindModel};
|
use super::find::{BlockListFindRun, BlockListMatch, TerminalFindModel};
|
||||||
use super::grid_renderer::CellGlyphCache;
|
use super::grid_renderer::CellGlyphCache;
|
||||||
|
|
||||||
use super::TerminalModel;
|
|
||||||
use super::meta_shortcuts::handle_keystroke_despite_composing;
|
use super::meta_shortcuts::handle_keystroke_despite_composing;
|
||||||
use super::model::SecretHandle;
|
|
||||||
use super::model::block::BlockId;
|
use super::model::block::BlockId;
|
||||||
use super::model::blocks::{RichContentItem, SelectionRange};
|
use super::model::blocks::{RichContentItem, SelectionRange};
|
||||||
use super::model::grid::grid_handler::{Link, TermMode};
|
use super::model::grid::grid_handler::{Link, TermMode};
|
||||||
@@ -79,22 +77,24 @@ use super::model::image_map::StoredImageMetadata;
|
|||||||
use super::model::mouse::{MouseAction, MouseButton, MouseState};
|
use super::model::mouse::{MouseAction, MouseButton, MouseState};
|
||||||
use super::model::session::SessionId;
|
use super::model::session::SessionId;
|
||||||
use super::model::terminal_model::{SelectedBlocks, WithinBlock, WithinModel};
|
use super::model::terminal_model::{SelectedBlocks, WithinBlock, WithinModel};
|
||||||
|
use super::model::SecretHandle;
|
||||||
use super::shared_session::presence_manager::{
|
use super::shared_session::presence_manager::{
|
||||||
MUTED_PARTICIPANT_COLOR, PresenceManager, text_selection_color,
|
text_selection_color, PresenceManager, MUTED_PARTICIPANT_COLOR,
|
||||||
};
|
};
|
||||||
use super::shared_session::render_util::SHARED_SESSION_AVATAR_DIAMETER;
|
use super::shared_session::render_util::SHARED_SESSION_AVATAR_DIAMETER;
|
||||||
use super::view::{
|
use super::view::{
|
||||||
BLOCK_BANNER_HEIGHT, BlocklistAIRenderContext, InlineBannerId, RichContentMetadata,
|
BlocklistAIRenderContext, InlineBannerId, RichContentMetadata, SeparatorId,
|
||||||
SeparatorId, SharedSessionBanners, TerminalEditor, TerminalViewRenderContext,
|
SharedSessionBanners, TerminalEditor, TerminalViewRenderContext, BLOCK_BANNER_HEIGHT,
|
||||||
};
|
};
|
||||||
use super::warpify::render::{draw_flag_pole, render_subshell_flag};
|
use super::warpify::render::{draw_flag_pole, render_subshell_flag};
|
||||||
use super::{HEIGHT_FUDGE_FACTOR_LINES, heights_approx_eq};
|
use super::TerminalModel;
|
||||||
|
use super::{heights_approx_eq, HEIGHT_FUDGE_FACTOR_LINES};
|
||||||
use crate::terminal::blockgrid_renderer::BlockGridParams;
|
use crate::terminal::blockgrid_renderer::BlockGridParams;
|
||||||
use crate::terminal::model::terminal_model::BlockIndex;
|
use crate::terminal::model::terminal_model::BlockIndex;
|
||||||
use crate::terminal::warpify::SubshellSource;
|
use crate::terminal::warpify::SubshellSource;
|
||||||
|
|
||||||
use crate::terminal::model::escape_sequences::{
|
use crate::terminal::model::escape_sequences::{
|
||||||
KeystrokeWithDetails, ToEscapeSequence, maybe_kitty_keyboard_escape_sequence,
|
maybe_kitty_keyboard_escape_sequence, KeystrokeWithDetails, ToEscapeSequence,
|
||||||
};
|
};
|
||||||
|
|
||||||
/// The number of pixels at the bottom of padding where selection scrolling is performed.
|
/// The number of pixels at the bottom of padding where selection scrolling is performed.
|
||||||
|
|||||||
@@ -568,7 +568,6 @@ const VERTICAL_TABS_PANEL_POSITION_ID: &str = "workspace_view:vertical_tabs_pane
|
|||||||
const TAB_CONTENT_POSITION_ID: &str = "workspace_view:tab_content";
|
const TAB_CONTENT_POSITION_ID: &str = "workspace_view:tab_content";
|
||||||
|
|
||||||
const WELCOME_TIPS_POSITION_ID: &str = "welcome_tips_pill";
|
const WELCOME_TIPS_POSITION_ID: &str = "welcome_tips_pill";
|
||||||
const ELLIPSE_SVG_PATH: &str = "bundled/svg/ellipse.svg";
|
|
||||||
|
|
||||||
const AI_ASSISTANT_BUTTON_ID: &str = "workspace_view:ai_assistant_button";
|
const AI_ASSISTANT_BUTTON_ID: &str = "workspace_view:ai_assistant_button";
|
||||||
|
|
||||||
@@ -17720,54 +17719,6 @@ impl Workspace {
|
|||||||
SavePosition::new(Align::new(button).finish(), USER_AVATAR_BUTTON_POSITION_ID).finish()
|
SavePosition::new(Align::new(button).finish(), USER_AVATAR_BUTTON_POSITION_ID).finish()
|
||||||
}
|
}
|
||||||
|
|
||||||
fn render_resource_center_button(
|
|
||||||
&self,
|
|
||||||
appearance: &Appearance,
|
|
||||||
ctx: &AppContext,
|
|
||||||
) -> Box<dyn Element> {
|
|
||||||
// only show the unread indicator if the tips are NOT completed
|
|
||||||
let should_show_unread_indicator = !self.tips_completed.as_ref(ctx).skipped_or_completed;
|
|
||||||
let mut button = self
|
|
||||||
.render_tab_bar_icon_button(
|
|
||||||
appearance,
|
|
||||||
icons::Icon::Lightbulb,
|
|
||||||
&self.mouse_states.resource_center_icon,
|
|
||||||
WorkspaceAction::ToggleResourceCenter,
|
|
||||||
"Warp Essentials".to_string(),
|
|
||||||
self.cached_keybindings[TOGGLE_RESOURCE_CENTER_KEYBINDING_NAME].clone(),
|
|
||||||
false,
|
|
||||||
false,
|
|
||||||
)
|
|
||||||
.finish();
|
|
||||||
|
|
||||||
if should_show_unread_indicator {
|
|
||||||
const INDICATOR_DIAMETER: f32 = 6.;
|
|
||||||
let indicator = Container::new(
|
|
||||||
ConstrainedBox::new(
|
|
||||||
WarpUiIcon::new(ELLIPSE_SVG_PATH, appearance.theme().accent()).finish(),
|
|
||||||
)
|
|
||||||
.with_height(INDICATOR_DIAMETER)
|
|
||||||
.with_width(INDICATOR_DIAMETER)
|
|
||||||
.finish(),
|
|
||||||
)
|
|
||||||
.finish();
|
|
||||||
let mut stack = Stack::new();
|
|
||||||
stack.add_child(button);
|
|
||||||
stack.add_positioned_child(
|
|
||||||
indicator,
|
|
||||||
OffsetPositioning::offset_from_parent(
|
|
||||||
Vector2F::zero(),
|
|
||||||
ParentOffsetBounds::WindowByPosition,
|
|
||||||
ParentAnchor::TopRight,
|
|
||||||
ChildAnchor::TopRight,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
button = stack.finish();
|
|
||||||
}
|
|
||||||
|
|
||||||
Align::new(button).finish()
|
|
||||||
}
|
|
||||||
|
|
||||||
fn render_settings_button(&self, appearance: &Appearance) -> Box<dyn Element> {
|
fn render_settings_button(&self, appearance: &Appearance) -> Box<dyn Element> {
|
||||||
Align::new(
|
Align::new(
|
||||||
self.render_tab_bar_icon_button(
|
self.render_tab_bar_icon_button(
|
||||||
|
|||||||
@@ -1440,7 +1440,10 @@ impl RightPanelView {
|
|||||||
terminal_status.is_available(),
|
terminal_status.is_available(),
|
||||||
Self::format_optional_path(terminal_status.active_session_path.as_deref()),
|
Self::format_optional_path(terminal_status.active_session_path.as_deref()),
|
||||||
Self::format_optional_path(terminal_status.current_repo_path.as_deref()),
|
Self::format_optional_path(terminal_status.current_repo_path.as_deref()),
|
||||||
terminal_status.active_cli_agent.as_deref().unwrap_or("<none>"),
|
terminal_status
|
||||||
|
.active_cli_agent
|
||||||
|
.as_deref()
|
||||||
|
.unwrap_or("<none>"),
|
||||||
terminal_status.is_executing,
|
terminal_status.is_executing,
|
||||||
terminal_status.is_input_box_visible,
|
terminal_status.is_input_box_visible,
|
||||||
unavailable_reasons,
|
unavailable_reasons,
|
||||||
|
|||||||
+1
@@ -0,0 +1 @@
|
|||||||
|
Overwrite content
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
Overwrite content
|
||||||
Executable
+123
@@ -0,0 +1,123 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
#
|
||||||
|
# install-galaxy.sh — Clone, build, and install Galaxy.app on macOS.
|
||||||
|
#
|
||||||
|
# Usage:
|
||||||
|
# curl -fsSL https://mng-web-sharing.mini-games.tv/wst-data/ryan-share/galaxy/install-galaxy.sh | bash
|
||||||
|
# — or —
|
||||||
|
# ./script/install-galaxy.sh
|
||||||
|
#
|
||||||
|
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
REPO_URL="git@gitlab.com:samnasbo/shared/galaxy.git"
|
||||||
|
CLONE_DIR="$HOME/.galaxy/source"
|
||||||
|
APP_NAME="Galaxy.app"
|
||||||
|
INSTALL_DIR="/Applications"
|
||||||
|
BUNDLE_BIN="galaxy-oss"
|
||||||
|
BUNDLE_PKG="galaxy"
|
||||||
|
|
||||||
|
# ---------- helpers ----------
|
||||||
|
info() { printf "\033[1;34m==>\033[0m %s\n" "$1"; }
|
||||||
|
warn() { printf "\033[1;33m==> WARNING:\033[0m %s\n" "$1"; }
|
||||||
|
fail() { printf "\033[1;31m==> ERROR:\033[0m %s\n" "$1"; exit 1; }
|
||||||
|
|
||||||
|
# ---------- 1. Xcode / CLI tools ----------
|
||||||
|
info "Checking Xcode and Command Line Tools..."
|
||||||
|
|
||||||
|
if ! xcode-select -p &>/dev/null; then
|
||||||
|
fail "Xcode Command Line Tools are not installed. Run: xcode-select --install"
|
||||||
|
fi
|
||||||
|
|
||||||
|
if ! xcrun --show-sdk-path &>/dev/null; then
|
||||||
|
fail "Xcode SDK not found. Ensure Xcode or Command Line Tools are properly installed."
|
||||||
|
fi
|
||||||
|
|
||||||
|
if ! xcrun -f metal &>/dev/null; then
|
||||||
|
warn "Metal compiler not found. Attempting to download Metal toolchain..."
|
||||||
|
xcodebuild -downloadComponent MetalToolchain || warn "Could not download Metal toolchain — build may fail."
|
||||||
|
fi
|
||||||
|
|
||||||
|
info "Xcode prerequisites look good."
|
||||||
|
|
||||||
|
# ---------- 2. Homebrew ----------
|
||||||
|
if ! command -v brew &>/dev/null; then
|
||||||
|
info "Installing Homebrew..."
|
||||||
|
/bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)"
|
||||||
|
|
||||||
|
# Add brew to PATH for Apple Silicon
|
||||||
|
if [[ -f /opt/homebrew/bin/brew ]]; then
|
||||||
|
eval "$(/opt/homebrew/bin/brew shellenv)"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
if ! command -v pkgconf &>/dev/null && ! command -v pkg-config &>/dev/null; then
|
||||||
|
info "Installing pkgconf via Homebrew..."
|
||||||
|
brew install pkgconf
|
||||||
|
fi
|
||||||
|
|
||||||
|
# ---------- 3. Rust toolchain ----------
|
||||||
|
if ! command -v rustup &>/dev/null; then
|
||||||
|
info "Installing Rust via rustup..."
|
||||||
|
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y
|
||||||
|
source "$HOME/.cargo/env"
|
||||||
|
fi
|
||||||
|
|
||||||
|
info "Syncing Rust toolchain (rust-toolchain.toml will pin the exact version)..."
|
||||||
|
rustup show active-toolchain &>/dev/null || rustup default stable
|
||||||
|
|
||||||
|
# aarch64-apple-darwin target for Apple Silicon
|
||||||
|
rustup target add aarch64-apple-darwin 2>/dev/null || true
|
||||||
|
|
||||||
|
# cargo-bundle for producing the .app
|
||||||
|
if ! cargo bundle --help &>/dev/null 2>&1; then
|
||||||
|
info "Installing cargo-bundle..."
|
||||||
|
cargo install cargo-bundle \
|
||||||
|
--git=https://github.com/burtonageo/cargo-bundle \
|
||||||
|
--rev ae4c76e92c08774bf54ff077b1c52e3d1cd6c16d
|
||||||
|
fi
|
||||||
|
|
||||||
|
# ---------- 4. Clone the repo ----------
|
||||||
|
if [[ -d "$CLONE_DIR/.git" ]]; then
|
||||||
|
info "Repository already exists at $CLONE_DIR — pulling latest..."
|
||||||
|
git -C "$CLONE_DIR" pull --ff-only || warn "Pull failed; building with current checkout."
|
||||||
|
else
|
||||||
|
info "Cloning Galaxy into $CLONE_DIR..."
|
||||||
|
mkdir -p "$(dirname "$CLONE_DIR")"
|
||||||
|
git clone "$REPO_URL" "$CLONE_DIR"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# ---------- 5. Build ----------
|
||||||
|
info "Building $BUNDLE_BIN (release)..."
|
||||||
|
cargo build --release --bin "$BUNDLE_BIN" --package "$BUNDLE_PKG" \
|
||||||
|
--manifest-path "$CLONE_DIR/Cargo.toml"
|
||||||
|
|
||||||
|
info "Bundling $APP_NAME..."
|
||||||
|
# cargo-bundle does not support --manifest-path; run from repo root
|
||||||
|
pushd "$CLONE_DIR" > /dev/null
|
||||||
|
cargo bundle --release --bin "$BUNDLE_BIN" --package "$BUNDLE_PKG"
|
||||||
|
popd > /dev/null
|
||||||
|
|
||||||
|
BUILT_APP="$CLONE_DIR/target/release/bundle/osx/$APP_NAME"
|
||||||
|
if [[ ! -d "$BUILT_APP" ]]; then
|
||||||
|
fail "Bundle not found at $BUILT_APP — build may have failed."
|
||||||
|
fi
|
||||||
|
|
||||||
|
# ---------- 6. Kill, remove, install, launch ----------
|
||||||
|
info "Stopping any running Galaxy processes..."
|
||||||
|
pkill -x "Galaxy" 2>/dev/null && sleep 1 || true
|
||||||
|
pkill -9 -x "Galaxy" 2>/dev/null || true
|
||||||
|
|
||||||
|
INSTALLED_APP="$INSTALL_DIR/$APP_NAME"
|
||||||
|
if [[ -d "$INSTALLED_APP" ]]; then
|
||||||
|
info "Removing existing $INSTALLED_APP..."
|
||||||
|
rm -rf "$INSTALLED_APP"
|
||||||
|
fi
|
||||||
|
|
||||||
|
info "Copying $APP_NAME to $INSTALL_DIR..."
|
||||||
|
cp -R "$BUILT_APP" "$INSTALL_DIR/"
|
||||||
|
|
||||||
|
info "Launching Galaxy..."
|
||||||
|
open "$INSTALLED_APP"
|
||||||
|
|
||||||
|
info "Done! Galaxy is running."
|
||||||
Reference in New Issue
Block a user