/feedback opens the Kigi GitHub issues page
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.
This commit is contained in:
@@ -623,10 +623,6 @@ pub enum Action {
|
|||||||
/// to config.toml). `/plan <desc>` uses `EnterPlanMode` instead
|
/// to config.toml). `/plan <desc>` uses `EnterPlanMode` instead
|
||||||
/// because it also starts a turn.
|
/// because it also starts a turn.
|
||||||
SetPlanMode(PlanModeKind),
|
SetPlanMode(PlanModeKind),
|
||||||
/// Enter feedback mode (visual prompt change, not a send).
|
|
||||||
EnterFeedbackMode,
|
|
||||||
/// Send feedback text collected in feedback mode.
|
|
||||||
SendFeedback(String),
|
|
||||||
/// Enter remember mode (visual prompt change, not a send).
|
/// Enter remember mode (visual prompt change, not a send).
|
||||||
EnterRememberMode,
|
EnterRememberMode,
|
||||||
/// Send a remember note from # mode. Routes through LLM rewrite when a
|
/// Send a remember note from # mode. Routes through LLM rewrite when a
|
||||||
@@ -1717,12 +1713,6 @@ pub enum Effect {
|
|||||||
FetchBundleStatus,
|
FetchBundleStatus,
|
||||||
/// Fetch a bundled entry's raw content via `kigi/bundle/entry/get`.
|
/// Fetch a bundled entry's raw content via `kigi/bundle/entry/get`.
|
||||||
FetchCatalogEntry { kind: String, name: String },
|
FetchCatalogEntry { kind: String, name: String },
|
||||||
/// Send feedback about the current session (fire-and-forget POST).
|
|
||||||
SendFeedback {
|
|
||||||
agent_id: AgentId,
|
|
||||||
session_id: acp::SessionId,
|
|
||||||
feedback_text: String,
|
|
||||||
},
|
|
||||||
/// Save a remember note to global MEMORY.md (async file write).
|
/// Save a remember note to global MEMORY.md (async file write).
|
||||||
SaveMemoryNote {
|
SaveMemoryNote {
|
||||||
agent_id: AgentId,
|
agent_id: AgentId,
|
||||||
@@ -2267,15 +2257,6 @@ pub enum TaskResult {
|
|||||||
agent_id: AgentId,
|
agent_id: AgentId,
|
||||||
error: String,
|
error: String,
|
||||||
},
|
},
|
||||||
/// Feedback submitted successfully (fire-and-forget).
|
|
||||||
FeedbackComplete {
|
|
||||||
agent_id: AgentId,
|
|
||||||
},
|
|
||||||
/// Feedback submission failed.
|
|
||||||
FeedbackFailed {
|
|
||||||
agent_id: AgentId,
|
|
||||||
error: String,
|
|
||||||
},
|
|
||||||
/// Memory note saved to global MEMORY.md.
|
/// Memory note saved to global MEMORY.md.
|
||||||
MemoryNoteSaved {
|
MemoryNoteSaved {
|
||||||
agent_id: AgentId,
|
agent_id: AgentId,
|
||||||
|
|||||||
@@ -287,8 +287,6 @@ pub enum PromptInputMode {
|
|||||||
Normal,
|
Normal,
|
||||||
/// Bash mode (`!` prefix): Enter sends `Action::SendBashCommand`.
|
/// Bash mode (`!` prefix): Enter sends `Action::SendBashCommand`.
|
||||||
Bash,
|
Bash,
|
||||||
/// Feedback mode (`~` prefix, teal accent): Enter sends `Action::SendFeedback`.
|
|
||||||
Feedback,
|
|
||||||
/// Remember mode (`#` prefix, green accent): Enter sends `Action::SendRememberNote`.
|
/// Remember mode (`#` prefix, green accent): Enter sends `Action::SendRememberNote`.
|
||||||
Remember,
|
Remember,
|
||||||
}
|
}
|
||||||
@@ -297,7 +295,6 @@ impl PromptInputMode {
|
|||||||
match self {
|
match self {
|
||||||
PromptInputMode::Normal => None,
|
PromptInputMode::Normal => None,
|
||||||
PromptInputMode::Bash => Some(theme.command),
|
PromptInputMode::Bash => Some(theme.command),
|
||||||
PromptInputMode::Feedback => Some(theme.accent_feedback),
|
|
||||||
PromptInputMode::Remember => Some(theme.accent_remember),
|
PromptInputMode::Remember => Some(theme.accent_remember),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -305,14 +302,12 @@ impl PromptInputMode {
|
|||||||
match self {
|
match self {
|
||||||
PromptInputMode::Normal => None,
|
PromptInputMode::Normal => None,
|
||||||
PromptInputMode::Bash => Some(("! ", theme.command)),
|
PromptInputMode::Bash => Some(("! ", theme.command)),
|
||||||
PromptInputMode::Feedback => Some(("~ ", theme.accent_feedback)),
|
|
||||||
PromptInputMode::Remember => Some(("# ", theme.accent_remember)),
|
PromptInputMode::Remember => Some(("# ", theme.accent_remember)),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
pub fn placeholder_override(self, multiline: bool) -> Option<&'static str> {
|
pub fn placeholder_override(self, multiline: bool) -> Option<&'static str> {
|
||||||
match self {
|
match self {
|
||||||
PromptInputMode::Normal | PromptInputMode::Bash => None,
|
PromptInputMode::Normal | PromptInputMode::Bash => None,
|
||||||
PromptInputMode::Feedback => Some("Type your feedback..."),
|
|
||||||
PromptInputMode::Remember => {
|
PromptInputMode::Remember => {
|
||||||
if multiline {
|
if multiline {
|
||||||
Some("Save a memory note... (Enter for newline, Shift+Enter to save)")
|
Some("Save a memory note... (Enter for newline, Shift+Enter to save)")
|
||||||
@@ -326,7 +321,6 @@ impl PromptInputMode {
|
|||||||
match self {
|
match self {
|
||||||
PromptInputMode::Normal => None,
|
PromptInputMode::Normal => None,
|
||||||
PromptInputMode::Bash => Some("Run shell command"),
|
PromptInputMode::Bash => Some("Run shell command"),
|
||||||
PromptInputMode::Feedback => Some("Send feedback"),
|
|
||||||
PromptInputMode::Remember => Some("Save memory note"),
|
PromptInputMode::Remember => Some("Save memory note"),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -334,7 +328,6 @@ impl PromptInputMode {
|
|||||||
match self {
|
match self {
|
||||||
PromptInputMode::Normal => Action::SendPrompt(text),
|
PromptInputMode::Normal => Action::SendPrompt(text),
|
||||||
PromptInputMode::Bash => Action::SendBashCommand(text),
|
PromptInputMode::Bash => Action::SendBashCommand(text),
|
||||||
PromptInputMode::Feedback => Action::SendFeedback(text),
|
|
||||||
PromptInputMode::Remember => Action::SendRememberNote(text),
|
PromptInputMode::Remember => Action::SendRememberNote(text),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -351,7 +344,6 @@ impl PromptInputMode {
|
|||||||
|| ctrl_u
|
|| ctrl_u
|
||||||
|| ctrl_c
|
|| ctrl_c
|
||||||
}
|
}
|
||||||
PromptInputMode::Feedback => key.code == KeyCode::Backspace || key.code == KeyCode::Esc,
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -3078,10 +3070,6 @@ mod prompt_input_mode_tests {
|
|||||||
PromptInputMode::Bash.accent_color(&theme),
|
PromptInputMode::Bash.accent_color(&theme),
|
||||||
Some(theme.command)
|
Some(theme.command)
|
||||||
);
|
);
|
||||||
assert_eq!(
|
|
||||||
PromptInputMode::Feedback.accent_color(&theme),
|
|
||||||
Some(theme.accent_feedback)
|
|
||||||
);
|
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
PromptInputMode::Remember.accent_color(&theme),
|
PromptInputMode::Remember.accent_color(&theme),
|
||||||
Some(theme.accent_remember)
|
Some(theme.accent_remember)
|
||||||
@@ -3095,10 +3083,6 @@ mod prompt_input_mode_tests {
|
|||||||
PromptInputMode::Bash.prefix_override(&theme),
|
PromptInputMode::Bash.prefix_override(&theme),
|
||||||
Some(("! ", theme.command))
|
Some(("! ", theme.command))
|
||||||
);
|
);
|
||||||
assert_eq!(
|
|
||||||
PromptInputMode::Feedback.prefix_override(&theme),
|
|
||||||
Some(("~ ", theme.accent_feedback))
|
|
||||||
);
|
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
PromptInputMode::Remember.prefix_override(&theme),
|
PromptInputMode::Remember.prefix_override(&theme),
|
||||||
Some(("# ", theme.accent_remember))
|
Some(("# ", theme.accent_remember))
|
||||||
@@ -3110,14 +3094,6 @@ mod prompt_input_mode_tests {
|
|||||||
assert_eq!(PromptInputMode::Normal.placeholder_override(true), None);
|
assert_eq!(PromptInputMode::Normal.placeholder_override(true), None);
|
||||||
assert_eq!(PromptInputMode::Bash.placeholder_override(false), None);
|
assert_eq!(PromptInputMode::Bash.placeholder_override(false), None);
|
||||||
assert_eq!(PromptInputMode::Bash.placeholder_override(true), None);
|
assert_eq!(PromptInputMode::Bash.placeholder_override(true), None);
|
||||||
assert_eq!(
|
|
||||||
PromptInputMode::Feedback.placeholder_override(false),
|
|
||||||
Some("Type your feedback...")
|
|
||||||
);
|
|
||||||
assert_eq!(
|
|
||||||
PromptInputMode::Feedback.placeholder_override(true),
|
|
||||||
Some("Type your feedback...")
|
|
||||||
);
|
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
PromptInputMode::Remember.placeholder_override(false),
|
PromptInputMode::Remember.placeholder_override(false),
|
||||||
Some("Save a memory note... (Shift+Enter for multiline)")
|
Some("Save a memory note... (Shift+Enter for multiline)")
|
||||||
@@ -3134,10 +3110,6 @@ mod prompt_input_mode_tests {
|
|||||||
PromptInputMode::Bash.prompt_info_override(),
|
PromptInputMode::Bash.prompt_info_override(),
|
||||||
Some("Run shell command")
|
Some("Run shell command")
|
||||||
);
|
);
|
||||||
assert_eq!(
|
|
||||||
PromptInputMode::Feedback.prompt_info_override(),
|
|
||||||
Some("Send feedback")
|
|
||||||
);
|
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
PromptInputMode::Remember.prompt_info_override(),
|
PromptInputMode::Remember.prompt_info_override(),
|
||||||
Some("Save memory note")
|
Some("Save memory note")
|
||||||
@@ -3151,9 +3123,6 @@ mod prompt_input_mode_tests {
|
|||||||
let t2 = "ls -l".to_string();
|
let t2 = "ls -l".to_string();
|
||||||
assert!(matches!(PromptInputMode::Bash.send_action(t2.clone()),
|
assert!(matches!(PromptInputMode::Bash.send_action(t2.clone()),
|
||||||
Action::SendBashCommand(t) if t == t2));
|
Action::SendBashCommand(t) if t == t2));
|
||||||
let t3 = "this is feedback".to_string();
|
|
||||||
assert!(matches!(PromptInputMode::Feedback.send_action(t3.clone()),
|
|
||||||
Action::SendFeedback(t) if t == t3));
|
|
||||||
let t4 = "remember this".to_string();
|
let t4 = "remember this".to_string();
|
||||||
assert!(matches!(PromptInputMode::Remember.send_action(t4.clone()),
|
assert!(matches!(PromptInputMode::Remember.send_action(t4.clone()),
|
||||||
Action::SendRememberNote(t) if t == t4));
|
Action::SendRememberNote(t) if t == t4));
|
||||||
@@ -3182,15 +3151,4 @@ mod prompt_input_mode_tests {
|
|||||||
assert!(!mode.is_exit_key(&KeyEvent::new(KeyCode::Char('a'), KeyModifiers::NONE)));
|
assert!(!mode.is_exit_key(&KeyEvent::new(KeyCode::Char('a'), KeyModifiers::NONE)));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
#[test]
|
|
||||||
fn is_exit_key_feedback_uses_stricter_set() {
|
|
||||||
let mode = PromptInputMode::Feedback;
|
|
||||||
assert!(mode.is_exit_key(&KeyEvent::new(KeyCode::Backspace, KeyModifiers::NONE)));
|
|
||||||
assert!(mode.is_exit_key(&KeyEvent::new(KeyCode::Esc, KeyModifiers::NONE)));
|
|
||||||
assert!(!mode.is_exit_key(&KeyEvent::new(KeyCode::Char('w'), KeyModifiers::CONTROL)));
|
|
||||||
assert!(!mode.is_exit_key(&KeyEvent::new(KeyCode::Char('u'), KeyModifiers::CONTROL)));
|
|
||||||
assert!(!mode.is_exit_key(&KeyEvent::new(KeyCode::Char('c'), KeyModifiers::CONTROL)));
|
|
||||||
assert!(!mode.is_exit_key(&KeyEvent::new(KeyCode::Enter, KeyModifiers::NONE)));
|
|
||||||
assert!(!mode.is_exit_key(&KeyEvent::new(KeyCode::Char('?'), KeyModifiers::NONE)));
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -868,10 +868,9 @@ impl AgentView {
|
|||||||
{
|
{
|
||||||
self.prompt.history_search.deactivate();
|
self.prompt.history_search.deactivate();
|
||||||
// Detect `! ` prefix to restore bash mode. Refined: only reset to Normal
|
// Detect `! ` prefix to restore bash mode. Refined: only reset to Normal
|
||||||
// if currently in Bash (preserve Feedback/Remember if active). The ! prefix
|
// if currently in Bash (preserve Remember if active). The ! prefix
|
||||||
// restore only applies when not in Feedback/Remember.
|
// restore only applies when not in Remember.
|
||||||
if self.prompt_input_mode != PromptInputMode::Feedback
|
if self.prompt_input_mode != PromptInputMode::Remember
|
||||||
&& self.prompt_input_mode != PromptInputMode::Remember
|
|
||||||
&& let Some(cmd) = text.strip_prefix("! ")
|
&& let Some(cmd) = text.strip_prefix("! ")
|
||||||
{
|
{
|
||||||
self.prompt_input_mode = PromptInputMode::Bash;
|
self.prompt_input_mode = PromptInputMode::Bash;
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
//! Feedback, remember-note, btw, and recap dispatchers.
|
//! Remember-note, btw, and recap dispatchers.
|
||||||
|
|
||||||
use super::ctx::with_active_agent;
|
use super::ctx::with_active_agent;
|
||||||
use crate::app::actions::Effect;
|
use crate::app::actions::Effect;
|
||||||
@@ -18,16 +18,6 @@ fn next_rewrite_nonce() -> u64 {
|
|||||||
REWRITE_NONCE.fetch_add(1, Ordering::Relaxed)
|
REWRITE_NONCE.fetch_add(1, Ordering::Relaxed)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Enter feedback mode: visual change to prompt bar (teal accent, pencil prefix).
|
|
||||||
/// No side effects — the user types feedback text and presses Enter to send.
|
|
||||||
pub(super) fn dispatch_enter_feedback_mode(app: &mut AppView) -> Vec<Effect> {
|
|
||||||
with_active_agent(app, |agent| {
|
|
||||||
agent.prompt_input_mode = PromptInputMode::Feedback;
|
|
||||||
agent.prompt.set_text("");
|
|
||||||
});
|
|
||||||
vec![]
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Enter remember mode: visual change to prompt bar (remember accent, `#` prefix).
|
/// Enter remember mode: visual change to prompt bar (remember accent, `#` prefix).
|
||||||
/// No side effects — the user types a memory note and presses Enter to send.
|
/// No side effects — the user types a memory note and presses Enter to send.
|
||||||
pub(super) fn dispatch_enter_remember_mode(app: &mut AppView) -> Vec<Effect> {
|
pub(super) fn dispatch_enter_remember_mode(app: &mut AppView) -> Vec<Effect> {
|
||||||
@@ -38,47 +28,6 @@ pub(super) fn dispatch_enter_remember_mode(app: &mut AppView) -> Vec<Effect> {
|
|||||||
vec![]
|
vec![]
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Send feedback text to the server. Shows a thank-you message immediately
|
|
||||||
/// and fires the HTTP POST as a background effect.
|
|
||||||
pub(super) fn dispatch_send_feedback(app: &mut AppView, text: String) -> Vec<Effect> {
|
|
||||||
let ActiveView::Agent(id) = app.active_view else {
|
|
||||||
return vec![];
|
|
||||||
};
|
|
||||||
let Some(agent) = app.agents.get_mut(&id) else {
|
|
||||||
return vec![];
|
|
||||||
};
|
|
||||||
|
|
||||||
agent.prompt_input_mode = PromptInputMode::Normal;
|
|
||||||
agent.prompt.set_text("");
|
|
||||||
// Submitting feedback retires any edit-contextual ephemeral tip.
|
|
||||||
agent.ephemeral_tip.clear_on_submit();
|
|
||||||
|
|
||||||
let trimmed = text.trim().to_string();
|
|
||||||
if trimmed.is_empty() {
|
|
||||||
agent.scrollback.push_block(RenderBlock::system(
|
|
||||||
"Please provide feedback text.".to_string(),
|
|
||||||
));
|
|
||||||
return vec![];
|
|
||||||
}
|
|
||||||
|
|
||||||
let Some(session_id) = agent.session.session_id.clone() else {
|
|
||||||
agent
|
|
||||||
.scrollback
|
|
||||||
.push_block(RenderBlock::system("No active session.".to_string()));
|
|
||||||
return vec![];
|
|
||||||
};
|
|
||||||
|
|
||||||
agent.scrollback.push_block(RenderBlock::system(
|
|
||||||
"Thanks for the feedback! The Kigi team is on it.".to_string(),
|
|
||||||
));
|
|
||||||
|
|
||||||
vec![Effect::SendFeedback {
|
|
||||||
agent_id: id,
|
|
||||||
session_id,
|
|
||||||
feedback_text: trimmed,
|
|
||||||
}]
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Send a raw remember note for LLM-powered rewriting via `kigi/memory/rewrite`.
|
/// Send a raw remember note for LLM-powered rewriting via `kigi/memory/rewrite`.
|
||||||
/// Clears remember mode and prompts the LLM to reformat the note with session
|
/// Clears remember mode and prompts the LLM to reformat the note with session
|
||||||
/// context. Falls back to direct `SaveMemoryNote` when no session is available.
|
/// context. Falls back to direct `SaveMemoryNote` when no session is available.
|
||||||
|
|||||||
@@ -33,8 +33,7 @@ use super::modes::{
|
|||||||
set_permission_mode, set_plan_mode, set_yolo_mode,
|
set_permission_mode, set_plan_mode, set_yolo_mode,
|
||||||
};
|
};
|
||||||
use super::notes::{
|
use super::notes::{
|
||||||
dispatch_enter_feedback_mode, dispatch_enter_remember_mode,
|
dispatch_enter_remember_mode, dispatch_save_remember_note_from_modal, dispatch_send_btw,
|
||||||
dispatch_save_remember_note_from_modal, dispatch_send_btw, dispatch_send_feedback,
|
|
||||||
dispatch_send_recap, dispatch_send_remember_note,
|
dispatch_send_recap, dispatch_send_remember_note,
|
||||||
};
|
};
|
||||||
use super::permissions::{
|
use super::permissions::{
|
||||||
@@ -784,8 +783,6 @@ pub(crate) fn dispatch(action: Action, app: &mut AppView) -> Vec<Effect> {
|
|||||||
Action::ShowPlan => dispatch_show_plan(app),
|
Action::ShowPlan => dispatch_show_plan(app),
|
||||||
Action::EnterPlanMode { description } => dispatch_enter_plan_mode(app, description),
|
Action::EnterPlanMode { description } => dispatch_enter_plan_mode(app, description),
|
||||||
Action::SetPlanMode(kind) => set_plan_mode(app, kind),
|
Action::SetPlanMode(kind) => set_plan_mode(app, kind),
|
||||||
Action::EnterFeedbackMode => dispatch_enter_feedback_mode(app),
|
|
||||||
Action::SendFeedback(text) => dispatch_send_feedback(app, text),
|
|
||||||
Action::EnterRememberMode => dispatch_enter_remember_mode(app),
|
Action::EnterRememberMode => dispatch_enter_remember_mode(app),
|
||||||
Action::SendRememberNote(text) => dispatch_send_remember_note(app, text),
|
Action::SendRememberNote(text) => dispatch_send_remember_note(app, text),
|
||||||
Action::SaveRememberNoteFromModal => dispatch_save_remember_note_from_modal(app),
|
Action::SaveRememberNoteFromModal => dispatch_save_remember_note_from_modal(app),
|
||||||
|
|||||||
@@ -663,17 +663,6 @@ pub(super) fn dispatch_task_result(result: TaskResult, app: &mut AppView) -> Vec
|
|||||||
}
|
}
|
||||||
vec![]
|
vec![]
|
||||||
}
|
}
|
||||||
TaskResult::FeedbackComplete { .. } => vec![],
|
|
||||||
TaskResult::FeedbackFailed { 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 send feedback: {error}"
|
|
||||||
)));
|
|
||||||
}
|
|
||||||
vec![]
|
|
||||||
}
|
|
||||||
TaskResult::MemoryNoteSaved { agent_id, result } => {
|
TaskResult::MemoryNoteSaved { agent_id, result } => {
|
||||||
handle_memory_note_saved(app, agent_id, result)
|
handle_memory_note_saved(app, agent_id, result)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -32,23 +32,6 @@ fn seed_foreign_resume_hint(
|
|||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
/// Sending feedback is a submit: it retires the active ephemeral tip.
|
|
||||||
#[test]
|
|
||||||
fn send_feedback_clears_active_ephemeral_tip() {
|
|
||||||
let mut app = test_app_with_agent();
|
|
||||||
let id = AgentId(0);
|
|
||||||
let agent = app.agents.get_mut(&id).unwrap();
|
|
||||||
let _ = agent.ephemeral_tip.show(
|
|
||||||
crate::tips::EphemeralTip::new("t", ratatui::text::Line::from("hint")),
|
|
||||||
&mut std::collections::HashMap::new(),
|
|
||||||
);
|
|
||||||
assert!(agent.ephemeral_tip.is_active());
|
|
||||||
let _ = dispatch(Action::SendFeedback("it broke".into()), &mut app);
|
|
||||||
assert!(
|
|
||||||
!app.agents.get(&id).unwrap().ephemeral_tip.is_active(),
|
|
||||||
"feedback submit must clear the tip"
|
|
||||||
);
|
|
||||||
}
|
|
||||||
/// Sending a remember note is a submit: it retires the active ephemeral tip.
|
/// Sending a remember note is a submit: it retires the active ephemeral tip.
|
||||||
#[test]
|
#[test]
|
||||||
fn send_remember_note_clears_active_ephemeral_tip() {
|
fn send_remember_note_clears_active_ephemeral_tip() {
|
||||||
|
|||||||
@@ -2599,61 +2599,6 @@ pub(crate) fn execute(
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
Effect::SendFeedback { agent_id, session_id, feedback_text } => {
|
|
||||||
use kigi_shell::session::ClientType;
|
|
||||||
use kigi_shell::session::acp_types::ClientFeedbackInput;
|
|
||||||
let terminal_info = Some(
|
|
||||||
crate::terminal::terminal_context().feedback_info(),
|
|
||||||
);
|
|
||||||
let tx = acp_tx.clone();
|
|
||||||
tasks
|
|
||||||
.spawn(async move {
|
|
||||||
let input = ClientFeedbackInput {
|
|
||||||
session_id: session_id.0.to_string(),
|
|
||||||
client_type: ClientType::Tui,
|
|
||||||
rating_type: None,
|
|
||||||
rating_value: None,
|
|
||||||
feedback_text: Some(feedback_text),
|
|
||||||
feedback_categories: vec![],
|
|
||||||
context_type: None,
|
|
||||||
turn_number: None,
|
|
||||||
request_id: None,
|
|
||||||
client_version: Some(kigi_version::VERSION.to_string()),
|
|
||||||
metadata: None,
|
|
||||||
terminal_info,
|
|
||||||
};
|
|
||||||
let raw_params = match serde_json::value::to_raw_value(&input) {
|
|
||||||
Ok(v) => v,
|
|
||||||
Err(e) => {
|
|
||||||
return TaskResult::FeedbackFailed {
|
|
||||||
agent_id,
|
|
||||||
error: sanitize_user_error(
|
|
||||||
&format!("couldn't serialize feedback: {e}"),
|
|
||||||
),
|
|
||||||
};
|
|
||||||
}
|
|
||||||
};
|
|
||||||
let request = acp::ExtRequest::new(
|
|
||||||
"kigi/feedback",
|
|
||||||
raw_params.into(),
|
|
||||||
);
|
|
||||||
match acp_send(request, &tx).await {
|
|
||||||
Ok(_) => {
|
|
||||||
TaskResult::FeedbackComplete {
|
|
||||||
agent_id,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Err(e) => {
|
|
||||||
TaskResult::FeedbackFailed {
|
|
||||||
agent_id,
|
|
||||||
error: sanitize_user_error(
|
|
||||||
&format!("couldn't send feedback: {e}"),
|
|
||||||
),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
Effect::RewriteMemoryNote {
|
Effect::RewriteMemoryNote {
|
||||||
agent_id,
|
agent_id,
|
||||||
session_id,
|
session_id,
|
||||||
|
|||||||
@@ -169,8 +169,7 @@ impl AgentView {
|
|||||||
.map(str::to_owned)
|
.map(str::to_owned)
|
||||||
{
|
{
|
||||||
self.prompt.history_search.deactivate();
|
self.prompt.history_search.deactivate();
|
||||||
if self.prompt_input_mode != PromptInputMode::Feedback
|
if self.prompt_input_mode != PromptInputMode::Remember
|
||||||
&& self.prompt_input_mode != PromptInputMode::Remember
|
|
||||||
&& let Some(cmd) = text.strip_prefix("! ")
|
&& let Some(cmd) = text.strip_prefix("! ")
|
||||||
{
|
{
|
||||||
self.prompt_input_mode = PromptInputMode::Bash;
|
self.prompt_input_mode = PromptInputMode::Bash;
|
||||||
|
|||||||
@@ -1,9 +1,14 @@
|
|||||||
//! `/feedback` -- send session feedback.
|
//! `/feedback` -- open the Kigi GitHub issues page.
|
||||||
|
|
||||||
use crate::app::actions::Action;
|
use crate::app::actions::Action;
|
||||||
use crate::slash::command::{CommandExecCtx, CommandResult, SlashCommand};
|
use crate::slash::command::{CommandExecCtx, CommandResult, SlashCommand};
|
||||||
|
|
||||||
/// Send session feedback inline or enter feedback mode.
|
/// Where feedback goes: the project's own issue tracker. Kigi is a community
|
||||||
|
/// build, so its feedback belongs on its GitHub repo — mirroring the official
|
||||||
|
/// kimi-cli, whose `/feedback` opens its repo's issues page.
|
||||||
|
pub const FEEDBACK_ISSUES_URL: &str = "https://github.com/ZacharyZhang-NY/Kigi-CLI/issues";
|
||||||
|
|
||||||
|
/// Open the Kigi issue tracker in the browser.
|
||||||
pub struct FeedbackCommand;
|
pub struct FeedbackCommand;
|
||||||
|
|
||||||
impl SlashCommand for FeedbackCommand {
|
impl SlashCommand for FeedbackCommand {
|
||||||
@@ -12,27 +17,75 @@ impl SlashCommand for FeedbackCommand {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn description(&self) -> &str {
|
fn description(&self) -> &str {
|
||||||
"Send feedback about the current session"
|
"Report feedback on the Kigi GitHub issues page"
|
||||||
}
|
}
|
||||||
|
|
||||||
fn usage(&self) -> &str {
|
fn usage(&self) -> &str {
|
||||||
"/feedback [text]"
|
"/feedback"
|
||||||
}
|
}
|
||||||
|
|
||||||
fn takes_args(&self) -> bool {
|
fn run(&self, _ctx: &mut CommandExecCtx, _args: &str) -> CommandResult {
|
||||||
true
|
CommandResult::Action(Action::OpenUrl(FEEDBACK_ISSUES_URL.into()))
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fn arg_placeholder(&self) -> Option<&str> {
|
#[cfg(test)]
|
||||||
Some("[feedback text]")
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
use crate::acp::model_state::ModelState;
|
||||||
|
|
||||||
|
static DEFAULT_BUNDLE_STATE: crate::app::bundle::BundleState =
|
||||||
|
crate::app::bundle::BundleState {
|
||||||
|
has_cache: false,
|
||||||
|
version: String::new(),
|
||||||
|
personas: Vec::new(),
|
||||||
|
roles: Vec::new(),
|
||||||
|
agents: Vec::new(),
|
||||||
|
skills: Vec::new(),
|
||||||
|
persona_details: Vec::new(),
|
||||||
|
role_details: Vec::new(),
|
||||||
|
};
|
||||||
|
|
||||||
|
fn make_ctx<'a>(models: &'a ModelState) -> CommandExecCtx<'a> {
|
||||||
|
CommandExecCtx {
|
||||||
|
models,
|
||||||
|
session_id: None,
|
||||||
|
bundle_state: &DEFAULT_BUNDLE_STATE,
|
||||||
|
screen_mode: crate::app::ScreenMode::Inline,
|
||||||
|
pager_state: crate::settings::PagerLocalSnapshot {
|
||||||
|
multiline_mode: false,
|
||||||
|
yolo_mode: false,
|
||||||
|
..crate::settings::PagerLocalSnapshot::default()
|
||||||
|
},
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn run(&self, _ctx: &mut CommandExecCtx, args: &str) -> CommandResult {
|
#[test]
|
||||||
let trimmed = args.trim();
|
fn feedback_opens_github_issues() {
|
||||||
if trimmed.is_empty() {
|
let models = ModelState::default();
|
||||||
CommandResult::Action(Action::EnterFeedbackMode)
|
let mut ctx = make_ctx(&models);
|
||||||
} else {
|
match FeedbackCommand.run(&mut ctx, "") {
|
||||||
CommandResult::Action(Action::SendFeedback(trimmed.to_string()))
|
CommandResult::Action(Action::OpenUrl(url)) => {
|
||||||
|
assert_eq!(url, FEEDBACK_ISSUES_URL);
|
||||||
|
}
|
||||||
|
other => panic!("expected OpenUrl, got {other:?}"),
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn feedback_ignores_stray_args() {
|
||||||
|
let models = ModelState::default();
|
||||||
|
let mut ctx = make_ctx(&models);
|
||||||
|
assert!(matches!(
|
||||||
|
FeedbackCommand.run(&mut ctx, "some typed text"),
|
||||||
|
CommandResult::Action(Action::OpenUrl(_))
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn feedback_metadata() {
|
||||||
|
let cmd = FeedbackCommand;
|
||||||
|
assert_eq!(cmd.name(), "feedback");
|
||||||
|
assert!(!cmd.takes_args());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -174,7 +174,7 @@ pub struct PromptStyle {
|
|||||||
pub prefix_override: Option<(&'static str, ratatui::style::Color)>,
|
pub prefix_override: Option<(&'static str, ratatui::style::Color)>,
|
||||||
/// Override the placeholder text shown when the textarea is empty.
|
/// Override the placeholder text shown when the textarea is empty.
|
||||||
/// When `Some(text)`, uses this instead of the default `"Build anything"`.
|
/// When `Some(text)`, uses this instead of the default `"Build anything"`.
|
||||||
/// Used for feedback mode (`"Type your feedback..."`).
|
/// Used for remember mode (`"Save a memory note..."`).
|
||||||
pub placeholder_override: Option<&'static str>,
|
pub placeholder_override: Option<&'static str>,
|
||||||
/// Compact mode (currently unused for info_block sizing).
|
/// Compact mode (currently unused for info_block sizing).
|
||||||
pub compact: bool,
|
pub compact: bool,
|
||||||
|
|||||||
Reference in New Issue
Block a user