mirror of
https://github.com/pchuan98/codex.git
synced 2026-07-01 00:31:56 +08:00
Pass turn id with feedback uploads (#17314)
## Summary - Add an optional `tags` dictionary to feedback upload params. - Capture the active app-server turn id in the TUI and submit it as `tags.turn_id` with `/feedback` uploads. - Merge client-provided feedback tags into Sentry feedback tags while preserving reserved system fields like `thread_id`, `classification`, `cli_version`, `session_source`, and `reason`. ## Behavior / impact Existing feedback upload callers remain compatible because `tags` is optional and nullable. The wire shape is still a normal JSON object / TypeScript dictionary, so adding future feedback metadata will not require a new top-level protocol field each time. This change only adds feedback metadata for Codex CLI/TUI uploads; it does not affect existing pipelines, DAGs, exports, or downstream consumers unless they choose to read the new `turn_id` feedback tag. ## Tests - `cargo fmt -- --config imports_granularity=Item` passed; stable rustfmt warned that `imports_granularity` is nightly-only. - `cargo run -p codex-app-server-protocol --bin write_schema_fixtures` - `cargo test -p codex-feedback upload_tags_include_client_tags_and_preserve_reserved_fields` - `cargo test -p codex-app-server-protocol schema_fixtures_match_generated` - `cargo test -p codex-tui build_feedback_upload_params` - `cargo test -p codex-tui live_app_server_turn_started_sets_feedback_turn_id` - `cargo check -p codex-app-server --tests` - `git diff --check` --------- Co-authored-by: Codex <noreply@openai.com>
This commit is contained in:
committed by
GitHub
Unverified
parent
dbfe855f4f
commit
be13f03c39
@@ -647,6 +647,15 @@
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"tags": {
|
||||
"additionalProperties": {
|
||||
"type": "string"
|
||||
},
|
||||
"type": [
|
||||
"object",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"threadId": {
|
||||
"type": [
|
||||
"string",
|
||||
|
||||
@@ -7479,6 +7479,15 @@
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"tags": {
|
||||
"additionalProperties": {
|
||||
"type": "string"
|
||||
},
|
||||
"type": [
|
||||
"object",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"threadId": {
|
||||
"type": [
|
||||
"string",
|
||||
|
||||
@@ -4120,6 +4120,15 @@
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"tags": {
|
||||
"additionalProperties": {
|
||||
"type": "string"
|
||||
},
|
||||
"type": [
|
||||
"object",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"threadId": {
|
||||
"type": [
|
||||
"string",
|
||||
|
||||
@@ -22,6 +22,15 @@
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"tags": {
|
||||
"additionalProperties": {
|
||||
"type": "string"
|
||||
},
|
||||
"type": [
|
||||
"object",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"threadId": {
|
||||
"type": [
|
||||
"string",
|
||||
|
||||
@@ -2,4 +2,4 @@
|
||||
|
||||
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
|
||||
|
||||
export type FeedbackUploadParams = { classification: string, reason?: string | null, threadId?: string | null, includeLogs: boolean, extraLogFiles?: Array<string> | null, };
|
||||
export type FeedbackUploadParams = { classification: string, reason?: string | null, threadId?: string | null, includeLogs: boolean, extraLogFiles?: Array<string> | null, tags?: { [key in string]?: string } | null, };
|
||||
|
||||
@@ -2242,6 +2242,8 @@ pub struct FeedbackUploadParams {
|
||||
pub include_logs: bool,
|
||||
#[ts(optional = nullable)]
|
||||
pub extra_log_files: Option<Vec<PathBuf>>,
|
||||
#[ts(optional = nullable)]
|
||||
pub tags: Option<BTreeMap<String, String>>,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)]
|
||||
|
||||
@@ -243,6 +243,7 @@ use codex_features::FEATURES;
|
||||
use codex_features::Feature;
|
||||
use codex_features::Stage;
|
||||
use codex_feedback::CodexFeedback;
|
||||
use codex_feedback::FeedbackUploadOptions;
|
||||
use codex_git_utils::git_diff_to_remote;
|
||||
use codex_git_utils::resolve_root_git_project_for_trust;
|
||||
use codex_login::AuthManager;
|
||||
@@ -7726,6 +7727,7 @@ impl CodexMessageProcessor {
|
||||
thread_id,
|
||||
include_logs,
|
||||
extra_log_files,
|
||||
tags,
|
||||
} = params;
|
||||
|
||||
let conversation_id = match thread_id.as_deref() {
|
||||
@@ -7853,14 +7855,15 @@ impl CodexMessageProcessor {
|
||||
let session_source = self.thread_manager.session_source();
|
||||
|
||||
let upload_result = tokio::task::spawn_blocking(move || {
|
||||
snapshot.upload_feedback(
|
||||
&classification,
|
||||
reason.as_deref(),
|
||||
snapshot.upload_feedback(FeedbackUploadOptions {
|
||||
classification: &classification,
|
||||
reason: reason.as_deref(),
|
||||
tags: tags.as_ref(),
|
||||
include_logs,
|
||||
&attachment_paths,
|
||||
Some(session_source),
|
||||
sqlite_feedback_logs,
|
||||
)
|
||||
extra_attachment_paths: &attachment_paths,
|
||||
session_source: Some(session_source),
|
||||
logs_override: sqlite_feedback_logs,
|
||||
})
|
||||
})
|
||||
.await;
|
||||
|
||||
|
||||
+151
-50
@@ -338,6 +338,16 @@ pub struct FeedbackSnapshot {
|
||||
pub thread_id: String,
|
||||
}
|
||||
|
||||
pub struct FeedbackUploadOptions<'a> {
|
||||
pub classification: &'a str,
|
||||
pub reason: Option<&'a str>,
|
||||
pub tags: Option<&'a BTreeMap<String, String>>,
|
||||
pub include_logs: bool,
|
||||
pub extra_attachment_paths: &'a [PathBuf],
|
||||
pub session_source: Option<SessionSource>,
|
||||
pub logs_override: Option<Vec<u8>>,
|
||||
}
|
||||
|
||||
impl FeedbackSnapshot {
|
||||
pub(crate) fn as_bytes(&self) -> &[u8] {
|
||||
&self.bytes
|
||||
@@ -369,16 +379,7 @@ impl FeedbackSnapshot {
|
||||
}
|
||||
|
||||
/// Upload feedback to Sentry with optional attachments.
|
||||
pub fn upload_feedback(
|
||||
&self,
|
||||
classification: &str,
|
||||
reason: Option<&str>,
|
||||
include_logs: bool,
|
||||
extra_attachment_paths: &[PathBuf],
|
||||
session_source: Option<SessionSource>,
|
||||
logs_override: Option<Vec<u8>>,
|
||||
) -> Result<()> {
|
||||
use std::collections::BTreeMap;
|
||||
pub fn upload_feedback(&self, options: FeedbackUploadOptions<'_>) -> Result<()> {
|
||||
use std::str::FromStr;
|
||||
use std::sync::Arc;
|
||||
|
||||
@@ -398,13 +399,70 @@ impl FeedbackSnapshot {
|
||||
..Default::default()
|
||||
});
|
||||
|
||||
let tags = self.upload_tags(
|
||||
options.classification,
|
||||
options.reason,
|
||||
options.tags,
|
||||
options.session_source.as_ref(),
|
||||
);
|
||||
|
||||
let level = match options.classification {
|
||||
"bug" | "bad_result" | "safety_check" => Level::Error,
|
||||
_ => Level::Info,
|
||||
};
|
||||
|
||||
let mut envelope = Envelope::new();
|
||||
let title = format!(
|
||||
"[{}]: Codex session {}",
|
||||
display_classification(options.classification),
|
||||
self.thread_id
|
||||
);
|
||||
|
||||
let mut event = Event {
|
||||
level,
|
||||
message: Some(title.clone()),
|
||||
tags,
|
||||
..Default::default()
|
||||
};
|
||||
if let Some(r) = options.reason {
|
||||
use sentry::protocol::Exception;
|
||||
use sentry::protocol::Values;
|
||||
|
||||
event.exception = Values::from(vec![Exception {
|
||||
ty: title,
|
||||
value: Some(r.to_string()),
|
||||
..Default::default()
|
||||
}]);
|
||||
}
|
||||
envelope.add_item(EnvelopeItem::Event(event));
|
||||
|
||||
for attachment in self.feedback_attachments(
|
||||
options.include_logs,
|
||||
options.extra_attachment_paths,
|
||||
options.logs_override,
|
||||
) {
|
||||
envelope.add_item(EnvelopeItem::Attachment(attachment));
|
||||
}
|
||||
|
||||
client.send_envelope(envelope);
|
||||
client.flush(Some(Duration::from_secs(UPLOAD_TIMEOUT_SECS)));
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn upload_tags(
|
||||
&self,
|
||||
classification: &str,
|
||||
reason: Option<&str>,
|
||||
client_tags: Option<&BTreeMap<String, String>>,
|
||||
session_source: Option<&SessionSource>,
|
||||
) -> BTreeMap<String, String> {
|
||||
let cli_version = env!("CARGO_PKG_VERSION");
|
||||
let mut tags = BTreeMap::from([
|
||||
(String::from("thread_id"), self.thread_id.to_string()),
|
||||
(String::from("classification"), classification.to_string()),
|
||||
(String::from("cli_version"), cli_version.to_string()),
|
||||
]);
|
||||
if let Some(source) = session_source.as_ref() {
|
||||
if let Some(source) = session_source {
|
||||
tags.insert(String::from("session_source"), source.to_string());
|
||||
}
|
||||
if let Some(r) = reason {
|
||||
@@ -418,6 +476,16 @@ impl FeedbackSnapshot {
|
||||
"session_source",
|
||||
"reason",
|
||||
];
|
||||
if let Some(client_tags) = client_tags {
|
||||
for (key, value) in client_tags {
|
||||
if reserved.contains(&key.as_str()) {
|
||||
continue;
|
||||
}
|
||||
if let Entry::Vacant(entry) = tags.entry(key.clone()) {
|
||||
entry.insert(value.clone());
|
||||
}
|
||||
}
|
||||
}
|
||||
for (key, value) in &self.tags {
|
||||
if reserved.contains(&key.as_str()) {
|
||||
continue;
|
||||
@@ -427,45 +495,7 @@ impl FeedbackSnapshot {
|
||||
}
|
||||
}
|
||||
|
||||
let level = match classification {
|
||||
"bug" | "bad_result" | "safety_check" => Level::Error,
|
||||
_ => Level::Info,
|
||||
};
|
||||
|
||||
let mut envelope = Envelope::new();
|
||||
let title = format!(
|
||||
"[{}]: Codex session {}",
|
||||
display_classification(classification),
|
||||
self.thread_id
|
||||
);
|
||||
|
||||
let mut event = Event {
|
||||
level,
|
||||
message: Some(title.clone()),
|
||||
tags,
|
||||
..Default::default()
|
||||
};
|
||||
if let Some(r) = reason {
|
||||
use sentry::protocol::Exception;
|
||||
use sentry::protocol::Values;
|
||||
|
||||
event.exception = Values::from(vec![Exception {
|
||||
ty: title,
|
||||
value: Some(r.to_string()),
|
||||
..Default::default()
|
||||
}]);
|
||||
}
|
||||
envelope.add_item(EnvelopeItem::Event(event));
|
||||
|
||||
for attachment in
|
||||
self.feedback_attachments(include_logs, extra_attachment_paths, logs_override)
|
||||
{
|
||||
envelope.add_item(EnvelopeItem::Attachment(attachment));
|
||||
}
|
||||
|
||||
client.send_envelope(envelope);
|
||||
client.flush(Some(Duration::from_secs(UPLOAD_TIMEOUT_SECS)));
|
||||
Ok(())
|
||||
tags
|
||||
}
|
||||
|
||||
fn feedback_attachments(
|
||||
@@ -697,4 +727,75 @@ mod tests {
|
||||
assert_eq!(attachments_without_diagnostics[0].buffer, vec![1]);
|
||||
fs::remove_file(extra_path).expect("extra attachment should be removed");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn upload_tags_include_client_tags_and_preserve_reserved_fields() {
|
||||
let mut tags = BTreeMap::new();
|
||||
tags.insert("thread_id".to_string(), "wrong-thread".to_string());
|
||||
tags.insert("turn_id".to_string(), "wrong-turn".to_string());
|
||||
tags.insert(
|
||||
"classification".to_string(),
|
||||
"wrong-classification".to_string(),
|
||||
);
|
||||
tags.insert("cli_version".to_string(), "wrong-version".to_string());
|
||||
tags.insert("session_source".to_string(), "wrong-source".to_string());
|
||||
tags.insert("reason".to_string(), "wrong-reason".to_string());
|
||||
tags.insert("model".to_string(), "gpt-5".to_string());
|
||||
let snapshot = FeedbackSnapshot {
|
||||
bytes: Vec::new(),
|
||||
tags,
|
||||
feedback_diagnostics: FeedbackDiagnostics::default(),
|
||||
thread_id: "thread-123".to_string(),
|
||||
};
|
||||
let mut client_tags = BTreeMap::new();
|
||||
client_tags.insert("thread_id".to_string(), "wrong-client-thread".to_string());
|
||||
client_tags.insert("turn_id".to_string(), "turn-456".to_string());
|
||||
client_tags.insert(
|
||||
"classification".to_string(),
|
||||
"wrong-client-classification".to_string(),
|
||||
);
|
||||
client_tags.insert(
|
||||
"cli_version".to_string(),
|
||||
"wrong-client-version".to_string(),
|
||||
);
|
||||
client_tags.insert(
|
||||
"session_source".to_string(),
|
||||
"wrong-client-source".to_string(),
|
||||
);
|
||||
client_tags.insert("reason".to_string(), "wrong-client-reason".to_string());
|
||||
client_tags.insert("client_tag".to_string(), "from-client".to_string());
|
||||
|
||||
let upload_tags = snapshot.upload_tags(
|
||||
"bug",
|
||||
Some("actual reason"),
|
||||
Some(&client_tags),
|
||||
Some(&SessionSource::Cli),
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
upload_tags.get("thread_id").map(String::as_str),
|
||||
Some("thread-123")
|
||||
);
|
||||
assert_eq!(
|
||||
upload_tags.get("turn_id").map(String::as_str),
|
||||
Some("turn-456")
|
||||
);
|
||||
assert_eq!(
|
||||
upload_tags.get("classification").map(String::as_str),
|
||||
Some("bug")
|
||||
);
|
||||
assert_eq!(
|
||||
upload_tags.get("session_source").map(String::as_str),
|
||||
Some("cli")
|
||||
);
|
||||
assert_eq!(
|
||||
upload_tags.get("reason").map(String::as_str),
|
||||
Some("actual reason")
|
||||
);
|
||||
assert_eq!(
|
||||
upload_tags.get("client_tag").map(String::as_str),
|
||||
Some("from-client")
|
||||
);
|
||||
assert_eq!(upload_tags.get("model").map(String::as_str), Some("gpt-5"));
|
||||
}
|
||||
}
|
||||
|
||||
+18
-1
@@ -2020,6 +2020,7 @@ impl App {
|
||||
app_server: &AppServerSession,
|
||||
category: FeedbackCategory,
|
||||
reason: Option<String>,
|
||||
turn_id: Option<String>,
|
||||
include_logs: bool,
|
||||
) {
|
||||
let request_handle = app_server.request_handle();
|
||||
@@ -2035,6 +2036,7 @@ impl App {
|
||||
rollout_path,
|
||||
category,
|
||||
reason,
|
||||
turn_id,
|
||||
include_logs,
|
||||
);
|
||||
tokio::spawn(async move {
|
||||
@@ -4630,9 +4632,10 @@ impl App {
|
||||
AppEvent::SubmitFeedback {
|
||||
category,
|
||||
reason,
|
||||
turn_id,
|
||||
include_logs,
|
||||
} => {
|
||||
self.submit_feedback(app_server, category, reason, include_logs);
|
||||
self.submit_feedback(app_server, category, reason, turn_id, include_logs);
|
||||
}
|
||||
AppEvent::FeedbackSubmitted {
|
||||
origin_thread_id,
|
||||
@@ -6249,6 +6252,7 @@ fn build_feedback_upload_params(
|
||||
rollout_path: Option<PathBuf>,
|
||||
category: FeedbackCategory,
|
||||
reason: Option<String>,
|
||||
turn_id: Option<String>,
|
||||
include_logs: bool,
|
||||
) -> FeedbackUploadParams {
|
||||
let extra_log_files = if include_logs {
|
||||
@@ -6256,12 +6260,14 @@ fn build_feedback_upload_params(
|
||||
} else {
|
||||
None
|
||||
};
|
||||
let tags = turn_id.map(|turn_id| BTreeMap::from([(String::from("turn_id"), turn_id)]));
|
||||
FeedbackUploadParams {
|
||||
classification: crate::bottom_pane::feedback_classification(category).to_string(),
|
||||
reason,
|
||||
thread_id: origin_thread_id.map(|thread_id| thread_id.to_string()),
|
||||
include_logs,
|
||||
extra_log_files,
|
||||
tags,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9644,12 +9650,21 @@ guardian_approval = true
|
||||
Some(rollout_path.clone()),
|
||||
FeedbackCategory::SafetyCheck,
|
||||
Some("needs follow-up".to_string()),
|
||||
Some("turn-123".to_string()),
|
||||
/*include_logs*/ true,
|
||||
);
|
||||
|
||||
assert_eq!(params.classification, "safety_check");
|
||||
assert_eq!(params.reason, Some("needs follow-up".to_string()));
|
||||
assert_eq!(params.thread_id, Some(thread_id.to_string()));
|
||||
assert_eq!(
|
||||
params
|
||||
.tags
|
||||
.as_ref()
|
||||
.and_then(|tags| tags.get("turn_id"))
|
||||
.map(String::as_str),
|
||||
Some("turn-123")
|
||||
);
|
||||
assert_eq!(params.include_logs, true);
|
||||
assert_eq!(params.extra_log_files, Some(vec![rollout_path]));
|
||||
}
|
||||
@@ -9661,12 +9676,14 @@ guardian_approval = true
|
||||
Some(PathBuf::from("/tmp/rollout.jsonl")),
|
||||
FeedbackCategory::GoodResult,
|
||||
/*reason*/ None,
|
||||
/*turn_id*/ None,
|
||||
/*include_logs*/ false,
|
||||
);
|
||||
|
||||
assert_eq!(params.classification, "good_result");
|
||||
assert_eq!(params.reason, None);
|
||||
assert_eq!(params.thread_id, None);
|
||||
assert_eq!(params.tags, None);
|
||||
assert_eq!(params.include_logs, false);
|
||||
assert_eq!(params.extra_log_files, None);
|
||||
}
|
||||
|
||||
@@ -559,6 +559,7 @@ pub(crate) enum AppEvent {
|
||||
SubmitFeedback {
|
||||
category: FeedbackCategory,
|
||||
reason: Option<String>,
|
||||
turn_id: Option<String>,
|
||||
include_logs: bool,
|
||||
},
|
||||
|
||||
|
||||
@@ -45,6 +45,7 @@ pub(crate) enum FeedbackAudience {
|
||||
/// through the app-server-managed feedback flow.
|
||||
pub(crate) struct FeedbackNoteView {
|
||||
category: FeedbackCategory,
|
||||
turn_id: Option<String>,
|
||||
app_event_tx: AppEventSender,
|
||||
include_logs: bool,
|
||||
|
||||
@@ -57,11 +58,13 @@ pub(crate) struct FeedbackNoteView {
|
||||
impl FeedbackNoteView {
|
||||
pub(crate) fn new(
|
||||
category: FeedbackCategory,
|
||||
turn_id: Option<String>,
|
||||
app_event_tx: AppEventSender,
|
||||
include_logs: bool,
|
||||
) -> Self {
|
||||
Self {
|
||||
category,
|
||||
turn_id,
|
||||
app_event_tx,
|
||||
include_logs,
|
||||
textarea: TextArea::new(),
|
||||
@@ -76,6 +79,7 @@ impl FeedbackNoteView {
|
||||
self.app_event_tx.send(AppEvent::SubmitFeedback {
|
||||
category: self.category,
|
||||
reason,
|
||||
turn_id: self.turn_id.clone(),
|
||||
include_logs: self.include_logs,
|
||||
});
|
||||
self.complete = true;
|
||||
@@ -607,7 +611,9 @@ mod tests {
|
||||
fn make_view(category: FeedbackCategory) -> FeedbackNoteView {
|
||||
let (tx_raw, _rx) = tokio::sync::mpsc::unbounded_channel::<AppEvent>();
|
||||
let tx = AppEventSender::new(tx_raw);
|
||||
FeedbackNoteView::new(category, tx, /*include_logs*/ true)
|
||||
FeedbackNoteView::new(
|
||||
category, /*turn_id*/ None, tx, /*include_logs*/ true,
|
||||
)
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -649,7 +655,12 @@ mod tests {
|
||||
fn feedback_view_with_connectivity_diagnostics() {
|
||||
let (tx_raw, _rx) = tokio::sync::mpsc::unbounded_channel::<AppEvent>();
|
||||
let tx = AppEventSender::new(tx_raw);
|
||||
let view = FeedbackNoteView::new(FeedbackCategory::Bug, tx, /*include_logs*/ false);
|
||||
let view = FeedbackNoteView::new(
|
||||
FeedbackCategory::Bug,
|
||||
/*turn_id*/ None,
|
||||
tx,
|
||||
/*include_logs*/ false,
|
||||
);
|
||||
let rendered = render(&view, /*width*/ 60);
|
||||
|
||||
insta::assert_snapshot!("feedback_view_with_connectivity_diagnostics", rendered);
|
||||
@@ -659,7 +670,12 @@ mod tests {
|
||||
fn submit_feedback_emits_submit_event_with_trimmed_note() {
|
||||
let (tx_raw, mut rx) = tokio::sync::mpsc::unbounded_channel::<AppEvent>();
|
||||
let tx = AppEventSender::new(tx_raw);
|
||||
let mut view = FeedbackNoteView::new(FeedbackCategory::Bug, tx, /*include_logs*/ true);
|
||||
let mut view = FeedbackNoteView::new(
|
||||
FeedbackCategory::Bug,
|
||||
Some("turn-123".to_string()),
|
||||
tx,
|
||||
/*include_logs*/ true,
|
||||
);
|
||||
view.textarea.insert_str(" something broke ");
|
||||
|
||||
view.submit();
|
||||
@@ -670,8 +686,9 @@ mod tests {
|
||||
AppEvent::SubmitFeedback {
|
||||
category: FeedbackCategory::Bug,
|
||||
reason: Some(reason),
|
||||
turn_id: Some(turn_id),
|
||||
include_logs: true,
|
||||
} if reason == "something broke"
|
||||
} if reason == "something broke" && turn_id == "turn-123"
|
||||
));
|
||||
assert_eq!(view.is_complete(), true);
|
||||
}
|
||||
@@ -682,6 +699,7 @@ mod tests {
|
||||
let tx = AppEventSender::new(tx_raw);
|
||||
let mut view = FeedbackNoteView::new(
|
||||
FeedbackCategory::GoodResult,
|
||||
/*turn_id*/ None,
|
||||
tx,
|
||||
/*include_logs*/ false,
|
||||
);
|
||||
@@ -694,6 +712,7 @@ mod tests {
|
||||
AppEvent::SubmitFeedback {
|
||||
category: FeedbackCategory::GoodResult,
|
||||
reason: None,
|
||||
turn_id: None,
|
||||
include_logs: false,
|
||||
}
|
||||
));
|
||||
|
||||
@@ -854,6 +854,7 @@ pub(crate) struct ChatWidget {
|
||||
pending_status_indicator_restore: bool,
|
||||
suppress_queue_autosend: bool,
|
||||
thread_id: Option<ThreadId>,
|
||||
last_turn_id: Option<String>,
|
||||
thread_name: Option<String>,
|
||||
forked_from: Option<ThreadId>,
|
||||
frame_requester: FrameRequester,
|
||||
@@ -1961,6 +1962,7 @@ impl ChatWidget {
|
||||
self.set_skills(/*skills*/ None);
|
||||
self.session_network_proxy = event.network_proxy.clone();
|
||||
self.thread_id = Some(event.session_id);
|
||||
self.last_turn_id = None;
|
||||
self.thread_name = event.thread_name.clone();
|
||||
self.forked_from = event.forked_from_id;
|
||||
self.current_rollout_path = event.rollout_path.clone();
|
||||
@@ -2143,6 +2145,7 @@ impl ChatWidget {
|
||||
) {
|
||||
let view = crate::bottom_pane::FeedbackNoteView::new(
|
||||
category,
|
||||
self.last_turn_id.clone(),
|
||||
self.app_event_tx.clone(),
|
||||
include_logs,
|
||||
);
|
||||
@@ -4827,6 +4830,7 @@ impl ChatWidget {
|
||||
pending_status_indicator_restore: false,
|
||||
suppress_queue_autosend: false,
|
||||
thread_id: None,
|
||||
last_turn_id: None,
|
||||
thread_name: None,
|
||||
forked_from: None,
|
||||
queued_user_messages: VecDeque::new(),
|
||||
@@ -6501,7 +6505,8 @@ impl ChatWidget {
|
||||
}
|
||||
}
|
||||
}
|
||||
ServerNotification::TurnStarted(_) => {
|
||||
ServerNotification::TurnStarted(notification) => {
|
||||
self.last_turn_id = Some(notification.turn.id);
|
||||
self.last_non_retry_error = None;
|
||||
if !matches!(replay_kind, Some(ReplayKind::ResumeInitialMessages)) {
|
||||
self.on_task_started();
|
||||
@@ -7072,8 +7077,11 @@ impl ChatWidget {
|
||||
}
|
||||
EventMsg::AgentReasoningSectionBreak(_) => self.on_reasoning_section_break(),
|
||||
EventMsg::TurnStarted(event) => {
|
||||
let turn_id = event.turn_id;
|
||||
let model_context_window = event.model_context_window;
|
||||
self.last_turn_id = Some(turn_id);
|
||||
if !is_resume_initial_replay {
|
||||
self.apply_turn_started_context_window(event.model_context_window);
|
||||
self.apply_turn_started_context_window(model_context_window);
|
||||
self.on_task_started();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -147,6 +147,43 @@ async fn live_app_server_turn_completed_clears_working_status_after_answer_item(
|
||||
assert!(chat.bottom_pane.status_widget().is_none());
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn live_app_server_turn_started_sets_feedback_turn_id() {
|
||||
let (mut chat, mut rx, _op_rx) = make_chatwidget_manual(/*model_override*/ None).await;
|
||||
|
||||
chat.handle_server_notification(
|
||||
ServerNotification::TurnStarted(TurnStartedNotification {
|
||||
thread_id: "thread-1".to_string(),
|
||||
turn: AppServerTurn {
|
||||
id: "turn-1".to_string(),
|
||||
items: Vec::new(),
|
||||
status: AppServerTurnStatus::InProgress,
|
||||
error: None,
|
||||
started_at: Some(0),
|
||||
completed_at: None,
|
||||
duration_ms: None,
|
||||
},
|
||||
}),
|
||||
/*replay_kind*/ None,
|
||||
);
|
||||
|
||||
chat.open_feedback_note(
|
||||
crate::app_event::FeedbackCategory::Bug,
|
||||
/*include_logs*/ false,
|
||||
);
|
||||
chat.handle_key_event(KeyEvent::new(KeyCode::Enter, KeyModifiers::NONE));
|
||||
|
||||
assert_matches!(
|
||||
rx.try_recv(),
|
||||
Ok(AppEvent::SubmitFeedback {
|
||||
category: crate::app_event::FeedbackCategory::Bug,
|
||||
reason: None,
|
||||
turn_id: Some(turn_id),
|
||||
include_logs: false,
|
||||
}) if turn_id == "turn-1"
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn live_app_server_file_change_item_started_preserves_changes() {
|
||||
let (mut chat, mut rx, _op_rx) = make_chatwidget_manual(/*model_override*/ None).await;
|
||||
|
||||
@@ -244,6 +244,7 @@ pub(super) async fn make_chatwidget_manual(
|
||||
pending_status_indicator_restore: false,
|
||||
suppress_queue_autosend: false,
|
||||
thread_id: None,
|
||||
last_turn_id: None,
|
||||
thread_name: None,
|
||||
forked_from: None,
|
||||
frame_requester: FrameRequester::test_dummy(),
|
||||
|
||||
Reference in New Issue
Block a user