Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
3425811
feat(flowchat): add persistent turn catalog navigation
wsp1911 Jul 31, 2026
1a8dfd4
feat(flow-chat): add windowed session turn loading
wsp1911 Aug 1, 2026
234b1f1
feat(flowchat): add windowed history presentation
wsp1911 Aug 1, 2026
01969ae
perf(flow-chat): virtualize turn rail markers
wsp1911 Aug 1, 2026
ada22a6
perf(flow-chat): hydrate full history on demand
wsp1911 Aug 1, 2026
d80f133
perf(flow-chat): bound loaded history with LRU eviction
wsp1911 Aug 1, 2026
5005558
perf(flow-chat): repair incomplete turn catalogs incrementally
wsp1911 Aug 1, 2026
a2728ff
fix(flow-chat): restore tail when submitting from history
wsp1911 Aug 1, 2026
f735d60
fix(flow-chat): make cross-feature turn navigation window-aware
wsp1911 Aug 1, 2026
7347239
fix(flow-chat): enable actions for windowed history turns
wsp1911 Aug 1, 2026
982550b
fix(flow-chat): persist incremental Turn catalog repairs
wsp1911 Aug 1, 2026
5057784
fix(flow-chat): release failed history window anchors
wsp1911 Aug 1, 2026
2199e5a
fix(flowchat): stabilize windowed history scrolling
wsp1911 Aug 1, 2026
14c2b6c
fix(flow-chat): unify history navigation on Virtuoso
wsp1911 Aug 1, 2026
1e05c00
fix(flow-chat): stabilize cached history reactivation
wsp1911 Aug 2, 2026
eb1ac58
refactor(flow-chat): preserve history projection at live tail
wsp1911 Aug 2, 2026
6d43e42
fix(flow-chat): invalidate cached history after rollback
wsp1911 Aug 2, 2026
1a04fcc
fix(session): separate local workspace identity from remote scope
wsp1911 Aug 2, 2026
b915c23
fix(flow-chat): stabilize turn navigation during virtualization
wsp1911 Aug 2, 2026
6ab6b44
Fix FlowChat initial projection horizontal shift
wsp1911 Aug 2, 2026
2b5a4fe
fix(flowchat): use best-effort alignment for turn navigation
wsp1911 Aug 2, 2026
71942a3
Merge remote-tracking branch 'upstream/main' into dev2
wsp1911 Aug 2, 2026
aaef036
fix(flow-chat): suspend geometry updates for inactive viewports
wsp1911 Aug 2, 2026
d3a9404
Merge remote-tracking branch 'upstream/main' into dev2
wsp1911 Aug 2, 2026
7d6fc06
fix(flow-chat): correct viewport callback dependencies
wsp1911 Aug 2, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions src/apps/cli/src/peer_host/commands/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,7 @@ pub(crate) async fn dispatch(
"list_persisted_sessions_count" => {
session::list_persisted_sessions_count(state, args).await
}
"load_session_turn_window" => session::load_session_turn_window(state, args).await,
"load_session_turns" => session::load_session_turns(state, args).await,
"restore_session_view" => session::restore_session_view(state, args).await,
"restore_session_with_turns" => session::restore_session_with_turns(state, args).await,
Expand Down
36 changes: 34 additions & 2 deletions src/apps/cli/src/peer_host/commands/session.rs
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ use bitfun_core::util::errors::BitFunError;
use bitfun_runtime_ports::{
AgentSessionArchiveRequest, AgentSessionCreateRequest, AgentSessionDeleteRequest,
AgentSessionModelUpdateRequest, AgentSessionRenameRequest, AgentThreadGoalGetRequest,
SessionStoragePathRequest,
SessionStoragePathRequest, SessionTurnWindowRequest,
};

use crate::diagnostics::{OUTCOME_UNKNOWN_ERROR_CODE, SESSION_IN_USE_ERROR_CODE};
Expand Down Expand Up @@ -200,6 +200,37 @@ pub(crate) async fn load_session_turns(
serde_json::to_value(turns).map_err(|e| format!("serialize turns: {e}"))
}

pub(crate) async fn load_session_turn_window(
state: &PeerHostState,
args: &Value,
) -> Result<Value, String> {
let request = request_value(args);
let session_id = validated_session_id(request)?;
let workspace_path = resolved_session_storage_path(state, request).await?;
let response = state
.compatibility
.load_session_turn_window_from_storage_path(
&workspace_path,
SessionTurnWindowRequest {
workspace_path: workspace_path.clone(),
session_id,
include_internal: optional_bool(request, "includeInternal").unwrap_or(false),
target_storage_turn_index: request
.get("targetStorageTurnIndex")
.and_then(Value::as_u64)
.ok_or_else(|| "targetStorageTurnIndex is required".to_string())?
as usize,
expected_turn_id: optional_string(request, "expectedTurnId"),
expected_catalog_revision: optional_string(request, "expectedCatalogRevision"),
before: request.get("before").and_then(Value::as_u64).unwrap_or(4) as usize,
after: request.get("after").and_then(Value::as_u64).unwrap_or(12) as usize,
},
)
.await
.map_err(|e| format!("Failed to load session Turn window: {e}"))?;
serde_json::to_value(response).map_err(|e| format!("serialize Turn window: {e}"))
}

pub(crate) async fn restore_session_view(
state: &PeerHostState,
args: &Value,
Expand All @@ -218,7 +249,7 @@ pub(crate) async fn restore_session_view(
.filter(|n| *n > 0)
.map(|n| n.min(16));

let (mut session, turns, total_turn_count, timings) = state
let (mut session, turns, total_turn_count, turn_catalog, timings) = state
.compatibility
.restore_session_view_for_workspace(
storage_request,
Expand All @@ -239,6 +270,7 @@ pub(crate) async fn restore_session_view(
Ok(json!({
"session": session_to_json(session, total_turn_count),
"turns": turns,
"turnCatalog": turn_catalog,
"contextRestoreState": "pending",
"isPartial": is_partial,
"loadedTurnCount": loaded_turn_count,
Expand Down
120 changes: 118 additions & 2 deletions src/apps/desktop/src/api/agentic_api.rs
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,7 @@ use bitfun_core::service::config::project_permission_store::{
use bitfun_core::service::remote_ssh::workspace_state::resolve_workspace_session_identity;
use bitfun_core::service::session::{
DialogTurnData, SessionMemoryMode, SessionMetadata, SessionRelationship,
SessionRelationshipKind,
SessionRelationshipKind, SessionTurnCatalog, SessionTurnWindowResponse,
};
use bitfun_core::service::workspace::WorkspaceKind;
use bitfun_core::service::workspace::{WorkspaceActivityMode, WorkspaceCreateOptions};
Expand All @@ -62,9 +62,12 @@ use bitfun_core_types::{
WorktreeError, WorktreeErrorCode,
};
use bitfun_product_domains::tool_permissions::PermissionRule;
use bitfun_runtime_ports::SessionTurnWindowRequest;

const SESSION_VIEW_TOOL_RESULT_TOTAL_CHAR_BUDGET: usize = 512 * 1024;
const SESSION_VIEW_TOOL_RESULT_STRING_CHAR_LIMIT: usize = 16 * 1024;
const SESSION_TURN_WINDOW_DEFAULT_BEFORE: usize = 4;
const SESSION_TURN_WINDOW_DEFAULT_AFTER: usize = 12;
const SESSION_VIEW_TRUNCATED_MARKER: &str = "\n... Output truncated for session preview";
const SESSION_VIEW_OMITTED_MARKER: &str = "Output omitted from session preview";

Expand Down Expand Up @@ -466,6 +469,7 @@ pub struct RestoreSessionWithTurnsResponse {
pub struct RestoreSessionViewResponse {
pub session: SessionResponse,
pub turns: Vec<DialogTurnData>,
pub turn_catalog: SessionTurnCatalog,
pub context_restore_state: String,
pub is_partial: bool,
pub loaded_turn_count: usize,
Expand Down Expand Up @@ -782,6 +786,28 @@ pub struct RestoreSessionRequest {
pub tail_turn_count: Option<usize>,
}

#[derive(Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct LoadSessionTurnWindowRequest {
pub session_id: String,
pub workspace_path: String,
#[serde(default)]
pub include_internal: bool,
pub target_storage_turn_index: usize,
#[serde(default)]
pub expected_turn_id: Option<String>,
#[serde(default)]
pub expected_catalog_revision: Option<String>,
#[serde(default)]
pub before: Option<usize>,
#[serde(default)]
pub after: Option<usize>,
#[serde(default)]
pub remote_connection_id: Option<String>,
#[serde(default)]
pub remote_ssh_host: Option<String>,
}

#[derive(Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct ListSessionsRequest {
Expand Down Expand Up @@ -2922,9 +2948,16 @@ pub async fn restore_session_view(
let session = restored.session;
let mut turns = restored.turns;
let total_turn_count = restored.total_turn_count;
let turn_catalog = restored.turn_catalog;
let timings = restored.timings;
let loaded_turn_count = turns.len();
let is_partial = loaded_turn_count < total_turn_count;
let turn_catalog_preview_chars = turn_catalog
.entries
.iter()
.filter_map(|entry| entry.preview.as_deref())
.map(|preview| preview.chars().count())
.sum::<usize>();

if log::log_enabled!(log::Level::Debug) {
let payload_stats = restore_turn_payload_stats(&turns);
Expand All @@ -2951,18 +2984,22 @@ pub async fn restore_session_view(
compact_tool_results_for_session_view(&mut turns);

debug!(
"restore_session_view completed: trace_id={}, session_id={}, turn_count={}, total_turn_count={}, is_partial={}, context_restore_state=pending, duration_ms={}",
"restore_session_view completed: trace_id={}, session_id={}, turn_count={}, total_turn_count={}, is_partial={}, turn_catalog_complete={}, turn_catalog_entry_count={}, turn_catalog_preview_chars={}, context_restore_state=pending, duration_ms={}",
trace_id,
request.session_id,
turns.len(),
total_turn_count,
is_partial,
turn_catalog.complete,
turn_catalog.entries.len(),
turn_catalog_preview_chars,
started_at.elapsed().as_millis()
);

Ok(RestoreSessionViewResponse {
session: session_to_response_with_turn_count(session, total_turn_count),
turns,
turn_catalog,
context_restore_state: "pending".to_string(),
is_partial,
loaded_turn_count,
Expand All @@ -2975,6 +3012,85 @@ pub async fn restore_session_view(
result
}

#[tauri::command]
pub async fn load_session_turn_window(
runtime: State<'_, DesktopRuntimeContext>,
startup_trace: State<'_, DesktopStartupTrace>,
request: LoadSessionTurnWindowRequest,
) -> Result<SessionTurnWindowResponse, String> {
let started_at = Instant::now();
let result = async {
debug!(
"load_session_turn_window request received: session_id={} target_storage_turn_index={} before={} after={}",
request.session_id,
request.target_storage_turn_index,
request.before.unwrap_or(SESSION_TURN_WINDOW_DEFAULT_BEFORE),
request.after.unwrap_or(SESSION_TURN_WINDOW_DEFAULT_AFTER)
);
let mut response = runtime
.session_application()
.load_session_turn_window(
desktop_session_scope(
request.workspace_path.clone(),
request.remote_connection_id.clone(),
request.remote_ssh_host.clone(),
),
SessionTurnWindowRequest {
workspace_path: PathBuf::from(&request.workspace_path),
session_id: request.session_id.clone(),
include_internal: request.include_internal,
target_storage_turn_index: request.target_storage_turn_index,
expected_turn_id: request.expected_turn_id.clone(),
expected_catalog_revision: request.expected_catalog_revision.clone(),
before: request.before.unwrap_or(SESSION_TURN_WINDOW_DEFAULT_BEFORE),
after: request.after.unwrap_or(SESSION_TURN_WINDOW_DEFAULT_AFTER),
},
)
.await
.map_err(|error| error.to_string())?;

if let Some(turns) = response.ready_turns_mut() {
compact_tool_results_for_session_view(turns);
}
match &response {
SessionTurnWindowResponse::Ready {
total_turn_count,
start_ordinal,
end_ordinal_exclusive,
turns,
..
} => debug!(
"load_session_turn_window completed: session_id={} status=ready target_storage_turn_index={} turn_count={} total_turn_count={} start_ordinal={} end_ordinal_exclusive={} duration_ms={}",
request.session_id,
request.target_storage_turn_index,
turns.len(),
total_turn_count,
start_ordinal,
end_ordinal_exclusive,
started_at.elapsed().as_millis()
),
SessionTurnWindowResponse::Stale { catalog } => debug!(
"load_session_turn_window completed: session_id={} status=stale target_storage_turn_index={} total_turn_count={} duration_ms={}",
request.session_id,
request.target_storage_turn_index,
catalog.total_turn_count,
started_at.elapsed().as_millis()
),
SessionTurnWindowResponse::NotFound { catalog } => debug!(
"load_session_turn_window completed: session_id={} status=not-found target_storage_turn_index={} total_turn_count={} duration_ms={}",
request.session_id,
request.target_storage_turn_index,
catalog.total_turn_count,
started_at.elapsed().as_millis()
),
}
Ok(response)
}
.await;
startup_trace.record_tauri_command_elapsed("load_session_turn_window", None, started_at);
result
}

#[tauri::command]
pub async fn restore_session_with_turns(
runtime: State<'_, DesktopRuntimeContext>,
Expand Down
4 changes: 4 additions & 0 deletions src/apps/desktop/src/api/remote_workspace_policy.rs
Original file line number Diff line number Diff line change
Expand Up @@ -916,6 +916,10 @@ pub const REMOTE_WORKSPACE_COMMAND_POLICIES: &[(&str, RemoteWorkspacePolicy)] =
"load_persisted_session_metadata",
RemoteWorkspacePolicy::LegacyUnaudited,
),
(
"load_session_turn_window",
RemoteWorkspacePolicy::RemoteRouted,
),
("load_session_turns", RemoteWorkspacePolicy::LegacyUnaudited),
(
"logout_subscription_account",
Expand Down
1 change: 1 addition & 0 deletions src/apps/desktop/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1145,6 +1145,7 @@ pub async fn run() {
api::agentic_api::delete_session,
api::agentic_api::restore_session,
api::agentic_api::restore_session_view,
api::agentic_api::load_session_turn_window,
api::agentic_api::restore_session_with_turns,
api::agentic_api::reset_memory,
api::agentic_api::get_memory_paths,
Expand Down
Loading
Loading