refactor(cli): adopt flex-width rendering, remove data-layer truncation
Replace hardcoded truncation widths across CLI commands with render::flex_width() calls that adapt to terminal size. Remove server-side truncate_to_chars() in timeline collect/seed stages so full text is preserved through the pipeline — truncation now happens only at the presentation layer where terminal width is known. Affected commands: explain, file-history, list (issues/mrs/notes), me, timeline, who (active/expert/workload).
This commit is contained in:
@@ -378,17 +378,10 @@ fn get_mr_assignees(conn: &Connection, mr_id: i64) -> Result<Vec<String>> {
|
|||||||
// Description excerpt helper
|
// Description excerpt helper
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
fn truncate_description(desc: Option<&str>, max_len: usize) -> String {
|
fn truncate_description(desc: Option<&str>) -> String {
|
||||||
match desc {
|
match desc {
|
||||||
None | Some("") => "(no description)".to_string(),
|
None | Some("") => "(no description)".to_string(),
|
||||||
Some(s) => {
|
Some(s) => s.to_string(),
|
||||||
if s.len() <= max_len {
|
|
||||||
s.to_string()
|
|
||||||
} else {
|
|
||||||
let boundary = s.floor_char_boundary(max_len);
|
|
||||||
format!("{}...", &s[..boundary])
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -413,7 +406,7 @@ pub fn run_explain(conn: &Connection, params: &ExplainParams) -> Result<ExplainR
|
|||||||
};
|
};
|
||||||
|
|
||||||
let description_excerpt = if should_include(¶ms.sections, "description") {
|
let description_excerpt = if should_include(¶ms.sections, "description") {
|
||||||
Some(truncate_description(description.as_deref(), 500))
|
Some(truncate_description(description.as_deref()))
|
||||||
} else {
|
} else {
|
||||||
None
|
None
|
||||||
};
|
};
|
||||||
@@ -537,8 +530,10 @@ const DECISION_WINDOW_MS: i64 = 60 * 60 * 1000;
|
|||||||
|
|
||||||
/// Maximum length (in bytes, snapped to a char boundary) for the
|
/// Maximum length (in bytes, snapped to a char boundary) for the
|
||||||
/// `context_note` field in a `KeyDecision`.
|
/// `context_note` field in a `KeyDecision`.
|
||||||
|
#[allow(dead_code)]
|
||||||
const NOTE_TRUNCATE_LEN: usize = 500;
|
const NOTE_TRUNCATE_LEN: usize = 500;
|
||||||
|
|
||||||
|
#[allow(dead_code)]
|
||||||
fn truncate_note(text: &str, max_len: usize) -> String {
|
fn truncate_note(text: &str, max_len: usize) -> String {
|
||||||
if text.len() <= max_len {
|
if text.len() <= max_len {
|
||||||
text.to_string()
|
text.to_string()
|
||||||
@@ -682,7 +677,7 @@ pub fn extract_key_decisions(
|
|||||||
timestamp: ms_to_iso(event.created_at),
|
timestamp: ms_to_iso(event.created_at),
|
||||||
actor: event.actor.clone(),
|
actor: event.actor.clone(),
|
||||||
action: event.description.clone(),
|
action: event.description.clone(),
|
||||||
context_note: truncate_note(¬e.body, NOTE_TRUNCATE_LEN),
|
context_note: note.body.clone(),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1257,7 +1252,7 @@ pub fn print_explain(result: &ExplainResult) {
|
|||||||
Theme::dim().render(&to_relative(&t.last_note_at))
|
Theme::dim().render(&to_relative(&t.last_note_at))
|
||||||
);
|
);
|
||||||
if let Some(ref excerpt) = t.first_note_excerpt {
|
if let Some(ref excerpt) = t.first_note_excerpt {
|
||||||
let preview = render::truncate(excerpt, 100);
|
let preview = render::truncate(excerpt, render::flex_width(8, 30));
|
||||||
// Show first line only in human output
|
// Show first line only in human output
|
||||||
if let Some(line) = preview.lines().next() {
|
if let Some(line) = preview.lines().next() {
|
||||||
println!(" {}", Theme::muted().render(line));
|
println!(" {}", Theme::muted().render(line));
|
||||||
@@ -1283,7 +1278,7 @@ pub fn print_explain(result: &ExplainResult) {
|
|||||||
" {} {} — {} {}",
|
" {} {} — {} {}",
|
||||||
Icons::success(),
|
Icons::success(),
|
||||||
Theme::mr_ref().render(&format!("!{}", mr.iid)),
|
Theme::mr_ref().render(&format!("!{}", mr.iid)),
|
||||||
render::truncate(&mr.title, 60),
|
render::truncate(&mr.title, render::flex_width(25, 20)),
|
||||||
mr_state.render(&format!("[{}]", mr.state))
|
mr_state.render(&format!("[{}]", mr.state))
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -1305,7 +1300,7 @@ pub fn print_explain(result: &ExplainResult) {
|
|||||||
println!(
|
println!(
|
||||||
" {arrow} {} {}{state_str} ({})",
|
" {arrow} {} {}{state_str} ({})",
|
||||||
ref_style.render(&format!("{ref_prefix}{}", ri.iid)),
|
ref_style.render(&format!("{ref_prefix}{}", ri.iid)),
|
||||||
render::truncate(ri.title.as_deref().unwrap_or("(untitled)"), 50),
|
render::truncate(ri.title.as_deref().unwrap_or("(untitled)"), render::flex_width(30, 20)),
|
||||||
Theme::dim().render(&ri.reference_type)
|
Theme::dim().render(&ri.reference_type)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -1596,14 +1591,13 @@ mod tests {
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_truncate_description() {
|
fn test_truncate_description() {
|
||||||
assert_eq!(truncate_description(None, 500), "(no description)");
|
assert_eq!(truncate_description(None), "(no description)");
|
||||||
assert_eq!(truncate_description(Some(""), 500), "(no description)");
|
assert_eq!(truncate_description(Some("")), "(no description)");
|
||||||
assert_eq!(truncate_description(Some("short"), 500), "short");
|
assert_eq!(truncate_description(Some("short")), "short");
|
||||||
|
|
||||||
let long = "a".repeat(600);
|
let long = "a".repeat(600);
|
||||||
let truncated = truncate_description(Some(&long), 500);
|
let result = truncate_description(Some(&long));
|
||||||
assert!(truncated.ends_with("..."));
|
assert_eq!(result, long); // no truncation — full description preserved
|
||||||
assert!(truncated.len() <= 504); // 500 + "..."
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// -----------------------------------------------------------------------
|
// -----------------------------------------------------------------------
|
||||||
|
|||||||
@@ -359,7 +359,7 @@ pub fn print_file_history(result: &FileHistoryResult) {
|
|||||||
" {} {} {} {} @{} {} {}",
|
" {} {} {} {} @{} {} {}",
|
||||||
icon,
|
icon,
|
||||||
Theme::accent().render(&format!("!{}", mr.iid)),
|
Theme::accent().render(&format!("!{}", mr.iid)),
|
||||||
render::truncate(&mr.title, 50),
|
render::truncate(&mr.title, render::flex_width(45, 20)),
|
||||||
state_style.render(&mr.state),
|
state_style.render(&mr.state),
|
||||||
mr.author_username,
|
mr.author_username,
|
||||||
date,
|
date,
|
||||||
|
|||||||
@@ -359,10 +359,10 @@ pub fn print_list_issues(result: &ListResult) {
|
|||||||
}
|
}
|
||||||
headers.extend(["Assignee", "Labels", "Disc", "Updated"]);
|
headers.extend(["Assignee", "Labels", "Disc", "Updated"]);
|
||||||
|
|
||||||
let mut table = LoreTable::new().headers(&headers).align(0, Align::Right);
|
let mut table = LoreTable::new().headers(&headers).align(0, Align::Right).flex_col(1);
|
||||||
|
|
||||||
for issue in &result.issues {
|
for issue in &result.issues {
|
||||||
let title = render::truncate(&issue.title, 45);
|
let title = issue.title.clone();
|
||||||
let relative_time = render::format_relative_time_compact(issue.updated_at);
|
let relative_time = render::format_relative_time_compact(issue.updated_at);
|
||||||
let labels = render::format_labels_bare(&issue.labels, 2);
|
let labels = render::format_labels_bare(&issue.labels, 2);
|
||||||
let assignee = format_assignees(&issue.assignees);
|
let assignee = format_assignees(&issue.assignees);
|
||||||
|
|||||||
@@ -329,17 +329,18 @@ pub fn print_list_mrs(result: &MrListResult) {
|
|||||||
.headers(&[
|
.headers(&[
|
||||||
"IID", "Title", "State", "Author", "Branches", "Disc", "Updated",
|
"IID", "Title", "State", "Author", "Branches", "Disc", "Updated",
|
||||||
])
|
])
|
||||||
.align(0, Align::Right);
|
.align(0, Align::Right)
|
||||||
|
.flex_col(1);
|
||||||
|
|
||||||
for mr in &result.mrs {
|
for mr in &result.mrs {
|
||||||
let title = if mr.draft {
|
let title = if mr.draft {
|
||||||
format!("{} {}", Icons::mr_draft(), render::truncate(&mr.title, 42))
|
format!("{} {}", Icons::mr_draft(), mr.title)
|
||||||
} else {
|
} else {
|
||||||
render::truncate(&mr.title, 45)
|
mr.title.clone()
|
||||||
};
|
};
|
||||||
|
|
||||||
let relative_time = render::format_relative_time_compact(mr.updated_at);
|
let relative_time = render::format_relative_time_compact(mr.updated_at);
|
||||||
let branches = format_branches(&mr.target_branch, &mr.source_branch, 25);
|
let branches = format_branches(&mr.target_branch, &mr.source_branch);
|
||||||
let discussions = format_discussions(mr.discussion_count, mr.unresolved_count);
|
let discussions = format_discussions(mr.discussion_count, mr.unresolved_count);
|
||||||
|
|
||||||
let (icon, style) = match mr.state.as_str() {
|
let (icon, style) = match mr.state.as_str() {
|
||||||
@@ -356,7 +357,7 @@ pub fn print_list_mrs(result: &MrListResult) {
|
|||||||
StyledCell::plain(title),
|
StyledCell::plain(title),
|
||||||
state_cell,
|
state_cell,
|
||||||
StyledCell::styled(
|
StyledCell::styled(
|
||||||
format!("@{}", render::truncate(&mr.author_username, 12)),
|
format!("@{}", mr.author_username),
|
||||||
Theme::accent(),
|
Theme::accent(),
|
||||||
),
|
),
|
||||||
StyledCell::styled(branches, Theme::info()),
|
StyledCell::styled(branches, Theme::info()),
|
||||||
|
|||||||
@@ -9,9 +9,7 @@ use crate::core::path_resolver::escape_like as note_escape_like;
|
|||||||
use crate::core::project::resolve_project;
|
use crate::core::project::resolve_project;
|
||||||
use crate::core::time::{iso_to_ms, ms_to_iso, parse_since};
|
use crate::core::time::{iso_to_ms, ms_to_iso, parse_since};
|
||||||
|
|
||||||
use super::render_helpers::{
|
use super::render_helpers::{format_note_parent, format_note_path, format_note_type};
|
||||||
format_note_parent, format_note_path, format_note_type, truncate_body,
|
|
||||||
};
|
|
||||||
|
|
||||||
#[derive(Debug, Serialize)]
|
#[derive(Debug, Serialize)]
|
||||||
pub struct NoteListRow {
|
pub struct NoteListRow {
|
||||||
@@ -161,13 +159,14 @@ pub fn print_list_notes(result: &NoteListResult) {
|
|||||||
"Parent",
|
"Parent",
|
||||||
"Created",
|
"Created",
|
||||||
])
|
])
|
||||||
.align(0, Align::Right);
|
.align(0, Align::Right)
|
||||||
|
.flex_col(3);
|
||||||
|
|
||||||
for note in &result.notes {
|
for note in &result.notes {
|
||||||
let body = note
|
let body = note
|
||||||
.body
|
.body
|
||||||
.as_deref()
|
.as_deref()
|
||||||
.map(|b| truncate_body(b, 60))
|
.map(std::borrow::ToOwned::to_owned)
|
||||||
.unwrap_or_default();
|
.unwrap_or_default();
|
||||||
let path = format_note_path(note.position_new_path.as_deref(), note.position_new_line);
|
let path = format_note_path(note.position_new_path.as_deref(), note.position_new_line);
|
||||||
let parent = format_note_parent(note.noteable_type.as_deref(), note.parent_iid);
|
let parent = format_note_parent(note.noteable_type.as_deref(), note.parent_iid);
|
||||||
@@ -177,7 +176,7 @@ pub fn print_list_notes(result: &NoteListResult) {
|
|||||||
table.add_row(vec![
|
table.add_row(vec![
|
||||||
StyledCell::styled(note.gitlab_id.to_string(), Theme::info()),
|
StyledCell::styled(note.gitlab_id.to_string(), Theme::info()),
|
||||||
StyledCell::styled(
|
StyledCell::styled(
|
||||||
format!("@{}", render::truncate(¬e.author_username, 12)),
|
format!("@{}", note.author_username),
|
||||||
Theme::accent(),
|
Theme::accent(),
|
||||||
),
|
),
|
||||||
StyledCell::plain(note_type),
|
StyledCell::plain(note_type),
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
use crate::cli::render::{self, StyledCell, Theme};
|
use crate::cli::render::{StyledCell, Theme};
|
||||||
|
|
||||||
pub(crate) fn format_assignees(assignees: &[String]) -> String {
|
pub(crate) fn format_assignees(assignees: &[String]) -> String {
|
||||||
if assignees.is_empty() {
|
if assignees.is_empty() {
|
||||||
@@ -9,7 +9,7 @@ pub(crate) fn format_assignees(assignees: &[String]) -> String {
|
|||||||
let shown: Vec<String> = assignees
|
let shown: Vec<String> = assignees
|
||||||
.iter()
|
.iter()
|
||||||
.take(max_shown)
|
.take(max_shown)
|
||||||
.map(|s| format!("@{}", render::truncate(s, 10)))
|
.map(|s| format!("@{s}"))
|
||||||
.collect();
|
.collect();
|
||||||
let overflow = assignees.len().saturating_sub(max_shown);
|
let overflow = assignees.len().saturating_sub(max_shown);
|
||||||
|
|
||||||
@@ -34,11 +34,11 @@ pub(crate) fn format_discussions(total: i64, unresolved: i64) -> StyledCell {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn format_branches(target: &str, source: &str, max_width: usize) -> String {
|
pub(crate) fn format_branches(target: &str, source: &str) -> String {
|
||||||
let full = format!("{} <- {}", target, source);
|
format!("{} <- {}", target, source)
|
||||||
render::truncate(&full, max_width)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
pub(crate) fn truncate_body(body: &str, max_len: usize) -> String {
|
pub(crate) fn truncate_body(body: &str, max_len: usize) -> String {
|
||||||
if body.chars().count() <= max_len {
|
if body.chars().count() <= max_len {
|
||||||
body.to_string()
|
body.to_string()
|
||||||
|
|||||||
@@ -10,9 +10,7 @@ use super::types::{
|
|||||||
/// Compute the title/summary column width for a section given its fixed overhead.
|
/// Compute the title/summary column width for a section given its fixed overhead.
|
||||||
/// Returns a width clamped to [20, 80].
|
/// Returns a width clamped to [20, 80].
|
||||||
fn title_width(overhead: usize) -> usize {
|
fn title_width(overhead: usize) -> usize {
|
||||||
render::terminal_width()
|
render::flex_width(overhead, 20)
|
||||||
.saturating_sub(overhead)
|
|
||||||
.clamp(20, 80)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// ─── Glyph Mode Helper ──────────────────────────────────────────────────────
|
// ─── Glyph Mode Helper ──────────────────────────────────────────────────────
|
||||||
@@ -416,13 +414,12 @@ pub fn print_activity_section(events: &[MeActivityEvent], single_project: bool)
|
|||||||
|
|
||||||
// Columns: badge | ref | summary | actor | time
|
// Columns: badge | ref | summary | actor | time
|
||||||
// Table handles alignment, padding, and truncation automatically.
|
// Table handles alignment, padding, and truncation automatically.
|
||||||
let summary_max = title_width(46);
|
|
||||||
let mut table = Table::new()
|
let mut table = Table::new()
|
||||||
.columns(5)
|
.columns(5)
|
||||||
.indent(4)
|
.indent(4)
|
||||||
.align(1, Align::Right)
|
.align(1, Align::Right)
|
||||||
.align(4, Align::Right)
|
.align(4, Align::Right)
|
||||||
.max_width(2, summary_max);
|
.flex_col(2);
|
||||||
|
|
||||||
for event in events {
|
for event in events {
|
||||||
let badge_label = activity_badge_label(&event.event_type);
|
let badge_label = activity_badge_label(&event.event_type);
|
||||||
@@ -508,7 +505,7 @@ pub fn print_activity_section(events: &[MeActivityEvent], single_project: bool)
|
|||||||
if let Some(preview) = &event.body_preview
|
if let Some(preview) = &event.body_preview
|
||||||
&& !preview.is_empty()
|
&& !preview.is_empty()
|
||||||
{
|
{
|
||||||
let truncated = render::truncate(preview, 60);
|
let truncated = render::truncate(preview, render::flex_width(8, 30));
|
||||||
println!(" {}", Theme::dim().render(&format!("\"{truncated}\"")));
|
println!(" {}", Theme::dim().render(&format!("\"{truncated}\"")));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -576,12 +573,11 @@ pub fn print_since_last_check_section(since: &SinceLastCheck, single_project: bo
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Sub-events as indented rows
|
// Sub-events as indented rows
|
||||||
let summary_max = title_width(42);
|
|
||||||
let mut table = Table::new()
|
let mut table = Table::new()
|
||||||
.columns(3)
|
.columns(3)
|
||||||
.indent(6)
|
.indent(6)
|
||||||
.align(2, Align::Right)
|
.align(2, Align::Right)
|
||||||
.max_width(1, summary_max);
|
.flex_col(1);
|
||||||
|
|
||||||
for event in &group.events {
|
for event in &group.events {
|
||||||
let badge = activity_badge_label(&event.event_type);
|
let badge = activity_badge_label(&event.event_type);
|
||||||
@@ -610,7 +606,7 @@ pub fn print_since_last_check_section(since: &SinceLastCheck, single_project: bo
|
|||||||
if let Some(preview) = &event.body_preview
|
if let Some(preview) = &event.body_preview
|
||||||
&& !preview.is_empty()
|
&& !preview.is_empty()
|
||||||
{
|
{
|
||||||
let truncated = render::truncate(preview, 60);
|
let truncated = render::truncate(preview, render::flex_width(10, 30));
|
||||||
println!(
|
println!(
|
||||||
" {}",
|
" {}",
|
||||||
Theme::dim().render(&format!("\"{truncated}\""))
|
Theme::dim().render(&format!("\"{truncated}\""))
|
||||||
|
|||||||
@@ -235,8 +235,9 @@ fn print_timeline_event(event: &TimelineEvent) {
|
|||||||
.unwrap_or_default();
|
.unwrap_or_default();
|
||||||
let expanded_marker = if event.is_seed { "" } else { " [expanded]" };
|
let expanded_marker = if event.is_seed { "" } else { " [expanded]" };
|
||||||
|
|
||||||
let summary = render::truncate(&event.summary, 50);
|
let summary_width = render::flex_width(40, 20);
|
||||||
println!("{date} {tag} {entity_icon}{entity_ref:7} {summary:50} {actor}{expanded_marker}");
|
let summary = render::truncate(&event.summary, summary_width);
|
||||||
|
println!("{date} {tag} {entity_icon}{entity_ref:7} {summary} {actor}{expanded_marker}");
|
||||||
|
|
||||||
// Show snippet for evidence notes
|
// Show snippet for evidence notes
|
||||||
if let TimelineEventType::NoteEvidence { snippet, .. } = &event.event_type
|
if let TimelineEventType::NoteEvidence { snippet, .. } = &event.event_type
|
||||||
|
|||||||
@@ -263,7 +263,7 @@ pub(super) fn print_active_human(r: &ActiveResult, project_path: Option<&str>) {
|
|||||||
println!(
|
println!(
|
||||||
" {} {} {} {} notes {}",
|
" {} {} {} {} notes {}",
|
||||||
Theme::info().render(&format!("{prefix}{}", disc.entity_iid)),
|
Theme::info().render(&format!("{prefix}{}", disc.entity_iid)),
|
||||||
render::truncate(&disc.entity_title, 40),
|
render::truncate(&disc.entity_title, render::flex_width(30, 20)),
|
||||||
Theme::dim().render(&render::format_relative_time(disc.last_note_at)),
|
Theme::dim().render(&render::format_relative_time(disc.last_note_at)),
|
||||||
disc.note_count,
|
disc.note_count,
|
||||||
Theme::dim().render(&disc.project_path),
|
Theme::dim().render(&disc.project_path),
|
||||||
|
|||||||
@@ -769,11 +769,12 @@ pub(super) fn print_expert_human(r: &ExpertResult, project_path: Option<&str>) {
|
|||||||
} else {
|
} else {
|
||||||
String::new()
|
String::new()
|
||||||
};
|
};
|
||||||
|
let title_budget = render::flex_width(55, 20);
|
||||||
println!(
|
println!(
|
||||||
" {:<3} {:<30} {:>30} {:>10} {}",
|
" {:<3} {} {} {:>10} {}",
|
||||||
Theme::dim().render(&d.role),
|
Theme::dim().render(&d.role),
|
||||||
d.mr_ref,
|
d.mr_ref,
|
||||||
render::truncate(&format!("\"{}\"", d.title), 30),
|
render::truncate(&format!("\"{}\"", d.title), title_budget),
|
||||||
notes_str,
|
notes_str,
|
||||||
Theme::dim().render(&render::format_relative_time(d.last_activity_ms)),
|
Theme::dim().render(&render::format_relative_time(d.last_activity_ms)),
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -217,7 +217,7 @@ pub(super) fn print_workload_human(r: &WorkloadResult) {
|
|||||||
println!(
|
println!(
|
||||||
" {} {} {}",
|
" {} {} {}",
|
||||||
Theme::info().render(&item.ref_),
|
Theme::info().render(&item.ref_),
|
||||||
render::truncate(&item.title, 40),
|
render::truncate(&item.title, render::flex_width(25, 20)),
|
||||||
Theme::dim().render(&render::format_relative_time(item.updated_at)),
|
Theme::dim().render(&render::format_relative_time(item.updated_at)),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -239,7 +239,7 @@ pub(super) fn print_workload_human(r: &WorkloadResult) {
|
|||||||
println!(
|
println!(
|
||||||
" {} {}{} {}",
|
" {} {}{} {}",
|
||||||
Theme::info().render(&mr.ref_),
|
Theme::info().render(&mr.ref_),
|
||||||
render::truncate(&mr.title, 35),
|
render::truncate(&mr.title, render::flex_width(33, 20)),
|
||||||
Theme::dim().render(draft),
|
Theme::dim().render(draft),
|
||||||
Theme::dim().render(&render::format_relative_time(mr.updated_at)),
|
Theme::dim().render(&render::format_relative_time(mr.updated_at)),
|
||||||
);
|
);
|
||||||
@@ -266,7 +266,7 @@ pub(super) fn print_workload_human(r: &WorkloadResult) {
|
|||||||
println!(
|
println!(
|
||||||
" {} {}{} {}",
|
" {} {}{} {}",
|
||||||
Theme::info().render(&mr.ref_),
|
Theme::info().render(&mr.ref_),
|
||||||
render::truncate(&mr.title, 30),
|
render::truncate(&mr.title, render::flex_width(40, 20)),
|
||||||
Theme::dim().render(&author),
|
Theme::dim().render(&author),
|
||||||
Theme::dim().render(&render::format_relative_time(mr.updated_at)),
|
Theme::dim().render(&render::format_relative_time(mr.updated_at)),
|
||||||
);
|
);
|
||||||
@@ -292,7 +292,7 @@ pub(super) fn print_workload_human(r: &WorkloadResult) {
|
|||||||
" {} {} {} {}",
|
" {} {} {} {}",
|
||||||
Theme::dim().render(&disc.entity_type),
|
Theme::dim().render(&disc.entity_type),
|
||||||
Theme::info().render(&disc.ref_),
|
Theme::info().render(&disc.ref_),
|
||||||
render::truncate(&disc.entity_title, 35),
|
render::truncate(&disc.entity_title, render::flex_width(30, 20)),
|
||||||
Theme::dim().render(&render::format_relative_time(disc.last_note_at)),
|
Theme::dim().render(&render::format_relative_time(disc.last_note_at)),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,8 +3,8 @@ use rusqlite::Connection;
|
|||||||
use std::collections::HashSet;
|
use std::collections::HashSet;
|
||||||
|
|
||||||
use super::types::{
|
use super::types::{
|
||||||
EntityRef, ExpandedEntityRef, MatchedDiscussion, THREAD_MAX_NOTES, THREAD_NOTE_MAX_CHARS,
|
EntityRef, ExpandedEntityRef, MatchedDiscussion, THREAD_MAX_NOTES, ThreadNote, TimelineEvent,
|
||||||
ThreadNote, TimelineEvent, TimelineEventType, truncate_to_chars,
|
TimelineEventType,
|
||||||
};
|
};
|
||||||
use crate::core::error::{LoreError, Result};
|
use crate::core::error::{LoreError, Result};
|
||||||
|
|
||||||
@@ -440,7 +440,7 @@ fn collect_discussion_threads(
|
|||||||
let mut notes = Vec::new();
|
let mut notes = Vec::new();
|
||||||
for row_result in rows {
|
for row_result in rows {
|
||||||
let (note_id, author, body, created_at) = row_result?;
|
let (note_id, author, body, created_at) = row_result?;
|
||||||
let body = truncate_to_chars(body.as_deref().unwrap_or(""), THREAD_NOTE_MAX_CHARS);
|
let body = body.as_deref().unwrap_or("").to_owned();
|
||||||
notes.push(ThreadNote {
|
notes.push(ThreadNote {
|
||||||
note_id,
|
note_id,
|
||||||
author,
|
author,
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ use tracing::debug;
|
|||||||
|
|
||||||
use super::types::{
|
use super::types::{
|
||||||
EntityRef, MatchedDiscussion, TimelineEvent, TimelineEventType, resolve_entity_by_iid,
|
EntityRef, MatchedDiscussion, TimelineEvent, TimelineEventType, resolve_entity_by_iid,
|
||||||
resolve_entity_ref, truncate_to_chars,
|
resolve_entity_ref,
|
||||||
};
|
};
|
||||||
use crate::core::error::Result;
|
use crate::core::error::Result;
|
||||||
use crate::embedding::ollama::OllamaClient;
|
use crate::embedding::ollama::OllamaClient;
|
||||||
@@ -337,7 +337,7 @@ fn find_evidence_notes(
|
|||||||
proj_id,
|
proj_id,
|
||||||
) = row_result?;
|
) = row_result?;
|
||||||
|
|
||||||
let snippet = truncate_to_chars(body.as_deref().unwrap_or(""), 200);
|
let snippet = body.as_deref().unwrap_or("").to_owned();
|
||||||
|
|
||||||
let entity_ref = resolve_entity_ref(conn, &parent_type, parent_entity_id, Some(proj_id))?;
|
let entity_ref = resolve_entity_ref(conn, &parent_type, parent_entity_id, Some(proj_id))?;
|
||||||
let (iid, project_path) = match entity_ref {
|
let (iid, project_path) = match entity_ref {
|
||||||
|
|||||||
@@ -553,9 +553,10 @@ fn test_collect_discussion_thread_body_truncation() {
|
|||||||
.unwrap();
|
.unwrap();
|
||||||
|
|
||||||
if let TimelineEventType::DiscussionThread { notes, .. } = &thread.event_type {
|
if let TimelineEventType::DiscussionThread { notes, .. } = &thread.event_type {
|
||||||
assert!(
|
assert_eq!(
|
||||||
notes[0].body.chars().count() <= crate::timeline::THREAD_NOTE_MAX_CHARS,
|
notes[0].body.chars().count(),
|
||||||
"Body should be truncated to THREAD_NOTE_MAX_CHARS"
|
10_000,
|
||||||
|
"Body should preserve full text without truncation"
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
panic!("Expected DiscussionThread");
|
panic!("Expected DiscussionThread");
|
||||||
|
|||||||
@@ -288,7 +288,7 @@ fn test_seed_evidence_snippet_truncated() {
|
|||||||
if let TimelineEventType::NoteEvidence { snippet, .. } =
|
if let TimelineEventType::NoteEvidence { snippet, .. } =
|
||||||
&result.evidence_notes[0].event_type
|
&result.evidence_notes[0].event_type
|
||||||
{
|
{
|
||||||
assert!(snippet.chars().count() <= 200);
|
assert_eq!(snippet.chars().count(), 500, "snippet should preserve full body text");
|
||||||
} else {
|
} else {
|
||||||
panic!("Expected NoteEvidence");
|
panic!("Expected NoteEvidence");
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user