v1.3.0: Bedrock translator refactor, usage metrics, session restore fixes, and predefined rules

Major changes:

- **Bedrock translator architecture**: Extract orchestration logic from `impl.rs` into
  a dedicated `translator.rs` module. Rename `convert_request.rs` → `request_translator.rs`
  and `stream.rs` → `response_translator.rs` for clarity. Remove `tool_docs.rs` (inlined).
  Remove `fallback_to_warp` setting and server fallback path — Bedrock is now the sole backend.

- **Unknown tool handling**: The response translator now detects hallucinated/unknown tool
  calls from the model and synthesizes error tool_results so the conversation doesn't
  deadlock waiting for a result that will never come.

- **Usage display overhaul**: Replace credit-based usage display with detailed token metrics
  showing context window %, cache hit rate (read/write/miss), and estimated cost in dollars.
  Add `total_input_tokens`, `total_cache_read_tokens`, `total_cache_write_tokens`, and
  `cache_miss_tokens` accessors to `AIConversation`.

- **Predefined rules system**: Add `predefined_rules.rs` with 11 system-defined behavioral
  rules that are auto-seeded on first launch. Add "Add Predefined Rules" button to the
  Rules UI for re-adding them later. Track seeding state via `has_seeded_predefined_rules`
  setting.

- **Session restore improvements**: Rename database file from `warp.sqlite` to
  `galaxy.sqlite` with automatic migration from both same-directory and state_dir legacy
  paths. Improve CWD persistence by falling back to `session_startup_path` for agent-mode
  and fresh tabs. Add extensive session-save/restore logging.

- **Shell bootstrap rebrand**: Rename `WARP_INITIAL_WORKING_DIR` environment variable to
  `GALAXY_INITIAL_WORKING_DIR` across bash, zsh, and fish bootstrap scripts.

- **Model defaults**: Change default Bedrock model from Opus 4.7 to Opus 4.6.
  Add `context_window_for_model()` helper with model-aware context sizes.
  Remove `is_bedrock_model()` (no longer needed without server fallback).

- **User query persistence**: The response translator now emits a `UserQuery` proto message
  at stream start so the user's prompt persists across sessions for conversation titles.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Ryan Ward
2026-05-20 15:15:28 -05:00
co-authored by Claude Opus 4.6
parent ec99146ccc
commit eaa2ddc75e
38 changed files with 1687 additions and 1129 deletions
+1
View File
@@ -16,6 +16,7 @@ use galaxy_core::ui::appearance::Appearance;
use serde::{Deserialize, Serialize};
pub mod manager;
pub mod predefined_rules;
pub mod view;
pub use manager::AIFactManager;
pub use view::{AIFactView, AIFactViewEvent};
+53
View File
@@ -0,0 +1,53 @@
pub struct PredefinedRule {
pub name: &'static str,
pub content: &'static str,
}
pub const SYSTEM_DEFINED_RULE_PREFIX: &str = "System Defined Rule";
pub const PREDEFINED_RULES: &[PredefinedRule] = &[
PredefinedRule {
name: "System Defined Rule #1",
content: "Prioritize correctness, completeness, and reliability over speed.",
},
PredefinedRule {
name: "System Defined Rule #2",
content: "Never guess. If uncertain, explicitly say so and verify before finalizing.",
},
PredefinedRule {
name: "System Defined Rule #3",
content: "Ground non-trivial claims in evidence (repo files, command output, tests, official documentation).",
},
PredefinedRule {
name: "System Defined Rule #4",
content: "If confidence is not high, or if a claim depends on external/current behavior, perform web verification before answering; prioritize official docs and cross-check with at least one additional reliable source.",
},
PredefinedRule {
name: "System Defined Rule #5",
content: "Clearly separate facts, assumptions, and hypotheses.",
},
PredefinedRule {
name: "System Defined Rule #6",
content: "Ask clarifying questions when ambiguity could change the solution or implementation.",
},
PredefinedRule {
name: "System Defined Rule #7",
content: "For code changes, run relevant validations when available (tests, lint, typecheck, build) and report what was run, what passed/failed, and what was not run.",
},
PredefinedRule {
name: "System Defined Rule #8",
content: "If validation cannot be run, state that explicitly and describe residual risk and recommended manual checks.",
},
PredefinedRule {
name: "System Defined Rule #9",
content: "Prefer \"I don't know yet\" over plausible speculation.",
},
PredefinedRule {
name: "System Defined Rule #10",
content: "If the user's idea is wrong, incomplete, risky, or non-optimal, say so directly and respectfully; explain why it may fail and provide a better alternative that still achieves the user's goal.",
},
PredefinedRule {
name: "System Defined Rule #11",
content: "Do not hide uncertainty, and do not avoid technical disagreement when correctness is at stake.",
},
];
+102
View File
@@ -47,6 +47,7 @@ use std::fmt::Debug;
use std::path::PathBuf;
use super::{is_edit_allowed, is_syncing, style, AIFact, CloudAIFact, CloudAIFactModel};
use crate::ai::facts::predefined_rules::{PREDEFINED_RULES, SYSTEM_DEFINED_RULE_PREFIX};
use crate::ai::facts::AIMemory;
pub const HEADER_TEXT: &str = "Rules";
@@ -80,6 +81,7 @@ pub enum RuleViewEvent {
#[derive(Debug, Clone)]
pub enum RuleViewAction {
AddRule,
AddPredefinedRules,
InitializeProject,
Edit(SyncId),
OpenSettings,
@@ -151,6 +153,7 @@ pub struct RuleView {
search_editor: ViewHandle<EditorView>,
search_bar: ViewHandle<SearchBar>,
add_button: ViewHandle<ActionButton>,
add_predefined_rules_button: ViewHandle<ActionButton>,
initialize_button: ViewHandle<ActionButton>,
disabled_banner_highlight_index: HighlightedHyperlink,
current_scope: RuleScope,
@@ -265,12 +268,44 @@ impl RuleView {
.on_click(|ctx| ctx.dispatch_typed_action(RuleViewAction::AddRule))
});
let add_predefined_rules_button = ctx.add_typed_action_view(|_| {
ActionButton::new("Add Predefined Rules", NakedTheme)
.with_icon(Icon::Plus)
.on_click(|ctx| ctx.dispatch_typed_action(RuleViewAction::AddPredefinedRules))
});
let initialize_button = ctx.add_typed_action_view(|_| {
ActionButton::new("Initialize Project", NakedTheme)
.with_icon(Icon::Plus)
.on_click(|ctx| ctx.dispatch_typed_action(RuleViewAction::InitializeProject))
});
// Seed predefined rules on first launch if no global rules exist
if ai_rules.is_empty() && !AISettings::as_ref(ctx).has_seeded_predefined_rules() {
if let Some(owner) = owner {
let update_manager = UpdateManager::handle(ctx);
update_manager.update(ctx, |update_manager, ctx| {
for rule in PREDEFINED_RULES {
let ai_fact = AIFact::Memory(AIMemory {
is_autogenerated: false,
name: Some(rule.name.to_string()),
content: rule.content.to_string(),
suggested_logging_id: None,
});
update_manager.create_ai_fact(
ai_fact,
ClientId::default(),
owner,
ctx,
);
}
});
}
AISettings::handle(ctx).update(ctx, |settings, ctx| {
settings.mark_predefined_rules_seeded(ctx);
});
}
Self {
owner,
global_rules: ai_rules,
@@ -278,6 +313,7 @@ impl RuleView {
search_editor,
search_bar,
add_button,
add_predefined_rules_button,
initialize_button,
disabled_banner_highlight_index: Default::default(),
current_scope: RuleScope::Global,
@@ -418,6 +454,60 @@ impl RuleView {
});
}
pub fn add_predefined_rules(&mut self, ctx: &mut ViewContext<Self>) {
let Some(owner) = self.owner else {
return;
};
// Build a map of existing system-defined rules by name for update detection
let existing_system_rules: std::collections::HashMap<String, (SyncId, Option<Revision>)> =
self.global_rules
.iter()
.filter_map(|row| {
let AIFact::Memory(AIMemory { ref name, .. }) = row.fact.model().string_model;
let name = name.as_deref().unwrap_or_default();
if name.starts_with(SYSTEM_DEFINED_RULE_PREFIX) {
Some((
name.to_string(),
(row.fact.sync_id(), row.fact.metadata().revision.clone()),
))
} else {
None
}
})
.collect();
let update_manager = UpdateManager::handle(ctx);
update_manager.update(ctx, |update_manager, ctx| {
for rule in PREDEFINED_RULES {
let ai_fact = AIFact::Memory(AIMemory {
is_autogenerated: false,
name: Some(rule.name.to_string()),
content: rule.content.to_string(),
suggested_logging_id: None,
});
if let Some((sync_id, revision)) =
existing_system_rules.get(rule.name)
{
update_manager.update_ai_fact(
ai_fact,
*sync_id,
revision.clone(),
ctx,
);
} else {
update_manager.create_ai_fact(
ai_fact,
ClientId::default(),
owner,
ctx,
);
}
}
});
}
fn render_header(&self, appearance: &Appearance) -> Box<dyn Element> {
Flex::row()
.with_cross_axis_alignment(CrossAxisAlignment::Center)
@@ -617,11 +707,20 @@ impl RuleView {
.finish()
}
fn render_add_predefined_rules_button(&self) -> Box<dyn Element> {
Container::new(ChildView::new(&self.add_predefined_rules_button).finish())
.with_margin_left(style::SECTION_MARGIN)
.finish()
}
fn render_search_bar_row(&self, filtered_rules: &[RuleRow]) -> Box<dyn Element> {
let mut row = Flex::row()
.with_cross_axis_alignment(CrossAxisAlignment::Center)
.with_child(Expanded::new(1., ChildView::new(&self.search_bar).finish()).finish());
if self.current_scope == RuleScope::Global {
row.add_child(self.render_add_predefined_rules_button());
}
if !filtered_rules.is_empty() {
row.add_child(self.render_add_button());
}
@@ -945,6 +1044,9 @@ impl TypedActionView for RuleView {
RuleViewAction::AddRule => {
ctx.emit(RuleViewEvent::AddRule);
}
RuleViewAction::AddPredefinedRules => {
self.add_predefined_rules(ctx);
}
RuleViewAction::Edit(sync_id) => {
ctx.emit(RuleViewEvent::Edit(*sync_id));
}