Skip to content

Commit a6944a2

Browse files
notime2claude
andcommitted
feat: adaptive AI response height in search results
AI responses now dynamically resize the result area based on text length instead of using a fixed 55px height. Added `is_ai_response` flag to App struct and `estimated_height()` method to calculate content-aware sizing. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent 3a03514 commit a6944a2

10 files changed

Lines changed: 73 additions & 11 deletions

File tree

src/app.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -137,6 +137,7 @@ impl ToApps for HashMap<String, String> {
137137
key.trim().to_owned(),
138138
)),
139139
search_name: key.to_owned(),
140+
is_ai_response: false,
140141
desc: "Switch Modes".to_string(),
141142
icons: icons.clone(),
142143
display_name,
@@ -152,6 +153,7 @@ impl ToApps for HashMap<String, String> {
152153
icons: icons.clone(),
153154
display_name: "Default mode".to_string(),
154155
search_name: "default".to_string(),
156+
is_ai_response: false,
155157
});
156158
};
157159

src/app/apps.rs

Lines changed: 47 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,23 @@ pub struct App {
4747
pub icons: Option<iced::widget::image::Handle>,
4848
pub display_name: String,
4949
pub search_name: String,
50+
/// When true, the result row expands to fit AI response text
51+
pub is_ai_response: bool,
52+
}
53+
54+
impl App {
55+
/// Estimate the rendered height for this result item.
56+
/// AI responses grow based on text length; normal items are fixed at 66px.
57+
pub fn estimated_height(&self) -> usize {
58+
if self.is_ai_response {
59+
// ~60 chars per line at size 16 in a 460px-wide area, 20px per line
60+
let lines = (self.display_name.len() as f32 / 55.0).ceil().max(1.0) as usize;
61+
let text_height = lines * 20 + 30; // 30 for desc + spacing + padding
62+
text_height.max(66).min(400) // min 66, max 400
63+
} else {
64+
66
65+
}
66+
}
5067
}
5168

5269
impl PartialEq for App {
@@ -55,6 +72,7 @@ impl PartialEq for App {
5572
&& self.icons == other.icons
5673
&& self.desc == other.desc
5774
&& self.display_name == other.display_name
75+
&& self.is_ai_response == other.is_ai_response
5876
}
5977
}
6078

@@ -68,6 +86,7 @@ impl App {
6886
icons: None,
6987
display_name: x.to_string(),
7088
search_name: x.name().to_string(),
89+
is_ai_response: false,
7190
open_command: AppCommand::Function(Function::CopyToClipboard(
7291
ClipBoardContentType::Text(x.to_string()),
7392
)),
@@ -99,6 +118,7 @@ impl App {
99118
desc: "Easter Egg".to_string(),
100119
display_name: "Ferris Plushies".to_string(),
101120
search_name: "ferris.rs".to_string(),
121+
is_ai_response: false,
102122
},
103123
App {
104124
ranking: 0,
@@ -107,6 +127,7 @@ impl App {
107127
icons: icons.clone(),
108128
display_name: "Quit RustCast".to_string(),
109129
search_name: "quit".to_string(),
130+
is_ai_response: false,
110131
},
111132
App {
112133
ranking: 0,
@@ -115,6 +136,7 @@ impl App {
115136
icons: icons.clone(),
116137
display_name: "Open RustCast Preferences".to_string(),
117138
search_name: "settings".to_string(),
139+
is_ai_response: false,
118140
},
119141
App {
120142
ranking: 0,
@@ -123,6 +145,7 @@ impl App {
123145
icons: icons.clone(),
124146
display_name: "Search for an Emoji".to_string(),
125147
search_name: "emoji".to_string(),
148+
is_ai_response: false,
126149
},
127150
App {
128151
ranking: 0,
@@ -131,6 +154,7 @@ impl App {
131154
icons: icons.clone(),
132155
display_name: "Clipboard History".to_string(),
133156
search_name: "clipboard".to_string(),
157+
is_ai_response: false,
134158
},
135159
App {
136160
ranking: 0,
@@ -139,6 +163,7 @@ impl App {
139163
icons: icons.clone(),
140164
display_name: "Search for a file".to_string(),
141165
search_name: "file search".to_string(),
166+
is_ai_response: false,
142167
},
143168
App {
144169
ranking: 0,
@@ -147,6 +172,7 @@ impl App {
147172
icons: icons.clone(),
148173
display_name: "Reload RustCast".to_string(),
149174
search_name: "refresh".to_string(),
175+
is_ai_response: false,
150176
},
151177
App {
152178
ranking: 0,
@@ -155,6 +181,7 @@ impl App {
155181
icons: icons.clone(),
156182
display_name: format!("Current RustCast Version: {app_version}"),
157183
search_name: "version".to_string(),
184+
is_ai_response: false,
158185
},
159186
]
160187
}
@@ -185,11 +212,21 @@ impl App {
185212
.color(theme.text_color(0.55)),
186213
);
187214

215+
let is_ai = self.is_ai_response;
216+
188217
let mut row = Row::new()
189-
.align_y(Alignment::Center)
218+
.align_y(if is_ai {
219+
Alignment::Start
220+
} else {
221+
Alignment::Center
222+
})
190223
.width(Fill)
191-
.spacing(10)
192-
.height(50);
224+
.spacing(10);
225+
226+
// Only set fixed height for non-AI results
227+
if !is_ai {
228+
row = row.height(50);
229+
}
193230

194231
if theme.show_icons
195232
&& let Some(icon) = &self.icons
@@ -210,12 +247,16 @@ impl App {
210247

211248
let theme_clone = theme.clone();
212249

213-
let content = Button::new(row)
250+
let mut content = Button::new(row)
214251
.on_press_maybe(msg)
215252
.style(move |_, _| result_button_style(&theme_clone))
216253
.width(Fill)
217-
.padding(0)
218-
.height(50);
254+
.padding(if is_ai { 10 } else { 0 });
255+
256+
// Only set fixed height for non-AI results
257+
if !is_ai {
258+
content = content.height(50);
259+
}
219260

220261
container(content)
221262
.id(format!("result-{}", id_num))

src/app/tile/elm.rs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -144,6 +144,9 @@ pub fn view(tile: &Tile, wid: window::Id) -> Element<'_, Message> {
144144

145145
let height = if tile.page == Page::ClipboardHistory {
146146
385
147+
} else if tile.results.iter().any(|app| app.is_ai_response) {
148+
// AI responses: use estimated height from content
149+
tile.results.iter().map(|app| app.estimated_height()).sum::<usize>().min(400)
147150
} else {
148151
std::cmp::min(tile.results.len() * 60, 290)
149152
};

src/app/tile/update.rs

Lines changed: 15 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -442,6 +442,7 @@ pub fn handle_update(tile: &mut Tile, message: Message) -> Task<Message> {
442442
icons: None,
443443
display_name: "Thinking...".to_string(),
444444
search_name: String::new(),
445+
is_ai_response: false,
445446
}];
446447
let ai_config = tile.config.ai.clone();
447448
Task::perform(
@@ -456,7 +457,7 @@ pub fn handle_update(tile: &mut Tile, message: Message) -> Task<Message> {
456457

457458
Message::AiResponse(response) => {
458459
info!("AI response received");
459-
tile.results = vec![App {
460+
let ai_app = App {
460461
ranking: 0,
461462
open_command: AppCommand::Function(Function::CopyToClipboard(
462463
ClipBoardContentType::Text(response.clone()),
@@ -465,15 +466,16 @@ pub fn handle_update(tile: &mut Tile, message: Message) -> Task<Message> {
465466
icons: None,
466467
display_name: response,
467468
search_name: String::new(),
468-
}];
469-
let len = tile.results.len();
470-
let max_elem = min(5, len);
469+
is_ai_response: true,
470+
};
471+
let content_height = ai_app.estimated_height();
472+
tile.results = vec![ai_app];
471473
window::latest()
472474
.map(|x| x.unwrap())
473475
.map(move |id| {
474476
Message::ResizeWindow(
475477
id,
476-
((max_elem * 55) + 35 + DEFAULT_WINDOW_HEIGHT as usize) as f32,
478+
(content_height + 35 + DEFAULT_WINDOW_HEIGHT as usize) as f32,
477479
)
478480
})
479481
}
@@ -525,6 +527,7 @@ pub fn handle_update(tile: &mut Tile, message: Message) -> Task<Message> {
525527
icons: None,
526528
display_name: rand_num.to_string(),
527529
search_name: String::new(),
530+
is_ai_response: false,
528531
}];
529532
return single_item_resize_task(id);
530533
}
@@ -536,6 +539,7 @@ pub fn handle_update(tile: &mut Tile, message: Message) -> Task<Message> {
536539
icons: lemon_icon_handle(),
537540
display_name: "Lemon".to_string(),
538541
search_name: "".to_string(),
542+
is_ai_response: false,
539543
}];
540544
return single_item_resize_task(id);
541545
}
@@ -547,6 +551,7 @@ pub fn handle_update(tile: &mut Tile, message: Message) -> Task<Message> {
547551
icons: None,
548552
display_name: 67.to_string(),
549553
search_name: String::new(),
554+
is_ai_response: false,
550555
}];
551556
return single_item_resize_task(id);
552557
}
@@ -574,6 +579,7 @@ pub fn handle_update(tile: &mut Tile, message: Message) -> Task<Message> {
574579
icons: None,
575580
display_name: format!("Ask AI: {}", ai_query),
576581
search_name: String::new(),
582+
is_ai_response: false,
577583
}];
578584
return single_item_resize_task(id);
579585
}
@@ -592,6 +598,7 @@ pub fn handle_update(tile: &mut Tile, message: Message) -> Task<Message> {
592598
icons: None,
593599
search_name: "".to_string(),
594600
desc: "Shell Command".to_string(),
601+
is_ai_response: false,
595602
}];
596603
return single_item_resize_task(id);
597604
}
@@ -633,6 +640,7 @@ pub fn handle_update(tile: &mut Tile, message: Message) -> Task<Message> {
633640
icons: None,
634641
display_name: "Open Website: ".to_string() + &tile.query,
635642
search_name: String::new(),
643+
is_ai_response: false,
636644
});
637645
} else if let Some(conversions) = unit_conversion::convert_query(&tile.query) {
638646
tile.results = conversions
@@ -648,6 +656,7 @@ pub fn handle_update(tile: &mut Tile, message: Message) -> Task<Message> {
648656
icons: None,
649657
display_name: res.eval().map(|x| x.to_string()).unwrap_or("".to_string()),
650658
search_name: "".to_string(),
659+
is_ai_response: false,
651660
});
652661
return single_item_resize_task(id);
653662
} else if tile.query.ends_with("?") || tile.query.split_whitespace().nth(2).is_some() {
@@ -658,6 +667,7 @@ pub fn handle_update(tile: &mut Tile, message: Message) -> Task<Message> {
658667
desc: "Web Search".to_string(),
659668
display_name: format!("Search for: {}", tile.query),
660669
search_name: String::new(),
670+
is_ai_response: false,
661671
}];
662672
return single_item_resize_task(id);
663673
}

src/clipboard.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@ impl ToApp for ClipBoardContentType {
3737
icons: None,
3838
display_name,
3939
search_name,
40+
is_ai_response: false,
4041
}
4142
}
4243
}

src/commands.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -166,6 +166,7 @@ impl ToApp for DirEntry {
166166
icons: None,
167167
display_name: self.file_name().to_str().unwrap_or("").to_string(),
168168
search_name: "".to_string(),
169+
is_ai_response: false,
169170
}
170171
}
171172
}

src/config.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -236,6 +236,7 @@ impl ToApp for Shelly {
236236
icons: icon,
237237
display_name: self_clone.alias,
238238
search_name: self_clone.alias_lc,
239+
is_ai_response: false,
239240
}
240241
}
241242
}

src/platform/cross.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -159,6 +159,7 @@ fn discover_apps(
159159
desc: "Application".to_string(),
160160
icons,
161161
search_name: name.to_lowercase(),
162+
is_ai_response: false,
162163
display_name: name,
163164
})
164165
})

src/platform/macos/discovery.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -267,6 +267,7 @@ fn query_app(url: impl AsRef<NSURL>, store_icons: bool) -> Option<App> {
267267
ranking: 0,
268268
display_name: name.clone(),
269269
search_name: name.to_lowercase(),
270+
is_ai_response: false,
270271
desc: "Application".to_string(),
271272
icons,
272273
open_command: AppCommand::Function(Function::OpenApp(path.to_string_lossy().into_owned())),

src/unit_conversion.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -262,6 +262,7 @@ impl ToApp for ConversionResult {
262262
icons: None,
263263
display_name: target,
264264
search_name: String::new(),
265+
is_ai_response: false,
265266
}
266267
}
267268
}

0 commit comments

Comments
 (0)