Feedback about an unofficial community build belongs on its own issue tracker, not Moonshot's feedback endpoint — and this mirrors the official kimi-cli, whose /feedback opens its repo's issues page (ISSUE_URL in ui/shell/slash.py). /feedback now returns Action::OpenUrl(https://github.com/ZacharyZhang-NY/Kigi-CLI/issues), the same battle-tested browser path /docs web uses. The now-dead TUI text-feedback pipeline is excised: PromptInputMode:: Feedback (~ prefix composer mode), Action::{EnterFeedbackMode, SendFeedback}, Effect::SendFeedback (the kigi/feedback ACP POST), TaskResult::{FeedbackComplete,FeedbackFailed}, and their dispatchers. The shell-side kigi/feedback ACP extension stays: it is protocol surface for editor embeddings, OAuth-gated, and shared with kigi/btw. Gates: workspace check/clippy --all-targets 0/0, fmt, kigi-tui lib 6621 passed / 0 failed.
911 lines
35 KiB
Rust
911 lines
35 KiB
Rust
//! Async task-result application: routes task results into state.
|
|
use super::auth::{
|
|
ensure_login_method, handle_auth_complete, handle_auth_url_ready, handle_mcp_auth_trigger_done,
|
|
};
|
|
use super::ctx::{find_agent_by_session_id, get_active_agent_mut};
|
|
use super::notes::{handle_btw_response, handle_memory_note_saved};
|
|
use super::prompt::{
|
|
defer_to_open_reload_window, handle_compact_complete, handle_prompt_response,
|
|
handle_suggestion_debounce_expired,
|
|
};
|
|
use super::rewind::{
|
|
dispatch_rewind_success, handle_rewind_execute_failed, handle_rewind_points_loaded,
|
|
handle_rewind_preview_complete, handle_rewind_preview_failed,
|
|
};
|
|
use super::router::{dispatch, dispatch_action_result};
|
|
use super::session::foreign::{
|
|
handle_foreign_sessions_scanned, handle_session_list_failed, handle_session_list_loaded,
|
|
};
|
|
use super::session::fork::{
|
|
handle_fork_session_failed, handle_fork_session_ready, handle_worktree_forked,
|
|
};
|
|
use super::session::lifecycle::{
|
|
dispatch_exit_session, handle_session_created, handle_switch_model_complete,
|
|
handle_worktree_session_created, handle_worktree_session_failed,
|
|
};
|
|
use super::session::load::{
|
|
handle_card_detail_loaded, handle_deep_search_results, handle_session_load_failed,
|
|
handle_session_loaded, handle_session_restore_failed, handle_session_restored,
|
|
handle_session_search_debounce_expired, remove_session_from_pickers,
|
|
};
|
|
use super::settings::ui::apply_setting_rollback;
|
|
use super::status::{handle_context_info_complete, handle_usage_fetched, scrub_error_for_toast};
|
|
use super::transcript::{
|
|
handle_hooks_list_loaded, handle_mcp_toggle_done, handle_plugins_list_loaded,
|
|
handle_skills_toggle_done,
|
|
};
|
|
use super::turn::handle_bg_task_killed;
|
|
use crate::app::actions::{
|
|
ClipboardPasteCompletion, ClipboardPasteContext, ClipboardPasteFailure, ClipboardPasteTarget,
|
|
Effect, ProbedAttachment, SubagentKillOutcome, TaskResult,
|
|
};
|
|
use crate::app::app_view::{ActiveView, AppView, AuthState};
|
|
use crate::scrollback::block::RenderBlock;
|
|
use agent_client_protocol as acp;
|
|
pub(super) fn unregister_session_effect(session_id: Option<acp::SessionId>) -> Vec<Effect> {
|
|
session_id
|
|
.map(|sid| Effect::UnregisterActiveSession { session_id: sid })
|
|
.into_iter()
|
|
.collect()
|
|
}
|
|
pub(super) fn unregister_all_active_sessions(app: &AppView) -> Vec<Effect> {
|
|
app.agents
|
|
.values()
|
|
.filter_map(|a| {
|
|
a.session
|
|
.session_id
|
|
.as_ref()
|
|
.map(|sid| Effect::UnregisterActiveSession {
|
|
session_id: sid.clone(),
|
|
})
|
|
})
|
|
.collect()
|
|
}
|
|
pub(super) const X11_PRIMARY_PASTE_HINT: &str = "Try Shift+Insert to paste selected text";
|
|
fn show_clipboard_toast(target: &ClipboardPasteTarget, message: &str, app: &mut AppView) {
|
|
match target {
|
|
ClipboardPasteTarget::AgentPrompt { agent_id, .. } => {
|
|
if let Some(agent) = app.agents.get_mut(agent_id) {
|
|
agent.show_toast(message);
|
|
}
|
|
}
|
|
ClipboardPasteTarget::DashboardDispatch | ClipboardPasteTarget::DashboardPeek { .. } => {
|
|
if let Some(dashboard) = app.dashboard.as_mut() {
|
|
dashboard.error_toast = Some(message.to_owned());
|
|
}
|
|
}
|
|
}
|
|
}
|
|
pub(super) fn maybe_show_x11_primary_paste_hint(
|
|
eligible: bool,
|
|
completion: ClipboardPasteCompletion,
|
|
target: &ClipboardPasteTarget,
|
|
app: &mut AppView,
|
|
) {
|
|
if !eligible || completion != ClipboardPasteCompletion::FullMiss {
|
|
return;
|
|
}
|
|
show_clipboard_toast(target, X11_PRIMARY_PASTE_HINT, app);
|
|
}
|
|
pub(super) fn show_clipboard_failure(
|
|
target: &ClipboardPasteTarget,
|
|
failure: ClipboardPasteFailure,
|
|
app: &mut AppView,
|
|
) {
|
|
let message = match failure {
|
|
ClipboardPasteFailure::AlreadyReported => return,
|
|
ClipboardPasteFailure::TextRead => "Couldn't read clipboard text",
|
|
ClipboardPasteFailure::AttachmentRead => "Couldn't read clipboard contents",
|
|
ClipboardPasteFailure::TargetInsertion => "Couldn't paste clipboard contents",
|
|
};
|
|
show_clipboard_toast(target, message, app);
|
|
}
|
|
fn apply_clipboard_paste_result(
|
|
ctx: ClipboardPasteContext,
|
|
image: ProbedAttachment,
|
|
file_urls: Option<String>,
|
|
app: &mut AppView,
|
|
) -> ClipboardPasteCompletion {
|
|
match ctx.target.clone() {
|
|
ClipboardPasteTarget::AgentPrompt { agent_id, .. } => app
|
|
.agents
|
|
.get_mut(&agent_id)
|
|
.map_or(ClipboardPasteCompletion::Dropped, |agent| {
|
|
agent.complete_clipboard_attachment_paste(ctx, image, file_urls)
|
|
}),
|
|
ClipboardPasteTarget::DashboardDispatch | ClipboardPasteTarget::DashboardPeek { .. } => app
|
|
.dashboard
|
|
.as_mut()
|
|
.map_or(ClipboardPasteCompletion::Dropped, |dashboard| {
|
|
dashboard.complete_clipboard_attachment_paste(ctx, image, file_urls)
|
|
}),
|
|
}
|
|
}
|
|
fn drain_clipboard_target(target: &ClipboardPasteTarget, app: &mut AppView) -> Vec<Effect> {
|
|
match target {
|
|
ClipboardPasteTarget::AgentPrompt { agent_id, .. } => {
|
|
let is_active = app.active_view == ActiveView::Agent(*agent_id);
|
|
let Some(agent) = app.agents.get_mut(agent_id) else {
|
|
return vec![];
|
|
};
|
|
let resend = agent.take_deferred_send_after_paste();
|
|
let action = if is_active {
|
|
resend.and_then(|kind| agent.build_deferred_send_action(kind))
|
|
} else {
|
|
None
|
|
};
|
|
let mut effects = std::mem::take(&mut agent.pending_effects);
|
|
if let Some(action) = action {
|
|
effects.extend(dispatch(action, app));
|
|
}
|
|
effects
|
|
}
|
|
ClipboardPasteTarget::DashboardDispatch | ClipboardPasteTarget::DashboardPeek { .. } => {
|
|
let Some(dashboard) = app.dashboard.as_mut() else {
|
|
return vec![];
|
|
};
|
|
let resends = dashboard.take_deferred_sends_after_paste();
|
|
let mut effects = std::mem::take(&mut dashboard.pending_effects);
|
|
if matches!(app.active_view, ActiveView::AgentDashboard) {
|
|
for action in resends {
|
|
effects.extend(dispatch(action, app));
|
|
}
|
|
}
|
|
effects
|
|
}
|
|
}
|
|
}
|
|
/// Handle a completed async task result.
|
|
pub(super) fn dispatch_task_result(result: TaskResult, app: &mut AppView) -> Vec<Effect> {
|
|
match result {
|
|
TaskResult::SessionCreated {
|
|
agent_id,
|
|
session_id,
|
|
models: new_models,
|
|
} => handle_session_created(app, agent_id, session_id, new_models),
|
|
TaskResult::SessionFailed { agent_id, error } => {
|
|
tracing::error!(
|
|
agent = ? agent_id, error = % error, "Session creation failed"
|
|
);
|
|
if let Some(agent) = app.agents.get_mut(&agent_id) {
|
|
agent.pending_extensions_fetch = false;
|
|
agent.session.prompt_history_loading = false;
|
|
}
|
|
vec![]
|
|
}
|
|
TaskResult::WorktreeSessionCreated {
|
|
agent_id,
|
|
session_id,
|
|
worktree_path,
|
|
session_cwd,
|
|
models: new_models,
|
|
} => handle_worktree_session_created(
|
|
app,
|
|
agent_id,
|
|
session_id,
|
|
worktree_path,
|
|
session_cwd,
|
|
new_models,
|
|
),
|
|
TaskResult::WorktreeForked {
|
|
agent_id,
|
|
session_id,
|
|
worktree_path,
|
|
session_cwd,
|
|
code_restored,
|
|
restore_summary,
|
|
restore_degree,
|
|
} => handle_worktree_forked(
|
|
app,
|
|
agent_id,
|
|
session_id,
|
|
worktree_path,
|
|
session_cwd,
|
|
code_restored,
|
|
restore_summary,
|
|
restore_degree,
|
|
),
|
|
TaskResult::WorktreeSessionFailed { agent_id, error } => {
|
|
handle_worktree_session_failed(app, agent_id, error)
|
|
}
|
|
TaskResult::ForkSessionReady {
|
|
agent_id,
|
|
new_session_id,
|
|
cwd,
|
|
} => handle_fork_session_ready(app, agent_id, new_session_id, cwd),
|
|
TaskResult::ForkSessionFailed { agent_id, error } => {
|
|
handle_fork_session_failed(app, agent_id, error)
|
|
}
|
|
TaskResult::UsageFetched { agent_id, result } => {
|
|
handle_usage_fetched(app, agent_id, result)
|
|
}
|
|
TaskResult::SessionLoaded {
|
|
agent_id,
|
|
session_id,
|
|
models: new_models,
|
|
code_restored,
|
|
restore_summary,
|
|
restore_degree,
|
|
running_prompt_id,
|
|
} => handle_session_loaded(
|
|
app,
|
|
agent_id,
|
|
session_id,
|
|
new_models,
|
|
code_restored,
|
|
restore_summary,
|
|
restore_degree,
|
|
running_prompt_id,
|
|
),
|
|
TaskResult::SessionTitleFromDisk { agent_id, title } => {
|
|
if let Some(agent) = app.agents.get_mut(&agent_id)
|
|
&& let Some((t, is_manual)) = title.filter(|(s, _)| !s.trim().is_empty())
|
|
{
|
|
if is_manual && agent.display_name.is_none() {
|
|
agent.display_name = Some(t.clone());
|
|
}
|
|
agent.generated_session_title = Some(t);
|
|
}
|
|
vec![]
|
|
}
|
|
TaskResult::SessionLoadFailed {
|
|
agent_id,
|
|
session_id,
|
|
error,
|
|
} => handle_session_load_failed(app, agent_id, session_id, error),
|
|
TaskResult::SessionListLoaded {
|
|
sessions,
|
|
seq,
|
|
query,
|
|
} => handle_session_list_loaded(app, sessions, seq, query),
|
|
TaskResult::ForeignSessionsScanned { entries, seq } => {
|
|
handle_foreign_sessions_scanned(app, entries, seq)
|
|
}
|
|
TaskResult::ForeignResumeCwdCanonicalized {
|
|
requested_cwd,
|
|
canonical_cwd,
|
|
launch_token,
|
|
} => {
|
|
let accepted_cwd = canonical_cwd.clone();
|
|
if app.accept_foreign_resume_canonical_cwd(launch_token, &requested_cwd, canonical_cwd)
|
|
&& let Some(canonical_cwd) = accepted_cwd
|
|
{
|
|
vec![Effect::DetectForeignResumeHint {
|
|
canonical_cwd,
|
|
compat: app.foreign_session_compat,
|
|
kigi_home: kigi_tools::util::kigi_home::kigi_home(),
|
|
launch_token,
|
|
}]
|
|
} else {
|
|
vec![]
|
|
}
|
|
}
|
|
TaskResult::ForeignResumeHintDetected {
|
|
canonical_cwd,
|
|
launch_token,
|
|
hint,
|
|
} => {
|
|
app.apply_foreign_resume_detection(launch_token, &canonical_cwd, hint);
|
|
vec![]
|
|
}
|
|
TaskResult::SessionListFailed { error, seq, query } => {
|
|
handle_session_list_failed(app, error, seq, query)
|
|
}
|
|
TaskResult::SessionSearchDebounceExpired { query, seq } => {
|
|
handle_session_search_debounce_expired(app, query, seq)
|
|
}
|
|
TaskResult::RosterLoaded { sessions } => {
|
|
app.leader_roster = sessions;
|
|
vec![]
|
|
}
|
|
TaskResult::RosterFailed { error } => {
|
|
tracing::debug!(error = % error, "leader roster fetch failed");
|
|
vec![]
|
|
}
|
|
TaskResult::DashboardSessionsLoaded { sessions } => {
|
|
app.dashboard_local_sessions = sessions;
|
|
app.dashboard_sessions_loading = false;
|
|
vec![]
|
|
}
|
|
TaskResult::CardDetailLoaded {
|
|
source,
|
|
session_id,
|
|
generation,
|
|
detail,
|
|
} => handle_card_detail_loaded(app, source, session_id, generation, detail),
|
|
TaskResult::SessionRestored {
|
|
agent_id,
|
|
local_session_id,
|
|
} => handle_session_restored(app, agent_id, local_session_id),
|
|
TaskResult::SessionRestoreFailed { agent_id, error } => {
|
|
handle_session_restore_failed(app, agent_id, error)
|
|
}
|
|
TaskResult::SessionRestoreProgress { agent_id, message } => {
|
|
if let Some(agent) = app.agents.get_mut(&agent_id)
|
|
&& !defer_to_open_reload_window(agent, agent_id, "SessionRestoreProgress")
|
|
{
|
|
agent.scrollback.push_block(RenderBlock::system(message));
|
|
}
|
|
vec![]
|
|
}
|
|
TaskResult::PromptResponse {
|
|
agent_id,
|
|
result,
|
|
http_status,
|
|
prompt_id,
|
|
} => handle_prompt_response(app, agent_id, result, http_status, prompt_id),
|
|
TaskResult::SendPromptNowFailed {
|
|
agent_id,
|
|
session_id,
|
|
prompt_id,
|
|
error,
|
|
blocks,
|
|
} => {
|
|
let sid = session_id.0.to_string();
|
|
super::queue::retire_optimistic_echo(
|
|
&mut app.optimistic_prompt_echoes,
|
|
&mut app.shared_prompt_queues,
|
|
&sid,
|
|
&prompt_id,
|
|
);
|
|
if let Some(agent) = app.agents.get_mut(&agent_id) {
|
|
agent.shared_queue.retain(|e| e.id != prompt_id);
|
|
agent.note_queue_echo_retired(&prompt_id);
|
|
if agent.expect_send_now_cancel.as_deref() == Some(prompt_id.as_str())
|
|
|| agent.follow_without_jump_prompt_id.as_deref() == Some(prompt_id.as_str())
|
|
{
|
|
agent.clear_send_now_expectation();
|
|
}
|
|
agent.retire_send_now_painted_block(&prompt_id);
|
|
let text = blocks
|
|
.iter()
|
|
.find_map(|b| match b {
|
|
acp::ContentBlock::Text(t) => Some(t.text.clone()),
|
|
_ => None,
|
|
})
|
|
.unwrap_or_default();
|
|
let id = agent.session.next_queue_id;
|
|
agent.session.next_queue_id += 1;
|
|
agent
|
|
.session
|
|
.pending_prompts
|
|
.push_front(crate::app::agent::QueuedPrompt {
|
|
wire_blocks: Some(blocks),
|
|
..crate::app::agent::QueuedPrompt::plain(
|
|
id,
|
|
&text,
|
|
crate::app::agent::QueueEntryKind::Prompt,
|
|
)
|
|
});
|
|
agent.show_toast(&format!("Send now failed — requeued: {error}"));
|
|
}
|
|
vec![]
|
|
}
|
|
TaskResult::PreferredModelPersisted { result } => {
|
|
if let Err(err) = result
|
|
&& let Some(agent) = get_active_agent_mut(app)
|
|
{
|
|
agent.scrollback.push_block(RenderBlock::system(format!(
|
|
"Couldn't save preferred model: {err} (still active for this session)"
|
|
)));
|
|
}
|
|
vec![]
|
|
}
|
|
TaskResult::CancelComplete => {
|
|
tracing::trace!("Cancel notification sent successfully");
|
|
vec![]
|
|
}
|
|
TaskResult::KillSubagentComplete {
|
|
session_id,
|
|
subagent_id,
|
|
outcome,
|
|
} => {
|
|
if let SubagentKillOutcome::NothingLive { status } = outcome {
|
|
let status = status.as_deref().unwrap_or("cancelled");
|
|
crate::app::acp_handler::finalize_killed_subagent(
|
|
app,
|
|
&session_id,
|
|
&subagent_id,
|
|
status,
|
|
);
|
|
}
|
|
vec![]
|
|
}
|
|
TaskResult::CompactComplete { agent_id, result } => {
|
|
handle_compact_complete(app, agent_id, result)
|
|
}
|
|
TaskResult::SwitchModelComplete {
|
|
agent_id,
|
|
model_id,
|
|
effort,
|
|
result,
|
|
prev_model_id,
|
|
} => handle_switch_model_complete(app, agent_id, model_id, effort, result, prev_model_id),
|
|
TaskResult::BgTaskKilled {
|
|
session_id,
|
|
task_id,
|
|
outcome,
|
|
} => handle_bg_task_killed(app, session_id, task_id, outcome),
|
|
TaskResult::BgTaskKillFailed {
|
|
session_id,
|
|
task_id,
|
|
error,
|
|
} => {
|
|
tracing::warn!(
|
|
task_id = % task_id, error = % error, "Failed to kill bg task"
|
|
);
|
|
if let Some(agent) = find_agent_by_session_id(&mut app.agents, &session_id)
|
|
&& let Some(task) = agent.session.bg_tasks.get_mut(&task_id)
|
|
{
|
|
task.pending_kill = false;
|
|
task.kill_requested_at = None;
|
|
}
|
|
vec![]
|
|
}
|
|
TaskResult::ChangelogFetched { markdown, entries } => {
|
|
app.changelog_markdown = markdown;
|
|
app.changelog_bullets = kigi_shell::util::changelog::bullets_from_entries(&entries, 3);
|
|
vec![]
|
|
}
|
|
TaskResult::ClipboardAttachmentProbed {
|
|
ctx,
|
|
image,
|
|
file_urls,
|
|
} => {
|
|
let is_clipboard_key = ctx.source.is_clipboard_key();
|
|
let primary_hint_eligible = is_clipboard_key
|
|
&& !app.screen_mode.is_minimal()
|
|
&& crate::clipboard::x11_primary_guidance_available();
|
|
let target = ctx.target.clone();
|
|
let wrap_text = if is_clipboard_key {
|
|
ctx.source.text().map(str::to_owned)
|
|
} else {
|
|
None
|
|
};
|
|
let completion = apply_clipboard_paste_result(ctx, image, file_urls, app);
|
|
let wrap_request_emitted = completion == ClipboardPasteCompletion::FullMiss
|
|
&& is_clipboard_key
|
|
&& crate::wrap_clipboard_image::maybe_request_wrap_host_image(
|
|
None,
|
|
wrap_text.as_deref(),
|
|
None,
|
|
);
|
|
let effects = drain_clipboard_target(&target, app);
|
|
maybe_show_x11_primary_paste_hint(
|
|
primary_hint_eligible && !wrap_request_emitted,
|
|
completion,
|
|
&target,
|
|
app,
|
|
);
|
|
if let ClipboardPasteCompletion::Failed(failure) = completion {
|
|
show_clipboard_failure(&target, failure, app);
|
|
}
|
|
effects
|
|
}
|
|
TaskResult::PromptImagePreviewPrepared => vec![],
|
|
TaskResult::PromptHistoryLoaded { agent_id, prompts } => {
|
|
use kigi_tools::implementations::skills::skill::extract_skill_display_text;
|
|
if let Some(agent) = app.agents.get_mut(&agent_id) {
|
|
agent.session.prompt_history_loading = false;
|
|
agent.session.prompt_history = prompts
|
|
.into_iter()
|
|
.map(|p| extract_skill_display_text(&p).unwrap_or(p))
|
|
.collect();
|
|
if agent.prompt.history_search.is_active() {
|
|
let history = agent.combined_prompt_history();
|
|
agent.prompt.history_search.refresh_items(&history);
|
|
if !agent.prompt.history_search.is_browse() {
|
|
let query = agent.prompt.text().to_owned();
|
|
agent.prompt.history_search.update_query(&query);
|
|
}
|
|
}
|
|
}
|
|
vec![]
|
|
}
|
|
TaskResult::AuthComplete { request_seq, meta } => {
|
|
handle_auth_complete(app, request_seq, meta)
|
|
}
|
|
TaskResult::AuthFailed { request_seq, error } => {
|
|
if let AuthState::Authenticating {
|
|
request_seq: current_seq,
|
|
..
|
|
} = &app.auth_state
|
|
&& *current_seq == request_seq
|
|
{
|
|
app.auth_state = AuthState::Pending { error: Some(error) };
|
|
app.auth_code_input.clear();
|
|
}
|
|
vec![]
|
|
}
|
|
TaskResult::AuthUrlReady {
|
|
request_seq,
|
|
auth_url,
|
|
external,
|
|
mode,
|
|
} => handle_auth_url_ready(app, request_seq, auth_url, external, mode),
|
|
TaskResult::AuthCodeSubmitted { .. } => vec![],
|
|
TaskResult::McpsListLoaded { agent_id, result } => {
|
|
use crate::views::extensions_modal::TabDataState;
|
|
if let Some(agent) = app.agents.get_mut(&agent_id)
|
|
&& let Some(ref mut modal) = agent.extensions_modal
|
|
{
|
|
modal.pending_action = None;
|
|
modal.pending_entry_index = None;
|
|
modal.mcps_data = match result {
|
|
Ok(response) => TabDataState::Loaded(response),
|
|
Err(e) => TabDataState::Error(e),
|
|
};
|
|
}
|
|
vec![]
|
|
}
|
|
TaskResult::McpAuthTriggerDone {
|
|
agent_id,
|
|
server_name,
|
|
result,
|
|
} => handle_mcp_auth_trigger_done(app, agent_id, server_name, result),
|
|
TaskResult::HooksListLoaded { agent_id, result } => {
|
|
handle_hooks_list_loaded(app, agent_id, result)
|
|
}
|
|
TaskResult::PluginsListLoaded { agent_id, result } => {
|
|
handle_plugins_list_loaded(app, agent_id, result)
|
|
}
|
|
TaskResult::HooksActionResult { agent_id, result }
|
|
| TaskResult::PluginsActionResult { agent_id, result } => {
|
|
dispatch_action_result(app, agent_id, result)
|
|
}
|
|
TaskResult::McpToggleDone { agent_id, result } => {
|
|
handle_mcp_toggle_done(app, agent_id, result)
|
|
}
|
|
TaskResult::SkillsListLoaded { agent_id, result } => {
|
|
use crate::views::extensions_modal::TabDataState;
|
|
if let Some(agent) = app.agents.get_mut(&agent_id)
|
|
&& let Some(ref mut modal) = agent.extensions_modal
|
|
{
|
|
modal.skills_data = match result {
|
|
Ok(skills) => TabDataState::Loaded(skills),
|
|
Err(e) => TabDataState::Error(e),
|
|
};
|
|
modal.pending_action = None;
|
|
modal.pending_entry_index = None;
|
|
}
|
|
vec![]
|
|
}
|
|
TaskResult::SkillsToggleDone { agent_id, result } => {
|
|
handle_skills_toggle_done(app, agent_id, result)
|
|
}
|
|
TaskResult::SessionAgentNameResolved {
|
|
agent_id,
|
|
agent_name,
|
|
} => {
|
|
if let Some(agent) = app.agents.get_mut(&agent_id) {
|
|
agent.session_agent_name = agent_name.clone();
|
|
if let Some(modal) = agent.agents_modal.as_mut() {
|
|
modal.active_agent = agent_name;
|
|
}
|
|
}
|
|
vec![]
|
|
}
|
|
TaskResult::SessionInfoComplete {
|
|
agent_id,
|
|
info,
|
|
text,
|
|
} => {
|
|
if let Some(agent) = app.agents.get_mut(&agent_id) {
|
|
agent.session_agent_name = info.data.agent_name.clone();
|
|
if let Some(modal) = agent.agents_modal.as_mut() {
|
|
modal.active_agent = info.data.agent_name.clone();
|
|
}
|
|
agent.apply_full_context_info(info.data.context);
|
|
agent
|
|
.scrollback
|
|
.push_block(crate::scrollback::block::RenderBlock::system(text));
|
|
}
|
|
vec![]
|
|
}
|
|
TaskResult::SessionInfoFailed { agent_id, error } => {
|
|
if let Some(agent) = app.agents.get_mut(&agent_id) {
|
|
agent
|
|
.scrollback
|
|
.push_block(crate::scrollback::block::RenderBlock::system(format!(
|
|
"Couldn't load session info: {error}"
|
|
)));
|
|
}
|
|
vec![]
|
|
}
|
|
TaskResult::RenameSessionComplete { agent_id, title } => {
|
|
if let Some(agent) = app.agents.get_mut(&agent_id) {
|
|
let safe = crate::views::session_title::sanitize_display_text(&title);
|
|
agent
|
|
.scrollback
|
|
.push_block(crate::scrollback::block::RenderBlock::system(format!(
|
|
"Session renamed to \"{safe}\""
|
|
)));
|
|
}
|
|
vec![]
|
|
}
|
|
TaskResult::RenameSessionFailed { agent_id, error } => {
|
|
if let Some(agent) = app.agents.get_mut(&agent_id) {
|
|
agent
|
|
.scrollback
|
|
.push_block(crate::scrollback::block::RenderBlock::system(format!(
|
|
"Couldn't rename session: {error}"
|
|
)));
|
|
}
|
|
vec![]
|
|
}
|
|
TaskResult::DeleteSessionComplete { source, session_id } => {
|
|
remove_session_from_pickers(app, &source, &session_id);
|
|
app.show_toast("Session deleted");
|
|
vec![]
|
|
}
|
|
TaskResult::DeleteSessionFailed {
|
|
source,
|
|
session_id,
|
|
error,
|
|
} => {
|
|
tracing::warn!(
|
|
source, session_id = % session_id, error = % error,
|
|
"session delete failed"
|
|
);
|
|
app.show_toast(&format!("Couldn't delete session: {error}"));
|
|
vec![]
|
|
}
|
|
TaskResult::ContextInfoComplete { agent_id, info } => {
|
|
handle_context_info_complete(app, agent_id, info)
|
|
}
|
|
TaskResult::ContextInfoFailed { agent_id, error } => {
|
|
if let Some(agent) = app.agents.get_mut(&agent_id) {
|
|
agent
|
|
.scrollback
|
|
.push_block(crate::scrollback::block::RenderBlock::system(format!(
|
|
"Couldn't load context info: {error}"
|
|
)));
|
|
}
|
|
vec![]
|
|
}
|
|
TaskResult::MemoryNoteSaved { agent_id, result } => {
|
|
handle_memory_note_saved(app, agent_id, result)
|
|
}
|
|
TaskResult::MemoryNoteRewritten {
|
|
agent_id,
|
|
result,
|
|
nonce,
|
|
} => {
|
|
if let Some(agent) = app.agents.get_mut(&agent_id)
|
|
&& let Ok(markdown) = result
|
|
&& let Some(crate::views::modal::ActiveModal::RememberNoteReview {
|
|
ref mut enhanced_content,
|
|
ref mut cached_lines,
|
|
rewrite_nonce,
|
|
..
|
|
}) = agent.active_modal
|
|
&& rewrite_nonce == nonce
|
|
{
|
|
*enhanced_content = Some(markdown);
|
|
*cached_lines = None;
|
|
}
|
|
vec![]
|
|
}
|
|
TaskResult::BundleStatusReady {
|
|
has_cache,
|
|
version,
|
|
personas,
|
|
roles,
|
|
agents,
|
|
skills,
|
|
persona_details,
|
|
role_details,
|
|
} => {
|
|
app.bundle_state.has_cache = has_cache;
|
|
app.bundle_state.version = version.unwrap_or_default();
|
|
app.bundle_state.personas = personas;
|
|
app.bundle_state.roles = roles;
|
|
app.bundle_state.agents = agents;
|
|
app.bundle_state.skills = skills;
|
|
app.bundle_state.persona_details = persona_details;
|
|
app.bundle_state.role_details = role_details;
|
|
vec![]
|
|
}
|
|
TaskResult::BundleStatusFailed { error } => {
|
|
tracing::warn!(error = % error, "bundle status fetch failed");
|
|
vec![]
|
|
}
|
|
TaskResult::CatalogEntryReady {
|
|
kind,
|
|
name,
|
|
content,
|
|
} => {
|
|
if let ActiveView::Agent(id) = app.active_view
|
|
&& let Some(agent) = app.agents.get_mut(&id)
|
|
{
|
|
let title = format!("{kind}: {name}");
|
|
agent.block_viewer = Some(
|
|
crate::views::block_viewer::BlockViewerPane::for_plain_text(&title, &content),
|
|
);
|
|
}
|
|
vec![]
|
|
}
|
|
TaskResult::CatalogEntryFailed { error } => {
|
|
tracing::warn!(error = % error, "catalog entry fetch failed");
|
|
if let ActiveView::Agent(id) = app.active_view
|
|
&& let Some(agent) = app.agents.get_mut(&id)
|
|
{
|
|
agent
|
|
.scrollback
|
|
.push_block(RenderBlock::system(format!("Couldn't load entry: {error}")));
|
|
}
|
|
vec![]
|
|
}
|
|
TaskResult::BtwResponse { agent_id, result } => handle_btw_response(app, agent_id, result),
|
|
TaskResult::InterjectQueued { .. } => vec![],
|
|
TaskResult::RecapRequested {
|
|
session_id,
|
|
auto,
|
|
error,
|
|
} => {
|
|
if let Some(error) = error {
|
|
tracing::debug!(% error, "recap request failed");
|
|
if !auto
|
|
&& let Some(agent) = find_agent_by_session_id(&mut app.agents, &session_id.0)
|
|
&& let Some(pending_id) = agent.pending_recap_entry.take()
|
|
{
|
|
agent.scrollback.remove_entry(pending_id);
|
|
agent.show_toast(super::recap_unavailable_toast(
|
|
super::scrollback_has_user_messages(&agent.scrollback),
|
|
));
|
|
}
|
|
}
|
|
vec![]
|
|
}
|
|
TaskResult::InterjectFailed {
|
|
agent_id,
|
|
error,
|
|
text,
|
|
blocks,
|
|
} => {
|
|
if let Some(agent) = app.agents.get_mut(&agent_id) {
|
|
let id = agent.session.next_queue_id;
|
|
agent.session.next_queue_id += 1;
|
|
agent
|
|
.session
|
|
.pending_prompts
|
|
.push_front(crate::app::agent::QueuedPrompt {
|
|
id,
|
|
text,
|
|
kind: crate::app::agent::QueueEntryKind::Prompt,
|
|
wire_blocks: blocks,
|
|
images: Vec::new(),
|
|
display_as_skill: false,
|
|
task_id: None,
|
|
human_schedule: None,
|
|
chip_elements: Vec::new(),
|
|
skill_token_ranges: Vec::new(),
|
|
});
|
|
agent.show_toast(&format!("Interjection failed — requeued: {error}"));
|
|
}
|
|
vec![]
|
|
}
|
|
TaskResult::AvailableCommandsRefreshed { agent_id, commands } => {
|
|
if !commands.is_empty()
|
|
&& let Some(agent) = app.agents.get_mut(&agent_id)
|
|
{
|
|
agent.session.available_commands = commands;
|
|
agent.session.available_commands_generation += 1;
|
|
}
|
|
vec![]
|
|
}
|
|
TaskResult::AuthCopiedTimeout => {
|
|
app.auth_clipboard_copied = false;
|
|
vec![]
|
|
}
|
|
TaskResult::LogoutComplete => {
|
|
app.auth_state = AuthState::Pending { error: None };
|
|
app.login_method_id = None;
|
|
ensure_login_method(app);
|
|
app.auth_clipboard_copied = false;
|
|
let effects = dispatch_exit_session(app);
|
|
app.welcome_prompt_focused = false;
|
|
effects
|
|
}
|
|
TaskResult::DeepSearchResults { results, seq } => {
|
|
handle_deep_search_results(app, results, seq)
|
|
}
|
|
TaskResult::RewindPointsLoaded { agent_id, points } => {
|
|
handle_rewind_points_loaded(app, agent_id, points)
|
|
}
|
|
TaskResult::RewindPointsFailed { agent_id, error } => {
|
|
let Some(agent) = app.agents.get_mut(&agent_id) else {
|
|
return vec![];
|
|
};
|
|
agent.rewind_state = None;
|
|
app.show_toast(&format!("Undo failed: {error}"));
|
|
vec![]
|
|
}
|
|
TaskResult::RewindPreviewComplete {
|
|
agent_id,
|
|
response,
|
|
target_prompt_index,
|
|
mode,
|
|
} => handle_rewind_preview_complete(app, agent_id, response, target_prompt_index, mode),
|
|
TaskResult::RewindPreviewFailed { agent_id, error } => {
|
|
handle_rewind_preview_failed(app, agent_id, error)
|
|
}
|
|
TaskResult::RewindExecuteComplete { agent_id, response } => {
|
|
dispatch_rewind_success(app, agent_id, response)
|
|
}
|
|
TaskResult::RewindExecuteFailed { agent_id, error } => {
|
|
handle_rewind_execute_failed(app, agent_id, error)
|
|
}
|
|
TaskResult::SuggestionDebounceExpired {
|
|
agent_id,
|
|
generation,
|
|
} => handle_suggestion_debounce_expired(app, agent_id, generation),
|
|
TaskResult::ShellSuggestionsLoaded {
|
|
agent_id,
|
|
response,
|
|
request_text,
|
|
request_cursor,
|
|
} => {
|
|
let Some(agent) = app.agents.get_mut(&agent_id) else {
|
|
return vec![];
|
|
};
|
|
if agent.prompt_input_mode != crate::app::agent_view::PromptInputMode::Bash {
|
|
return vec![];
|
|
}
|
|
let generation = response.generation;
|
|
agent
|
|
.prompt
|
|
.suggestions
|
|
.on_suggestions_loaded(response, &request_text, request_cursor);
|
|
let text = agent.prompt.text().to_owned();
|
|
agent.prompt.suggestions.set_last_request_text(&text);
|
|
let mark = agent.pending_effects.len();
|
|
if agent.prompt.suggestions.take_pending_tab(generation) {
|
|
agent.shell_completion_tab();
|
|
}
|
|
agent.pending_effects.split_off(mark)
|
|
}
|
|
TaskResult::PromptSuggestionLoaded {
|
|
agent_id,
|
|
suggestion,
|
|
generation,
|
|
} => {
|
|
if let Some(agent) = app.agents.get_mut(&agent_id) {
|
|
agent
|
|
.prompt
|
|
.prompt_suggestion
|
|
.on_loaded(suggestion, generation);
|
|
agent.refresh_prompt_suggestion_gate();
|
|
}
|
|
vec![]
|
|
}
|
|
TaskResult::SettingPersisted { key, value } => {
|
|
tracing::trace!(target : "settings", ? key, ? value, "setting persisted");
|
|
vec![]
|
|
}
|
|
TaskResult::SettingPersistFailed {
|
|
key,
|
|
rollback_value,
|
|
error,
|
|
} => {
|
|
let rollback_effects = apply_setting_rollback(app, key, &rollback_value);
|
|
tracing::warn!(
|
|
target : "settings", ? key, ? rollback_value, % error,
|
|
"setting persist failed; rolled back"
|
|
);
|
|
let scrubbed = scrub_error_for_toast(&error);
|
|
app.show_toast(&format!("\u{2717} Could not save {key}: {scrubbed}"));
|
|
rollback_effects
|
|
}
|
|
TaskResult::SettingPersistFailedBestEffort { key, error } => {
|
|
tracing::warn!(
|
|
target : "settings", ? key, % error,
|
|
"setting persist failed (best-effort); in-memory state stays at optimistic value",
|
|
);
|
|
let scrubbed = scrub_error_for_toast(&error);
|
|
app.show_toast(&format!("\u{2717} Could not save {key}: {scrubbed}"));
|
|
vec![]
|
|
}
|
|
}
|
|
}
|