fix: clean v0.7.2 release prep
This commit is contained in:
Binary file not shown.
|
Before Width: | Height: | Size: 84 KiB |
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
@@ -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.
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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
@@ -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
|
||||
|
||||
@@ -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
@@ -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,
|
||||
|
||||
@@ -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};
|
||||
|
||||
|
||||
@@ -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",
|
||||
),
|
||||
|
||||
@@ -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(),
|
||||
));
|
||||
}
|
||||
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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"
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -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
@@ -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]
|
||||
|
||||
@@ -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",
|
||||
|
||||
Reference in New Issue
Block a user