fix: clean v0.7.2 release prep

This commit is contained in:
Hunter Bown
2026-04-28 21:54:43 -05:00
parent 35db361a87
commit 0f8c363012
28 changed files with 225 additions and 12902 deletions
Binary file not shown.

Before

Width:  |  Height:  |  Size: 84 KiB

+1 -1
View File
@@ -7,5 +7,5 @@ repository.workspace = true
description = "Model/provider registry and fallback strategy for DeepSeek workspace architecture"
[dependencies]
deepseek-config = { path = "../config", version = "0.7.1" }
deepseek-config = { path = "../config", version = "0.7.2" }
serde.workspace = true
+9 -9
View File
@@ -10,15 +10,15 @@ description = "Codex-style app-server transport for DeepSeek workspace architect
anyhow.workspace = true
axum.workspace = true
clap.workspace = true
deepseek-agent = { path = "../agent", version = "0.7.1" }
deepseek-config = { path = "../config", version = "0.7.1" }
deepseek-core = { path = "../core", version = "0.7.1" }
deepseek-execpolicy = { path = "../execpolicy", version = "0.7.1" }
deepseek-hooks = { path = "../hooks", version = "0.7.1" }
deepseek-mcp = { path = "../mcp", version = "0.7.1" }
deepseek-protocol = { path = "../protocol", version = "0.7.1" }
deepseek-state = { path = "../state", version = "0.7.1" }
deepseek-tools = { path = "../tools", version = "0.7.1" }
deepseek-agent = { path = "../agent", version = "0.7.2" }
deepseek-config = { path = "../config", version = "0.7.2" }
deepseek-core = { path = "../core", version = "0.7.2" }
deepseek-execpolicy = { path = "../execpolicy", version = "0.7.2" }
deepseek-hooks = { path = "../hooks", version = "0.7.2" }
deepseek-mcp = { path = "../mcp", version = "0.7.2" }
deepseek-protocol = { path = "../protocol", version = "0.7.2" }
deepseek-state = { path = "../state", version = "0.7.2" }
deepseek-tools = { path = "../tools", version = "0.7.2" }
serde.workspace = true
serde_json.workspace = true
tokio.workspace = true
+7 -7
View File
@@ -14,13 +14,13 @@ path = "src/main.rs"
anyhow.workspace = true
clap.workspace = true
clap_complete.workspace = true
deepseek-agent = { path = "../agent", version = "0.7.1" }
deepseek-app-server = { path = "../app-server", version = "0.7.1" }
deepseek-config = { path = "../config", version = "0.7.1" }
deepseek-execpolicy = { path = "../execpolicy", version = "0.7.1" }
deepseek-mcp = { path = "../mcp", version = "0.7.1" }
deepseek-secrets = { path = "../secrets", version = "0.7.1" }
deepseek-state = { path = "../state", version = "0.7.1" }
deepseek-agent = { path = "../agent", version = "0.7.2" }
deepseek-app-server = { path = "../app-server", version = "0.7.2" }
deepseek-config = { path = "../config", version = "0.7.2" }
deepseek-execpolicy = { path = "../execpolicy", version = "0.7.2" }
deepseek-mcp = { path = "../mcp", version = "0.7.2" }
deepseek-secrets = { path = "../secrets", version = "0.7.2" }
deepseek-state = { path = "../state", version = "0.7.2" }
chrono.workspace = true
dirs.workspace = true
serde.workspace = true
+1 -1
View File
@@ -8,7 +8,7 @@ description = "Config schema and precedence model for DeepSeek workspace archite
[dependencies]
anyhow.workspace = true
deepseek-secrets = { path = "../secrets", version = "0.7.1" }
deepseek-secrets = { path = "../secrets", version = "0.7.2" }
dirs.workspace = true
serde.workspace = true
serde_json.workspace = true
+8 -8
View File
@@ -9,14 +9,14 @@ description = "Core runtime boundaries for DeepSeek workspace architecture"
[dependencies]
anyhow.workspace = true
chrono.workspace = true
deepseek-agent = { path = "../agent", version = "0.7.1" }
deepseek-config = { path = "../config", version = "0.7.1" }
deepseek-execpolicy = { path = "../execpolicy", version = "0.7.1" }
deepseek-hooks = { path = "../hooks", version = "0.7.1" }
deepseek-mcp = { path = "../mcp", version = "0.7.1" }
deepseek-protocol = { path = "../protocol", version = "0.7.1" }
deepseek-state = { path = "../state", version = "0.7.1" }
deepseek-tools = { path = "../tools", version = "0.7.1" }
deepseek-agent = { path = "../agent", version = "0.7.2" }
deepseek-config = { path = "../config", version = "0.7.2" }
deepseek-execpolicy = { path = "../execpolicy", version = "0.7.2" }
deepseek-hooks = { path = "../hooks", version = "0.7.2" }
deepseek-mcp = { path = "../mcp", version = "0.7.2" }
deepseek-protocol = { path = "../protocol", version = "0.7.2" }
deepseek-state = { path = "../state", version = "0.7.2" }
deepseek-tools = { path = "../tools", version = "0.7.2" }
serde_json.workspace = true
tokio.workspace = true
uuid.workspace = true
+1 -1
View File
@@ -8,5 +8,5 @@ description = "Execution policy and approval model parity for DeepSeek workspace
[dependencies]
anyhow.workspace = true
deepseek-protocol = { path = "../protocol", version = "0.7.1" }
deepseek-protocol = { path = "../protocol", version = "0.7.2" }
serde.workspace = true
+1 -1
View File
@@ -10,7 +10,7 @@ description = "Hook dispatch and notifications parity for DeepSeek workspace arc
anyhow.workspace = true
async-trait.workspace = true
chrono.workspace = true
deepseek-protocol = { path = "../protocol", version = "0.7.1" }
deepseek-protocol = { path = "../protocol", version = "0.7.2" }
reqwest.workspace = true
serde.workspace = true
serde_json.workspace = true
+1 -1
View File
@@ -8,6 +8,6 @@ description = "MCP server lifecycle and tool proxy compatibility for DeepSeek wo
[dependencies]
anyhow.workspace = true
deepseek-protocol = { path = "../protocol", version = "0.7.1" }
deepseek-protocol = { path = "../protocol", version = "0.7.2" }
serde.workspace = true
serde_json.workspace = true
+1 -1
View File
@@ -9,7 +9,7 @@ description = "Tool invocation lifecycle, schema validation, and scheduler paral
[dependencies]
anyhow.workspace = true
async-trait.workspace = true
deepseek-protocol = { path = "../protocol", version = "0.7.1" }
deepseek-protocol = { path = "../protocol", version = "0.7.2" }
serde.workspace = true
serde_json.workspace = true
tokio.workspace = true
+17 -20
View File
@@ -185,26 +185,23 @@ impl ToolResult {
/// Helper to extract a required string field from JSON input.
pub fn required_str<'a>(input: &'a Value, field: &str) -> std::result::Result<&'a str, ToolError> {
input
.get(field)
.and_then(Value::as_str)
.ok_or_else(|| {
// When the field is missing, list the fields the caller *did*
// supply so the model can spot the mismatch without a retry.
let provided: Vec<&str> = input
.as_object()
.map(|obj| obj.keys().map(|k| k.as_str()).collect())
.unwrap_or_default();
if provided.is_empty() {
ToolError::missing_field(field)
} else {
let hint = format!(
"missing required field '{field}'. Input provided: {}",
provided.join(", ")
);
ToolError::invalid_input(hint)
}
})
input.get(field).and_then(Value::as_str).ok_or_else(|| {
// When the field is missing, list the fields the caller *did*
// supply so the model can spot the mismatch without a retry.
let provided: Vec<&str> = input
.as_object()
.map(|obj| obj.keys().map(|k| k.as_str()).collect())
.unwrap_or_default();
if provided.is_empty() {
ToolError::missing_field(field)
} else {
let hint = format!(
"missing required field '{field}'. Input provided: {}",
provided.join(", ")
);
ToolError::invalid_input(hint)
}
})
}
/// Helper to extract an optional string field from JSON input.
+2 -2
View File
@@ -13,8 +13,8 @@ path = "src/main.rs"
[dependencies]
anyhow = "1.0.100"
arboard = "3.4"
deepseek-secrets = { path = "../secrets", version = "0.7.1" }
deepseek-tools = { path = "../tools", version = "0.7.1" }
deepseek-secrets = { path = "../secrets", version = "0.7.2" }
deepseek-tools = { path = "../tools", version = "0.7.2" }
async-stream = "0.3.6"
async-trait = "0.1"
bytes = "1.11.0"
+15 -3
View File
@@ -750,7 +750,11 @@ pub(super) fn apply_reasoning_effort(
"off" | "disabled" | "none" | "false" => match provider {
// OpenRouter / Novita relay the same DeepSeek V4 payload shape
// as DeepSeek native; they pass through `thinking` / `reasoning_effort`.
ApiProvider::Deepseek | ApiProvider::Openrouter | ApiProvider::Novita | ApiProvider::Fireworks | ApiProvider::Sglang => {
ApiProvider::Deepseek
| ApiProvider::Openrouter
| ApiProvider::Novita
| ApiProvider::Fireworks
| ApiProvider::Sglang => {
body["thinking"] = json!({ "type": "disabled" });
}
ApiProvider::NvidiaNim => {
@@ -760,7 +764,11 @@ pub(super) fn apply_reasoning_effort(
}
},
"low" | "minimal" | "medium" | "mid" | "high" | "" => match provider {
ApiProvider::Deepseek | ApiProvider::Openrouter | ApiProvider::Novita | ApiProvider::Fireworks | ApiProvider::Sglang => {
ApiProvider::Deepseek
| ApiProvider::Openrouter
| ApiProvider::Novita
| ApiProvider::Fireworks
| ApiProvider::Sglang => {
body["reasoning_effort"] = json!("high");
body["thinking"] = json!({ "type": "enabled" });
}
@@ -772,7 +780,11 @@ pub(super) fn apply_reasoning_effort(
}
},
"xhigh" | "max" | "highest" => match provider {
ApiProvider::Deepseek | ApiProvider::Openrouter | ApiProvider::Novita | ApiProvider::Fireworks | ApiProvider::Sglang => {
ApiProvider::Deepseek
| ApiProvider::Openrouter
| ApiProvider::Novita
| ApiProvider::Fireworks
| ApiProvider::Sglang => {
body["reasoning_effort"] = json!("max");
body["thinking"] = json!({ "type": "enabled" });
}
File diff suppressed because it is too large Load Diff
+16 -5
View File
@@ -928,8 +928,10 @@ impl Config {
.as_ref()
.filter(|base| base.contains("integrate.api.nvidia.com"))
.cloned(),
ApiProvider::Openrouter | ApiProvider::Novita
| ApiProvider::Fireworks | ApiProvider::Sglang => None,
ApiProvider::Openrouter
| ApiProvider::Novita
| ApiProvider::Fireworks
| ApiProvider::Sglang => None,
};
let base = provider_base.or(root_base).unwrap_or_else(|| {
match provider {
@@ -1663,9 +1665,18 @@ fn merge_config(base: Config, override_cfg: Config) -> Config {
.context
.verbatim_window_turns
.or(base.context.verbatim_window_turns),
l1_threshold: override_cfg.context.l1_threshold.or(base.context.l1_threshold),
l2_threshold: override_cfg.context.l2_threshold.or(base.context.l2_threshold),
l3_threshold: override_cfg.context.l3_threshold.or(base.context.l3_threshold),
l1_threshold: override_cfg
.context
.l1_threshold
.or(base.context.l1_threshold),
l2_threshold: override_cfg
.context
.l2_threshold
.or(base.context.l2_threshold),
l3_threshold: override_cfg
.context
.l3_threshold
.or(base.context.l3_threshold),
cycle_threshold: override_cfg
.context
.cycle_threshold
+21 -16
View File
@@ -24,7 +24,6 @@ use crate::compaction::{
CompactionConfig, compact_messages_safe, estimate_tokens, merge_system_prompts, should_compact,
};
use crate::config::{Config, DEFAULT_MAX_SUBAGENTS, DEFAULT_TEXT_MODEL};
use crate::seam_manager::{SeamConfig, SeamManager};
use crate::cycle_manager::{
CycleBriefing, CycleConfig, StructuredState, archive_cycle, build_seed_messages,
estimate_briefing_tokens, produce_briefing, should_advance_cycle,
@@ -38,6 +37,7 @@ use crate::models::{
StreamEvent, SystemBlock, SystemPrompt, Tool, ToolCaller, Usage, context_window_for_model,
};
use crate::prompts;
use crate::seam_manager::{SeamConfig, SeamManager};
use crate::tools::plan::{SharedPlanState, new_shared_plan_state};
use crate::tools::shell::{SharedShellManager, new_shared_shell_manager};
use crate::tools::spec::{ApprovalRequirement, ToolError, ToolResult, required_str};
@@ -1264,21 +1264,26 @@ impl Engine {
let seam_manager = deepseek_client.as_ref().map(|main_client| {
let seam_config = SeamConfig {
enabled: api_config.context.enabled.unwrap_or(true),
verbatim_window_turns: api_config.context.verbatim_window_turns.unwrap_or(
crate::seam_manager::VERBATIM_WINDOW_TURNS,
),
l1_threshold: api_config.context.l1_threshold.unwrap_or(
crate::seam_manager::DEFAULT_L1_THRESHOLD,
),
l2_threshold: api_config.context.l2_threshold.unwrap_or(
crate::seam_manager::DEFAULT_L2_THRESHOLD,
),
l3_threshold: api_config.context.l3_threshold.unwrap_or(
crate::seam_manager::DEFAULT_L3_THRESHOLD,
),
cycle_threshold: api_config.context.cycle_threshold.unwrap_or(
crate::seam_manager::DEFAULT_CYCLE_THRESHOLD,
),
verbatim_window_turns: api_config
.context
.verbatim_window_turns
.unwrap_or(crate::seam_manager::VERBATIM_WINDOW_TURNS),
l1_threshold: api_config
.context
.l1_threshold
.unwrap_or(crate::seam_manager::DEFAULT_L1_THRESHOLD),
l2_threshold: api_config
.context
.l2_threshold
.unwrap_or(crate::seam_manager::DEFAULT_L2_THRESHOLD),
l3_threshold: api_config
.context
.l3_threshold
.unwrap_or(crate::seam_manager::DEFAULT_L3_THRESHOLD),
cycle_threshold: api_config
.context
.cycle_threshold
.unwrap_or(crate::seam_manager::DEFAULT_CYCLE_THRESHOLD),
seam_model: api_config
.context
.seam_model
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
+2 -2
View File
@@ -157,7 +157,7 @@ pub struct ContainerInfo {
}
/// Server-side tool usage counters.
#[derive(Debug, Serialize, Deserialize, Clone, Default)]
#[derive(Debug, Serialize, Deserialize, Clone, Default, PartialEq, Eq)]
pub struct ServerToolUsage {
#[serde(skip_serializing_if = "Option::is_none")]
pub code_execution_requests: Option<u32>,
@@ -181,7 +181,7 @@ pub struct MessageResponse {
}
/// Token usage metadata for a response.
#[derive(Debug, Serialize, Deserialize, Clone, Default)]
#[derive(Debug, Serialize, Deserialize, Clone, Default, PartialEq, Eq)]
pub struct Usage {
pub input_tokens: u32,
pub output_tokens: u32,
+1 -1
View File
@@ -36,8 +36,8 @@ use chrono::{DateTime, Utc};
use tokio::sync::Mutex;
use crate::client::DeepSeekClient;
use crate::compaction::plan_compaction;
use crate::compaction::KEEP_RECENT_MESSAGES;
use crate::compaction::plan_compaction;
use crate::llm_client::LlmClient;
use crate::models::{ContentBlock, Message, MessageRequest, SystemBlock, SystemPrompt};
+16 -10
View File
@@ -19,6 +19,8 @@ use serde::{Deserialize, Serialize};
use tokio::sync::{Mutex, mpsc, watch};
use tokio_util::sync::CancellationToken;
use crate::models::Usage;
use super::SubAgentType;
/// Stable, structured progress envelope shared across the sub-agent surface.
@@ -62,10 +64,10 @@ pub enum MailboxMessage {
/// Published after each turn so the parent's cost counter updates live.
TokenUsage {
agent_id: String,
/// Prompt tokens consumed (input, including cached).
prompt_tokens: u32,
/// Completion tokens consumed (output).
completion_tokens: u32,
/// Model that produced this usage, used for pricing.
model: String,
/// Provider usage payload, including cache-hit/cache-miss fields.
usage: Usage,
},
}
@@ -103,13 +105,13 @@ impl MailboxMessage {
pub(crate) fn token_usage(
agent_id: impl Into<String>,
prompt_tokens: u32,
completion_tokens: u32,
model: impl Into<String>,
usage: Usage,
) -> Self {
Self::TokenUsage {
agent_id: agent_id.into(),
prompt_tokens,
completion_tokens,
model: model.into(),
usage,
}
}
}
@@ -459,8 +461,12 @@ mod tests {
(
MailboxMessage::TokenUsage {
agent_id: "a9".into(),
prompt_tokens: 100,
completion_tokens: 50,
model: "deepseek-v4-flash".into(),
usage: Usage {
input_tokens: 100,
output_tokens: 50,
..Default::default()
},
},
"a9",
),
+2 -2
View File
@@ -2730,8 +2730,8 @@ async fn run_subagent(
if let Some(mb) = runtime.mailbox.as_ref() {
let _ = mb.send(MailboxMessage::token_usage(
&agent_id,
response.usage.input_tokens,
response.usage.output_tokens,
response.model.clone(),
response.usage.clone(),
));
}
+90 -80
View File
@@ -207,9 +207,7 @@ impl HistoryCell {
} => render_thinking(content, width, *streaming, *duration_secs, false, false),
HistoryCell::Tool(cell) => cell.lines_with_motion(width, false),
HistoryCell::SubAgent(cell) => cell.lines(width),
HistoryCell::ArchivedContext { .. } => {
render_archived_context(self, width, false)
}
HistoryCell::ArchivedContext { .. } => render_archived_context(self, width, false),
}
}
@@ -318,9 +316,7 @@ impl HistoryCell {
),
HistoryCell::Tool(cell) => cell.transcript_lines(width),
HistoryCell::SubAgent(cell) => cell.lines(width),
HistoryCell::ArchivedContext { .. } => {
render_archived_context(self, width, true)
}
HistoryCell::ArchivedContext { .. } => render_archived_context(self, width, true),
}
}
@@ -359,47 +355,19 @@ fn parse_archived_context(text: &str) -> Option<HistoryCell> {
let tag_end = text.find('>')?;
let tag = &text[..tag_end];
let level = tag
.split(' ')
.find(|part| part.starts_with("level="))
.and_then(|part| part.split('"').nth(1))
let level = archived_context_attr(tag, "level")
.and_then(|v| v.parse::<u8>().ok())
.unwrap_or(0);
let range = tag
.split(' ')
.find(|part| part.starts_with("range="))
.and_then(|part| part.split('"').nth(1))
.unwrap_or("")
.to_string();
let range = archived_context_attr(tag, "range").unwrap_or_default();
let tokens = tag
.split(' ')
.find(|part| part.starts_with("tokens="))
.and_then(|part| part.split('"').nth(1))
.unwrap_or("")
.to_string();
let tokens = archived_context_attr(tag, "tokens").unwrap_or_default();
let density = tag
.split(' ')
.find(|part| part.starts_with("density="))
.and_then(|part| part.split('"').nth(1))
.unwrap_or("")
.to_string();
let density = archived_context_attr(tag, "density").unwrap_or_default();
let model = tag
.split(' ')
.find(|part| part.starts_with("model="))
.and_then(|part| part.split('"').nth(1))
.unwrap_or("")
.to_string();
let model = archived_context_attr(tag, "model").unwrap_or_default();
let timestamp = tag
.split(' ')
.find(|part| part.starts_with("timestamp="))
.and_then(|part| part.split('"').nth(1))
.unwrap_or("")
.to_string();
let timestamp = archived_context_attr(tag, "timestamp").unwrap_or_default();
let close_tag = text.rfind("</archived_context>")?;
let summary_start = tag_end + 1;
@@ -416,8 +384,20 @@ fn parse_archived_context(text: &str) -> Option<HistoryCell> {
})
}
fn archived_context_attr(tag: &str, name: &str) -> Option<String> {
let needle = format!("{name}=\"");
let start = tag.find(&needle)? + needle.len();
let rest = &tag[start..];
let end = rest.find('"')?;
Some(rest[..end].to_string())
}
/// Render an `<archived_context>` block with dimmed/italic styling.
fn render_archived_context(cell: &HistoryCell, width: u16, _low_motion: bool) -> Vec<Line<'static>> {
fn render_archived_context(
cell: &HistoryCell,
width: u16,
_low_motion: bool,
) -> Vec<Line<'static>> {
let HistoryCell::ArchivedContext {
level,
range,
@@ -441,9 +421,7 @@ fn render_archived_context(cell: &HistoryCell, width: u16, _low_motion: bool) ->
let label_style = Style::default()
.fg(palette::TEXT_DIM)
.add_modifier(Modifier::BOLD);
let body_style = Style::default()
.fg(palette::TEXT_DIM)
.italic();
let body_style = Style::default().fg(palette::TEXT_DIM).italic();
let content_width = width.saturating_sub(4).max(1);
@@ -493,10 +471,7 @@ fn render_archived_context(cell: &HistoryCell, width: u16, _low_motion: bool) ->
let rendered = crate::tui::markdown_render::render_markdown(&body, content_width, body_style);
for (idx, line) in rendered.into_iter().enumerate() {
if idx == 0 {
let mut spans = vec![Span::styled(
"",
Style::default().fg(palette::TEXT_DIM),
)];
let mut spans = vec![Span::styled("", Style::default().fg(palette::TEXT_DIM))];
spans.extend(line.spans);
lines.push(Line::from(spans));
} else {
@@ -527,46 +502,46 @@ pub fn history_cells_from_message(msg: &Message) -> Vec<HistoryCell> {
continue;
}
match msg.role.as_str() {
"user" => {
if let Some(HistoryCell::User { content }) = cells.last_mut() {
if !content.is_empty() {
content.push('\n');
"user" => {
if let Some(HistoryCell::User { content }) = cells.last_mut() {
if !content.is_empty() {
content.push('\n');
}
content.push_str(text);
} else {
cells.push(HistoryCell::User {
content: text.clone(),
});
}
content.push_str(text);
} else {
cells.push(HistoryCell::User {
content: text.clone(),
});
}
}
"assistant" => {
if let Some(HistoryCell::Assistant { content, .. }) = cells.last_mut() {
if !content.is_empty() {
content.push('\n');
"assistant" => {
if let Some(HistoryCell::Assistant { content, .. }) = cells.last_mut() {
if !content.is_empty() {
content.push('\n');
}
content.push_str(text);
} else {
cells.push(HistoryCell::Assistant {
content: text.clone(),
streaming: false,
});
}
content.push_str(text);
} else {
cells.push(HistoryCell::Assistant {
content: text.clone(),
streaming: false,
});
}
}
"system" => {
if let Some(HistoryCell::System { content }) = cells.last_mut() {
if !content.is_empty() {
content.push('\n');
"system" => {
if let Some(HistoryCell::System { content }) = cells.last_mut() {
if !content.is_empty() {
content.push('\n');
}
content.push_str(text);
} else {
cells.push(HistoryCell::System {
content: text.clone(),
});
}
content.push_str(text);
} else {
cells.push(HistoryCell::System {
content: text.clone(),
});
}
_ => {}
}
_ => {}
}
},
ContentBlock::Thinking { thinking } => {
if let Some(HistoryCell::Thinking { content, .. }) = cells.last_mut() {
if !content.is_empty() {
@@ -2396,6 +2371,7 @@ mod tests {
running_status_label_with_elapsed,
};
use crate::deepseek_theme::Theme;
use crate::models::{ContentBlock, Message};
use crate::palette;
use ratatui::style::Modifier;
use std::time::{Duration, Instant};
@@ -2433,6 +2409,40 @@ mod tests {
assert_eq!(summary, "Line one\nLine two");
}
#[test]
fn archived_context_metadata_preserves_spaces_in_attributes() {
let msg = Message {
role: "assistant".to_string(),
content: vec![ContentBlock::Text {
text: "<archived_context level=\"1\" range=\"msg 0-128\" tokens=\"2499\" density=\"~2,500 tokens\" model=\"deepseek-v4-flash\" timestamp=\"2026-04-28T00:00:00Z\">\nSummary body\n</archived_context>".to_string(),
cache_control: None,
}],
};
let cells = super::history_cells_from_message(&msg);
assert_eq!(cells.len(), 1);
let HistoryCell::ArchivedContext {
level,
range,
tokens,
density,
model,
timestamp,
summary,
} = &cells[0]
else {
panic!("expected archived context cell");
};
assert_eq!(*level, 1);
assert_eq!(range, "msg 0-128");
assert_eq!(tokens, "2499");
assert_eq!(density, "~2,500 tokens");
assert_eq!(model, "deepseek-v4-flash");
assert_eq!(timestamp, "2026-04-28T00:00:00Z");
assert_eq!(summary, "Summary body");
}
#[test]
fn render_thinking_collapsed_shows_details_affordance() {
let lines = render_thinking(
+8 -1
View File
@@ -351,7 +351,14 @@ mod tests {
.collect();
assert_eq!(
names,
vec!["DeepSeek", "NVIDIA NIM", "OpenRouter", "Novita AI", "Fireworks AI", "SGLang"]
vec![
"DeepSeek",
"NVIDIA NIM",
"OpenRouter",
"Novita AI",
"Fireworks AI",
"SGLang"
]
);
}
+2 -9
View File
@@ -5502,15 +5502,8 @@ fn handle_subagent_mailbox(app: &mut App, _seq: u64, message: &MailboxMessage) {
};
// Accumulate sub-agent token costs for the real-time footer counter (#166).
if let MailboxMessage::TokenUsage {
prompt_tokens,
completion_tokens,
..
} = message
{
if let Some(cost) =
crate::pricing::calculate_turn_cost(&app.model, *prompt_tokens, *completion_tokens)
{
if let MailboxMessage::TokenUsage { model, usage, .. } = message {
if let Some(cost) = crate::pricing::calculate_turn_cost_from_usage(model, usage) {
app.subagent_cost += cost;
}
return; // No card visual change needed; the footer handles display.
File diff suppressed because it is too large Load Diff
+1 -4
View File
@@ -700,10 +700,7 @@ mod tests {
assert_eq!(a, b, "deterministic given the same frame");
// 750 ms → 5 ticks, crest A advances every 2 ticks → ≥2 steps.
let c = super::footer_working_strip_string(40, 750);
assert_ne!(
a, c,
"advancing 4 ticks must change the strip",
);
assert_ne!(a, c, "advancing 4 ticks must change the strip",);
}
#[test]
+2 -2
View File
@@ -1,7 +1,7 @@
{
"name": "deepseek-tui",
"version": "0.7.1",
"deepseekBinaryVersion": "0.7.1",
"version": "0.7.2",
"deepseekBinaryVersion": "0.7.2",
"description": "Install and run deepseek and deepseek-tui binaries from GitHub release artifacts.",
"author": "Hmbown",
"license": "MIT",