Files
galaxy/app/src/ai/facts/view/mod.rs
T
Ryan WardandClaude Opus 4.6 6f54e2cb30 v1.4.0: Auto-compact streaming, Bedrock summarization support, subagent orchestration, and Galaxy rebrand continuation
Major features:
- Auto-compact: triggers conversation summarization when context window >= 85%,
  compacts Bedrock message history to a summary pair, and tracks live context tokens
- Bedrock summarization: plumbs `is_summarization` flag through translator/client/response
  pipeline, handles SummarizeConversation input type, and marks `summarized` in metadata
- Session restore: rebuilds bedrock_message_history from persisted task messages via
  newly-public `convert_proto_message`, preventing empty history on reconnect
- Subagent orchestration: adds SubagentQuestion/Answer/CompletionSummary event types,
  parent-child question routing with depth limits, retry counting, and drain methods
- Summarization UI: inline SummarizationView in AI blocks with progress/finished states

Refactors:
- Rename WarpTheme → GalaxyTheme across ~100 files (rebrand continuation)
- Rename warp_home_config_dir → galaxy_home_config_dir and related path functions
- Predefined rules: replace "System Defined Rule #N" with descriptive names
  (e.g. "Correctness Over Speed", "Never Guess") and add lookup helpers
- Usage view: replace cumulative input/output token display with live context tokens,
  cache hit rate calculation, and separate cache read/write stats
- Telemetry: remove verbose doc comments, simplify trait definitions
- Facts view: simplify delete permission check (always allow local deletion)
- Remove warp_managed_paths_watcher.rs (dead code)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-05-21 11:59:37 -05:00

357 lines
11 KiB
Rust

use crate::cloud_object::{CloudObject, CloudObjectSyncStatus};
use crate::network::NetworkStatus;
use crate::pane_group::focus_state::PaneFocusHandle;
use crate::pane_group::{pane::view, BackingView, PaneConfiguration, PaneEvent};
use crate::server::ids::SyncId;
use crate::server::sync_queue::SyncQueue;
use galaxy_core::ui::appearance::Appearance;
use galaxyui::{
elements::{
Align, ClippedScrollStateHandle, ClippedScrollable, ConstrainedBox, Container,
CrossAxisAlignment, Expanded, Flex, MainAxisAlignment, MainAxisSize, ParentElement,
ScrollbarWidth,
},
ui_components::components::UiComponent,
AppContext, Element, Entity, FocusContext, ModelHandle, TypedActionView, View, ViewContext,
};
use std::path::PathBuf;
use crate::ui_components::icons::Icon;
use galaxyui::elements::ChildView;
use galaxyui::{SingletonEntity, ViewHandle};
use super::{AIFact, CloudAIFact, CloudAIFactModel};
pub mod rule;
pub mod rule_editor;
mod style;
use rule::*;
use rule_editor::*;
const OFFLINE_TEXT: &str = "You are offline. Some rules will be read only.";
#[derive(Debug, Default, Copy, Clone, PartialEq, Eq)]
pub enum AIFactPage {
#[default]
Rules,
RuleEditor {
sync_id: Option<SyncId>,
},
}
impl std::fmt::Display for AIFactPage {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
AIFactPage::Rules => write!(f, "Rules"),
AIFactPage::RuleEditor { .. } => write!(f, "Rule Editor"),
}
}
}
#[derive(Debug, Clone)]
pub enum AIFactViewEvent {
Pane(PaneEvent),
OpenSettings,
OpenFile(PathBuf),
InitializeProject(PathBuf),
}
#[derive(Debug, Clone)]
pub enum AIFactViewAction {
AddRule,
UpdatePage(AIFactPage),
}
pub struct AIFactView {
pane_configuration: ModelHandle<PaneConfiguration>,
focus_handle: Option<PaneFocusHandle>,
current_page: AIFactPage,
rule_view: ViewHandle<RuleView>,
rule_editor_view: ViewHandle<RuleEditorView>,
clipped_scroll_state: ClippedScrollStateHandle,
}
impl AIFactView {
pub fn new(ctx: &mut ViewContext<Self>) -> Self {
let pane_configuration = ctx.add_model(|_ctx| PaneConfiguration::new(HEADER_TEXT));
let rule_view = ctx.add_typed_action_view(RuleView::new);
ctx.subscribe_to_view(&rule_view, |me, _, event, ctx| {
me.handle_rule_view_event(event, ctx);
});
let rule_editor_view = ctx.add_typed_action_view(RuleEditorView::new);
ctx.subscribe_to_view(&rule_editor_view, |me, _, event, ctx| {
me.handle_rule_editor_view_event(event, ctx);
});
Self {
pane_configuration,
focus_handle: None,
rule_editor_view,
rule_view,
current_page: AIFactPage::default(),
clipped_scroll_state: Default::default(),
}
}
pub fn pane_configuration(&self) -> ModelHandle<PaneConfiguration> {
self.pane_configuration.clone()
}
pub fn current_page(&self) -> AIFactPage {
self.current_page
}
pub fn focus(&mut self, ctx: &mut ViewContext<Self>) {
match self.current_page {
AIFactPage::Rules => ctx.focus(&self.rule_view),
AIFactPage::RuleEditor { .. } => ctx.focus(&self.rule_editor_view),
}
}
fn handle_rule_view_event(&mut self, event: &RuleViewEvent, ctx: &mut ViewContext<Self>) {
match event {
RuleViewEvent::AddRule => {
self.update_page(AIFactPage::RuleEditor { sync_id: None }, ctx);
}
RuleViewEvent::Edit(sync_id) => {
self.update_page(
AIFactPage::RuleEditor {
sync_id: Some(*sync_id),
},
ctx,
);
}
RuleViewEvent::OpenSettings => {
ctx.emit(AIFactViewEvent::OpenSettings);
}
RuleViewEvent::OpenFile(path) => {
ctx.emit(AIFactViewEvent::OpenFile(path.clone()));
}
RuleViewEvent::InitializeProject(path) => {
ctx.emit(AIFactViewEvent::InitializeProject(path.clone()));
}
}
}
fn handle_rule_editor_view_event(
&mut self,
event: &RuleEditorViewEvent,
ctx: &mut ViewContext<Self>,
) {
self.update_page(AIFactPage::Rules, ctx);
match event {
RuleEditorViewEvent::Add { name, content } => {
self.rule_view.update(ctx, |rule_view, ctx| {
rule_view.add_ai_rule(name.clone(), content.clone(), ctx);
});
}
RuleEditorViewEvent::Edit {
name,
content,
sync_id,
revision_ts,
} => {
self.rule_view.update(ctx, |rule_view, ctx| {
rule_view.edit_ai_rule(
name.clone(),
content.clone(),
*sync_id,
revision_ts.clone(),
ctx,
);
});
}
RuleEditorViewEvent::Delete { sync_id } => {
self.rule_view.update(ctx, |rule_view, ctx| {
rule_view.delete_ai_rule(*sync_id, ctx);
});
}
_ => {}
}
}
pub fn update_page(&mut self, page: AIFactPage, ctx: &mut ViewContext<Self>) {
self.current_page = page;
if let AIFactPage::RuleEditor { sync_id } = page {
self.rule_editor_view.update(ctx, |rule_editor_view, ctx| {
rule_editor_view.set_ai_rule(sync_id, ctx);
});
}
self.focus(ctx);
ctx.notify();
}
fn render_offline_banner(&self, appearance: &Appearance) -> Box<dyn Element> {
Container::new(
Flex::row()
.with_child(
ConstrainedBox::new(
Icon::CloudOffline
.to_galaxyui_icon(
appearance
.theme()
.sub_text_color(appearance.theme().surface_2()),
)
.finish(),
)
.with_width(style::ICON_SIZE)
.with_height(style::ICON_SIZE)
.finish(),
)
.with_child(
Expanded::new(
1.,
Container::new(
appearance
.ui_builder()
.wrappable_text(OFFLINE_TEXT, true)
.build()
.finish(),
)
.with_margin_left(style::ICON_MARGIN)
.finish(),
)
.finish(),
)
.with_main_axis_alignment(MainAxisAlignment::Center)
.with_cross_axis_alignment(CrossAxisAlignment::Center)
.finish(),
)
.with_background(appearance.theme().surface_2())
.with_vertical_padding(4.)
.with_horizontal_padding(style::PANE_PADDING)
.with_margin_bottom(style::ITEM_BOTTOM_MARGIN)
.finish()
}
}
impl Entity for AIFactView {
type Event = AIFactViewEvent;
}
impl View for AIFactView {
fn ui_name() -> &'static str {
"AIFactView"
}
fn on_focus(&mut self, focus_ctx: &FocusContext, ctx: &mut ViewContext<Self>) {
if focus_ctx.is_self_focused() {
match self.current_page {
AIFactPage::Rules => ctx.focus(&self.rule_view),
AIFactPage::RuleEditor { .. } => ctx.focus(&self.rule_editor_view),
}
}
}
fn render(&self, app: &AppContext) -> Box<dyn Element> {
let appearance = Appearance::as_ref(app);
let mut col = Flex::column().with_main_axis_size(MainAxisSize::Min);
if !is_online(app) {
col.add_child(self.render_offline_banner(appearance));
}
match self.current_page {
AIFactPage::Rules => col.add_child(ChildView::new(&self.rule_view).finish()),
AIFactPage::RuleEditor { .. } => {
col.add_child(ChildView::new(&self.rule_editor_view).finish())
}
}
ClippedScrollable::vertical(
self.clipped_scroll_state.clone(),
Align::new(
Container::new(
ConstrainedBox::new(col.finish())
.with_max_width(style::PANE_WIDTH)
.finish(),
)
.with_uniform_padding(style::PANE_PADDING)
.finish(),
)
.top_center()
.finish(),
ScrollbarWidth::Auto,
appearance.theme().nonactive_ui_detail().into(),
appearance.theme().active_ui_detail().into(),
galaxyui::elements::Fill::None,
)
.finish()
}
}
impl TypedActionView for AIFactView {
type Action = AIFactViewAction;
fn handle_action(&mut self, action: &AIFactViewAction, ctx: &mut ViewContext<Self>) {
match action {
AIFactViewAction::AddRule => {
self.rule_editor_view.update(ctx, |rule_editor_view, ctx| {
rule_editor_view.set_ai_rule(None, ctx);
});
self.update_page(AIFactPage::RuleEditor { sync_id: None }, ctx);
}
AIFactViewAction::UpdatePage(page) => self.update_page(*page, ctx),
}
}
}
impl BackingView for AIFactView {
type PaneHeaderOverflowMenuAction = AIFactViewAction;
type CustomAction = ();
type AssociatedData = ();
fn handle_pane_header_overflow_menu_action(
&mut self,
_action: &Self::PaneHeaderOverflowMenuAction,
_ctx: &mut galaxyui::ViewContext<Self>,
) {
self.handle_action(_action, _ctx)
}
fn close(&mut self, ctx: &mut galaxyui::ViewContext<Self>) {
ctx.emit(AIFactViewEvent::Pane(PaneEvent::Close));
}
fn focus_contents(&mut self, ctx: &mut galaxyui::ViewContext<Self>) {
self.focus(ctx);
}
fn render_header_content(
&self,
_ctx: &view::HeaderRenderContext<'_>,
_app: &AppContext,
) -> view::HeaderContent {
view::HeaderContent::simple(HEADER_TEXT)
}
fn set_focus_handle(&mut self, focus_handle: PaneFocusHandle, _ctx: &mut ViewContext<Self>) {
self.focus_handle = Some(focus_handle);
}
}
pub fn is_online(app: &AppContext) -> bool {
NetworkStatus::as_ref(app).is_online()
}
pub fn is_delete_allowed(_ai_fact: CloudAIFact, _app: &AppContext) -> bool {
true
}
pub fn is_edit_allowed(_ai_fact: CloudAIFact, _app: &AppContext) -> bool {
true
}
pub fn is_syncing(ai_fact: CloudAIFact, app: &AppContext) -> bool {
let sync_queue_is_dequeueing = SyncQueue::as_ref(app).is_dequeueing();
let sync_status = &ai_fact.metadata().pending_changes_statuses;
let has_in_flight_requests = matches!(
&sync_status.content_sync_status,
CloudObjectSyncStatus::InFlight(reqs) if reqs.0 > 0
);
(has_in_flight_requests && sync_queue_is_dequeueing)
|| sync_status.has_pending_metadata_change
|| sync_status.has_pending_permissions_change
|| sync_status.pending_untrash
}