Upgrade Iced and render Markdown tables
This commit is contained in:
@@ -47,7 +47,7 @@ pub(super) struct ToolResultCheck {
|
||||
stage: ToolCheckStage,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
#[derive(Debug)]
|
||||
pub(crate) struct ChatMessage {
|
||||
pub(super) id: i32,
|
||||
pub(super) user: bool,
|
||||
@@ -59,7 +59,7 @@ pub(crate) struct ChatMessage {
|
||||
pub(super) reasoning_complete: bool,
|
||||
pub(super) reasoning_open: bool,
|
||||
pub(super) content: String,
|
||||
pub(super) markdown: Vec<markdown::Item>,
|
||||
pub(super) markdown: markdown::Content,
|
||||
}
|
||||
|
||||
impl ChatMessage {
|
||||
@@ -80,7 +80,7 @@ impl ChatMessage {
|
||||
} else {
|
||||
visible
|
||||
};
|
||||
self.markdown = markdown::parse(content).collect();
|
||||
self.markdown = markdown::Content::parse(content);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -98,7 +98,7 @@ impl From<StoredMessage> for ChatMessage {
|
||||
reasoning_complete: message.reasoning_complete,
|
||||
reasoning_open: false,
|
||||
content: message.content,
|
||||
markdown: Vec::new(),
|
||||
markdown: iced::widget::markdown::Content::new(),
|
||||
};
|
||||
message.refresh_markdown();
|
||||
message
|
||||
@@ -1349,6 +1349,37 @@ mod tests {
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn assistant_markdown_keeps_llm_tables() {
|
||||
let mut message = ChatMessage {
|
||||
id: 1,
|
||||
user: false,
|
||||
tool: false,
|
||||
system: false,
|
||||
compaction: false,
|
||||
compaction_tail_start: None,
|
||||
reasoning: None,
|
||||
reasoning_complete: true,
|
||||
reasoning_open: false,
|
||||
content: "### Core / Setup\n\n| File | Lines |\n|---|---:|\n| `src/app.rs` | **1,750** |\n| `src/engine.rs` | 2,400 |\n\n### Summary\n\nDone."
|
||||
.to_owned(),
|
||||
markdown: iced::widget::markdown::Content::new(),
|
||||
};
|
||||
|
||||
message.refresh_markdown();
|
||||
|
||||
let table = message.markdown.items().iter().find_map(|item| {
|
||||
if let iced::widget::markdown::Item::Table { columns, rows } = item {
|
||||
Some((columns, rows))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
});
|
||||
let (columns, rows) = table.expect("the completed Markdown table must remain renderable");
|
||||
assert_eq!(columns.len(), 2);
|
||||
assert_eq!(rows.len(), 2);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn last_compaction_selects_its_tail_without_hiding_history() {
|
||||
let message = |id: i32, compaction: bool, tail: Option<i32>| ChatMessage {
|
||||
@@ -1362,7 +1393,7 @@ mod tests {
|
||||
reasoning_complete: true,
|
||||
reasoning_open: false,
|
||||
content: format!("message {id}"),
|
||||
markdown: Vec::new(),
|
||||
markdown: iced::widget::markdown::Content::new(),
|
||||
};
|
||||
let history = vec![
|
||||
message(1, false, None),
|
||||
@@ -1394,7 +1425,7 @@ mod tests {
|
||||
reasoning_complete: true,
|
||||
reasoning_open: false,
|
||||
content: format!("message {id}"),
|
||||
markdown: Vec::new(),
|
||||
markdown: iced::widget::markdown::Content::new(),
|
||||
};
|
||||
let mut history = vec![
|
||||
message(1, true, false, false, false),
|
||||
|
||||
@@ -16,9 +16,8 @@ use crate::model::{
|
||||
use crate::settings::{GIB, REASONING_MODES};
|
||||
use iced::theme::{Palette, palette};
|
||||
use iced::widget::{
|
||||
Button, Space, Svg, Tooltip, button, checkbox, column, container, horizontal_rule, markdown,
|
||||
mouse_area, opaque, pick_list, progress_bar, row, scrollable, stack, svg, text, text_input,
|
||||
tooltip,
|
||||
Button, Space, Svg, Tooltip, button, checkbox, column, container, markdown, mouse_area, opaque,
|
||||
pick_list, progress_bar, row, rule, scrollable, stack, svg, text, text_input, tooltip,
|
||||
};
|
||||
use iced::{Alignment, Background, Border, Color, Element, Length, Padding, Theme, window};
|
||||
use std::collections::VecDeque;
|
||||
@@ -87,7 +86,7 @@ impl App {
|
||||
body = body.push(self.sidebar());
|
||||
body = body.push(
|
||||
mouse_area(
|
||||
container(Space::with_width(Length::Fill))
|
||||
container(Space::new().width(Length::Fill))
|
||||
.width(5)
|
||||
.height(Length::Fill)
|
||||
.style(divider_style),
|
||||
@@ -107,7 +106,7 @@ impl App {
|
||||
// way to the top edge; the spacer below it lets clicks through.
|
||||
let mut shell = column![stack![
|
||||
body,
|
||||
column![self.title_bar(), Space::with_height(Length::Fill)],
|
||||
column![self.title_bar(), Space::new().height(Length::Fill)],
|
||||
]];
|
||||
if let ModelDownload::Active(download) = &self.model_download {
|
||||
shell = shell.push(download_status_bar(download));
|
||||
@@ -163,7 +162,7 @@ impl App {
|
||||
text("Working directory").size(11).color(muted_text()),
|
||||
text(prompt.working_directory.display().to_string()).size(13),
|
||||
row![
|
||||
Space::with_width(Length::Fill),
|
||||
Space::new().width(Length::Fill),
|
||||
action_button("Deny").on_press(Message::DenyTool),
|
||||
action_button("Allow once").on_press(Message::AllowToolOnce),
|
||||
]
|
||||
@@ -188,7 +187,7 @@ impl App {
|
||||
let preference_content = row![
|
||||
icon(ICON_SETTINGS, 17),
|
||||
text("Preferences").size(14),
|
||||
Space::with_width(Length::Fill),
|
||||
Space::new().width(Length::Fill),
|
||||
text("⌘,").size(12),
|
||||
]
|
||||
.spacing(9)
|
||||
@@ -212,14 +211,14 @@ impl App {
|
||||
};
|
||||
let header = row![
|
||||
text("DS4Server").size(20),
|
||||
Space::with_width(Length::Fill),
|
||||
Space::new().width(Length::Fill),
|
||||
text("Local").size(12),
|
||||
]
|
||||
.align_y(Alignment::Center);
|
||||
let mut projects = column![
|
||||
row![
|
||||
text("PROJECTS").size(11),
|
||||
Space::with_width(Length::Fill),
|
||||
Space::new().width(Length::Fill),
|
||||
text("LOCAL").size(10),
|
||||
],
|
||||
add_project.style(button::text),
|
||||
@@ -275,7 +274,7 @@ impl App {
|
||||
let expanded = self.expanded_archives.contains(&project.id);
|
||||
projects = projects.push(
|
||||
row![
|
||||
Space::with_width(26),
|
||||
Space::new().width(26),
|
||||
button(
|
||||
text(format!(
|
||||
"{} Archived sessions ({})",
|
||||
@@ -302,7 +301,7 @@ impl App {
|
||||
let draft_selected = self.draft_selected(project.id);
|
||||
projects = projects.push(
|
||||
row![
|
||||
Space::with_width(26),
|
||||
Space::new().width(26),
|
||||
button(
|
||||
row![
|
||||
icon(ICON_CHAT, 15),
|
||||
@@ -363,7 +362,7 @@ impl App {
|
||||
}
|
||||
label = label.push(text(&session.title).size(13));
|
||||
row![
|
||||
Space::with_width(26),
|
||||
Space::new().width(26),
|
||||
button(label)
|
||||
.width(Length::Fill)
|
||||
.on_press(Message::SelectSession(project_id, session.id))
|
||||
@@ -404,12 +403,12 @@ impl App {
|
||||
};
|
||||
stack![
|
||||
row![
|
||||
Space::with_width(detail_offset),
|
||||
Space::new().width(detail_offset),
|
||||
container(self.detail_tabs())
|
||||
.center_x(Length::Fill)
|
||||
.center_y(Length::Fill),
|
||||
],
|
||||
container(row![Space::with_width(TRAFFIC_LIGHT_WIDTH), toggle]).center_y(Length::Fill),
|
||||
container(row![Space::new().width(TRAFFIC_LIGHT_WIDTH), toggle]).center_y(Length::Fill),
|
||||
]
|
||||
.width(Length::Fill)
|
||||
.height(TITLE_BAR_HEIGHT)
|
||||
@@ -463,7 +462,7 @@ impl App {
|
||||
.on_submit(Message::ConfirmProject)
|
||||
.padding(10),
|
||||
row![
|
||||
Space::with_width(Length::Fill),
|
||||
Space::new().width(Length::Fill),
|
||||
action_button("Cancel").on_press(Message::CancelProject),
|
||||
action_button("Add project").on_press(Message::ConfirmProject),
|
||||
]
|
||||
@@ -547,7 +546,7 @@ impl App {
|
||||
.color(muted_text()),
|
||||
actions,
|
||||
row![
|
||||
Space::with_width(Length::Fill),
|
||||
Space::new().width(Length::Fill),
|
||||
action_button("Close").on_press(Message::DismissPanel),
|
||||
],
|
||||
]
|
||||
@@ -576,7 +575,7 @@ impl App {
|
||||
.on_submit(Message::ConfirmRenameSession)
|
||||
.padding(10),
|
||||
row![
|
||||
Space::with_width(Length::Fill),
|
||||
Space::new().width(Length::Fill),
|
||||
action_button("Cancel").on_press(Message::DismissPanel),
|
||||
action_button("Rename").on_press(Message::ConfirmRenameSession),
|
||||
]
|
||||
@@ -668,9 +667,10 @@ pub(crate) fn app_theme() -> Theme {
|
||||
text: Color::from_rgb8(235, 235, 235),
|
||||
primary: Color::from_rgb8(85, 118, 255),
|
||||
success: Color::from_rgb8(72, 176, 112),
|
||||
warning: Color::from_rgb8(224, 168, 72),
|
||||
danger: Color::from_rgb8(220, 80, 86),
|
||||
};
|
||||
Theme::custom_with_fn("DS4Server".into(), palette, |palette| {
|
||||
Theme::custom_with_fn("DS4Server", palette, |palette| {
|
||||
let mut extended = palette::Extended::generate(palette);
|
||||
extended.background.weak = palette::Pair::new(Color::from_rgb8(38, 38, 40), palette.text);
|
||||
extended.background.strong = palette::Pair::new(Color::from_rgb8(58, 58, 61), palette.text);
|
||||
@@ -714,7 +714,7 @@ fn metric_card(label: &'static str, value: String, detail: String) -> Element<'s
|
||||
fn metric_row(label: &'static str, value: impl ToString) -> Element<'static, Message> {
|
||||
row![
|
||||
text(label).size(12).color(muted_text()),
|
||||
Space::with_width(Length::Fill),
|
||||
Space::new().width(Length::Fill),
|
||||
text(value.to_string()).size(12),
|
||||
]
|
||||
.spacing(12)
|
||||
@@ -750,7 +750,7 @@ fn mini_chart(
|
||||
1.0
|
||||
};
|
||||
bars = bars.push(
|
||||
container(Space::new(Length::Fill, height))
|
||||
container(Space::new().width(Length::Fill).height(height))
|
||||
.width(Length::FillPortion(1))
|
||||
.style(move |_| chart_bar_style(color)),
|
||||
);
|
||||
|
||||
@@ -17,7 +17,7 @@ impl App {
|
||||
icon(ICON_SPARK, 36),
|
||||
text("Start a local coding session").size(28),
|
||||
text("Choose a project folder to create your first session.").size(14),
|
||||
Space::with_height(10),
|
||||
Space::new().height(10),
|
||||
open_project,
|
||||
]
|
||||
.spacing(10)
|
||||
@@ -40,7 +40,7 @@ impl App {
|
||||
);
|
||||
}
|
||||
let header = header
|
||||
.push(Space::with_width(Length::Fill))
|
||||
.push(Space::new().width(Length::Fill))
|
||||
.spacing(10)
|
||||
.align_y(Alignment::Center);
|
||||
|
||||
@@ -124,7 +124,7 @@ impl App {
|
||||
}
|
||||
}
|
||||
if !message.content.is_empty() {
|
||||
if message.user || message.tool || message.markdown.is_empty() {
|
||||
if message.user || message.tool || message.markdown.items().is_empty() {
|
||||
let content = if message.reasoning.is_some() {
|
||||
crate::agent::visible_content(&message.content).trim_start()
|
||||
} else {
|
||||
@@ -134,9 +134,8 @@ impl App {
|
||||
} else {
|
||||
body = body.push(
|
||||
markdown::view(
|
||||
&message.markdown,
|
||||
markdown::Settings::with_text_size(14),
|
||||
markdown_style,
|
||||
message.markdown.items(),
|
||||
markdown::Settings::with_text_size(14, markdown_style),
|
||||
)
|
||||
.map(Message::OpenLink),
|
||||
);
|
||||
@@ -283,7 +282,7 @@ impl App {
|
||||
))
|
||||
.size(11)
|
||||
.color(muted_text()),
|
||||
Space::with_width(Length::Fill),
|
||||
Space::new().width(Length::Fill),
|
||||
icon(ICON_MODEL, 16),
|
||||
text(self.config.model.to_string()).size(12),
|
||||
action,
|
||||
@@ -339,7 +338,7 @@ fn tool_cards(cards: Vec<crate::agent::ToolCard>) -> Element<'static, Message> {
|
||||
let mut rows = column![].spacing(0);
|
||||
for (index, card) in cards.into_iter().enumerate() {
|
||||
if index > 0 {
|
||||
rows = rows.push(horizontal_rule(1));
|
||||
rows = rows.push(rule::horizontal(1));
|
||||
}
|
||||
let parameters = crate::agent::tool_parameters(&card.call);
|
||||
let call = crate::agent::tool_call_text(&card.call);
|
||||
@@ -391,7 +390,7 @@ fn tool_cards(cards: Vec<crate::agent::ToolCard>) -> Element<'static, Message> {
|
||||
let mut content = column![
|
||||
row![
|
||||
text(card.call.name).size(13),
|
||||
Space::with_width(Length::Fill),
|
||||
Space::new().width(Length::Fill),
|
||||
text(card.state.label()).size(11).color(muted_text()),
|
||||
actions,
|
||||
]
|
||||
|
||||
@@ -7,7 +7,7 @@ impl App {
|
||||
let mut artifacts = column![];
|
||||
for (index, artifact) in model::managed_artifacts(&models_path()).iter().enumerate() {
|
||||
if index > 0 {
|
||||
artifacts = artifacts.push(horizontal_rule(1));
|
||||
artifacts = artifacts.push(rule::horizontal(1));
|
||||
}
|
||||
artifacts = artifacts.push(model_artifact_row(artifact, busy));
|
||||
}
|
||||
@@ -29,7 +29,7 @@ impl App {
|
||||
if let Some(error) = &self.error {
|
||||
content = content.push(text(error).style(iced::widget::text::danger));
|
||||
}
|
||||
content = content.push(Space::with_height(8)).push(
|
||||
content = content.push(Space::new().height(8)).push(
|
||||
container(scrollable(artifacts).height(Length::Fill))
|
||||
.height(Length::Fill)
|
||||
.style(overview_style),
|
||||
@@ -51,7 +51,7 @@ impl App {
|
||||
))
|
||||
.size(13),
|
||||
row![
|
||||
Space::with_width(Length::Fill),
|
||||
Space::new().width(Length::Fill),
|
||||
action_button("Cancel").on_press(Message::CancelDeleteArtifact),
|
||||
danger_button("Delete").on_press(Message::ConfirmDeleteArtifact),
|
||||
]
|
||||
@@ -119,7 +119,7 @@ fn model_artifact_row(artifact: &ManagedArtifact, busy: bool) -> Element<'static
|
||||
.color(muted_text()),
|
||||
]
|
||||
.spacing(5),
|
||||
Space::with_width(Length::Fill),
|
||||
Space::new().width(Length::Fill),
|
||||
row![download, validate, delete]
|
||||
.spacing(8)
|
||||
.align_y(Alignment::Center),
|
||||
@@ -167,11 +167,11 @@ pub(super) fn download_status_bar(download: &ActiveDownload) -> Element<'_, Mess
|
||||
row![
|
||||
text(phase_text(progress.phase)).size(12),
|
||||
progress_bar(0.0..=1.0, progress.fraction())
|
||||
.width(180)
|
||||
.height(7),
|
||||
.length(180)
|
||||
.girth(7),
|
||||
text(measurement).size(12),
|
||||
text(transfer).size(12),
|
||||
Space::with_width(Length::Fill),
|
||||
Space::new().width(Length::Fill),
|
||||
stop,
|
||||
]
|
||||
.spacing(12)
|
||||
@@ -234,7 +234,7 @@ fn model_download_status(download: &ModelDownload) -> Element<'_, Message> {
|
||||
} else {
|
||||
text(heading).size(12)
|
||||
};
|
||||
let mut heading_row = row![heading, Space::with_width(Length::Fill)]
|
||||
let mut heading_row = row![heading, Space::new().width(Length::Fill)]
|
||||
.align_y(Alignment::Center)
|
||||
.spacing(8);
|
||||
if let ModelDownload::Active(active) = download {
|
||||
@@ -246,7 +246,7 @@ fn model_download_status(download: &ModelDownload) -> Element<'_, Message> {
|
||||
}
|
||||
let mut status = column![
|
||||
heading_row,
|
||||
progress_bar(0.0..=1.0, progress.fraction()).height(8),
|
||||
progress_bar(0.0..=1.0, progress.fraction()).girth(8),
|
||||
text(measurement).size(12),
|
||||
]
|
||||
.spacing(6);
|
||||
|
||||
@@ -9,11 +9,9 @@ impl App {
|
||||
.supports_dspark()
|
||||
.then_some(Message::PreferenceLegacyMtpChanged);
|
||||
let legacy_mtp = hint(
|
||||
checkbox(
|
||||
"Enable legacy MTP for this model",
|
||||
self.preference_draft.legacy_mtp_enabled,
|
||||
)
|
||||
.on_toggle_maybe(legacy_mtp_toggle),
|
||||
checkbox(self.preference_draft.legacy_mtp_enabled)
|
||||
.label("Enable legacy MTP for this model")
|
||||
.on_toggle_maybe(legacy_mtp_toggle),
|
||||
"Uses the managed one-stage MTP support GGUF. The target model verifies every drafted token; it is mutually exclusive with DSpark.",
|
||||
);
|
||||
let dspark_toggle: Option<fn(bool) -> Message> = self
|
||||
@@ -22,11 +20,9 @@ impl App {
|
||||
.supports_dspark()
|
||||
.then_some(Message::PreferenceDsparkChanged);
|
||||
let dspark = hint(
|
||||
checkbox(
|
||||
"Enable DSpark for this model",
|
||||
self.preference_draft.dspark_enabled,
|
||||
)
|
||||
.on_toggle_maybe(dspark_toggle),
|
||||
checkbox(self.preference_draft.dspark_enabled)
|
||||
.label("Enable DSpark for this model")
|
||||
.on_toggle_maybe(dspark_toggle),
|
||||
"Speculative decoding with the managed DSpark draft artifact: a small model proposes tokens that the main model verifies in one pass. Usually a large speedup; the target model may also stream routed experts from SSD.",
|
||||
);
|
||||
let glm_mtp_toggle: Option<fn(bool) -> Message> = (self.preference_draft.model
|
||||
@@ -130,10 +126,8 @@ impl App {
|
||||
"LOCAL ENDPOINT",
|
||||
column![
|
||||
hint(
|
||||
checkbox(
|
||||
"Enable OpenAI-compatible endpoint",
|
||||
self.preference_draft.endpoint_enabled,
|
||||
)
|
||||
checkbox(self.preference_draft.endpoint_enabled)
|
||||
.label("Enable OpenAI-compatible endpoint")
|
||||
.on_toggle(Message::PreferenceEndpointEnabledChanged),
|
||||
"Serves the loaded model over an OpenAI-style HTTP API, so editors, scripts and agents on this machine can use it. Turned off, only this window can generate.",
|
||||
),
|
||||
@@ -144,10 +138,8 @@ impl App {
|
||||
.on_input(Message::PreferenceEndpointPortChanged),
|
||||
),
|
||||
hint(
|
||||
checkbox(
|
||||
"Allow browser clients (CORS)",
|
||||
self.preference_draft.endpoint_cors,
|
||||
)
|
||||
checkbox(self.preference_draft.endpoint_cors)
|
||||
.label("Allow browser clients (CORS)")
|
||||
.on_toggle(Message::PreferenceEndpointCorsChanged),
|
||||
"Answers with permissive CORS headers so JavaScript running in a web page may call the endpoint. Leave it off when only native tools connect.",
|
||||
),
|
||||
@@ -181,7 +173,7 @@ impl App {
|
||||
)
|
||||
.on_input(Message::PreferenceSystemPromptChanged)
|
||||
.padding(9),
|
||||
Space::with_height(4),
|
||||
Space::new().height(4),
|
||||
text("SAMPLING & REASONING").size(11).color(muted_text()),
|
||||
preference_input_row(
|
||||
"Temperature",
|
||||
@@ -261,12 +253,14 @@ impl App {
|
||||
prefill,
|
||||
),
|
||||
hint(
|
||||
checkbox("Prefer exact quality kernels", self.preference_draft.quality)
|
||||
checkbox(self.preference_draft.quality)
|
||||
.label("Prefer exact quality kernels")
|
||||
.on_toggle(Message::PreferenceQualityChanged),
|
||||
"Runs the exact Metal kernels instead of the fast approximations. Slightly slower, and it removes the small numeric differences those approximations introduce.",
|
||||
),
|
||||
hint(
|
||||
checkbox("Warm mapped weights at load time", self.preference_draft.warm_weights)
|
||||
checkbox(self.preference_draft.warm_weights)
|
||||
.label("Warm mapped weights at load time")
|
||||
.on_toggle(Message::PreferenceWarmWeightsChanged),
|
||||
"Reads every mapped weight page once at load, so the first reply is not interrupted by page faults from disk. Loading takes longer and memory pressure rises immediately.",
|
||||
),
|
||||
@@ -312,15 +306,14 @@ impl App {
|
||||
.on_input(Message::PreferenceMtpMarginChanged),
|
||||
),
|
||||
hint(
|
||||
checkbox("Enable integrated GLM MTP", self.preference_draft.glm_mtp)
|
||||
checkbox(self.preference_draft.glm_mtp)
|
||||
.label("Enable integrated GLM MTP")
|
||||
.on_toggle_maybe(glm_mtp_toggle),
|
||||
"Uses the prediction head built into GLM 5.2 for speculative decoding, so no separate draft model is loaded. Available for GLM 5.2 only.",
|
||||
),
|
||||
hint(
|
||||
checkbox(
|
||||
"Log GLM MTP timing counters",
|
||||
self.preference_draft.glm_mtp_timing,
|
||||
)
|
||||
checkbox(self.preference_draft.glm_mtp_timing)
|
||||
.label("Log GLM MTP timing counters")
|
||||
.on_toggle_maybe(glm_mtp_timing_toggle),
|
||||
"Records per-stage timings of the speculative path to the log, to show where the acceleration actually goes. A diagnostic aid that costs a little throughput.",
|
||||
),
|
||||
@@ -332,10 +325,8 @@ impl App {
|
||||
dspark_confidence,
|
||||
),
|
||||
hint(
|
||||
checkbox(
|
||||
"DSpark target-only decode",
|
||||
self.preference_draft.dspark_strict,
|
||||
)
|
||||
checkbox(self.preference_draft.dspark_strict)
|
||||
.label("DSpark target-only decode")
|
||||
.on_toggle_maybe(dspark_strict_toggle),
|
||||
"Lets the draft model only propose, never decide: every token is sampled by the full model. Gives up some of the speedup in exchange for output identical to non-speculative decoding.",
|
||||
),
|
||||
@@ -367,15 +358,17 @@ impl App {
|
||||
},
|
||||
))
|
||||
.size(12),
|
||||
Space::with_height(6),
|
||||
Space::new().height(6),
|
||||
text("SSD STREAMING").size(11).color(muted_text()),
|
||||
hint(
|
||||
checkbox("Enable SSD-backed model streaming", self.preference_draft.ssd_streaming)
|
||||
checkbox(self.preference_draft.ssd_streaming)
|
||||
.label("Enable SSD-backed model streaming")
|
||||
.on_toggle(Message::PreferenceSsdChanged),
|
||||
"Leaves the routed expert weights on disk and pages them in as they are needed, so a model larger than this machine's memory still runs. Every cache miss waits for the SSD; speculative support weights remain resident while target experts stream.",
|
||||
),
|
||||
hint(
|
||||
checkbox("Skip automatic expert preload", self.preference_draft.ssd_streaming_cold)
|
||||
checkbox(self.preference_draft.ssd_streaming_cold)
|
||||
.label("Skip automatic expert preload")
|
||||
.on_toggle(Message::PreferenceSsdColdChanged),
|
||||
"Starts with an empty expert cache instead of reading the likely experts up front. The model is ready sooner and uses less memory, at the price of slow first replies.",
|
||||
),
|
||||
@@ -461,7 +454,7 @@ impl App {
|
||||
),
|
||||
))
|
||||
.size(12),
|
||||
Space::with_height(6),
|
||||
Space::new().height(6),
|
||||
text("ADVANCED DIAGNOSTICS").size(11).color(muted_text()),
|
||||
preference_input_row(
|
||||
"Simulated used memory (GiB)",
|
||||
@@ -561,14 +554,14 @@ impl App {
|
||||
let header = row![
|
||||
icon(ICON_SETTINGS, 22),
|
||||
text("Preferences").size(24),
|
||||
Space::with_width(Length::Fill),
|
||||
Space::new().width(Length::Fill),
|
||||
text("⌘,").size(12),
|
||||
]
|
||||
.spacing(10)
|
||||
.align_y(Alignment::Center);
|
||||
let footer = row![
|
||||
action_button("Reset DS4 defaults").on_press(Message::ResetPreferences),
|
||||
Space::with_width(Length::Fill),
|
||||
Space::new().width(Length::Fill),
|
||||
action_button("Cancel").on_press(Message::DismissPanel),
|
||||
action_button("Save").on_press(Message::SavePreferences),
|
||||
]
|
||||
|
||||
@@ -47,7 +47,7 @@ impl App {
|
||||
.color(muted_text()),
|
||||
]
|
||||
.spacing(5),
|
||||
Space::with_width(Length::Fill),
|
||||
Space::new().width(Length::Fill),
|
||||
container(text(stats.phase.label()).size(12).color(phase_color))
|
||||
.padding([7, 11])
|
||||
.style(move |_| status_badge_style(phase_color)),
|
||||
@@ -109,7 +109,7 @@ impl App {
|
||||
text("Decode")
|
||||
.size(12)
|
||||
.color(Color::from_rgb8(84, 170, 255)),
|
||||
Space::with_width(Length::Fill),
|
||||
Space::new().width(Length::Fill),
|
||||
text(format!("{:.1} tok/s", stats.decode_tokens_per_second))
|
||||
.size(12)
|
||||
.color(muted_text()),
|
||||
@@ -123,7 +123,7 @@ impl App {
|
||||
text("Prefill")
|
||||
.size(12)
|
||||
.color(Color::from_rgb8(157, 119, 255)),
|
||||
Space::with_width(Length::Fill),
|
||||
Space::new().width(Length::Fill),
|
||||
text(format!("{:.1} tok/s", stats.prefill_tokens_per_second))
|
||||
.size(12)
|
||||
.color(muted_text()),
|
||||
@@ -142,7 +142,7 @@ impl App {
|
||||
),
|
||||
row![
|
||||
text(format!("{} requests", format_count(stats.http_requests))).size(12),
|
||||
Space::with_width(Length::Fill),
|
||||
Space::new().width(Length::Fill),
|
||||
text(format!(
|
||||
"{} errors · {} streaming",
|
||||
stats.http_errors, stats.http_streaming_requests
|
||||
@@ -171,7 +171,7 @@ impl App {
|
||||
})
|
||||
.size(12)
|
||||
.color(Color::from_rgb8(67, 194, 203)),
|
||||
Space::with_width(Length::Fill),
|
||||
Space::new().width(Length::Fill),
|
||||
text(format_rate(latest.kv_read_bytes_per_second))
|
||||
.size(12)
|
||||
.color(muted_text()),
|
||||
@@ -189,7 +189,7 @@ impl App {
|
||||
})
|
||||
.size(12)
|
||||
.color(Color::from_rgb8(240, 180, 70)),
|
||||
Space::with_width(Length::Fill),
|
||||
Space::new().width(Length::Fill),
|
||||
text(format_rate(latest.kv_write_bytes_per_second))
|
||||
.size(12)
|
||||
.color(muted_text()),
|
||||
@@ -210,7 +210,7 @@ impl App {
|
||||
text("Selected loads")
|
||||
.size(12)
|
||||
.color(Color::from_rgb8(240, 180, 70)),
|
||||
Space::with_width(Length::Fill),
|
||||
Space::new().width(Length::Fill),
|
||||
text(format!("{:.1}/s", latest.ssd_requests_per_second))
|
||||
.size(12)
|
||||
.color(muted_text()),
|
||||
@@ -224,7 +224,7 @@ impl App {
|
||||
text("Requested expert data")
|
||||
.size(12)
|
||||
.color(Color::from_rgb8(67, 194, 203)),
|
||||
Space::with_width(Length::Fill),
|
||||
Space::new().width(Length::Fill),
|
||||
text(format_rate(latest.ssd_bytes_per_second))
|
||||
.size(12)
|
||||
.color(muted_text()),
|
||||
@@ -238,7 +238,7 @@ impl App {
|
||||
text("Inference wait")
|
||||
.size(12)
|
||||
.color(Color::from_rgb8(220, 80, 86)),
|
||||
Space::with_width(Length::Fill),
|
||||
Space::new().width(Length::Fill),
|
||||
text(format!("{:.0} ms/s", latest.ssd_wait_ms_per_second))
|
||||
.size(12)
|
||||
.color(muted_text()),
|
||||
@@ -443,7 +443,7 @@ impl App {
|
||||
format_milliseconds(stats.last_kv_write_ms),
|
||||
),
|
||||
),
|
||||
progress_bar(0.0..=1.0, cache_fraction.min(1.0)).height(4),
|
||||
progress_bar(0.0..=1.0, cache_fraction.min(1.0)).girth(4),
|
||||
]
|
||||
.spacing(9)
|
||||
.into(),
|
||||
@@ -522,15 +522,16 @@ impl App {
|
||||
continue;
|
||||
}
|
||||
bar = bar.push(
|
||||
container(Space::new(Length::Fill, Length::Fill))
|
||||
container(Space::new().width(Length::Fill).height(Length::Fill))
|
||||
.width(Length::FillPortion(portion(bytes, capacity)))
|
||||
.style(move |_| chart_bar_style(color)),
|
||||
);
|
||||
legend = legend.push(
|
||||
row![
|
||||
container(Space::new(9, 9)).style(move |_| chart_bar_style(color)),
|
||||
container(Space::new().width(9).height(9))
|
||||
.style(move |_| chart_bar_style(color)),
|
||||
text(label).size(12).color(muted_text()),
|
||||
Space::with_width(Length::Fill),
|
||||
Space::new().width(Length::Fill),
|
||||
text(format!(
|
||||
"{} · {:.0}%",
|
||||
format_bytes(bytes),
|
||||
@@ -544,7 +545,7 @@ impl App {
|
||||
}
|
||||
if total == 0 {
|
||||
bar = bar.push(
|
||||
container(Space::new(Length::Fill, Length::Fill))
|
||||
container(Space::new().width(Length::Fill).height(Length::Fill))
|
||||
.style(|_| chart_bar_style(muted_text().scale_alpha(0.25))),
|
||||
);
|
||||
}
|
||||
@@ -562,7 +563,7 @@ impl App {
|
||||
rows = rows.push(
|
||||
row![
|
||||
text(label).size(12),
|
||||
Space::with_width(Length::Fill),
|
||||
Space::new().width(Length::Fill),
|
||||
text(format!(
|
||||
"{} · {} old",
|
||||
format_bytes(entry.bytes),
|
||||
@@ -607,7 +608,7 @@ impl App {
|
||||
usage.transient_bytes as f32 / usage.budget_bytes.max(1) as f32 * 100.0,
|
||||
),
|
||||
),
|
||||
Space::with_height(2),
|
||||
Space::new().height(2),
|
||||
text(if usage.entries.is_empty() {
|
||||
"No checkpoints stored yet."
|
||||
} else {
|
||||
@@ -620,7 +621,7 @@ impl App {
|
||||
text("Discarding a checkpoint costs one prefill to rebuild it.")
|
||||
.size(11)
|
||||
.color(muted_text()),
|
||||
Space::with_width(Length::Fill),
|
||||
Space::new().width(Length::Fill),
|
||||
action_button(text("Clear transient cache").size(12))
|
||||
.on_press(Message::ClearTransientCache),
|
||||
]
|
||||
|
||||
Reference in New Issue
Block a user