fix(prompt): trim first-turn context noise

This commit is contained in:
Hunter Bown
2026-05-13 13:12:14 -05:00
parent 656f8e4b15
commit f0a4e25360
15 changed files with 331 additions and 71 deletions
+10 -3
View File
@@ -34,9 +34,6 @@ mega-files that had grown around the agent loop and TUI.
by round-trip.
- **Cancel-all shell jobs.** A single action stops every running
background shell command instead of cancelling them one-by-one.
- **Session title in composer border.** The top-right of the composer
shows the derived session title so the active thread is visible
without opening the sessions panel.
- **`edit_file` tolerates typographic punctuation drift.** When the
exact-match and leading-whitespace-fuzzy passes both fail and
`fuzz: true` is set, the tool retries with smart quotes (`"`/`"`
@@ -68,6 +65,14 @@ mega-files that had grown around the agent loop and TUI.
(v0.8.6 era), `PROMPT_ANALYSIS.md`, and the redundant
`DEPENDENCY_GRAPH.md` no longer ship in releases; `docs/ARCHITECTURE.md`
remains the canonical crate-layout reference.
- **First-turn prompt context is leaner and easier to audit.** The
generated project context pack now ignores hidden tool/cache state,
balances top-level directories before descending, and `/context`
shows named prompt layers instead of a single opaque system blob.
- **Model-visible prompt policy de-conflicted.** The base and mode
prompts no longer forbid useful `deepseek` CLI diagnostics, no
longer require checklists for simple one-step work, and align
long-session compaction guidance around the 60% threshold.
### Fixed
@@ -98,6 +103,8 @@ mega-files that had grown around the agent loop and TUI.
users reach the same surface.
- **VTE flicker terminals get reduced motion** by default to avoid
thrashing on terminals that mishandle frequent partial redraws.
- **Composer border no longer shows the derived session title**, keeping
the composer chrome reserved for editor and mode state.
## [0.8.33] - 2026-05-12
+10 -3
View File
@@ -34,9 +34,6 @@ mega-files that had grown around the agent loop and TUI.
by round-trip.
- **Cancel-all shell jobs.** A single action stops every running
background shell command instead of cancelling them one-by-one.
- **Session title in composer border.** The top-right of the composer
shows the derived session title so the active thread is visible
without opening the sessions panel.
- **`edit_file` tolerates typographic punctuation drift.** When the
exact-match and leading-whitespace-fuzzy passes both fail and
`fuzz: true` is set, the tool retries with smart quotes (`"`/`"`
@@ -68,6 +65,14 @@ mega-files that had grown around the agent loop and TUI.
(v0.8.6 era), `PROMPT_ANALYSIS.md`, and the redundant
`DEPENDENCY_GRAPH.md` no longer ship in releases; `docs/ARCHITECTURE.md`
remains the canonical crate-layout reference.
- **First-turn prompt context is leaner and easier to audit.** The
generated project context pack now ignores hidden tool/cache state,
balances top-level directories before descending, and `/context`
shows named prompt layers instead of a single opaque system blob.
- **Model-visible prompt policy de-conflicted.** The base and mode
prompts no longer forbid useful `deepseek` CLI diagnostics, no
longer require checklists for simple one-step work, and align
long-session compaction guidance around the 60% threshold.
### Fixed
@@ -98,6 +103,8 @@ mega-files that had grown around the agent loop and TUI.
users reach the same surface.
- **VTE flicker terminals get reduced motion** by default to avoid
thrashing on terminals that mishandle frequent partial redraws.
- **Composer border no longer shows the derived session title**, keeping
the composer chrome reserved for editor and mode state.
## [0.8.33] - 2026-05-12
+142 -27
View File
@@ -11,7 +11,7 @@
//! The loaded content is injected into the system prompt to give the agent
//! context about the project's conventions, structure, and requirements.
use std::collections::BTreeMap;
use std::collections::{BTreeMap, VecDeque};
use std::fs;
use std::path::{Path, PathBuf};
@@ -33,9 +33,9 @@ const GLOBAL_AGENTS_RELATIVE_PATH: &[&str] = &[".deepseek", "AGENTS.md"];
/// Maximum size for project context files (to prevent loading huge files)
const MAX_CONTEXT_SIZE: usize = 100 * 1024; // 100KB
const PACK_README_MAX_CHARS: usize = 4_000;
const PACK_MAX_ENTRIES: usize = 400;
const PACK_MAX_SOURCE_FILES: usize = 80;
const PACK_MAX_CONFIG_FILES: usize = 80;
const PACK_MAX_ENTRIES: usize = 220;
const PACK_MAX_SOURCE_FILES: usize = 60;
const PACK_MAX_CONFIG_FILES: usize = 60;
const PACK_MAX_DEPTH: usize = 4;
const PACK_IGNORED_DIRS: &[&str] = &[
".git",
@@ -51,6 +51,13 @@ const PACK_IGNORED_DIRS: &[&str] = &[
".pytest_cache",
".DS_Store",
];
const PACK_ALLOWED_HIDDEN_DIRS: &[&str] = &[".github"];
const PACK_ALLOWED_HIDDEN_FILES: &[&str] = &[".editorconfig", ".gitattributes", ".gitignore"];
const PACK_IGNORED_FILE_NAMES: &[&str] = &[".DS_Store"];
const PACK_IGNORED_FILE_EXTENSIONS: &[&str] = &[
"7z", "avif", "db", "gif", "gz", "ico", "jpeg", "jpg", "log", "mov", "mp3", "mp4", "pdf",
"png", "sqlite", "tar", "tgz", "wav", "webp", "zip",
];
// === Errors ===
@@ -197,38 +204,70 @@ fn collect_pack_entries(root: &Path, dir: &Path, depth: usize, out: &mut Vec<Str
return;
}
let Ok(read_dir) = fs::read_dir(dir) else {
return;
};
let mut children = read_dir.filter_map(Result::ok).collect::<Vec<_>>();
children.sort_by_key(|entry| entry.path());
let mut queue = VecDeque::new();
queue.push_back((dir.to_path_buf(), depth));
for entry in children {
if out.len() >= PACK_MAX_ENTRIES {
break;
}
let path = entry.path();
let Some(name) = path.file_name().and_then(|name| name.to_str()) else {
continue;
};
let Ok(file_type) = entry.file_type() else {
continue;
};
if file_type.is_dir() && PACK_IGNORED_DIRS.contains(&name) {
while let Some((current_dir, current_depth)) = queue.pop_front() {
if current_depth > PACK_MAX_DEPTH || out.len() >= PACK_MAX_ENTRIES {
continue;
}
if let Some(relative) = relative_slash_path(root, &path) {
if file_type.is_dir() {
out.push(format!("{relative}/"));
collect_pack_entries(root, &path, depth + 1, out);
} else if file_type.is_file() {
out.push(relative);
let Ok(read_dir) = fs::read_dir(&current_dir) else {
continue;
};
let mut children = read_dir.filter_map(Result::ok).collect::<Vec<_>>();
children.sort_by_key(|entry| entry.path());
for entry in children {
if out.len() >= PACK_MAX_ENTRIES {
break;
}
let path = entry.path();
let Some(name) = path.file_name().and_then(|name| name.to_str()) else {
continue;
};
let Ok(file_type) = entry.file_type() else {
continue;
};
if file_type.is_dir() && should_ignore_pack_dir(name) {
continue;
}
if file_type.is_file() && should_ignore_pack_file(name) {
continue;
}
if let Some(relative) = relative_slash_path(root, &path) {
if file_type.is_dir() {
out.push(format!("{relative}/"));
if current_depth < PACK_MAX_DEPTH {
queue.push_back((path, current_depth + 1));
}
} else if file_type.is_file() {
out.push(relative);
}
}
}
}
}
fn should_ignore_pack_dir(name: &str) -> bool {
PACK_IGNORED_DIRS.contains(&name)
|| (name.starts_with('.') && !PACK_ALLOWED_HIDDEN_DIRS.contains(&name))
}
fn should_ignore_pack_file(name: &str) -> bool {
if name.starts_with('.') && !PACK_ALLOWED_HIDDEN_FILES.contains(&name) {
return true;
}
if PACK_IGNORED_FILE_NAMES.contains(&name) {
return true;
}
let Some((_, ext)) = name.rsplit_once('.') else {
return false;
};
PACK_IGNORED_FILE_EXTENSIONS.contains(&ext.to_ascii_lowercase().as_str())
}
fn relative_slash_path(root: &Path, path: &Path) -> Option<String> {
let relative = path.strip_prefix(root).ok()?;
let mut parts = Vec::new();
@@ -865,6 +904,82 @@ mod tests {
);
}
#[test]
fn project_context_pack_ignores_agent_state_and_binary_noise() {
let tmp = tempdir().expect("tempdir");
fs::create_dir_all(tmp.path().join("src")).expect("mkdir src");
fs::write(tmp.path().join("src").join("main.rs"), "fn main() {}").expect("write src");
fs::write(tmp.path().join(".DS_Store"), "noise").expect("write ds store");
fs::write(tmp.path().join("paper.pdf"), "not a real pdf").expect("write pdf");
fs::create_dir_all(tmp.path().join(".deepseek").join("state")).expect("mkdir state");
fs::write(
tmp.path()
.join(".deepseek")
.join("state")
.join("subagents.v1.json"),
"{}",
)
.expect("write state");
fs::create_dir_all(tmp.path().join(".playwright-mcp")).expect("mkdir playwright");
fs::write(
tmp.path().join(".playwright-mcp").join("trace.log"),
"noise",
)
.expect("write log");
fs::create_dir_all(tmp.path().join(".agents").join("skills").join("demo"))
.expect("mkdir skills");
fs::write(
tmp.path()
.join(".agents")
.join("skills")
.join("demo")
.join("SKILL.md"),
"skill body",
)
.expect("write skill");
fs::create_dir_all(tmp.path().join(".github").join("workflows")).expect("mkdir workflows");
fs::write(
tmp.path().join(".github").join("workflows").join("ci.yml"),
"name: ci",
)
.expect("write workflow");
let pack = generate_project_context_pack(tmp.path()).expect("pack");
assert!(pack.contains("\"src/main.rs\""), "{pack}");
assert!(pack.contains("\".github/\""), "{pack}");
assert!(pack.contains("\".github/workflows/ci.yml\""), "{pack}");
assert!(!pack.contains(".deepseek"), "{pack}");
assert!(!pack.contains(".playwright-mcp"), "{pack}");
assert!(!pack.contains(".agents"), "{pack}");
assert!(!pack.contains(".DS_Store"), "{pack}");
assert!(!pack.contains("paper.pdf"), "{pack}");
assert!(!pack.contains("trace.log"), "{pack}");
}
#[test]
fn project_context_pack_keeps_later_top_level_dirs_under_budget() {
let tmp = tempdir().expect("tempdir");
let noisy = tmp.path().join("aaa-many-files");
fs::create_dir_all(&noisy).expect("mkdir noisy");
for i in 0..(PACK_MAX_ENTRIES + 20) {
fs::write(noisy.join(format!("file-{i:03}.rs")), "fn f() {}").expect("write noisy");
}
fs::create_dir_all(tmp.path().join("zzz-important")).expect("mkdir important");
fs::write(
tmp.path().join("zzz-important").join("main.rs"),
"fn important() {}",
)
.expect("write important");
let pack = generate_project_context_pack(tmp.path()).expect("pack");
assert!(
pack.contains("\"zzz-important/\""),
"breadth-first packing should keep later top-level directories visible:\n{pack}"
);
}
#[test]
fn test_load_global_agents_when_project_has_no_context() {
let workspace = tempdir().expect("workspace tempdir");
+1 -1
View File
@@ -664,7 +664,7 @@ pub fn system_prompt_for_mode_with_context_skills_session_and_approval(
1. Use `/compact` to summarize earlier context and free up space\n\
2. The system will preserve important information (files you're working on, recent messages, tool results)\n\
3. After compaction, you'll see a summary of what was discussed and can continue seamlessly\n\n\
If you notice context is getting long (>80%), proactively suggest using `/compact` to the user.\n\n\
If you notice context is getting long (>60% during sustained work), proactively suggest using `/compact` to the user.\n\n\
### Prompt-cache awareness\n\n\
DeepSeek caches the longest *byte-stable prefix* of every request and charges roughly 100× less for cache-hit tokens than miss tokens. The system prompt above is layered most-static-first specifically so the prefix stays stable turn-over-turn. To keep cache hits high:\n\
- **Working set location:** the current repo working set is stored on new user messages inside a `<turn_meta>` block. Treat it as high-priority turn metadata, not as a stable system-prompt section.\n\
+4 -3
View File
@@ -3,9 +3,10 @@
Read-only tools (reads, searches, persistent RLM session tools, agent status queries, git inspection) run silently.
Any write, patch, shell execution, sub-agent session open, or CSV batch operation will ask for approval first.
Before requesting approval for writes, lay out your work with `checklist_write` so the user can see what
you intend to do and approve with context. Complex changes should also get an `update_plan` first.
Decomposition builds trust — a clear plan gets faster approvals.
Before requesting approval for multi-step writes, lay out your work with `checklist_write` so the user
can see what you intend to do and approve with context. Complex changes should also get an
`update_plan` first. For simple writes, state the direct edit and proceed through the normal approval
flow.
## Sub-agent completion sentinel
+1 -1
View File
@@ -4,6 +4,6 @@ All tool calls are pre-approved. You will not see approval prompts — your acti
This means you carry more responsibility:
- Pause before destructive operations (deletes, force-pushes, `rm -rf`).
- Use `checklist_write` to make your work visible even though no one is watching.
- Use `checklist_write` for multi-step work so progress stays visible even though no one is watching.
- If you're uncertain about a course of action, state your reasoning before proceeding.
- The user can interrupt you at any time.
+1 -1
View File
@@ -3,7 +3,7 @@
All write operations are blocked. You can read, search, and investigate, but you cannot modify the workspace.
This is a read-only mode. Use it to:
- Build thorough plans with `update_plan` and `checklist_write`.
- Build thorough plans with `checklist_write` and, for complex initiatives, `update_plan`.
- Investigate codebases, trace logic, and gather context.
- Spawn read-only sub-agents for parallel exploration.
+1 -1
View File
@@ -3,7 +3,7 @@
Read-only operations run silently. Write operations (file edits, patches, shell execution, sub-agent spawns, CSV batches) require user approval before executing.
When you need approval:
1. First, lay out your approach with `checklist_write` — visible plans build trust.
1. For multi-step changes, lay out your approach with `checklist_write`.
2. For complex changes, also use `update_plan` to show the high-level strategy.
3. The user will see your proposed action and can approve or deny it.
+4 -4
View File
@@ -1,4 +1,4 @@
You are DeepSeek TUI. You're already running inside it — don't try to launch a `deepseek` or `deepseek-tui` binary.
You are DeepSeek TUI. You're already running inside it. Do not launch a nested interactive `deepseek` or `deepseek-tui` session unless the user explicitly asks. Using `deepseek` CLI subcommands such as `deepseek --version`, `deepseek -p`, `deepseek doctor`, or `deepseek auth status` is allowed when it directly helps the task.
## Language
@@ -34,7 +34,7 @@ The user can see their own message. Use the first line to show forward motion.
## Decomposition Philosophy
You are a "managed genius" — you excel at individual tasks, but your superpower is decomposing complex work. **Always decompose before you act.** A few minutes spent planning saves many minutes of thrashing.
Decompose work when the task is complex enough to benefit from it. For simple lookups, focused one-file fixes, or direct commands, act directly and keep the response short. For larger work, a few minutes spent planning saves many minutes of thrashing.
Use three decomposition patterns, selected by task scope:
@@ -44,7 +44,7 @@ Use three decomposition patterns, selected by task scope:
**RECURSIVE** — When sub-tasks reveal sub-problems: decompose recursively until each leaf is tractable. Keep the active leaves in `checklist_write`; use `update_plan` only when a genuinely complex initiative needs durable high-level strategy metadata. Propagate findings upward when sub-problems resolve.
Your default workflow for any non-trivial request:
Your default workflow for tasks estimated at 5+ concrete steps:
1. **`checklist_write`** — break the work into concrete, verifiable steps. Mark the first one `in_progress`. This populates the sidebar so the user can see what you're doing.
2. **Execute** — work through each checklist item, updating status as you go.
3. **For complex initiatives only**, add `update_plan` as high-level strategy. Do not mirror the checklist into a second tracker.
@@ -130,7 +130,7 @@ The RLM paper's core design is symbolic state: the long input and intermediate v
For exact counts or structured aggregates, compute them directly in Python inside the REPL (`len`, regexes, parsers, counters) and use child LLM calls only for semantic interpretation. When you chunk a whole input, use `chunk()` and report coverage explicitly: chunks processed, total chunks, line/char ranges, and any skipped sections. Cross-check surprising aggregate results with deterministic code before presenting them. Use `finalize(...)` for the answer you want returned; if it comes back as a `var_handle`, call `handle_read` for a bounded slice, count, or JSON projection instead of asking the runtime to replay the whole value.
## Context
You have a 1M-token context window. When usage creeps above ~80%, suggest `/compact` to the user — it summarises earlier turns so you can keep working without losing thread.
You have a 1M-token context window. During long coding sessions, suggest `/compact` when usage approaches ~60% or when the app marks context pressure as high. It summarizes earlier turns so you can keep working without losing thread.
Model notes: DeepSeek V4 models emit *thinking tokens* (`ContentBlock::Thinking`) before final answers. These are invisible to the user but count against context. Cost/token estimates are approximate; treat them as a rough guide.
+4 -4
View File
@@ -1,10 +1,10 @@
You are DeepSeek TUI. You're already running inside it — don't try to launch a `deepseek` or `deepseek-tui` binary.
You are DeepSeek TUI. You're already running inside it. Do not launch a nested interactive `deepseek` or `deepseek-tui` session unless the user explicitly asks. Using `deepseek` CLI subcommands such as `deepseek --version`, `deepseek -p`, `deepseek doctor`, or `deepseek auth status` is allowed when it directly helps the task.
## Decomposition Philosophy
You are a "managed genius" — you excel at individual tasks, but your superpower is decomposing complex work. **Always decompose before you act.** A few minutes spent planning saves many minutes of thrashing.
Decompose work when the task is complex enough to benefit from it. For simple lookups, focused one-file fixes, or direct commands, act directly and keep the response short. For larger work, a few minutes spent planning saves many minutes of thrashing.
Your default workflow for any non-trivial request:
Your default workflow for tasks estimated at 5+ concrete steps:
1. **`checklist_write`** — break the work into concrete, verifiable steps. Mark the first one `in_progress`. This populates the sidebar so the user can see what you're doing.
2. **Execute** — work through each checklist item, updating status as you go.
3. **For complex initiatives only**, add `update_plan` as high-level strategy. Do not mirror the checklist into a second tracker.
@@ -31,7 +31,7 @@ RLM works by keeping the long input and intermediate values as symbolic REPL sta
The Python helpers visible inside the REPL (`sub_query`, `sub_query_batch`, `sub_query_map`, `sub_rlm`, `finalize`, and related context helpers) are NOT separately-callable tools — they are functions the sub-agent uses inside its Python code.
## Context
You have a 1M-token context window. When usage creeps above ~80%, suggest `/compact` to the user — it summarises earlier turns so you can keep working without losing thread.
You have a 1M-token context window. During long coding sessions, suggest `/compact` when usage approaches ~60% or when the app marks context pressure as high. It summarizes earlier turns so you can keep working without losing thread.
Model notes: DeepSeek V4 models emit *thinking tokens* (`ContentBlock::Thinking`) before final answers. These are invisible to the user but count against context. Cost/token estimates are approximate; treat them as a rough guide.
+5 -5
View File
@@ -5,10 +5,10 @@ You are running in Agent mode — autonomous task execution with tool access.
Read-only tools (reads, searches, persistent RLM session tools, agent status queries, git inspection) run silently.
Any write, patch, shell execution, sub-agent session open, or CSV batch operation will ask for approval first.
Before requesting approval for writes, lay out your work with `checklist_write` so the user can see what
you intend to do and approve with context. Use `update_plan` only when a complex initiative needs
high-level strategy metadata that is not just a copy of the checklist.
Decomposition builds trust — a clear Work checklist gets faster approvals.
Before requesting approval for multi-step writes, lay out your work with `checklist_write` so the user
can see what you intend to do and approve with context. Use `update_plan` only when a complex
initiative needs high-level strategy metadata that is not just a copy of the checklist.
For simple writes, state the direct edit and proceed through the normal approval flow.
For multi-step initiatives, keep `checklist_write` current. Add `update_plan` only for genuinely useful strategy.
@@ -26,6 +26,6 @@ Don't sequence approvals one at a time — the user wants context, not interrupt
Long sessions accumulate context. To stay fast:
- Open sub-agent sessions for independent work instead of doing everything sequentially
- Batch reads/searches/git-inspections into parallel tool calls
- Suggest `/compact` when context nears 80% — the compaction relay preserves open blockers
- Suggest `/compact` when context nears 60% during sustained work — the compaction relay preserves open blockers
- Use `note` for decisions you'll need across compaction boundaries
- A 3-turn session that fans out to sub-agents finishes faster AND stays responsive longer than a 15-turn sequential grind
+2 -2
View File
@@ -2,8 +2,8 @@
You are running in Plan mode — design before implementing.
Investigate first, act later. Use `checklist_write` for visible, granular progress. Add `update_plan`
only when high-level strategy adds value beyond the checklist.
Investigate first, act later. Use `checklist_write` for visible, granular progress on multi-step
investigations. Add `update_plan` only when high-level strategy adds value beyond the checklist.
All writes and patches are blocked — you can read the world but you
can't change it. Shell and code execution are unavailable.
+2 -2
View File
@@ -5,7 +5,7 @@ You are running in YOLO mode — full autonomy, all actions pre-approved.
All actions auto-approved. Move fast, but think before you write. If you're about to delete files,
overwrite user work, or run destructive commands, pause and double-check. The undo button is the user's Git history.
Even with auto-approval, create a `checklist_write` first so your work is visible and trackable in the
sidebar. Decomposition is not red tape — it's how you organize complex work and demonstrate thoroughness.
Even with auto-approval, use `checklist_write` for work that has several concrete steps so progress is
visible and trackable in the sidebar. Keep simple commands and focused edits direct.
For multi-step initiatives, keep `checklist_write` current. Add `update_plan` only when a high-level strategy
would help and do not duplicate the checklist there.
+5 -7
View File
@@ -22,7 +22,7 @@ use std::collections::{HashMap, HashSet};
use crate::logging;
const MAX_SKILL_DESCRIPTION_CHARS: usize = 512;
const MAX_SKILL_DESCRIPTION_CHARS: usize = 280;
const MAX_AVAILABLE_SKILLS_CHARS: usize = 12_000;
// === Defaults ===
@@ -551,12 +551,10 @@ instructions when using a specific skill.\n\n",
out.push_str(
"\n### How to use skills\n\
- Discovery: The list above is the skills available in this session. Skill bodies live on disk at the listed paths.\n\
- Trigger rules: If the user names a skill (with `$SkillName`, `/skill <name>`, or plain text) OR the task clearly matches a skill description above, use that skill for that turn. Multiple mentions mean use them all. Do not carry skills across turns unless re-mentioned.\n\
- Missing/blocked: If a named skill is missing or its `SKILL.md` cannot be read, say so briefly and continue with the best fallback.\n\
- Progressive disclosure: After deciding to use a skill, read only that skill's `SKILL.md`. When it references relative paths such as `scripts/foo.py`, resolve them relative to the skill directory.\n\
- Context hygiene: Load only the specific referenced files needed for the task. Avoid bulk-loading unrelated skill resources.\n\
- Safety: Do not execute scripts from a community skill unless the user explicitly asks or the skill has been trusted for script use.\n",
- Skill bodies live on disk at the listed paths. When a skill is relevant, open only that skill's `SKILL.md` and the specific companion files it references.\n\
- Trigger rules: use a skill when the user names it (`$SkillName`, `/skill <name>`, or plain text) or the task clearly matches its description. Do not carry skills across turns unless re-mentioned.\n\
- Missing/blocked: if a named skill is missing or cannot be read, say so briefly and continue with the best fallback.\n\
- Safety: do not execute scripts from a community skill unless the user explicitly asks or the skill has been trusted for script use.\n",
);
Some(out)
+139 -7
View File
@@ -22,6 +22,70 @@ const CONTEXT_CRITICAL_THRESHOLD_PERCENT: f64 = 95.0;
const MAX_REFERENCE_ROWS: usize = 12;
const MAX_TOOL_ROWS: usize = 8;
const SYSTEM_LAYER_MARKERS: &[(&str, &str, PromptLayerKind)] = &[
(
"Project context",
"<project_instructions",
PromptLayerKind::Static,
),
(
"Project context pack",
"## Project Context Pack",
PromptLayerKind::Static,
),
("Environment", "## Environment", PromptLayerKind::Static),
("Skills", "## Skills", PromptLayerKind::Static),
(
"Context management",
"## Context Management",
PromptLayerKind::Static,
),
("Compact template", "## Compact", PromptLayerKind::Static),
(
"Configured instructions",
"<instructions ",
PromptLayerKind::Dynamic,
),
("User memory", "## User Memory", PromptLayerKind::Dynamic),
(
"Current session goal",
"## Current Session Goal",
PromptLayerKind::Dynamic,
),
(
"Previous session relay",
"## Previous Session Relay",
PromptLayerKind::Dynamic,
),
(
"Volatile working set",
WORKING_SET_MARKER,
PromptLayerKind::Dynamic,
),
];
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
enum PromptLayerKind {
Static,
Dynamic,
}
impl PromptLayerKind {
fn label(self) -> &'static str {
match self {
Self::Static => "cache-friendly",
Self::Dynamic => "changes by session/turn",
}
}
}
#[derive(Debug)]
struct PromptTextLayer<'a> {
name: &'static str,
kind: PromptLayerKind,
body: &'a str,
}
#[must_use]
pub fn build_context_inspector_text(app: &App) -> String {
let mut out = String::new();
@@ -146,13 +210,27 @@ fn push_system_prompt_structure(out: &mut String, app: &App) {
);
}
Some(SystemPrompt::Text(text)) => {
// Single text blob — stable/volatile not distinguishable
let has_working = text.contains(WORKING_SET_MARKER);
if has_working {
let layers = split_text_prompt_layers(text);
if layers.len() > 1
|| layers
.first()
.is_some_and(|layer| layer.name != "System prompt")
{
let _ = writeln!(
out,
" Single text blob (~{total_est} tokens) [contains working-set marker — structure unclear]"
" Text prompt layers: {} layer(s), ~{total_est} tokens",
layers.len()
);
for layer in layers {
let tokens = text_tokens(layer.body);
let _ = writeln!(
out,
" - {}: ~{} tokens [{}]",
layer.name,
tokens,
layer.kind.label()
);
}
} else {
let _ = writeln!(
out,
@@ -173,6 +251,42 @@ fn push_system_prompt_structure(out: &mut String, app: &App) {
);
}
fn split_text_prompt_layers(text: &str) -> Vec<PromptTextLayer<'_>> {
let mut starts = SYSTEM_LAYER_MARKERS
.iter()
.filter_map(|(name, marker, kind)| text.find(marker).map(|idx| (idx, *name, *kind)))
.collect::<Vec<_>>();
starts.sort_by_key(|(idx, _, _)| *idx);
let Some((first_idx, _, _)) = starts.first().copied() else {
return vec![PromptTextLayer {
name: "System prompt",
kind: PromptLayerKind::Static,
body: text.trim(),
}];
};
let mut layers = Vec::new();
if first_idx > 0 {
layers.push(PromptTextLayer {
name: "Global system prefix",
kind: PromptLayerKind::Static,
body: text[..first_idx].trim(),
});
}
for (i, (start, name, kind)) in starts.iter().enumerate() {
let end = starts.get(i + 1).map_or(text.len(), |(idx, _, _)| *idx);
layers.push(PromptTextLayer {
name,
kind: *kind,
body: text[*start..end].trim(),
});
}
layers
}
fn push_references(out: &mut String, references: &[SessionContextReference]) {
let _ = writeln!(out, "References");
let _ = writeln!(out, "----------");
@@ -453,15 +567,33 @@ mod tests {
}
#[test]
fn inspector_text_prompt_shows_single_blob() {
fn inspector_text_prompt_shows_layer_map() {
let mut app = test_app();
app.system_prompt = Some(SystemPrompt::Text(
"You are DeepSeek TUI.\n## Repo Working Set\nsrc/".to_string(),
"You are DeepSeek TUI.\n\n<project_instructions source=\"AGENTS.md\">\nRules\n</project_instructions>\n\n## Project Context Pack\n{}\n\n## Environment\n- lang: en\n\n## Skills\n- rust\n\n## Context Management\nKeep compact\n\n## Compact\nTemplate\n\n## Repo Working Set\nsrc/".to_string(),
));
let text = build_context_inspector_text(&app);
assert!(text.contains("System Prompt Structure"));
assert!(text.contains("Text prompt layers"));
assert!(text.contains("Global system prefix"));
assert!(text.contains("Project context"));
assert!(text.contains("Project context pack"));
assert!(text.contains("Environment"));
assert!(text.contains("Skills"));
assert!(text.contains("Context management"));
assert!(text.contains("Compact template"));
assert!(text.contains("Volatile working set"));
assert!(text.contains("changes by session/turn"));
}
#[test]
fn inspector_text_prompt_without_markers_shows_single_blob() {
let mut app = test_app();
app.system_prompt = Some(SystemPrompt::Text("You are DeepSeek TUI.".to_string()));
let text = build_context_inspector_text(&app);
assert!(text.contains("Single text blob"));
assert!(text.contains("working-set marker"));
assert!(text.contains("stable prefix only"));
}
}