Add log upload support (#5257)
This commit is contained in:
190
codex-rs/Cargo.lock
generated
190
codex-rs/Cargo.lock
generated
@@ -1144,6 +1144,17 @@ dependencies = [
|
|||||||
"tempfile",
|
"tempfile",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "codex-feedback"
|
||||||
|
version = "0.0.0"
|
||||||
|
dependencies = [
|
||||||
|
"anyhow",
|
||||||
|
"codex-protocol",
|
||||||
|
"pretty_assertions",
|
||||||
|
"sentry",
|
||||||
|
"tracing-subscriber",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "codex-file-search"
|
name = "codex-file-search"
|
||||||
version = "0.0.0"
|
version = "0.0.0"
|
||||||
@@ -1388,6 +1399,7 @@ dependencies = [
|
|||||||
"codex-arg0",
|
"codex-arg0",
|
||||||
"codex-common",
|
"codex-common",
|
||||||
"codex-core",
|
"codex-core",
|
||||||
|
"codex-feedback",
|
||||||
"codex-file-search",
|
"codex-file-search",
|
||||||
"codex-git-tooling",
|
"codex-git-tooling",
|
||||||
"codex-login",
|
"codex-login",
|
||||||
@@ -1824,6 +1836,16 @@ version = "0.1.4"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "092966b41edc516079bdf31ec78a2e0588d1d0c08f78b91d8307215928642b2b"
|
checksum = "092966b41edc516079bdf31ec78a2e0588d1d0c08f78b91d8307215928642b2b"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "debugid"
|
||||||
|
version = "0.8.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "bef552e6f588e446098f6ba40d89ac146c8c7b64aade83c051ee00bb5d2bc18d"
|
||||||
|
dependencies = [
|
||||||
|
"serde",
|
||||||
|
"uuid",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "debugserver-types"
|
name = "debugserver-types"
|
||||||
version = "0.5.0"
|
version = "0.5.0"
|
||||||
@@ -2302,6 +2324,18 @@ dependencies = [
|
|||||||
"winapi",
|
"winapi",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "findshlibs"
|
||||||
|
version = "0.10.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "40b9e59cd0f7e0806cca4be089683ecb6434e602038df21fe6bf6711b2f07f64"
|
||||||
|
dependencies = [
|
||||||
|
"cc",
|
||||||
|
"lazy_static",
|
||||||
|
"libc",
|
||||||
|
"winapi",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "fixed_decimal"
|
name = "fixed_decimal"
|
||||||
version = "0.7.0"
|
version = "0.7.0"
|
||||||
@@ -2670,6 +2704,17 @@ dependencies = [
|
|||||||
"windows-sys 0.59.0",
|
"windows-sys 0.59.0",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "hostname"
|
||||||
|
version = "0.4.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "a56f203cd1c76362b69e3863fd987520ac36cf70a8c92627449b2f64a8cf7d65"
|
||||||
|
dependencies = [
|
||||||
|
"cfg-if",
|
||||||
|
"libc",
|
||||||
|
"windows-link 0.1.3",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "http"
|
name = "http"
|
||||||
version = "1.3.1"
|
version = "1.3.1"
|
||||||
@@ -4906,6 +4951,15 @@ version = "2.1.1"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "357703d41365b4b27c590e3ed91eabb1b663f07c4c084095e60cbed4362dff0d"
|
checksum = "357703d41365b4b27c590e3ed91eabb1b663f07c4c084095e60cbed4362dff0d"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "rustc_version"
|
||||||
|
version = "0.4.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "cfcb3a22ef46e85b45de6ee7e79d063319ebb6594faafcf1c225ea92ab6e9b92"
|
||||||
|
dependencies = [
|
||||||
|
"semver",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "rustix"
|
name = "rustix"
|
||||||
version = "0.38.44"
|
version = "0.38.44"
|
||||||
@@ -5220,6 +5274,120 @@ dependencies = [
|
|||||||
"libc",
|
"libc",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "semver"
|
||||||
|
version = "1.0.27"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "d767eb0aabc880b29956c35734170f26ed551a859dbd361d140cdbeca61ab1e2"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "sentry"
|
||||||
|
version = "0.34.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "5484316556650182f03b43d4c746ce0e3e48074a21e2f51244b648b6542e1066"
|
||||||
|
dependencies = [
|
||||||
|
"httpdate",
|
||||||
|
"native-tls",
|
||||||
|
"reqwest",
|
||||||
|
"sentry-backtrace",
|
||||||
|
"sentry-contexts",
|
||||||
|
"sentry-core",
|
||||||
|
"sentry-debug-images",
|
||||||
|
"sentry-panic",
|
||||||
|
"sentry-tracing",
|
||||||
|
"tokio",
|
||||||
|
"ureq",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "sentry-backtrace"
|
||||||
|
version = "0.34.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "40aa225bb41e2ec9d7c90886834367f560efc1af028f1c5478a6cce6a59c463a"
|
||||||
|
dependencies = [
|
||||||
|
"backtrace",
|
||||||
|
"once_cell",
|
||||||
|
"regex",
|
||||||
|
"sentry-core",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "sentry-contexts"
|
||||||
|
version = "0.34.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "1a8dd746da3d16cb8c39751619cefd4fcdbd6df9610f3310fd646b55f6e39910"
|
||||||
|
dependencies = [
|
||||||
|
"hostname",
|
||||||
|
"libc",
|
||||||
|
"os_info",
|
||||||
|
"rustc_version",
|
||||||
|
"sentry-core",
|
||||||
|
"uname",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "sentry-core"
|
||||||
|
version = "0.34.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "161283cfe8e99c8f6f236a402b9ccf726b201f365988b5bb637ebca0abbd4a30"
|
||||||
|
dependencies = [
|
||||||
|
"once_cell",
|
||||||
|
"rand 0.8.5",
|
||||||
|
"sentry-types",
|
||||||
|
"serde",
|
||||||
|
"serde_json",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "sentry-debug-images"
|
||||||
|
version = "0.34.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "8fc6b25e945fcaa5e97c43faee0267eebda9f18d4b09a251775d8fef1086238a"
|
||||||
|
dependencies = [
|
||||||
|
"findshlibs",
|
||||||
|
"once_cell",
|
||||||
|
"sentry-core",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "sentry-panic"
|
||||||
|
version = "0.34.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "bc74f229c7186dd971a9491ffcbe7883544aa064d1589bd30b83fb856cd22d63"
|
||||||
|
dependencies = [
|
||||||
|
"sentry-backtrace",
|
||||||
|
"sentry-core",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "sentry-tracing"
|
||||||
|
version = "0.34.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "cd3c5faf2103cd01eeda779ea439b68c4ee15adcdb16600836e97feafab362ec"
|
||||||
|
dependencies = [
|
||||||
|
"sentry-backtrace",
|
||||||
|
"sentry-core",
|
||||||
|
"tracing-core",
|
||||||
|
"tracing-subscriber",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "sentry-types"
|
||||||
|
version = "0.34.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "5d68cdf6bc41b8ff3ae2a9c4671e97426dcdd154cc1d4b6b72813f285d6b163f"
|
||||||
|
dependencies = [
|
||||||
|
"debugid",
|
||||||
|
"hex",
|
||||||
|
"rand 0.8.5",
|
||||||
|
"serde",
|
||||||
|
"serde_json",
|
||||||
|
"thiserror 1.0.69",
|
||||||
|
"time",
|
||||||
|
"url",
|
||||||
|
"uuid",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "serde"
|
name = "serde"
|
||||||
version = "1.0.226"
|
version = "1.0.226"
|
||||||
@@ -6427,6 +6595,15 @@ dependencies = [
|
|||||||
"winapi",
|
"winapi",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "uname"
|
||||||
|
version = "0.1.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "b72f89f0ca32e4db1c04e2a72f5345d59796d4866a1ee0609084569f73683dc8"
|
||||||
|
dependencies = [
|
||||||
|
"libc",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "unicase"
|
name = "unicase"
|
||||||
version = "2.8.1"
|
version = "2.8.1"
|
||||||
@@ -6486,6 +6663,19 @@ version = "0.9.0"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1"
|
checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "ureq"
|
||||||
|
version = "2.12.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "02d1a66277ed75f640d608235660df48c8e3c19f3b4edb6a263315626cc3c01d"
|
||||||
|
dependencies = [
|
||||||
|
"base64",
|
||||||
|
"log",
|
||||||
|
"native-tls",
|
||||||
|
"once_cell",
|
||||||
|
"url",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "url"
|
name = "url"
|
||||||
version = "2.5.4"
|
version = "2.5.4"
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ members = [
|
|||||||
"app-server-protocol",
|
"app-server-protocol",
|
||||||
"apply-patch",
|
"apply-patch",
|
||||||
"arg0",
|
"arg0",
|
||||||
|
"feedback",
|
||||||
"codex-backend-openapi-models",
|
"codex-backend-openapi-models",
|
||||||
"cloud-tasks",
|
"cloud-tasks",
|
||||||
"cloud-tasks-client",
|
"cloud-tasks-client",
|
||||||
@@ -56,6 +57,7 @@ codex-chatgpt = { path = "chatgpt" }
|
|||||||
codex-common = { path = "common" }
|
codex-common = { path = "common" }
|
||||||
codex-core = { path = "core" }
|
codex-core = { path = "core" }
|
||||||
codex-exec = { path = "exec" }
|
codex-exec = { path = "exec" }
|
||||||
|
codex-feedback = { path = "feedback" }
|
||||||
codex-file-search = { path = "file-search" }
|
codex-file-search = { path = "file-search" }
|
||||||
codex-git-tooling = { path = "git-tooling" }
|
codex-git-tooling = { path = "git-tooling" }
|
||||||
codex-linux-sandbox = { path = "linux-sandbox" }
|
codex-linux-sandbox = { path = "linux-sandbox" }
|
||||||
@@ -83,8 +85,8 @@ ansi-to-tui = "7.0.0"
|
|||||||
anyhow = "1"
|
anyhow = "1"
|
||||||
arboard = "3"
|
arboard = "3"
|
||||||
askama = "0.12"
|
askama = "0.12"
|
||||||
assert_matches = "1.5.0"
|
|
||||||
assert_cmd = "2"
|
assert_cmd = "2"
|
||||||
|
assert_matches = "1.5.0"
|
||||||
async-channel = "2.3.1"
|
async-channel = "2.3.1"
|
||||||
async-stream = "0.3.6"
|
async-stream = "0.3.6"
|
||||||
async-trait = "0.1.89"
|
async-trait = "0.1.89"
|
||||||
@@ -147,6 +149,7 @@ reqwest = "0.12"
|
|||||||
rmcp = { version = "0.8.0", default-features = false }
|
rmcp = { version = "0.8.0", default-features = false }
|
||||||
schemars = "0.8.22"
|
schemars = "0.8.22"
|
||||||
seccompiler = "0.5.0"
|
seccompiler = "0.5.0"
|
||||||
|
sentry = "0.34.0"
|
||||||
serde = "1"
|
serde = "1"
|
||||||
serde_json = "1"
|
serde_json = "1"
|
||||||
serde_with = "3.14"
|
serde_with = "3.14"
|
||||||
|
|||||||
13
codex-rs/feedback/Cargo.toml
Normal file
13
codex-rs/feedback/Cargo.toml
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
[package]
|
||||||
|
edition.workspace = true
|
||||||
|
name = "codex-feedback"
|
||||||
|
version.workspace = true
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
anyhow = { workspace = true }
|
||||||
|
codex-protocol = { workspace = true }
|
||||||
|
sentry = { version = "0.34" }
|
||||||
|
tracing-subscriber = { workspace = true }
|
||||||
|
|
||||||
|
[dev-dependencies]
|
||||||
|
pretty_assertions = { workspace = true }
|
||||||
231
codex-rs/feedback/src/lib.rs
Normal file
231
codex-rs/feedback/src/lib.rs
Normal file
@@ -0,0 +1,231 @@
|
|||||||
|
use std::collections::VecDeque;
|
||||||
|
use std::fs;
|
||||||
|
use std::io::Write;
|
||||||
|
use std::io::{self};
|
||||||
|
use std::path::PathBuf;
|
||||||
|
use std::sync::Arc;
|
||||||
|
use std::sync::Mutex;
|
||||||
|
use std::time::Duration;
|
||||||
|
|
||||||
|
use anyhow::Result;
|
||||||
|
use anyhow::anyhow;
|
||||||
|
use codex_protocol::ConversationId;
|
||||||
|
use tracing_subscriber::fmt::writer::MakeWriter;
|
||||||
|
|
||||||
|
const DEFAULT_MAX_BYTES: usize = 2 * 1024 * 1024; // 2 MiB
|
||||||
|
const SENTRY_DSN: &str =
|
||||||
|
"https://ae32ed50620d7a7792c1ce5df38b3e3e@o33249.ingest.us.sentry.io/4510195390611458";
|
||||||
|
const UPLOAD_TIMEOUT_SECS: u64 = 10;
|
||||||
|
|
||||||
|
#[derive(Clone)]
|
||||||
|
pub struct CodexFeedback {
|
||||||
|
inner: Arc<FeedbackInner>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for CodexFeedback {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self::new()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl CodexFeedback {
|
||||||
|
pub fn new() -> Self {
|
||||||
|
Self::with_capacity(DEFAULT_MAX_BYTES)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn with_capacity(max_bytes: usize) -> Self {
|
||||||
|
Self {
|
||||||
|
inner: Arc::new(FeedbackInner::new(max_bytes)),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn make_writer(&self) -> FeedbackMakeWriter {
|
||||||
|
FeedbackMakeWriter {
|
||||||
|
inner: self.inner.clone(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn snapshot(&self, session_id: Option<ConversationId>) -> CodexLogSnapshot {
|
||||||
|
let bytes = {
|
||||||
|
let guard = self.inner.ring.lock().expect("mutex poisoned");
|
||||||
|
guard.snapshot_bytes()
|
||||||
|
};
|
||||||
|
CodexLogSnapshot {
|
||||||
|
bytes,
|
||||||
|
thread_id: session_id
|
||||||
|
.map(|id| id.to_string())
|
||||||
|
.unwrap_or("no-active-thread-".to_string() + &ConversationId::new().to_string()),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
struct FeedbackInner {
|
||||||
|
ring: Mutex<RingBuffer>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl FeedbackInner {
|
||||||
|
fn new(max_bytes: usize) -> Self {
|
||||||
|
Self {
|
||||||
|
ring: Mutex::new(RingBuffer::new(max_bytes)),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone)]
|
||||||
|
pub struct FeedbackMakeWriter {
|
||||||
|
inner: Arc<FeedbackInner>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a> MakeWriter<'a> for FeedbackMakeWriter {
|
||||||
|
type Writer = FeedbackWriter;
|
||||||
|
|
||||||
|
fn make_writer(&'a self) -> Self::Writer {
|
||||||
|
FeedbackWriter {
|
||||||
|
inner: self.inner.clone(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct FeedbackWriter {
|
||||||
|
inner: Arc<FeedbackInner>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Write for FeedbackWriter {
|
||||||
|
fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
|
||||||
|
let mut guard = self.inner.ring.lock().map_err(|_| io::ErrorKind::Other)?;
|
||||||
|
guard.push_bytes(buf);
|
||||||
|
Ok(buf.len())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn flush(&mut self) -> io::Result<()> {
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
struct RingBuffer {
|
||||||
|
max: usize,
|
||||||
|
buf: VecDeque<u8>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl RingBuffer {
|
||||||
|
fn new(capacity: usize) -> Self {
|
||||||
|
Self {
|
||||||
|
max: capacity,
|
||||||
|
buf: VecDeque::with_capacity(capacity),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn len(&self) -> usize {
|
||||||
|
self.buf.len()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn push_bytes(&mut self, data: &[u8]) {
|
||||||
|
if data.is_empty() {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// If the incoming chunk is larger than capacity, keep only the trailing bytes.
|
||||||
|
if data.len() >= self.max {
|
||||||
|
self.buf.clear();
|
||||||
|
let start = data.len() - self.max;
|
||||||
|
self.buf.extend(data[start..].iter().copied());
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Evict from the front if we would exceed capacity.
|
||||||
|
let needed = self.len() + data.len();
|
||||||
|
if needed > self.max {
|
||||||
|
let to_drop = needed - self.max;
|
||||||
|
for _ in 0..to_drop {
|
||||||
|
let _ = self.buf.pop_front();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
self.buf.extend(data.iter().copied());
|
||||||
|
}
|
||||||
|
|
||||||
|
fn snapshot_bytes(&self) -> Vec<u8> {
|
||||||
|
self.buf.iter().copied().collect()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct CodexLogSnapshot {
|
||||||
|
bytes: Vec<u8>,
|
||||||
|
pub thread_id: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl CodexLogSnapshot {
|
||||||
|
pub(crate) fn as_bytes(&self) -> &[u8] {
|
||||||
|
&self.bytes
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn save_to_temp_file(&self) -> io::Result<PathBuf> {
|
||||||
|
let dir = std::env::temp_dir();
|
||||||
|
let filename = format!("codex-feedback-{}.log", self.thread_id);
|
||||||
|
let path = dir.join(filename);
|
||||||
|
fs::write(&path, self.as_bytes())?;
|
||||||
|
Ok(path)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn upload_to_sentry(&self) -> Result<()> {
|
||||||
|
use std::collections::BTreeMap;
|
||||||
|
use std::str::FromStr;
|
||||||
|
use std::sync::Arc;
|
||||||
|
|
||||||
|
use sentry::Client;
|
||||||
|
use sentry::ClientOptions;
|
||||||
|
use sentry::protocol::Attachment;
|
||||||
|
use sentry::protocol::Envelope;
|
||||||
|
use sentry::protocol::EnvelopeItem;
|
||||||
|
use sentry::protocol::Event;
|
||||||
|
use sentry::protocol::Level;
|
||||||
|
use sentry::transports::DefaultTransportFactory;
|
||||||
|
use sentry::types::Dsn;
|
||||||
|
|
||||||
|
let client = Client::from_config(ClientOptions {
|
||||||
|
dsn: Some(Dsn::from_str(SENTRY_DSN).map_err(|e| anyhow!("invalid DSN: {}", e))?),
|
||||||
|
transport: Some(Arc::new(DefaultTransportFactory {})),
|
||||||
|
..Default::default()
|
||||||
|
});
|
||||||
|
|
||||||
|
let tags = BTreeMap::from([(String::from("thread_id"), self.thread_id.to_string())]);
|
||||||
|
|
||||||
|
let event = Event {
|
||||||
|
level: Level::Error,
|
||||||
|
message: Some("Codex Log Upload ".to_string() + &self.thread_id),
|
||||||
|
tags,
|
||||||
|
..Default::default()
|
||||||
|
};
|
||||||
|
let mut envelope = Envelope::new();
|
||||||
|
envelope.add_item(EnvelopeItem::Event(event));
|
||||||
|
envelope.add_item(EnvelopeItem::Attachment(Attachment {
|
||||||
|
buffer: self.bytes.clone(),
|
||||||
|
filename: String::from("codex-logs.log"),
|
||||||
|
content_type: Some("text/plain".to_string()),
|
||||||
|
ty: None,
|
||||||
|
}));
|
||||||
|
|
||||||
|
client.send_envelope(envelope);
|
||||||
|
client.flush(Some(Duration::from_secs(UPLOAD_TIMEOUT_SECS)));
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn ring_buffer_drops_front_when_full() {
|
||||||
|
let fb = CodexFeedback::with_capacity(8);
|
||||||
|
{
|
||||||
|
let mut w = fb.make_writer().make_writer();
|
||||||
|
w.write_all(b"abcdefgh").unwrap();
|
||||||
|
w.write_all(b"ij").unwrap();
|
||||||
|
}
|
||||||
|
let snap = fb.snapshot(None);
|
||||||
|
// Capacity 8: after writing 10 bytes, we should keep the last 8.
|
||||||
|
pretty_assertions::assert_eq!(std::str::from_utf8(snap.as_bytes()).unwrap(), "cdefghij");
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -40,6 +40,7 @@ codex-login = { workspace = true }
|
|||||||
codex-ollama = { workspace = true }
|
codex-ollama = { workspace = true }
|
||||||
codex-protocol = { workspace = true }
|
codex-protocol = { workspace = true }
|
||||||
codex-app-server-protocol = { workspace = true }
|
codex-app-server-protocol = { workspace = true }
|
||||||
|
codex-feedback = { workspace = true }
|
||||||
color-eyre = { workspace = true }
|
color-eyre = { workspace = true }
|
||||||
crossterm = { workspace = true, features = ["bracketed-paste", "event-stream"] }
|
crossterm = { workspace = true, features = ["bracketed-paste", "event-stream"] }
|
||||||
diffy = { workspace = true }
|
diffy = { workspace = true }
|
||||||
|
|||||||
@@ -74,12 +74,13 @@ pub(crate) struct App {
|
|||||||
|
|
||||||
// Esc-backtracking state grouped
|
// Esc-backtracking state grouped
|
||||||
pub(crate) backtrack: crate::app_backtrack::BacktrackState,
|
pub(crate) backtrack: crate::app_backtrack::BacktrackState,
|
||||||
|
pub(crate) feedback: codex_feedback::CodexFeedback,
|
||||||
/// Set when the user confirms an update; propagated on exit.
|
/// Set when the user confirms an update; propagated on exit.
|
||||||
pub(crate) pending_update_action: Option<UpdateAction>,
|
pub(crate) pending_update_action: Option<UpdateAction>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl App {
|
impl App {
|
||||||
|
#[allow(clippy::too_many_arguments)]
|
||||||
pub async fn run(
|
pub async fn run(
|
||||||
tui: &mut tui::Tui,
|
tui: &mut tui::Tui,
|
||||||
auth_manager: Arc<AuthManager>,
|
auth_manager: Arc<AuthManager>,
|
||||||
@@ -88,6 +89,7 @@ impl App {
|
|||||||
initial_prompt: Option<String>,
|
initial_prompt: Option<String>,
|
||||||
initial_images: Vec<PathBuf>,
|
initial_images: Vec<PathBuf>,
|
||||||
resume_selection: ResumeSelection,
|
resume_selection: ResumeSelection,
|
||||||
|
feedback: codex_feedback::CodexFeedback,
|
||||||
) -> Result<AppExitInfo> {
|
) -> Result<AppExitInfo> {
|
||||||
use tokio_stream::StreamExt;
|
use tokio_stream::StreamExt;
|
||||||
let (app_event_tx, mut app_event_rx) = unbounded_channel();
|
let (app_event_tx, mut app_event_rx) = unbounded_channel();
|
||||||
@@ -110,6 +112,7 @@ impl App {
|
|||||||
initial_images: initial_images.clone(),
|
initial_images: initial_images.clone(),
|
||||||
enhanced_keys_supported,
|
enhanced_keys_supported,
|
||||||
auth_manager: auth_manager.clone(),
|
auth_manager: auth_manager.clone(),
|
||||||
|
feedback: feedback.clone(),
|
||||||
};
|
};
|
||||||
ChatWidget::new(init, conversation_manager.clone())
|
ChatWidget::new(init, conversation_manager.clone())
|
||||||
}
|
}
|
||||||
@@ -132,6 +135,7 @@ impl App {
|
|||||||
initial_images: initial_images.clone(),
|
initial_images: initial_images.clone(),
|
||||||
enhanced_keys_supported,
|
enhanced_keys_supported,
|
||||||
auth_manager: auth_manager.clone(),
|
auth_manager: auth_manager.clone(),
|
||||||
|
feedback: feedback.clone(),
|
||||||
};
|
};
|
||||||
ChatWidget::new_from_existing(
|
ChatWidget::new_from_existing(
|
||||||
init,
|
init,
|
||||||
@@ -158,6 +162,7 @@ impl App {
|
|||||||
has_emitted_history_lines: false,
|
has_emitted_history_lines: false,
|
||||||
commit_anim_running: Arc::new(AtomicBool::new(false)),
|
commit_anim_running: Arc::new(AtomicBool::new(false)),
|
||||||
backtrack: BacktrackState::default(),
|
backtrack: BacktrackState::default(),
|
||||||
|
feedback: feedback.clone(),
|
||||||
pending_update_action: None,
|
pending_update_action: None,
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -236,6 +241,7 @@ impl App {
|
|||||||
initial_images: Vec::new(),
|
initial_images: Vec::new(),
|
||||||
enhanced_keys_supported: self.enhanced_keys_supported,
|
enhanced_keys_supported: self.enhanced_keys_supported,
|
||||||
auth_manager: self.auth_manager.clone(),
|
auth_manager: self.auth_manager.clone(),
|
||||||
|
feedback: self.feedback.clone(),
|
||||||
};
|
};
|
||||||
self.chat_widget = ChatWidget::new(init, self.server.clone());
|
self.chat_widget = ChatWidget::new(init, self.server.clone());
|
||||||
tui.frame_requester().schedule_frame();
|
tui.frame_requester().schedule_frame();
|
||||||
@@ -549,6 +555,7 @@ mod tests {
|
|||||||
enhanced_keys_supported: false,
|
enhanced_keys_supported: false,
|
||||||
commit_anim_running: Arc::new(AtomicBool::new(false)),
|
commit_anim_running: Arc::new(AtomicBool::new(false)),
|
||||||
backtrack: BacktrackState::default(),
|
backtrack: BacktrackState::default(),
|
||||||
|
feedback: codex_feedback::CodexFeedback::new(),
|
||||||
pending_update_action: None,
|
pending_update_action: None,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -339,6 +339,7 @@ impl App {
|
|||||||
initial_images: Vec::new(),
|
initial_images: Vec::new(),
|
||||||
enhanced_keys_supported: self.enhanced_keys_supported,
|
enhanced_keys_supported: self.enhanced_keys_supported,
|
||||||
auth_manager: self.auth_manager.clone(),
|
auth_manager: self.auth_manager.clone(),
|
||||||
|
feedback: self.feedback.clone(),
|
||||||
};
|
};
|
||||||
self.chat_widget =
|
self.chat_widget =
|
||||||
crate::chatwidget::ChatWidget::new_from_existing(init, conv, session_configured);
|
crate::chatwidget::ChatWidget::new_from_existing(init, conv, session_configured);
|
||||||
|
|||||||
224
codex-rs/tui/src/bottom_pane/feedback_view.rs
Normal file
224
codex-rs/tui/src/bottom_pane/feedback_view.rs
Normal file
@@ -0,0 +1,224 @@
|
|||||||
|
use crate::app_event::AppEvent;
|
||||||
|
use crate::app_event_sender::AppEventSender;
|
||||||
|
use crate::history_cell;
|
||||||
|
use crate::history_cell::PlainHistoryCell;
|
||||||
|
use crate::render::renderable::Renderable;
|
||||||
|
use ratatui::buffer::Buffer;
|
||||||
|
use ratatui::layout::Rect;
|
||||||
|
use ratatui::style::Stylize;
|
||||||
|
use ratatui::text::Line;
|
||||||
|
use std::path::PathBuf;
|
||||||
|
|
||||||
|
use super::BottomPane;
|
||||||
|
use super::SelectionAction;
|
||||||
|
use super::SelectionItem;
|
||||||
|
use super::SelectionViewParams;
|
||||||
|
|
||||||
|
const BASE_ISSUE_URL: &str = "https://github.com/openai/codex/issues/new?template=2-bug-report.yml";
|
||||||
|
|
||||||
|
pub(crate) struct FeedbackView;
|
||||||
|
|
||||||
|
impl FeedbackView {
|
||||||
|
pub fn show(
|
||||||
|
bottom_pane: &mut BottomPane,
|
||||||
|
file_path: PathBuf,
|
||||||
|
snapshot: codex_feedback::CodexLogSnapshot,
|
||||||
|
) {
|
||||||
|
bottom_pane.show_selection_view(Self::selection_params(file_path, snapshot));
|
||||||
|
}
|
||||||
|
|
||||||
|
fn selection_params(
|
||||||
|
file_path: PathBuf,
|
||||||
|
snapshot: codex_feedback::CodexLogSnapshot,
|
||||||
|
) -> SelectionViewParams {
|
||||||
|
let header = FeedbackHeader::new(file_path);
|
||||||
|
|
||||||
|
let thread_id = snapshot.thread_id.clone();
|
||||||
|
|
||||||
|
let upload_action_tread_id = thread_id.clone();
|
||||||
|
let upload_action: SelectionAction = Box::new(move |tx: &AppEventSender| {
|
||||||
|
match snapshot.upload_to_sentry() {
|
||||||
|
Ok(()) => {
|
||||||
|
let issue_url = format!(
|
||||||
|
"{BASE_ISSUE_URL}&steps=Uploaded%20thread:%20{upload_action_tread_id}",
|
||||||
|
);
|
||||||
|
tx.send(AppEvent::InsertHistoryCell(Box::new(PlainHistoryCell::new(vec![
|
||||||
|
Line::from(
|
||||||
|
"• Codex logs uploaded. Please open an issue using the following URL:",
|
||||||
|
),
|
||||||
|
"".into(),
|
||||||
|
Line::from(vec![" ".into(), issue_url.cyan().underlined()]),
|
||||||
|
"".into(),
|
||||||
|
Line::from(vec![" Or mention your thread ID ".into(), upload_action_tread_id.clone().bold(), " in an existing issue.".into()])
|
||||||
|
]))));
|
||||||
|
}
|
||||||
|
Err(e) => {
|
||||||
|
tx.send(AppEvent::InsertHistoryCell(Box::new(
|
||||||
|
history_cell::new_error_event(format!("Failed to upload logs: {e}")),
|
||||||
|
)));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
let upload_item = SelectionItem {
|
||||||
|
name: "Yes".to_string(),
|
||||||
|
description: Some(
|
||||||
|
"Share the current Codex session logs with the team for troubleshooting."
|
||||||
|
.to_string(),
|
||||||
|
),
|
||||||
|
actions: vec![upload_action],
|
||||||
|
dismiss_on_select: true,
|
||||||
|
..Default::default()
|
||||||
|
};
|
||||||
|
|
||||||
|
let no_action: SelectionAction = Box::new(move |tx: &AppEventSender| {
|
||||||
|
let issue_url = format!("{BASE_ISSUE_URL}&steps=Thread%20ID:%20{thread_id}",);
|
||||||
|
|
||||||
|
tx.send(AppEvent::InsertHistoryCell(Box::new(
|
||||||
|
PlainHistoryCell::new(vec![
|
||||||
|
Line::from("• Please open an issue using the following URL:"),
|
||||||
|
"".into(),
|
||||||
|
Line::from(vec![" ".into(), issue_url.cyan().underlined()]),
|
||||||
|
"".into(),
|
||||||
|
Line::from(vec![
|
||||||
|
" Or mention your thread ID ".into(),
|
||||||
|
thread_id.clone().bold(),
|
||||||
|
" in an existing issue.".into(),
|
||||||
|
]),
|
||||||
|
]),
|
||||||
|
)));
|
||||||
|
});
|
||||||
|
|
||||||
|
let no_item = SelectionItem {
|
||||||
|
name: "No".to_string(),
|
||||||
|
actions: vec![no_action],
|
||||||
|
dismiss_on_select: true,
|
||||||
|
..Default::default()
|
||||||
|
};
|
||||||
|
|
||||||
|
let cancel_item = SelectionItem {
|
||||||
|
name: "Cancel".to_string(),
|
||||||
|
dismiss_on_select: true,
|
||||||
|
..Default::default()
|
||||||
|
};
|
||||||
|
|
||||||
|
SelectionViewParams {
|
||||||
|
header: Box::new(header),
|
||||||
|
items: vec![upload_item, no_item, cancel_item],
|
||||||
|
..Default::default()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
struct FeedbackHeader {
|
||||||
|
file_path: PathBuf,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl FeedbackHeader {
|
||||||
|
fn new(file_path: PathBuf) -> Self {
|
||||||
|
Self { file_path }
|
||||||
|
}
|
||||||
|
|
||||||
|
fn lines(&self) -> Vec<Line<'static>> {
|
||||||
|
vec![
|
||||||
|
Line::from("Do you want to upload logs before reporting issue?".bold()),
|
||||||
|
"".into(),
|
||||||
|
Line::from(
|
||||||
|
"Logs may include the full conversation history of this Codex process, including prompts, tool calls, and their results.",
|
||||||
|
),
|
||||||
|
Line::from(
|
||||||
|
"These logs are retained for 90 days and are used solely for troubleshooting and diagnostic purposes.",
|
||||||
|
),
|
||||||
|
"".into(),
|
||||||
|
Line::from(vec![
|
||||||
|
"You can review the exact content of the logs before they’re uploaded at:".into(),
|
||||||
|
]),
|
||||||
|
Line::from(self.file_path.display().to_string().dim()),
|
||||||
|
"".into(),
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Renderable for FeedbackHeader {
|
||||||
|
fn render(&self, area: Rect, buf: &mut Buffer) {
|
||||||
|
if area.width == 0 || area.height == 0 {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (i, line) in self.lines().into_iter().enumerate() {
|
||||||
|
let y = area.y.saturating_add(i as u16);
|
||||||
|
if y >= area.y.saturating_add(area.height) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
let line_area = Rect::new(area.x, y, area.width, 1).intersection(area);
|
||||||
|
line.render(line_area, buf);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn desired_height(&self, width: u16) -> u16 {
|
||||||
|
self.lines()
|
||||||
|
.iter()
|
||||||
|
.map(|line| line.desired_height(width))
|
||||||
|
.sum()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
use crate::app_event::AppEvent;
|
||||||
|
use crate::bottom_pane::list_selection_view::ListSelectionView;
|
||||||
|
use crate::style::user_message_style;
|
||||||
|
use codex_feedback::CodexFeedback;
|
||||||
|
use codex_protocol::ConversationId;
|
||||||
|
use insta::assert_snapshot;
|
||||||
|
use ratatui::buffer::Buffer;
|
||||||
|
use ratatui::layout::Rect;
|
||||||
|
use ratatui::style::Color;
|
||||||
|
use tokio::sync::mpsc::unbounded_channel;
|
||||||
|
|
||||||
|
fn buffer_to_string(buffer: &Buffer) -> String {
|
||||||
|
(0..buffer.area.height)
|
||||||
|
.map(|row| {
|
||||||
|
let mut line = String::new();
|
||||||
|
for col in 0..buffer.area.width {
|
||||||
|
let symbol = buffer[(buffer.area.x + col, buffer.area.y + row)].symbol();
|
||||||
|
if symbol.is_empty() {
|
||||||
|
line.push(' ');
|
||||||
|
} else {
|
||||||
|
line.push_str(symbol);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
line.trim_end().to_string()
|
||||||
|
})
|
||||||
|
.collect::<Vec<_>>()
|
||||||
|
.join("\n")
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn renders_feedback_view_header() {
|
||||||
|
let (tx_raw, _rx) = unbounded_channel::<AppEvent>();
|
||||||
|
let app_event_tx = AppEventSender::new(tx_raw);
|
||||||
|
let snapshot = CodexFeedback::new().snapshot(Some(
|
||||||
|
ConversationId::from_string("550e8400-e29b-41d4-a716-446655440000").unwrap(),
|
||||||
|
));
|
||||||
|
let file_path = PathBuf::from("/tmp/codex-feedback.log");
|
||||||
|
|
||||||
|
let params = FeedbackView::selection_params(file_path.clone(), snapshot);
|
||||||
|
let view = ListSelectionView::new(params, app_event_tx);
|
||||||
|
|
||||||
|
let width = 72;
|
||||||
|
let height = view.desired_height(width).max(1);
|
||||||
|
let area = Rect::new(0, 0, width, height);
|
||||||
|
let mut buf = Buffer::empty(area);
|
||||||
|
view.render(area, &mut buf);
|
||||||
|
|
||||||
|
let rendered =
|
||||||
|
buffer_to_string(&buf).replace(&file_path.display().to_string(), "<LOG_PATH>");
|
||||||
|
assert_snapshot!("feedback_view_render", rendered);
|
||||||
|
|
||||||
|
let cell_style = buf[(area.x, area.y)].style();
|
||||||
|
let expected_bg = user_message_style().bg.unwrap_or(Color::Reset);
|
||||||
|
assert_eq!(cell_style.bg.unwrap_or(Color::Reset), expected_bg);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -27,11 +27,13 @@ mod footer;
|
|||||||
mod list_selection_view;
|
mod list_selection_view;
|
||||||
mod prompt_args;
|
mod prompt_args;
|
||||||
pub(crate) use list_selection_view::SelectionViewParams;
|
pub(crate) use list_selection_view::SelectionViewParams;
|
||||||
|
mod feedback_view;
|
||||||
mod paste_burst;
|
mod paste_burst;
|
||||||
pub mod popup_consts;
|
pub mod popup_consts;
|
||||||
mod scroll_state;
|
mod scroll_state;
|
||||||
mod selection_popup_common;
|
mod selection_popup_common;
|
||||||
mod textarea;
|
mod textarea;
|
||||||
|
pub(crate) use feedback_view::FeedbackView;
|
||||||
|
|
||||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||||
pub(crate) enum CancellationEvent {
|
pub(crate) enum CancellationEvent {
|
||||||
|
|||||||
@@ -0,0 +1,17 @@
|
|||||||
|
---
|
||||||
|
source: tui/src/bottom_pane/feedback_view.rs
|
||||||
|
expression: rendered
|
||||||
|
---
|
||||||
|
Do you want to upload logs before reporting issue?
|
||||||
|
|
||||||
|
Logs may include the full conversation history of this Codex process
|
||||||
|
These logs are retained for 90 days and are used solely for troubles
|
||||||
|
|
||||||
|
You can review the exact content of the logs before they’re uploaded
|
||||||
|
<LOG_PATH>
|
||||||
|
|
||||||
|
|
||||||
|
› 1. Yes Share the current Codex session logs with the team for
|
||||||
|
troubleshooting.
|
||||||
|
2. No
|
||||||
|
3. Cancel
|
||||||
@@ -224,6 +224,7 @@ pub(crate) struct ChatWidgetInit {
|
|||||||
pub(crate) initial_images: Vec<PathBuf>,
|
pub(crate) initial_images: Vec<PathBuf>,
|
||||||
pub(crate) enhanced_keys_supported: bool,
|
pub(crate) enhanced_keys_supported: bool,
|
||||||
pub(crate) auth_manager: Arc<AuthManager>,
|
pub(crate) auth_manager: Arc<AuthManager>,
|
||||||
|
pub(crate) feedback: codex_feedback::CodexFeedback,
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) struct ChatWidget {
|
pub(crate) struct ChatWidget {
|
||||||
@@ -272,6 +273,8 @@ pub(crate) struct ChatWidget {
|
|||||||
needs_final_message_separator: bool,
|
needs_final_message_separator: bool,
|
||||||
|
|
||||||
last_rendered_width: std::cell::Cell<Option<usize>>,
|
last_rendered_width: std::cell::Cell<Option<usize>>,
|
||||||
|
// Feedback sink for /feedback
|
||||||
|
feedback: codex_feedback::CodexFeedback,
|
||||||
}
|
}
|
||||||
|
|
||||||
struct UserMessage {
|
struct UserMessage {
|
||||||
@@ -917,6 +920,7 @@ impl ChatWidget {
|
|||||||
initial_images,
|
initial_images,
|
||||||
enhanced_keys_supported,
|
enhanced_keys_supported,
|
||||||
auth_manager,
|
auth_manager,
|
||||||
|
feedback,
|
||||||
} = common;
|
} = common;
|
||||||
let mut rng = rand::rng();
|
let mut rng = rand::rng();
|
||||||
let placeholder = EXAMPLE_PROMPTS[rng.random_range(0..EXAMPLE_PROMPTS.len())].to_string();
|
let placeholder = EXAMPLE_PROMPTS[rng.random_range(0..EXAMPLE_PROMPTS.len())].to_string();
|
||||||
@@ -963,6 +967,7 @@ impl ChatWidget {
|
|||||||
ghost_snapshots_disabled: true,
|
ghost_snapshots_disabled: true,
|
||||||
needs_final_message_separator: false,
|
needs_final_message_separator: false,
|
||||||
last_rendered_width: std::cell::Cell::new(None),
|
last_rendered_width: std::cell::Cell::new(None),
|
||||||
|
feedback,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -980,6 +985,7 @@ impl ChatWidget {
|
|||||||
initial_images,
|
initial_images,
|
||||||
enhanced_keys_supported,
|
enhanced_keys_supported,
|
||||||
auth_manager,
|
auth_manager,
|
||||||
|
feedback,
|
||||||
} = common;
|
} = common;
|
||||||
let mut rng = rand::rng();
|
let mut rng = rand::rng();
|
||||||
let placeholder = EXAMPLE_PROMPTS[rng.random_range(0..EXAMPLE_PROMPTS.len())].to_string();
|
let placeholder = EXAMPLE_PROMPTS[rng.random_range(0..EXAMPLE_PROMPTS.len())].to_string();
|
||||||
@@ -1028,6 +1034,7 @@ impl ChatWidget {
|
|||||||
ghost_snapshots_disabled: true,
|
ghost_snapshots_disabled: true,
|
||||||
needs_final_message_separator: false,
|
needs_final_message_separator: false,
|
||||||
last_rendered_width: std::cell::Cell::new(None),
|
last_rendered_width: std::cell::Cell::new(None),
|
||||||
|
feedback,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1131,6 +1138,25 @@ impl ChatWidget {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
match cmd {
|
match cmd {
|
||||||
|
SlashCommand::Feedback => {
|
||||||
|
let snapshot = self.feedback.snapshot(self.conversation_id);
|
||||||
|
match snapshot.save_to_temp_file() {
|
||||||
|
Ok(path) => {
|
||||||
|
crate::bottom_pane::FeedbackView::show(
|
||||||
|
&mut self.bottom_pane,
|
||||||
|
path,
|
||||||
|
snapshot,
|
||||||
|
);
|
||||||
|
self.request_redraw();
|
||||||
|
}
|
||||||
|
Err(e) => {
|
||||||
|
self.add_to_history(history_cell::new_error_event(format!(
|
||||||
|
"Failed to save feedback logs: {e}"
|
||||||
|
)));
|
||||||
|
self.request_redraw();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
SlashCommand::New => {
|
SlashCommand::New => {
|
||||||
self.app_event_tx.send(AppEvent::NewSession);
|
self.app_event_tx.send(AppEvent::NewSession);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -235,6 +235,7 @@ async fn helpers_are_available_and_do_not_panic() {
|
|||||||
initial_images: Vec::new(),
|
initial_images: Vec::new(),
|
||||||
enhanced_keys_supported: false,
|
enhanced_keys_supported: false,
|
||||||
auth_manager,
|
auth_manager,
|
||||||
|
feedback: codex_feedback::CodexFeedback::new(),
|
||||||
};
|
};
|
||||||
let mut w = ChatWidget::new(init, conversation_manager);
|
let mut w = ChatWidget::new(init, conversation_manager);
|
||||||
// Basic construction sanity.
|
// Basic construction sanity.
|
||||||
@@ -291,6 +292,7 @@ fn make_chatwidget_manual() -> (
|
|||||||
ghost_snapshots_disabled: false,
|
ghost_snapshots_disabled: false,
|
||||||
needs_final_message_separator: false,
|
needs_final_message_separator: false,
|
||||||
last_rendered_width: std::cell::Cell::new(None),
|
last_rendered_width: std::cell::Cell::new(None),
|
||||||
|
feedback: codex_feedback::CodexFeedback::new(),
|
||||||
};
|
};
|
||||||
(widget, rx, op_rx)
|
(widget, rx, op_rx)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -23,6 +23,7 @@ use std::path::PathBuf;
|
|||||||
use tracing::error;
|
use tracing::error;
|
||||||
use tracing_appender::non_blocking;
|
use tracing_appender::non_blocking;
|
||||||
use tracing_subscriber::EnvFilter;
|
use tracing_subscriber::EnvFilter;
|
||||||
|
use tracing_subscriber::filter::Targets;
|
||||||
use tracing_subscriber::prelude::*;
|
use tracing_subscriber::prelude::*;
|
||||||
|
|
||||||
mod app;
|
mod app;
|
||||||
@@ -219,13 +220,21 @@ pub async fn run_main(
|
|||||||
})
|
})
|
||||||
};
|
};
|
||||||
|
|
||||||
// Build layered subscriber:
|
|
||||||
let file_layer = tracing_subscriber::fmt::layer()
|
let file_layer = tracing_subscriber::fmt::layer()
|
||||||
.with_writer(non_blocking)
|
.with_writer(non_blocking)
|
||||||
.with_target(false)
|
.with_target(false)
|
||||||
.with_span_events(tracing_subscriber::fmt::format::FmtSpan::CLOSE)
|
.with_span_events(tracing_subscriber::fmt::format::FmtSpan::CLOSE)
|
||||||
.with_filter(env_filter());
|
.with_filter(env_filter());
|
||||||
|
|
||||||
|
let feedback = codex_feedback::CodexFeedback::new();
|
||||||
|
let targets = Targets::new().with_default(tracing::Level::TRACE);
|
||||||
|
|
||||||
|
let feedback_layer = tracing_subscriber::fmt::layer()
|
||||||
|
.with_writer(feedback.make_writer())
|
||||||
|
.with_ansi(false)
|
||||||
|
.with_target(false)
|
||||||
|
.with_filter(targets);
|
||||||
|
|
||||||
if cli.oss {
|
if cli.oss {
|
||||||
codex_ollama::ensure_oss_ready(&config)
|
codex_ollama::ensure_oss_ready(&config)
|
||||||
.await
|
.await
|
||||||
@@ -250,15 +259,26 @@ pub async fn run_main(
|
|||||||
|
|
||||||
let _ = tracing_subscriber::registry()
|
let _ = tracing_subscriber::registry()
|
||||||
.with(file_layer)
|
.with(file_layer)
|
||||||
|
.with(feedback_layer)
|
||||||
.with(otel_layer)
|
.with(otel_layer)
|
||||||
.try_init();
|
.try_init();
|
||||||
} else {
|
} else {
|
||||||
let _ = tracing_subscriber::registry().with(file_layer).try_init();
|
let _ = tracing_subscriber::registry()
|
||||||
|
.with(file_layer)
|
||||||
|
.with(feedback_layer)
|
||||||
|
.try_init();
|
||||||
};
|
};
|
||||||
|
|
||||||
run_ratatui_app(cli, config, overrides, cli_kv_overrides, active_profile)
|
run_ratatui_app(
|
||||||
.await
|
cli,
|
||||||
.map_err(|err| std::io::Error::other(err.to_string()))
|
config,
|
||||||
|
overrides,
|
||||||
|
cli_kv_overrides,
|
||||||
|
active_profile,
|
||||||
|
feedback,
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
.map_err(|err| std::io::Error::other(err.to_string()))
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn run_ratatui_app(
|
async fn run_ratatui_app(
|
||||||
@@ -267,6 +287,7 @@ async fn run_ratatui_app(
|
|||||||
overrides: ConfigOverrides,
|
overrides: ConfigOverrides,
|
||||||
cli_kv_overrides: Vec<(String, toml::Value)>,
|
cli_kv_overrides: Vec<(String, toml::Value)>,
|
||||||
active_profile: Option<String>,
|
active_profile: Option<String>,
|
||||||
|
feedback: codex_feedback::CodexFeedback,
|
||||||
) -> color_eyre::Result<AppExitInfo> {
|
) -> color_eyre::Result<AppExitInfo> {
|
||||||
color_eyre::install()?;
|
color_eyre::install()?;
|
||||||
|
|
||||||
@@ -462,6 +483,7 @@ async fn run_ratatui_app(
|
|||||||
prompt,
|
prompt,
|
||||||
images,
|
images,
|
||||||
resume_selection,
|
resume_selection,
|
||||||
|
feedback,
|
||||||
)
|
)
|
||||||
.await;
|
.await;
|
||||||
|
|
||||||
|
|||||||
@@ -25,6 +25,7 @@ pub enum SlashCommand {
|
|||||||
Mcp,
|
Mcp,
|
||||||
Logout,
|
Logout,
|
||||||
Quit,
|
Quit,
|
||||||
|
Feedback,
|
||||||
#[cfg(debug_assertions)]
|
#[cfg(debug_assertions)]
|
||||||
TestApproval,
|
TestApproval,
|
||||||
}
|
}
|
||||||
@@ -33,6 +34,7 @@ impl SlashCommand {
|
|||||||
/// User-visible description shown in the popup.
|
/// User-visible description shown in the popup.
|
||||||
pub fn description(self) -> &'static str {
|
pub fn description(self) -> &'static str {
|
||||||
match self {
|
match self {
|
||||||
|
SlashCommand::Feedback => "send logs to maintainers",
|
||||||
SlashCommand::New => "start a new chat during a conversation",
|
SlashCommand::New => "start a new chat during a conversation",
|
||||||
SlashCommand::Init => "create an AGENTS.md file with instructions for Codex",
|
SlashCommand::Init => "create an AGENTS.md file with instructions for Codex",
|
||||||
SlashCommand::Compact => "summarize conversation to prevent hitting the context limit",
|
SlashCommand::Compact => "summarize conversation to prevent hitting the context limit",
|
||||||
@@ -72,6 +74,7 @@ impl SlashCommand {
|
|||||||
| SlashCommand::Mention
|
| SlashCommand::Mention
|
||||||
| SlashCommand::Status
|
| SlashCommand::Status
|
||||||
| SlashCommand::Mcp
|
| SlashCommand::Mcp
|
||||||
|
| SlashCommand::Feedback
|
||||||
| SlashCommand::Quit => true,
|
| SlashCommand::Quit => true,
|
||||||
|
|
||||||
#[cfg(debug_assertions)]
|
#[cfg(debug_assertions)]
|
||||||
@@ -85,13 +88,7 @@ pub fn built_in_slash_commands() -> Vec<(&'static str, SlashCommand)> {
|
|||||||
let show_beta_features = beta_features_enabled();
|
let show_beta_features = beta_features_enabled();
|
||||||
|
|
||||||
SlashCommand::iter()
|
SlashCommand::iter()
|
||||||
.filter(|cmd| {
|
.filter(|cmd| *cmd != SlashCommand::Undo || show_beta_features)
|
||||||
if *cmd == SlashCommand::Undo {
|
|
||||||
show_beta_features
|
|
||||||
} else {
|
|
||||||
true
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.map(|c| (c.command(), c))
|
.map(|c| (c.command(), c))
|
||||||
.collect()
|
.collect()
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user