Upgrade Iced and render Markdown tables
This commit is contained in:
1618
Cargo.lock
generated
1618
Cargo.lock
generated
File diff suppressed because it is too large
Load Diff
@@ -14,7 +14,7 @@ cc = "1.3.0"
|
|||||||
[dependencies]
|
[dependencies]
|
||||||
diesel = { version = "2.3.11", features = ["sqlite", "returning_clauses_for_sqlite_3_35", "64-column-tables"] }
|
diesel = { version = "2.3.11", features = ["sqlite", "returning_clauses_for_sqlite_3_35", "64-column-tables"] }
|
||||||
diesel_migrations = "2.3.2"
|
diesel_migrations = "2.3.2"
|
||||||
iced = { version = "0.13.1", features = ["advanced", "highlighter", "markdown", "svg", "tokio"] }
|
iced = { version = "0.14.0", features = ["advanced", "highlighter", "markdown", "svg", "tokio"] }
|
||||||
memmap2 = "0.9.11"
|
memmap2 = "0.9.11"
|
||||||
png = "0.17.16"
|
png = "0.17.16"
|
||||||
rfd = "0.15.4"
|
rfd = "0.15.4"
|
||||||
@@ -23,6 +23,7 @@ serde_json = { version = "1.0.149", features = ["preserve_order", "raw_value"] }
|
|||||||
serde_norway = "0.9.42"
|
serde_norway = "0.9.42"
|
||||||
sha2 = "0.11.0"
|
sha2 = "0.11.0"
|
||||||
ureq = { version = "3.3.0", default-features = false, features = ["rustls"] }
|
ureq = { version = "3.3.0", default-features = false, features = ["rustls"] }
|
||||||
|
url = "2.5.8"
|
||||||
|
|
||||||
[target.'cfg(target_os = "macos")'.dependencies]
|
[target.'cfg(target_os = "macos")'.dependencies]
|
||||||
muda = "0.15.3"
|
muda = "0.15.3"
|
||||||
|
|||||||
3
PLAN.md
3
PLAN.md
@@ -62,6 +62,9 @@ execution targets one self-contained Mac.
|
|||||||
- The remaining model-independent execution gaps are fine-grained SSD cache
|
- The remaining model-independent execution gaps are fine-grained SSD cache
|
||||||
telemetry, the DS4 expert-locality profiler, and resident multi-session
|
telemetry, the DS4 expert-locality profiler, and resident multi-session
|
||||||
server batching/scheduling.
|
server batching/scheduling.
|
||||||
|
- The native UI is on Iced 0.14. Chat transcripts use its table-aware Markdown
|
||||||
|
content and viewer path, with a regression for code-styled line-count tables
|
||||||
|
produced by coding models.
|
||||||
|
|
||||||
## Delivery order
|
## Delivery order
|
||||||
|
|
||||||
|
|||||||
82
src/app.rs
82
src/app.rs
@@ -25,7 +25,7 @@ use crate::settings::{
|
|||||||
ReasoningMode, RuntimePreferences, SpeculativePreferences, SsdPreferences, SteeringPreferences,
|
ReasoningMode, RuntimePreferences, SpeculativePreferences, SsdPreferences, SteeringPreferences,
|
||||||
StreamingCacheBudget,
|
StreamingCacheBudget,
|
||||||
};
|
};
|
||||||
use iced::widget::{markdown, scrollable, text_input};
|
use iced::widget::{markdown, scrollable};
|
||||||
use iced::{Size, Subscription, Task, keyboard, mouse, window};
|
use iced::{Size, Subscription, Task, keyboard, mouse, window};
|
||||||
use rfd::AsyncFileDialog;
|
use rfd::AsyncFileDialog;
|
||||||
use std::collections::{HashMap, HashSet, VecDeque};
|
use std::collections::{HashMap, HashSet, VecDeque};
|
||||||
@@ -210,7 +210,7 @@ pub(crate) enum Message {
|
|||||||
DownloadProgressTick,
|
DownloadProgressTick,
|
||||||
ComposerChanged(String),
|
ComposerChanged(String),
|
||||||
ToggleReasoning(usize),
|
ToggleReasoning(usize),
|
||||||
OpenLink(markdown::Url),
|
OpenLink(markdown::Uri),
|
||||||
CopyToolText(String),
|
CopyToolText(String),
|
||||||
OpenToolOutput(PathBuf),
|
OpenToolOutput(PathBuf),
|
||||||
AllowToolOnce,
|
AllowToolOnce,
|
||||||
@@ -514,9 +514,11 @@ impl App {
|
|||||||
self.error = None;
|
self.error = None;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Message::FocusNext => return iced::widget::focus_next().chain(reveal_focused()),
|
Message::FocusNext => {
|
||||||
|
return iced::widget::operation::focus_next().chain(reveal_focused());
|
||||||
|
}
|
||||||
Message::FocusPrevious => {
|
Message::FocusPrevious => {
|
||||||
return iced::widget::focus_previous().chain(reveal_focused());
|
return iced::widget::operation::focus_previous().chain(reveal_focused());
|
||||||
}
|
}
|
||||||
Message::ShowChat => self.detail_tab = DetailTab::Chat,
|
Message::ShowChat => self.detail_tab = DetailTab::Chat,
|
||||||
Message::ShowStats => {
|
Message::ShowStats => {
|
||||||
@@ -807,8 +809,9 @@ impl App {
|
|||||||
message.reasoning_open = !message.reasoning_open;
|
message.reasoning_open = !message.reasoning_open;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Message::OpenLink(url) => {
|
Message::OpenLink(uri) => {
|
||||||
if matches!(url.scheme(), "http" | "https")
|
if let Ok(url) = url::Url::parse(&uri)
|
||||||
|
&& matches!(url.scheme(), "http" | "https")
|
||||||
&& let Err(error) = std::process::Command::new("open").arg(url.as_str()).spawn()
|
&& let Err(error) = std::process::Command::new("open").arg(url.as_str()).spawn()
|
||||||
{
|
{
|
||||||
self.error = Some(format!("Could not open the link: {error}"));
|
self.error = Some(format!("Could not open the link: {error}"));
|
||||||
@@ -1165,7 +1168,10 @@ impl App {
|
|||||||
|
|
||||||
pub(crate) fn subscription(&self) -> Subscription<Message> {
|
pub(crate) fn subscription(&self) -> Subscription<Message> {
|
||||||
let mut subscriptions = vec![
|
let mut subscriptions = vec![
|
||||||
keyboard::on_key_press(shortcut),
|
keyboard::listen().filter_map(|event| match event {
|
||||||
|
keyboard::Event::KeyPressed { key, modifiers, .. } => shortcut(key, modifiers),
|
||||||
|
_ => None,
|
||||||
|
}),
|
||||||
// A focused text field takes escape for itself to drop its own
|
// A focused text field takes escape for itself to drop its own
|
||||||
// focus, so a dialog would never see it through `on_key_press`.
|
// focus, so a dialog would never see it through `on_key_press`.
|
||||||
iced::event::listen_with(|event, _, _| {
|
iced::event::listen_with(|event, _, _| {
|
||||||
@@ -1422,16 +1428,16 @@ fn config_path() -> PathBuf {
|
|||||||
application_support_path().join("config.yaml")
|
application_support_path().join("config.yaml")
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(super) fn chat_scroll_id() -> scrollable::Id {
|
pub(super) fn chat_scroll_id() -> iced::widget::Id {
|
||||||
scrollable::Id::new("chat-transcript")
|
iced::widget::Id::new("chat-transcript")
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(super) fn composer_id() -> text_input::Id {
|
pub(super) fn composer_id() -> iced::widget::Id {
|
||||||
text_input::Id::new("chat-composer")
|
iced::widget::Id::new("chat-composer")
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(super) fn preferences_scroll_id() -> scrollable::Id {
|
pub(super) fn preferences_scroll_id() -> iced::widget::Id {
|
||||||
scrollable::Id::new("preferences-fields")
|
iced::widget::Id::new("preferences-fields")
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Scrolls the preferences form so the field that just took focus is inside
|
/// Scrolls the preferences form so the field that just took focus is inside
|
||||||
@@ -1447,24 +1453,34 @@ fn reveal_focused() -> Task<Message> {
|
|||||||
|
|
||||||
struct Locate {
|
struct Locate {
|
||||||
rows: Vec<Rectangle>,
|
rows: Vec<Rectangle>,
|
||||||
|
next_container: Option<Rectangle>,
|
||||||
focused: Option<Rectangle>,
|
focused: Option<Rectangle>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<T> Operation<T> for Locate {
|
impl<T> Operation<T> for Locate {
|
||||||
fn container(
|
fn traverse(&mut self, operate: &mut dyn FnMut(&mut dyn Operation<T>)) {
|
||||||
|
let container = self.next_container.take();
|
||||||
|
if let Some(bounds) = container {
|
||||||
|
self.rows.push(bounds);
|
||||||
|
}
|
||||||
|
operate(self);
|
||||||
|
if container.is_some() {
|
||||||
|
self.rows.pop();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn container(&mut self, _id: Option<&Id>, bounds: Rectangle) {
|
||||||
|
self.next_container = Some(bounds);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn focusable(
|
||||||
&mut self,
|
&mut self,
|
||||||
_id: Option<&Id>,
|
_id: Option<&Id>,
|
||||||
bounds: Rectangle,
|
bounds: Rectangle,
|
||||||
operate_on_children: &mut dyn FnMut(&mut dyn Operation<T>),
|
state: &mut dyn operation::Focusable,
|
||||||
) {
|
) {
|
||||||
self.rows.push(bounds);
|
|
||||||
operate_on_children(self);
|
|
||||||
self.rows.pop();
|
|
||||||
}
|
|
||||||
|
|
||||||
fn focusable(&mut self, state: &mut dyn operation::Focusable, _id: Option<&Id>) {
|
|
||||||
if state.is_focused() {
|
if state.is_focused() {
|
||||||
self.focused = self.rows.last().copied();
|
self.focused = self.rows.last().copied().or(Some(bounds));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1480,24 +1496,19 @@ fn reveal_focused() -> Task<Message> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl<T> Operation<T> for Reveal {
|
impl<T> Operation<T> for Reveal {
|
||||||
fn container(
|
fn traverse(&mut self, operate: &mut dyn FnMut(&mut dyn Operation<T>)) {
|
||||||
&mut self,
|
operate(self);
|
||||||
_id: Option<&Id>,
|
|
||||||
_bounds: Rectangle,
|
|
||||||
operate_on_children: &mut dyn FnMut(&mut dyn Operation<T>),
|
|
||||||
) {
|
|
||||||
operate_on_children(self);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn scrollable(
|
fn scrollable(
|
||||||
&mut self,
|
&mut self,
|
||||||
state: &mut dyn operation::Scrollable,
|
|
||||||
id: Option<&Id>,
|
id: Option<&Id>,
|
||||||
bounds: Rectangle,
|
bounds: Rectangle,
|
||||||
content_bounds: Rectangle,
|
content_bounds: Rectangle,
|
||||||
translation: Vector,
|
translation: Vector,
|
||||||
|
state: &mut dyn operation::Scrollable,
|
||||||
) {
|
) {
|
||||||
if id != Some(&Id::from(preferences_scroll_id())) {
|
if id != Some(&preferences_scroll_id()) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
let Some(offset) = reveal_offset(self.field, bounds, translation.y) else {
|
let Some(offset) = reveal_offset(self.field, bounds, translation.y) else {
|
||||||
@@ -1513,6 +1524,7 @@ fn reveal_focused() -> Task<Message> {
|
|||||||
|
|
||||||
iced::advanced::widget::operate(Locate {
|
iced::advanced::widget::operate(Locate {
|
||||||
rows: Vec::new(),
|
rows: Vec::new(),
|
||||||
|
next_container: None,
|
||||||
focused: None,
|
focused: None,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@@ -1541,11 +1553,11 @@ fn reveal_offset(field: iced::Rectangle, viewport: iced::Rectangle, scrolled: f3
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn focus_composer() -> Task<Message> {
|
fn focus_composer() -> Task<Message> {
|
||||||
text_input::focus(composer_id())
|
iced::widget::operation::focus(composer_id())
|
||||||
}
|
}
|
||||||
|
|
||||||
fn scroll_chat_to_end() -> Task<Message> {
|
fn scroll_chat_to_end() -> Task<Message> {
|
||||||
scrollable::snap_to(chat_scroll_id(), scrollable::RelativeOffset::END)
|
iced::widget::operation::snap_to(chat_scroll_id(), scrollable::RelativeOffset::END)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Deletes session checkpoints whose session is gone. Deleting a session or a
|
/// Deletes session checkpoints whose session is gone. Deleting a session or a
|
||||||
@@ -1723,7 +1735,7 @@ mod tests {
|
|||||||
reasoning_complete: false,
|
reasoning_complete: false,
|
||||||
reasoning_open: true,
|
reasoning_open: true,
|
||||||
content: String::new(),
|
content: String::new(),
|
||||||
markdown: Vec::new(),
|
markdown: markdown::Content::new(),
|
||||||
};
|
};
|
||||||
message.append(true, "working it out");
|
message.append(true, "working it out");
|
||||||
message.append(false, "**final answer**");
|
message.append(false, "**final answer**");
|
||||||
@@ -1731,6 +1743,6 @@ mod tests {
|
|||||||
assert_eq!(message.reasoning.as_deref(), Some("working it out"));
|
assert_eq!(message.reasoning.as_deref(), Some("working it out"));
|
||||||
assert!(message.reasoning_complete);
|
assert!(message.reasoning_complete);
|
||||||
assert_eq!(message.content, "**final answer**");
|
assert_eq!(message.content, "**final answer**");
|
||||||
assert!(!message.markdown.is_empty());
|
assert!(!message.markdown.items().is_empty());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -47,7 +47,7 @@ pub(super) struct ToolResultCheck {
|
|||||||
stage: ToolCheckStage,
|
stage: ToolCheckStage,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone, Debug)]
|
#[derive(Debug)]
|
||||||
pub(crate) struct ChatMessage {
|
pub(crate) struct ChatMessage {
|
||||||
pub(super) id: i32,
|
pub(super) id: i32,
|
||||||
pub(super) user: bool,
|
pub(super) user: bool,
|
||||||
@@ -59,7 +59,7 @@ pub(crate) struct ChatMessage {
|
|||||||
pub(super) reasoning_complete: bool,
|
pub(super) reasoning_complete: bool,
|
||||||
pub(super) reasoning_open: bool,
|
pub(super) reasoning_open: bool,
|
||||||
pub(super) content: String,
|
pub(super) content: String,
|
||||||
pub(super) markdown: Vec<markdown::Item>,
|
pub(super) markdown: markdown::Content,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl ChatMessage {
|
impl ChatMessage {
|
||||||
@@ -80,7 +80,7 @@ impl ChatMessage {
|
|||||||
} else {
|
} else {
|
||||||
visible
|
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_complete: message.reasoning_complete,
|
||||||
reasoning_open: false,
|
reasoning_open: false,
|
||||||
content: message.content,
|
content: message.content,
|
||||||
markdown: Vec::new(),
|
markdown: iced::widget::markdown::Content::new(),
|
||||||
};
|
};
|
||||||
message.refresh_markdown();
|
message.refresh_markdown();
|
||||||
message
|
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]
|
#[test]
|
||||||
fn last_compaction_selects_its_tail_without_hiding_history() {
|
fn last_compaction_selects_its_tail_without_hiding_history() {
|
||||||
let message = |id: i32, compaction: bool, tail: Option<i32>| ChatMessage {
|
let message = |id: i32, compaction: bool, tail: Option<i32>| ChatMessage {
|
||||||
@@ -1362,7 +1393,7 @@ mod tests {
|
|||||||
reasoning_complete: true,
|
reasoning_complete: true,
|
||||||
reasoning_open: false,
|
reasoning_open: false,
|
||||||
content: format!("message {id}"),
|
content: format!("message {id}"),
|
||||||
markdown: Vec::new(),
|
markdown: iced::widget::markdown::Content::new(),
|
||||||
};
|
};
|
||||||
let history = vec![
|
let history = vec![
|
||||||
message(1, false, None),
|
message(1, false, None),
|
||||||
@@ -1394,7 +1425,7 @@ mod tests {
|
|||||||
reasoning_complete: true,
|
reasoning_complete: true,
|
||||||
reasoning_open: false,
|
reasoning_open: false,
|
||||||
content: format!("message {id}"),
|
content: format!("message {id}"),
|
||||||
markdown: Vec::new(),
|
markdown: iced::widget::markdown::Content::new(),
|
||||||
};
|
};
|
||||||
let mut history = vec![
|
let mut history = vec![
|
||||||
message(1, true, false, false, false),
|
message(1, true, false, false, false),
|
||||||
|
|||||||
@@ -16,9 +16,8 @@ use crate::model::{
|
|||||||
use crate::settings::{GIB, REASONING_MODES};
|
use crate::settings::{GIB, REASONING_MODES};
|
||||||
use iced::theme::{Palette, palette};
|
use iced::theme::{Palette, palette};
|
||||||
use iced::widget::{
|
use iced::widget::{
|
||||||
Button, Space, Svg, Tooltip, button, checkbox, column, container, horizontal_rule, markdown,
|
Button, Space, Svg, Tooltip, button, checkbox, column, container, markdown, mouse_area, opaque,
|
||||||
mouse_area, opaque, pick_list, progress_bar, row, scrollable, stack, svg, text, text_input,
|
pick_list, progress_bar, row, rule, scrollable, stack, svg, text, text_input, tooltip,
|
||||||
tooltip,
|
|
||||||
};
|
};
|
||||||
use iced::{Alignment, Background, Border, Color, Element, Length, Padding, Theme, window};
|
use iced::{Alignment, Background, Border, Color, Element, Length, Padding, Theme, window};
|
||||||
use std::collections::VecDeque;
|
use std::collections::VecDeque;
|
||||||
@@ -87,7 +86,7 @@ impl App {
|
|||||||
body = body.push(self.sidebar());
|
body = body.push(self.sidebar());
|
||||||
body = body.push(
|
body = body.push(
|
||||||
mouse_area(
|
mouse_area(
|
||||||
container(Space::with_width(Length::Fill))
|
container(Space::new().width(Length::Fill))
|
||||||
.width(5)
|
.width(5)
|
||||||
.height(Length::Fill)
|
.height(Length::Fill)
|
||||||
.style(divider_style),
|
.style(divider_style),
|
||||||
@@ -107,7 +106,7 @@ impl App {
|
|||||||
// way to the top edge; the spacer below it lets clicks through.
|
// way to the top edge; the spacer below it lets clicks through.
|
||||||
let mut shell = column![stack![
|
let mut shell = column![stack![
|
||||||
body,
|
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 {
|
if let ModelDownload::Active(download) = &self.model_download {
|
||||||
shell = shell.push(download_status_bar(download));
|
shell = shell.push(download_status_bar(download));
|
||||||
@@ -163,7 +162,7 @@ impl App {
|
|||||||
text("Working directory").size(11).color(muted_text()),
|
text("Working directory").size(11).color(muted_text()),
|
||||||
text(prompt.working_directory.display().to_string()).size(13),
|
text(prompt.working_directory.display().to_string()).size(13),
|
||||||
row![
|
row![
|
||||||
Space::with_width(Length::Fill),
|
Space::new().width(Length::Fill),
|
||||||
action_button("Deny").on_press(Message::DenyTool),
|
action_button("Deny").on_press(Message::DenyTool),
|
||||||
action_button("Allow once").on_press(Message::AllowToolOnce),
|
action_button("Allow once").on_press(Message::AllowToolOnce),
|
||||||
]
|
]
|
||||||
@@ -188,7 +187,7 @@ impl App {
|
|||||||
let preference_content = row![
|
let preference_content = row![
|
||||||
icon(ICON_SETTINGS, 17),
|
icon(ICON_SETTINGS, 17),
|
||||||
text("Preferences").size(14),
|
text("Preferences").size(14),
|
||||||
Space::with_width(Length::Fill),
|
Space::new().width(Length::Fill),
|
||||||
text("⌘,").size(12),
|
text("⌘,").size(12),
|
||||||
]
|
]
|
||||||
.spacing(9)
|
.spacing(9)
|
||||||
@@ -212,14 +211,14 @@ impl App {
|
|||||||
};
|
};
|
||||||
let header = row![
|
let header = row![
|
||||||
text("DS4Server").size(20),
|
text("DS4Server").size(20),
|
||||||
Space::with_width(Length::Fill),
|
Space::new().width(Length::Fill),
|
||||||
text("Local").size(12),
|
text("Local").size(12),
|
||||||
]
|
]
|
||||||
.align_y(Alignment::Center);
|
.align_y(Alignment::Center);
|
||||||
let mut projects = column![
|
let mut projects = column![
|
||||||
row![
|
row![
|
||||||
text("PROJECTS").size(11),
|
text("PROJECTS").size(11),
|
||||||
Space::with_width(Length::Fill),
|
Space::new().width(Length::Fill),
|
||||||
text("LOCAL").size(10),
|
text("LOCAL").size(10),
|
||||||
],
|
],
|
||||||
add_project.style(button::text),
|
add_project.style(button::text),
|
||||||
@@ -275,7 +274,7 @@ impl App {
|
|||||||
let expanded = self.expanded_archives.contains(&project.id);
|
let expanded = self.expanded_archives.contains(&project.id);
|
||||||
projects = projects.push(
|
projects = projects.push(
|
||||||
row![
|
row![
|
||||||
Space::with_width(26),
|
Space::new().width(26),
|
||||||
button(
|
button(
|
||||||
text(format!(
|
text(format!(
|
||||||
"{} Archived sessions ({})",
|
"{} Archived sessions ({})",
|
||||||
@@ -302,7 +301,7 @@ impl App {
|
|||||||
let draft_selected = self.draft_selected(project.id);
|
let draft_selected = self.draft_selected(project.id);
|
||||||
projects = projects.push(
|
projects = projects.push(
|
||||||
row![
|
row![
|
||||||
Space::with_width(26),
|
Space::new().width(26),
|
||||||
button(
|
button(
|
||||||
row![
|
row![
|
||||||
icon(ICON_CHAT, 15),
|
icon(ICON_CHAT, 15),
|
||||||
@@ -363,7 +362,7 @@ impl App {
|
|||||||
}
|
}
|
||||||
label = label.push(text(&session.title).size(13));
|
label = label.push(text(&session.title).size(13));
|
||||||
row![
|
row![
|
||||||
Space::with_width(26),
|
Space::new().width(26),
|
||||||
button(label)
|
button(label)
|
||||||
.width(Length::Fill)
|
.width(Length::Fill)
|
||||||
.on_press(Message::SelectSession(project_id, session.id))
|
.on_press(Message::SelectSession(project_id, session.id))
|
||||||
@@ -404,12 +403,12 @@ impl App {
|
|||||||
};
|
};
|
||||||
stack![
|
stack![
|
||||||
row![
|
row![
|
||||||
Space::with_width(detail_offset),
|
Space::new().width(detail_offset),
|
||||||
container(self.detail_tabs())
|
container(self.detail_tabs())
|
||||||
.center_x(Length::Fill)
|
.center_x(Length::Fill)
|
||||||
.center_y(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)
|
.width(Length::Fill)
|
||||||
.height(TITLE_BAR_HEIGHT)
|
.height(TITLE_BAR_HEIGHT)
|
||||||
@@ -463,7 +462,7 @@ impl App {
|
|||||||
.on_submit(Message::ConfirmProject)
|
.on_submit(Message::ConfirmProject)
|
||||||
.padding(10),
|
.padding(10),
|
||||||
row![
|
row![
|
||||||
Space::with_width(Length::Fill),
|
Space::new().width(Length::Fill),
|
||||||
action_button("Cancel").on_press(Message::CancelProject),
|
action_button("Cancel").on_press(Message::CancelProject),
|
||||||
action_button("Add project").on_press(Message::ConfirmProject),
|
action_button("Add project").on_press(Message::ConfirmProject),
|
||||||
]
|
]
|
||||||
@@ -547,7 +546,7 @@ impl App {
|
|||||||
.color(muted_text()),
|
.color(muted_text()),
|
||||||
actions,
|
actions,
|
||||||
row![
|
row![
|
||||||
Space::with_width(Length::Fill),
|
Space::new().width(Length::Fill),
|
||||||
action_button("Close").on_press(Message::DismissPanel),
|
action_button("Close").on_press(Message::DismissPanel),
|
||||||
],
|
],
|
||||||
]
|
]
|
||||||
@@ -576,7 +575,7 @@ impl App {
|
|||||||
.on_submit(Message::ConfirmRenameSession)
|
.on_submit(Message::ConfirmRenameSession)
|
||||||
.padding(10),
|
.padding(10),
|
||||||
row![
|
row![
|
||||||
Space::with_width(Length::Fill),
|
Space::new().width(Length::Fill),
|
||||||
action_button("Cancel").on_press(Message::DismissPanel),
|
action_button("Cancel").on_press(Message::DismissPanel),
|
||||||
action_button("Rename").on_press(Message::ConfirmRenameSession),
|
action_button("Rename").on_press(Message::ConfirmRenameSession),
|
||||||
]
|
]
|
||||||
@@ -668,9 +667,10 @@ pub(crate) fn app_theme() -> Theme {
|
|||||||
text: Color::from_rgb8(235, 235, 235),
|
text: Color::from_rgb8(235, 235, 235),
|
||||||
primary: Color::from_rgb8(85, 118, 255),
|
primary: Color::from_rgb8(85, 118, 255),
|
||||||
success: Color::from_rgb8(72, 176, 112),
|
success: Color::from_rgb8(72, 176, 112),
|
||||||
|
warning: Color::from_rgb8(224, 168, 72),
|
||||||
danger: Color::from_rgb8(220, 80, 86),
|
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);
|
let mut extended = palette::Extended::generate(palette);
|
||||||
extended.background.weak = palette::Pair::new(Color::from_rgb8(38, 38, 40), palette.text);
|
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);
|
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> {
|
fn metric_row(label: &'static str, value: impl ToString) -> Element<'static, Message> {
|
||||||
row![
|
row![
|
||||||
text(label).size(12).color(muted_text()),
|
text(label).size(12).color(muted_text()),
|
||||||
Space::with_width(Length::Fill),
|
Space::new().width(Length::Fill),
|
||||||
text(value.to_string()).size(12),
|
text(value.to_string()).size(12),
|
||||||
]
|
]
|
||||||
.spacing(12)
|
.spacing(12)
|
||||||
@@ -750,7 +750,7 @@ fn mini_chart(
|
|||||||
1.0
|
1.0
|
||||||
};
|
};
|
||||||
bars = bars.push(
|
bars = bars.push(
|
||||||
container(Space::new(Length::Fill, height))
|
container(Space::new().width(Length::Fill).height(height))
|
||||||
.width(Length::FillPortion(1))
|
.width(Length::FillPortion(1))
|
||||||
.style(move |_| chart_bar_style(color)),
|
.style(move |_| chart_bar_style(color)),
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ impl App {
|
|||||||
icon(ICON_SPARK, 36),
|
icon(ICON_SPARK, 36),
|
||||||
text("Start a local coding session").size(28),
|
text("Start a local coding session").size(28),
|
||||||
text("Choose a project folder to create your first session.").size(14),
|
text("Choose a project folder to create your first session.").size(14),
|
||||||
Space::with_height(10),
|
Space::new().height(10),
|
||||||
open_project,
|
open_project,
|
||||||
]
|
]
|
||||||
.spacing(10)
|
.spacing(10)
|
||||||
@@ -40,7 +40,7 @@ impl App {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
let header = header
|
let header = header
|
||||||
.push(Space::with_width(Length::Fill))
|
.push(Space::new().width(Length::Fill))
|
||||||
.spacing(10)
|
.spacing(10)
|
||||||
.align_y(Alignment::Center);
|
.align_y(Alignment::Center);
|
||||||
|
|
||||||
@@ -124,7 +124,7 @@ impl App {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
if !message.content.is_empty() {
|
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() {
|
let content = if message.reasoning.is_some() {
|
||||||
crate::agent::visible_content(&message.content).trim_start()
|
crate::agent::visible_content(&message.content).trim_start()
|
||||||
} else {
|
} else {
|
||||||
@@ -134,9 +134,8 @@ impl App {
|
|||||||
} else {
|
} else {
|
||||||
body = body.push(
|
body = body.push(
|
||||||
markdown::view(
|
markdown::view(
|
||||||
&message.markdown,
|
message.markdown.items(),
|
||||||
markdown::Settings::with_text_size(14),
|
markdown::Settings::with_text_size(14, markdown_style),
|
||||||
markdown_style,
|
|
||||||
)
|
)
|
||||||
.map(Message::OpenLink),
|
.map(Message::OpenLink),
|
||||||
);
|
);
|
||||||
@@ -283,7 +282,7 @@ impl App {
|
|||||||
))
|
))
|
||||||
.size(11)
|
.size(11)
|
||||||
.color(muted_text()),
|
.color(muted_text()),
|
||||||
Space::with_width(Length::Fill),
|
Space::new().width(Length::Fill),
|
||||||
icon(ICON_MODEL, 16),
|
icon(ICON_MODEL, 16),
|
||||||
text(self.config.model.to_string()).size(12),
|
text(self.config.model.to_string()).size(12),
|
||||||
action,
|
action,
|
||||||
@@ -339,7 +338,7 @@ fn tool_cards(cards: Vec<crate::agent::ToolCard>) -> Element<'static, Message> {
|
|||||||
let mut rows = column![].spacing(0);
|
let mut rows = column![].spacing(0);
|
||||||
for (index, card) in cards.into_iter().enumerate() {
|
for (index, card) in cards.into_iter().enumerate() {
|
||||||
if index > 0 {
|
if index > 0 {
|
||||||
rows = rows.push(horizontal_rule(1));
|
rows = rows.push(rule::horizontal(1));
|
||||||
}
|
}
|
||||||
let parameters = crate::agent::tool_parameters(&card.call);
|
let parameters = crate::agent::tool_parameters(&card.call);
|
||||||
let call = crate::agent::tool_call_text(&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![
|
let mut content = column![
|
||||||
row![
|
row![
|
||||||
text(card.call.name).size(13),
|
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()),
|
text(card.state.label()).size(11).color(muted_text()),
|
||||||
actions,
|
actions,
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ impl App {
|
|||||||
let mut artifacts = column![];
|
let mut artifacts = column![];
|
||||||
for (index, artifact) in model::managed_artifacts(&models_path()).iter().enumerate() {
|
for (index, artifact) in model::managed_artifacts(&models_path()).iter().enumerate() {
|
||||||
if index > 0 {
|
if index > 0 {
|
||||||
artifacts = artifacts.push(horizontal_rule(1));
|
artifacts = artifacts.push(rule::horizontal(1));
|
||||||
}
|
}
|
||||||
artifacts = artifacts.push(model_artifact_row(artifact, busy));
|
artifacts = artifacts.push(model_artifact_row(artifact, busy));
|
||||||
}
|
}
|
||||||
@@ -29,7 +29,7 @@ impl App {
|
|||||||
if let Some(error) = &self.error {
|
if let Some(error) = &self.error {
|
||||||
content = content.push(text(error).style(iced::widget::text::danger));
|
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))
|
container(scrollable(artifacts).height(Length::Fill))
|
||||||
.height(Length::Fill)
|
.height(Length::Fill)
|
||||||
.style(overview_style),
|
.style(overview_style),
|
||||||
@@ -51,7 +51,7 @@ impl App {
|
|||||||
))
|
))
|
||||||
.size(13),
|
.size(13),
|
||||||
row![
|
row![
|
||||||
Space::with_width(Length::Fill),
|
Space::new().width(Length::Fill),
|
||||||
action_button("Cancel").on_press(Message::CancelDeleteArtifact),
|
action_button("Cancel").on_press(Message::CancelDeleteArtifact),
|
||||||
danger_button("Delete").on_press(Message::ConfirmDeleteArtifact),
|
danger_button("Delete").on_press(Message::ConfirmDeleteArtifact),
|
||||||
]
|
]
|
||||||
@@ -119,7 +119,7 @@ fn model_artifact_row(artifact: &ManagedArtifact, busy: bool) -> Element<'static
|
|||||||
.color(muted_text()),
|
.color(muted_text()),
|
||||||
]
|
]
|
||||||
.spacing(5),
|
.spacing(5),
|
||||||
Space::with_width(Length::Fill),
|
Space::new().width(Length::Fill),
|
||||||
row![download, validate, delete]
|
row![download, validate, delete]
|
||||||
.spacing(8)
|
.spacing(8)
|
||||||
.align_y(Alignment::Center),
|
.align_y(Alignment::Center),
|
||||||
@@ -167,11 +167,11 @@ pub(super) fn download_status_bar(download: &ActiveDownload) -> Element<'_, Mess
|
|||||||
row![
|
row![
|
||||||
text(phase_text(progress.phase)).size(12),
|
text(phase_text(progress.phase)).size(12),
|
||||||
progress_bar(0.0..=1.0, progress.fraction())
|
progress_bar(0.0..=1.0, progress.fraction())
|
||||||
.width(180)
|
.length(180)
|
||||||
.height(7),
|
.girth(7),
|
||||||
text(measurement).size(12),
|
text(measurement).size(12),
|
||||||
text(transfer).size(12),
|
text(transfer).size(12),
|
||||||
Space::with_width(Length::Fill),
|
Space::new().width(Length::Fill),
|
||||||
stop,
|
stop,
|
||||||
]
|
]
|
||||||
.spacing(12)
|
.spacing(12)
|
||||||
@@ -234,7 +234,7 @@ fn model_download_status(download: &ModelDownload) -> Element<'_, Message> {
|
|||||||
} else {
|
} else {
|
||||||
text(heading).size(12)
|
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)
|
.align_y(Alignment::Center)
|
||||||
.spacing(8);
|
.spacing(8);
|
||||||
if let ModelDownload::Active(active) = download {
|
if let ModelDownload::Active(active) = download {
|
||||||
@@ -246,7 +246,7 @@ fn model_download_status(download: &ModelDownload) -> Element<'_, Message> {
|
|||||||
}
|
}
|
||||||
let mut status = column![
|
let mut status = column![
|
||||||
heading_row,
|
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),
|
text(measurement).size(12),
|
||||||
]
|
]
|
||||||
.spacing(6);
|
.spacing(6);
|
||||||
|
|||||||
@@ -9,10 +9,8 @@ impl App {
|
|||||||
.supports_dspark()
|
.supports_dspark()
|
||||||
.then_some(Message::PreferenceLegacyMtpChanged);
|
.then_some(Message::PreferenceLegacyMtpChanged);
|
||||||
let legacy_mtp = hint(
|
let legacy_mtp = hint(
|
||||||
checkbox(
|
checkbox(self.preference_draft.legacy_mtp_enabled)
|
||||||
"Enable legacy MTP for this model",
|
.label("Enable legacy MTP for this model")
|
||||||
self.preference_draft.legacy_mtp_enabled,
|
|
||||||
)
|
|
||||||
.on_toggle_maybe(legacy_mtp_toggle),
|
.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.",
|
"Uses the managed one-stage MTP support GGUF. The target model verifies every drafted token; it is mutually exclusive with DSpark.",
|
||||||
);
|
);
|
||||||
@@ -22,10 +20,8 @@ impl App {
|
|||||||
.supports_dspark()
|
.supports_dspark()
|
||||||
.then_some(Message::PreferenceDsparkChanged);
|
.then_some(Message::PreferenceDsparkChanged);
|
||||||
let dspark = hint(
|
let dspark = hint(
|
||||||
checkbox(
|
checkbox(self.preference_draft.dspark_enabled)
|
||||||
"Enable DSpark for this model",
|
.label("Enable DSpark for this model")
|
||||||
self.preference_draft.dspark_enabled,
|
|
||||||
)
|
|
||||||
.on_toggle_maybe(dspark_toggle),
|
.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.",
|
"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.",
|
||||||
);
|
);
|
||||||
@@ -130,10 +126,8 @@ impl App {
|
|||||||
"LOCAL ENDPOINT",
|
"LOCAL ENDPOINT",
|
||||||
column![
|
column![
|
||||||
hint(
|
hint(
|
||||||
checkbox(
|
checkbox(self.preference_draft.endpoint_enabled)
|
||||||
"Enable OpenAI-compatible endpoint",
|
.label("Enable OpenAI-compatible endpoint")
|
||||||
self.preference_draft.endpoint_enabled,
|
|
||||||
)
|
|
||||||
.on_toggle(Message::PreferenceEndpointEnabledChanged),
|
.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.",
|
"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),
|
.on_input(Message::PreferenceEndpointPortChanged),
|
||||||
),
|
),
|
||||||
hint(
|
hint(
|
||||||
checkbox(
|
checkbox(self.preference_draft.endpoint_cors)
|
||||||
"Allow browser clients (CORS)",
|
.label("Allow browser clients (CORS)")
|
||||||
self.preference_draft.endpoint_cors,
|
|
||||||
)
|
|
||||||
.on_toggle(Message::PreferenceEndpointCorsChanged),
|
.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.",
|
"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)
|
.on_input(Message::PreferenceSystemPromptChanged)
|
||||||
.padding(9),
|
.padding(9),
|
||||||
Space::with_height(4),
|
Space::new().height(4),
|
||||||
text("SAMPLING & REASONING").size(11).color(muted_text()),
|
text("SAMPLING & REASONING").size(11).color(muted_text()),
|
||||||
preference_input_row(
|
preference_input_row(
|
||||||
"Temperature",
|
"Temperature",
|
||||||
@@ -261,12 +253,14 @@ impl App {
|
|||||||
prefill,
|
prefill,
|
||||||
),
|
),
|
||||||
hint(
|
hint(
|
||||||
checkbox("Prefer exact quality kernels", self.preference_draft.quality)
|
checkbox(self.preference_draft.quality)
|
||||||
|
.label("Prefer exact quality kernels")
|
||||||
.on_toggle(Message::PreferenceQualityChanged),
|
.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.",
|
"Runs the exact Metal kernels instead of the fast approximations. Slightly slower, and it removes the small numeric differences those approximations introduce.",
|
||||||
),
|
),
|
||||||
hint(
|
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),
|
.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.",
|
"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),
|
.on_input(Message::PreferenceMtpMarginChanged),
|
||||||
),
|
),
|
||||||
hint(
|
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),
|
.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.",
|
"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(
|
hint(
|
||||||
checkbox(
|
checkbox(self.preference_draft.glm_mtp_timing)
|
||||||
"Log GLM MTP timing counters",
|
.label("Log GLM MTP timing counters")
|
||||||
self.preference_draft.glm_mtp_timing,
|
|
||||||
)
|
|
||||||
.on_toggle_maybe(glm_mtp_timing_toggle),
|
.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.",
|
"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,
|
dspark_confidence,
|
||||||
),
|
),
|
||||||
hint(
|
hint(
|
||||||
checkbox(
|
checkbox(self.preference_draft.dspark_strict)
|
||||||
"DSpark target-only decode",
|
.label("DSpark target-only decode")
|
||||||
self.preference_draft.dspark_strict,
|
|
||||||
)
|
|
||||||
.on_toggle_maybe(dspark_strict_toggle),
|
.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.",
|
"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),
|
.size(12),
|
||||||
Space::with_height(6),
|
Space::new().height(6),
|
||||||
text("SSD STREAMING").size(11).color(muted_text()),
|
text("SSD STREAMING").size(11).color(muted_text()),
|
||||||
hint(
|
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),
|
.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.",
|
"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(
|
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),
|
.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.",
|
"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),
|
.size(12),
|
||||||
Space::with_height(6),
|
Space::new().height(6),
|
||||||
text("ADVANCED DIAGNOSTICS").size(11).color(muted_text()),
|
text("ADVANCED DIAGNOSTICS").size(11).color(muted_text()),
|
||||||
preference_input_row(
|
preference_input_row(
|
||||||
"Simulated used memory (GiB)",
|
"Simulated used memory (GiB)",
|
||||||
@@ -561,14 +554,14 @@ impl App {
|
|||||||
let header = row![
|
let header = row![
|
||||||
icon(ICON_SETTINGS, 22),
|
icon(ICON_SETTINGS, 22),
|
||||||
text("Preferences").size(24),
|
text("Preferences").size(24),
|
||||||
Space::with_width(Length::Fill),
|
Space::new().width(Length::Fill),
|
||||||
text("⌘,").size(12),
|
text("⌘,").size(12),
|
||||||
]
|
]
|
||||||
.spacing(10)
|
.spacing(10)
|
||||||
.align_y(Alignment::Center);
|
.align_y(Alignment::Center);
|
||||||
let footer = row![
|
let footer = row![
|
||||||
action_button("Reset DS4 defaults").on_press(Message::ResetPreferences),
|
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("Cancel").on_press(Message::DismissPanel),
|
||||||
action_button("Save").on_press(Message::SavePreferences),
|
action_button("Save").on_press(Message::SavePreferences),
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -47,7 +47,7 @@ impl App {
|
|||||||
.color(muted_text()),
|
.color(muted_text()),
|
||||||
]
|
]
|
||||||
.spacing(5),
|
.spacing(5),
|
||||||
Space::with_width(Length::Fill),
|
Space::new().width(Length::Fill),
|
||||||
container(text(stats.phase.label()).size(12).color(phase_color))
|
container(text(stats.phase.label()).size(12).color(phase_color))
|
||||||
.padding([7, 11])
|
.padding([7, 11])
|
||||||
.style(move |_| status_badge_style(phase_color)),
|
.style(move |_| status_badge_style(phase_color)),
|
||||||
@@ -109,7 +109,7 @@ impl App {
|
|||||||
text("Decode")
|
text("Decode")
|
||||||
.size(12)
|
.size(12)
|
||||||
.color(Color::from_rgb8(84, 170, 255)),
|
.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))
|
text(format!("{:.1} tok/s", stats.decode_tokens_per_second))
|
||||||
.size(12)
|
.size(12)
|
||||||
.color(muted_text()),
|
.color(muted_text()),
|
||||||
@@ -123,7 +123,7 @@ impl App {
|
|||||||
text("Prefill")
|
text("Prefill")
|
||||||
.size(12)
|
.size(12)
|
||||||
.color(Color::from_rgb8(157, 119, 255)),
|
.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))
|
text(format!("{:.1} tok/s", stats.prefill_tokens_per_second))
|
||||||
.size(12)
|
.size(12)
|
||||||
.color(muted_text()),
|
.color(muted_text()),
|
||||||
@@ -142,7 +142,7 @@ impl App {
|
|||||||
),
|
),
|
||||||
row![
|
row![
|
||||||
text(format!("{} requests", format_count(stats.http_requests))).size(12),
|
text(format!("{} requests", format_count(stats.http_requests))).size(12),
|
||||||
Space::with_width(Length::Fill),
|
Space::new().width(Length::Fill),
|
||||||
text(format!(
|
text(format!(
|
||||||
"{} errors · {} streaming",
|
"{} errors · {} streaming",
|
||||||
stats.http_errors, stats.http_streaming_requests
|
stats.http_errors, stats.http_streaming_requests
|
||||||
@@ -171,7 +171,7 @@ impl App {
|
|||||||
})
|
})
|
||||||
.size(12)
|
.size(12)
|
||||||
.color(Color::from_rgb8(67, 194, 203)),
|
.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))
|
text(format_rate(latest.kv_read_bytes_per_second))
|
||||||
.size(12)
|
.size(12)
|
||||||
.color(muted_text()),
|
.color(muted_text()),
|
||||||
@@ -189,7 +189,7 @@ impl App {
|
|||||||
})
|
})
|
||||||
.size(12)
|
.size(12)
|
||||||
.color(Color::from_rgb8(240, 180, 70)),
|
.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))
|
text(format_rate(latest.kv_write_bytes_per_second))
|
||||||
.size(12)
|
.size(12)
|
||||||
.color(muted_text()),
|
.color(muted_text()),
|
||||||
@@ -210,7 +210,7 @@ impl App {
|
|||||||
text("Selected loads")
|
text("Selected loads")
|
||||||
.size(12)
|
.size(12)
|
||||||
.color(Color::from_rgb8(240, 180, 70)),
|
.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))
|
text(format!("{:.1}/s", latest.ssd_requests_per_second))
|
||||||
.size(12)
|
.size(12)
|
||||||
.color(muted_text()),
|
.color(muted_text()),
|
||||||
@@ -224,7 +224,7 @@ impl App {
|
|||||||
text("Requested expert data")
|
text("Requested expert data")
|
||||||
.size(12)
|
.size(12)
|
||||||
.color(Color::from_rgb8(67, 194, 203)),
|
.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))
|
text(format_rate(latest.ssd_bytes_per_second))
|
||||||
.size(12)
|
.size(12)
|
||||||
.color(muted_text()),
|
.color(muted_text()),
|
||||||
@@ -238,7 +238,7 @@ impl App {
|
|||||||
text("Inference wait")
|
text("Inference wait")
|
||||||
.size(12)
|
.size(12)
|
||||||
.color(Color::from_rgb8(220, 80, 86)),
|
.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))
|
text(format!("{:.0} ms/s", latest.ssd_wait_ms_per_second))
|
||||||
.size(12)
|
.size(12)
|
||||||
.color(muted_text()),
|
.color(muted_text()),
|
||||||
@@ -443,7 +443,7 @@ impl App {
|
|||||||
format_milliseconds(stats.last_kv_write_ms),
|
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)
|
.spacing(9)
|
||||||
.into(),
|
.into(),
|
||||||
@@ -522,15 +522,16 @@ impl App {
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
bar = bar.push(
|
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)))
|
.width(Length::FillPortion(portion(bytes, capacity)))
|
||||||
.style(move |_| chart_bar_style(color)),
|
.style(move |_| chart_bar_style(color)),
|
||||||
);
|
);
|
||||||
legend = legend.push(
|
legend = legend.push(
|
||||||
row![
|
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()),
|
text(label).size(12).color(muted_text()),
|
||||||
Space::with_width(Length::Fill),
|
Space::new().width(Length::Fill),
|
||||||
text(format!(
|
text(format!(
|
||||||
"{} · {:.0}%",
|
"{} · {:.0}%",
|
||||||
format_bytes(bytes),
|
format_bytes(bytes),
|
||||||
@@ -544,7 +545,7 @@ impl App {
|
|||||||
}
|
}
|
||||||
if total == 0 {
|
if total == 0 {
|
||||||
bar = bar.push(
|
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))),
|
.style(|_| chart_bar_style(muted_text().scale_alpha(0.25))),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -562,7 +563,7 @@ impl App {
|
|||||||
rows = rows.push(
|
rows = rows.push(
|
||||||
row![
|
row![
|
||||||
text(label).size(12),
|
text(label).size(12),
|
||||||
Space::with_width(Length::Fill),
|
Space::new().width(Length::Fill),
|
||||||
text(format!(
|
text(format!(
|
||||||
"{} · {} old",
|
"{} · {} old",
|
||||||
format_bytes(entry.bytes),
|
format_bytes(entry.bytes),
|
||||||
@@ -607,7 +608,7 @@ impl App {
|
|||||||
usage.transient_bytes as f32 / usage.budget_bytes.max(1) as f32 * 100.0,
|
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() {
|
text(if usage.entries.is_empty() {
|
||||||
"No checkpoints stored yet."
|
"No checkpoints stored yet."
|
||||||
} else {
|
} else {
|
||||||
@@ -620,7 +621,7 @@ impl App {
|
|||||||
text("Discarding a checkpoint costs one prefill to rebuild it.")
|
text("Discarding a checkpoint costs one prefill to rebuild it.")
|
||||||
.size(11)
|
.size(11)
|
||||||
.color(muted_text()),
|
.color(muted_text()),
|
||||||
Space::with_width(Length::Fill),
|
Space::new().width(Length::Fill),
|
||||||
action_button(text("Clear transient cache").size(12))
|
action_button(text("Clear transient cache").size(12))
|
||||||
.on_press(Message::ClearTransientCache),
|
.on_press(Message::ClearTransientCache),
|
||||||
]
|
]
|
||||||
|
|||||||
15
src/main.rs
15
src/main.rs
@@ -25,10 +25,8 @@ fn main() -> iced::Result {
|
|||||||
if let Err(error) = engine::configure_metal_sources() {
|
if let Err(error) = engine::configure_metal_sources() {
|
||||||
eprintln!("DS4Server: {error}");
|
eprintln!("DS4Server: {error}");
|
||||||
}
|
}
|
||||||
iced::daemon(App::title, App::update, App::view)
|
iced::daemon(
|
||||||
.subscription(App::subscription)
|
|| {
|
||||||
.theme(|_, _| app_theme())
|
|
||||||
.run_with(|| {
|
|
||||||
let (main_window, open) = window::open(window::Settings {
|
let (main_window, open) = window::open(window::Settings {
|
||||||
size: Size::new(1120.0, 720.0),
|
size: Size::new(1120.0, 720.0),
|
||||||
min_size: Some(Size::new(760.0, 480.0)),
|
min_size: Some(Size::new(760.0, 480.0)),
|
||||||
@@ -44,5 +42,12 @@ fn main() -> iced::Result {
|
|||||||
..Default::default()
|
..Default::default()
|
||||||
});
|
});
|
||||||
(App::load(main_window), open.map(Message::WindowOpened))
|
(App::load(main_window), open.map(Message::WindowOpened))
|
||||||
})
|
},
|
||||||
|
App::update,
|
||||||
|
App::view,
|
||||||
|
)
|
||||||
|
.title(App::title)
|
||||||
|
.subscription(App::subscription)
|
||||||
|
.theme(app_theme())
|
||||||
|
.run()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,7 +6,6 @@ use iced::advanced::overlay;
|
|||||||
use iced::advanced::renderer;
|
use iced::advanced::renderer;
|
||||||
use iced::advanced::widget::{Operation, Tree, tree};
|
use iced::advanced::widget::{Operation, Tree, tree};
|
||||||
use iced::advanced::{Clipboard, Layout, Shell, Widget};
|
use iced::advanced::{Clipboard, Layout, Shell, Widget};
|
||||||
use iced::event;
|
|
||||||
use iced::keyboard::{self, Key, Location, Modifiers, key};
|
use iced::keyboard::{self, Key, Location, Modifiers, key};
|
||||||
use iced::mouse;
|
use iced::mouse;
|
||||||
use iced::{Element, Event, Length, Rectangle, Size, Vector};
|
use iced::{Element, Event, Length, Rectangle, Size, Vector};
|
||||||
@@ -95,9 +94,12 @@ fn command_events(command: EditCommand) -> [Event; 4] {
|
|||||||
location: Location::Standard,
|
location: Location::Standard,
|
||||||
modifiers,
|
modifiers,
|
||||||
text: None,
|
text: None,
|
||||||
|
repeat: false,
|
||||||
}),
|
}),
|
||||||
Event::Keyboard(keyboard::Event::KeyReleased {
|
Event::Keyboard(keyboard::Event::KeyReleased {
|
||||||
key,
|
key,
|
||||||
|
modified_key: Key::Character(modified_character.into()),
|
||||||
|
physical_key: key::Physical::Code(physical_key),
|
||||||
location: Location::Standard,
|
location: Location::Standard,
|
||||||
modifiers,
|
modifiers,
|
||||||
}),
|
}),
|
||||||
@@ -131,67 +133,63 @@ where
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn layout(
|
fn layout(
|
||||||
&self,
|
&mut self,
|
||||||
tree: &mut Tree,
|
tree: &mut Tree,
|
||||||
renderer: &Renderer,
|
renderer: &Renderer,
|
||||||
limits: &layout::Limits,
|
limits: &layout::Limits,
|
||||||
) -> layout::Node {
|
) -> layout::Node {
|
||||||
self.content
|
self.content
|
||||||
.as_widget()
|
.as_widget_mut()
|
||||||
.layout(&mut tree.children[0], renderer, limits)
|
.layout(&mut tree.children[0], renderer, limits)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn operate(
|
fn operate(
|
||||||
&self,
|
&mut self,
|
||||||
tree: &mut Tree,
|
tree: &mut Tree,
|
||||||
layout: Layout<'_>,
|
layout: Layout<'_>,
|
||||||
renderer: &Renderer,
|
renderer: &Renderer,
|
||||||
operation: &mut dyn Operation,
|
operation: &mut dyn Operation,
|
||||||
) {
|
) {
|
||||||
self.content
|
self.content
|
||||||
.as_widget()
|
.as_widget_mut()
|
||||||
.operate(&mut tree.children[0], layout, renderer, operation);
|
.operate(&mut tree.children[0], layout, renderer, operation);
|
||||||
}
|
}
|
||||||
|
|
||||||
fn on_event(
|
fn update(
|
||||||
&mut self,
|
&mut self,
|
||||||
tree: &mut Tree,
|
tree: &mut Tree,
|
||||||
event: Event,
|
event: &Event,
|
||||||
layout: Layout<'_>,
|
layout: Layout<'_>,
|
||||||
cursor: mouse::Cursor,
|
cursor: mouse::Cursor,
|
||||||
renderer: &Renderer,
|
renderer: &Renderer,
|
||||||
clipboard: &mut dyn Clipboard,
|
clipboard: &mut dyn Clipboard,
|
||||||
shell: &mut Shell<'_, Message>,
|
shell: &mut Shell<'_, Message>,
|
||||||
viewport: &Rectangle,
|
viewport: &Rectangle,
|
||||||
) -> event::Status {
|
) {
|
||||||
let mut status = event::Status::Ignored;
|
|
||||||
if matches!(
|
if matches!(
|
||||||
event,
|
event,
|
||||||
Event::Window(iced::window::Event::RedrawRequested(_))
|
Event::Window(iced::window::Event::RedrawRequested(_))
|
||||||
) {
|
) {
|
||||||
while let Some(command) = pop_command(&self.commands) {
|
while let Some(command) = pop_command(&self.commands) {
|
||||||
for command_event in command_events(command) {
|
for command_event in command_events(command) {
|
||||||
if self.content.as_widget_mut().on_event(
|
self.content.as_widget_mut().update(
|
||||||
&mut tree.children[0],
|
&mut tree.children[0],
|
||||||
command_event,
|
&command_event,
|
||||||
layout,
|
layout,
|
||||||
cursor,
|
cursor,
|
||||||
renderer,
|
renderer,
|
||||||
clipboard,
|
clipboard,
|
||||||
shell,
|
shell,
|
||||||
viewport,
|
viewport,
|
||||||
) == event::Status::Captured
|
);
|
||||||
{
|
|
||||||
status = event::Status::Captured;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if let Some(sync_event) = modifier_sync_event(&event) {
|
if let Some(sync_event) = modifier_sync_event(event) {
|
||||||
let _ = self.content.as_widget_mut().on_event(
|
self.content.as_widget_mut().update(
|
||||||
&mut tree.children[0],
|
&mut tree.children[0],
|
||||||
sync_event,
|
&sync_event,
|
||||||
layout,
|
layout,
|
||||||
cursor,
|
cursor,
|
||||||
renderer,
|
renderer,
|
||||||
@@ -201,7 +199,7 @@ where
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if self.content.as_widget_mut().on_event(
|
self.content.as_widget_mut().update(
|
||||||
&mut tree.children[0],
|
&mut tree.children[0],
|
||||||
event,
|
event,
|
||||||
layout,
|
layout,
|
||||||
@@ -210,12 +208,7 @@ where
|
|||||||
clipboard,
|
clipboard,
|
||||||
shell,
|
shell,
|
||||||
viewport,
|
viewport,
|
||||||
) == event::Status::Captured
|
);
|
||||||
{
|
|
||||||
event::Status::Captured
|
|
||||||
} else {
|
|
||||||
status
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn mouse_interaction(
|
fn mouse_interaction(
|
||||||
@@ -259,13 +252,18 @@ where
|
|||||||
fn overlay<'b>(
|
fn overlay<'b>(
|
||||||
&'b mut self,
|
&'b mut self,
|
||||||
tree: &'b mut Tree,
|
tree: &'b mut Tree,
|
||||||
layout: Layout<'_>,
|
layout: Layout<'b>,
|
||||||
renderer: &Renderer,
|
renderer: &Renderer,
|
||||||
|
viewport: &Rectangle,
|
||||||
translation: Vector,
|
translation: Vector,
|
||||||
) -> Option<overlay::Element<'b, Message, Theme, Renderer>> {
|
) -> Option<overlay::Element<'b, Message, Theme, Renderer>> {
|
||||||
self.content
|
self.content.as_widget_mut().overlay(
|
||||||
.as_widget_mut()
|
&mut tree.children[0],
|
||||||
.overlay(&mut tree.children[0], layout, renderer, translation)
|
layout,
|
||||||
|
renderer,
|
||||||
|
viewport,
|
||||||
|
translation,
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user