Vision Preprocessor: Auto-Config from /codingplan Implementation Plan
For agentic workers: REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development to implement this plan task-by-task. Steps use checkbox (
- [ ]) syntax for tracking.
Goal: When /codingplan populates the AtomGit-* provider list, automatically set vision_preprocessor_provider to the first vision-capable model in the list. Recognizes both vision-language models (e.g. Qwen3-VL-32B-Instruct) and OCR models (e.g. PaddleOCR-2.0, GOT-OCR-2.0). Preserves user-supplied non-AtomGit values; clears stale AtomGit-* references when the list contains no VL candidate.
Architecture: Three small additions, all confined to atomcode-core: extend the existing model_name_suggests_vision heuristic, add VL-detection-and-precedence logic to coding_plan::setup::step_models_and_register, surface the outcome in ModelsInfo + SetupReport::render. No new modules, no agent / TUI changes.
Tech Stack: Rust. Reuses is_codingplan_provider_name, model_name_suggests_vision, provider_names_for, all already present in the file under modification.
Reference
Spec at docs/superpowers/specs/2026-05-08-vision-preprocessor-design.md (the original feature). This plan addresses the §风险与权衡 item 4 follow-up plus the user's request to detect OCR-named models.
Original feature commits 1379510..4ce8bc0 are already merged. This plan adds three more commits on top.
Precedence Rule (Encoded in Task 8)
Current config.vision_preprocessor_provider |
List has VL/OCR | Action |
|---|---|---|
None |
yes | set to first VL/OCR provider key |
None |
no | leave None |
Some("AtomGit-*") (was set by previous /codingplan) |
yes | replace with new VL/OCR key |
Some("AtomGit-*") |
no | clear to None (avoid pointing at a wiped key) |
Some("X") where X is NOT AtomGit-* (user manual setting) |
yes or no | leave unchanged |
The is_codingplan_provider_name helper (already in setup.rs) is the precise discriminator.
File Structure
| File | Action | Responsibility |
|---|---|---|
crates/atomcode-core/src/provider/mod.rs |
Modify | Extend model_name_suggests_vision to match ocr substring + tests |
crates/atomcode-core/src/coding_plan/setup.rs |
Modify | Auto-set logic in step_models_and_register; new field on ModelsInfo; render line in SetupReport::render + tests |
Task 7: Extend model_name_suggests_vision to recognize OCR
Files:
-
Modify:
crates/atomcode-core/src/provider/mod.rs:298-336(themodel_name_suggests_visionfunction and its tests) -
Step 1: Update the heuristic body
In crates/atomcode-core/src/provider/mod.rs, locate pub fn model_name_suggests_vision(name: &str) -> bool (around line 312) and add an ocr clause. The function currently has a chain of ||. Add this clause anywhere in the chain (before the closing }):
|| n.contains("ocr")
A reasonable position: after n.contains("vl-"), before n.contains("-4v"), so the OCR-family substring sits next to the VL substrings semantically. Final shape:
pub fn model_name_suggests_vision(name: &str) -> bool {
let n = name.to_lowercase();
n.contains("vision")
|| n.contains("-vl")
|| n.contains("vl-")
|| n.contains("ocr")
|| n.contains("-4v")
|| n.contains("-4.1v")
|| n.starts_with("gpt-4o")
// ... rest unchanged
}
- Step 2: Update the doc comment to explain the OCR addition
The function's doc comment (lines 298-311) currently explains the rationale for the heuristic and the false-positive vs. false-negative trade-off. Add a sentence about OCR:
Replace the existing doc block ending at false-positives waste a turn on a 400, so when in doubt this returns false. with:
/// Heuristic: does this model name look like a vision-capable model?
///
/// Used by the TUI's Ctrl+V image-paste handler to refuse attaching an
/// image when the active model almost certainly can't accept it (e.g.
/// `glm-5.1`, `deepseek-v4-flash`, `qwen3-coder`). Without this gate
/// the user wastes a turn on a 400 from the upstream — see the
/// `ModelArts.81001` `message[3].content[0] has invalid field(s):
/// text, type` failure pattern that surfaced in production.
///
/// Also used by `vision_preprocessor::maybe_preprocess` to decide
/// whether the active main provider needs preprocessing (vision-capable
/// → skip) and by `coding_plan::setup` to auto-pick a VL preprocessor
/// from the AtomGit model list.
///
/// "OCR" is included because OCR-on-VLM endpoints (PaddleOCR-VL,
/// GOT-OCR, MonkeyOCR, etc.) accept image input via the same
/// OpenAI-compatible `image_url` schema and are first-class candidates
/// for the vision-preprocessor role.
///
/// Conservative — only matches well-known vision/OCR patterns.
/// False-negatives are safe: extend this list when a new vision/OCR model
/// ships rather than threading a per-provider config knob (no
/// user-discoverable opt-in exists). False-positives waste a turn on
/// a 400, so when in doubt this returns false.
pub fn model_name_suggests_vision(name: &str) -> bool {
- Step 3: Add OCR tests
In the existing mod tests block of provider/mod.rs (the vision_heuristic_* tests around lines 462-499), append:
/// OCR family: PaddleOCR-VL is already covered by the `-vl` clause,
/// but pure-OCR names (no VL/vision substring) need the dedicated
/// `ocr` clause to be recognized as vision-eligible.
#[test]
fn vision_heuristic_recognises_ocr_models() {
// Names with both ocr + vl/vision (already worked, regression check).
assert!(model_name_suggests_vision("PaddleOCR-VL-0.9B"));
assert!(model_name_suggests_vision("Qwen2-VL-OCR-7B"));
// Pure OCR names — should now match via the dedicated clause.
assert!(model_name_suggests_vision("GOT-OCR-2.0"));
assert!(model_name_suggests_vision("PaddleOCR-2.0"));
assert!(model_name_suggests_vision("MinerU-OCR"));
assert!(model_name_suggests_vision("MonkeyOCR-1.2B"));
assert!(model_name_suggests_vision("got-ocr-1.0")); // lowercase
}
/// Non-OCR model names containing the substring `ocr` as a
/// coincidence (rare; document the false-positive risk). Today none
/// of these ship as actual atomgit models — if one does, we'll
/// tighten the heuristic. Test left as a placeholder so future
/// regressions get caught.
#[test]
fn vision_heuristic_documented_false_positives() {
// These COULD theoretically false-positive on the `ocr` clause
// if such names ever ship as text-only models. Today none do.
// Listed here so a maintainer adding such a model sees the
// expected failure and reconsiders the heuristic.
assert!(model_name_suggests_vision("focar-text-7b")); // contrived
}
The second test is informational — it documents the trade-off. If a real model name contains ocr but isn't visual, the test will need adjustment.
- Step 4: Run tests
cd /Users/theo/Documents/workspace/atomcode/
cargo test -p atomcode-core --lib provider::tests::vision_heuristic
Expected: all vision_heuristic_* tests pass (existing 2 + new 2).
- Step 5: Commit
cat > /tmp/atomcode-task7-msg.txt <<'EOF'
feat(provider): include OCR substring in vision-capable heuristic
OCR-on-VLM endpoints (PaddleOCR, GOT-OCR, MonkeyOCR, MinerU-OCR, etc.)
accept image inputs via the same OpenAI-compatible image_url schema and
are excellent vision_preprocessor candidates — often more accurate and
cheaper for code/error screenshots than general-purpose VL. Extend the
heuristic so they're auto-recognized by Ctrl+V paste gating, the
preprocessor short-circuit, and /codingplan auto-detection (next commit).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
EOF
cd /Users/theo/Documents/workspace/atomcode/
git add crates/atomcode-core/src/provider/mod.rs
git commit -F /tmp/atomcode-task7-msg.txt -- crates/atomcode-core/src/provider/mod.rs
Task 8: Auto-set vision_preprocessor_provider in /codingplan
Files:
-
Modify:
crates/atomcode-core/src/coding_plan/setup.rs— functionstep_models_and_register(around line 422-469) andModelsInfostruct (around line 257-265) -
Step 1: Add a new variant enum to communicate the outcome
Near the top of setup.rs (after the existing StepResult definition or near ModelsInfo), add:
/// Describes how the auto-detected vision_preprocessor_provider was
/// (or was not) updated by `step_models_and_register`. Surfaces in
/// `SetupReport::render` so the user can see what happened to that
/// config knob across the /codingplan flow.
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum VisionPreprocessorOutcome {
/// Field was None and remains None (no VL/OCR in list).
UnchangedNone,
/// Field was a non-AtomGit user-supplied value; preserved.
/// Carries the value for display.
UserSupplied(String),
/// Field was None or a stale AtomGit-* key; auto-pointed at a
/// vision-capable provider in the freshly-installed list.
/// Carries the new key.
AutoSet(String),
/// Field was an AtomGit-* key but the new list has no VL/OCR
/// candidate, so the field was cleared to None to avoid pointing
/// at a wiped provider key.
Cleared,
}
- Step 2: Add the field to
ModelsInfo
The existing struct (around line 257-265):
#[derive(Debug, Clone)]
pub struct ModelsInfo {
pub display_names: Vec<String>,
pub provider_names: Vec<String>,
pub default_provider: String,
}
Add a fourth field:
#[derive(Debug, Clone)]
pub struct ModelsInfo {
pub display_names: Vec<String>,
pub provider_names: Vec<String>,
pub default_provider: String,
/// Outcome of vision_preprocessor_provider auto-config. Drives the
/// "Vision preprocessor → ..." line in the rendered report.
pub vision_preprocessor: VisionPreprocessorOutcome,
}
- Step 3: Implement the auto-set logic in
step_models_and_register
The existing function (around line 422-469) currently ends:
config.default_provider = default_provider.clone();
StepResult::Ok(ModelsInfo {
display_names: names,
provider_names,
default_provider,
})
}
Insert the auto-set logic after config.default_provider = ... and before the StepResult::Ok(...):
config.default_provider = default_provider.clone();
// Auto-detect a vision_preprocessor candidate from the freshly
// installed list. Precedence:
// - User-supplied non-AtomGit value: leave alone.
// - None / AtomGit-* (i.e. previous /codingplan run): replace
// with first VL/OCR model's provider key from the new list,
// or clear to None when the new list has no VL candidate.
let vl_idx = names.iter().position(|n| {
crate::provider::model_name_suggests_vision(n)
});
let new_vl_key = vl_idx.map(|i| provider_names[i].clone());
let vision_preprocessor = {
let current = config.vision_preprocessor_provider.clone();
let user_supplied_non_atomgit = current
.as_deref()
.map(|k| !k.is_empty() && !is_codingplan_provider_name(k))
.unwrap_or(false);
if user_supplied_non_atomgit {
VisionPreprocessorOutcome::UserSupplied(current.unwrap())
} else {
match new_vl_key {
Some(k) => {
config.vision_preprocessor_provider = Some(k.clone());
VisionPreprocessorOutcome::AutoSet(k)
}
None => {
if current.is_some() {
// Was AtomGit-* (per the precedence above) and
// the new list has no VL — clearing prevents a
// dangling reference.
config.vision_preprocessor_provider = None;
VisionPreprocessorOutcome::Cleared
} else {
VisionPreprocessorOutcome::UnchangedNone
}
}
}
}
};
StepResult::Ok(ModelsInfo {
display_names: names,
provider_names,
default_provider,
vision_preprocessor,
})
}
- Step 4: Update render() to print the outcome
In SetupReport::render (around line 132-162, the match &self.models { StepResult::Ok(info) => { ... } } arm), after the existing bullet loop that prints provider names, add:
for (pname, model) in info.provider_names.iter().zip(info.display_names.iter()) {
let suffix = if pname == &info.default_provider {
" (default)"
} else {
""
};
out.push_str(&format!(" • {} → {}{}\n", pname, model, suffix));
}
// Add: vision-preprocessor line.
match &info.vision_preprocessor {
VisionPreprocessorOutcome::AutoSet(k) => {
out.push_str(&format!(
" ✔ Vision preprocessor → {} (auto-detected)\n",
k,
));
}
VisionPreprocessorOutcome::UserSupplied(k) => {
out.push_str(&format!(
" ✔ Vision preprocessor → {} (user setting kept)\n",
k,
));
}
VisionPreprocessorOutcome::Cleared => {
out.push_str(
" ⚠ Vision preprocessor cleared — no VL/OCR model in current list\n",
);
}
VisionPreprocessorOutcome::UnchangedNone => {
// No-op: nothing to say when both the previous and
// new state are "no preprocessor configured".
}
}
- Step 5: Update existing render tests to construct the new field
The render tests in setup.rs (around render_happy_path_has_all_checkmarks, render_claim_duplicate_renders_as_success, render_status_pending_activation_omits_zero_expiry, render_login_failed_blocks_persist_and_suppresses_cascade, render_multi_model_lists_all_providers_with_default_mark, render_claim_failed_suppresses_cascade_rows, render_skipped_with_non_cascade_reason_still_shows, render_status_error_truncates_long_message) construct ModelsInfo literals. Each of those literals needs the new field.
Run:
cd /Users/theo/Documents/workspace/atomcode/
grep -n "ModelsInfo {" crates/atomcode-core/src/coding_plan/setup.rs
For each ModelsInfo { literal that's StepResult::Ok(ModelsInfo { ... }) in a test fixture, add vision_preprocessor: VisionPreprocessorOutcome::UnchangedNone, (the no-op variant — keeps test output unchanged). Example:
models: StepResult::Ok(ModelsInfo {
display_names: vec!["a/b".into()],
provider_names: vec!["AtomGit".into()],
default_provider: "AtomGit".into(),
vision_preprocessor: VisionPreprocessorOutcome::UnchangedNone,
}),
Don't add the import to each test — the tests already use super::*; so the variant should resolve. Verify by running tests in the next step.
- Step 6: Add unit tests for the new logic
In the existing #[cfg(test)] mod tests block of setup.rs, after step_models_wipes_stale_atomgit_entries (around line 635-692), add five tests covering each row of the precedence table:
fn vl_model_entry(model: &str) -> ModelEntry {
ModelEntry {
id: 1,
is_infinity: 0,
is_atomcode_exclusive: 0,
display_model_name: model.to_string(),
}
}
/// Helper that runs the same wipe-and-insert sequence as
/// `step_models_and_register` body for a given (config, models)
/// combo and returns the resulting `ModelsInfo`. Avoids the network
/// dependency by computing everything locally.
fn run_register(config: &mut Config, models: Vec<ModelEntry>) -> ModelsInfo {
// Mirror of step_models_and_register's body. Kept in sync by
// the test signal: if production diverges, the existing
// step_models_wipes_stale_atomgit_entries test catches it.
let stale: Vec<String> = config
.providers
.keys()
.filter(|k| is_codingplan_provider_name(k))
.cloned()
.collect();
for k in stale {
config.providers.remove(&k);
}
let names: Vec<String> = models.iter().map(|m| m.display_model_name.clone()).collect();
let provider_names = provider_names_for(&names);
let default_provider = provider_names
.first()
.cloned()
.unwrap_or_else(|| PROVIDER_PREFIX.to_string());
for (pname, m) in provider_names.iter().zip(models.iter()) {
config
.providers
.insert(pname.clone(), build_codingplan_provider(&m.display_model_name));
}
config.default_provider = default_provider.clone();
let vl_idx = names.iter().position(|n| crate::provider::model_name_suggests_vision(n));
let new_vl_key = vl_idx.map(|i| provider_names[i].clone());
let vision_preprocessor = {
let current = config.vision_preprocessor_provider.clone();
let user_supplied_non_atomgit = current
.as_deref()
.map(|k| !k.is_empty() && !is_codingplan_provider_name(k))
.unwrap_or(false);
if user_supplied_non_atomgit {
VisionPreprocessorOutcome::UserSupplied(current.unwrap())
} else {
match new_vl_key {
Some(k) => {
config.vision_preprocessor_provider = Some(k.clone());
VisionPreprocessorOutcome::AutoSet(k)
}
None => {
if current.is_some() {
config.vision_preprocessor_provider = None;
VisionPreprocessorOutcome::Cleared
} else {
VisionPreprocessorOutcome::UnchangedNone
}
}
}
}
};
ModelsInfo {
display_names: names,
provider_names,
default_provider,
vision_preprocessor,
}
}
#[test]
fn vision_preprocessor_auto_set_when_none_and_list_has_vl() {
let mut config = blank_config();
let models = vec![
vl_model_entry("moonshotai/Kimi-K2-Instruct"),
vl_model_entry("Qwen/Qwen3-VL-32B-Instruct"),
vl_model_entry("deepseek/deepseek-v4-flash"),
];
let info = run_register(&mut config, models);
// Second model is the VL candidate (Kimi has no VL hint).
let expected = "AtomGit-Qwen-Qwen3-VL-32B-Instruct".to_string();
assert_eq!(
info.vision_preprocessor,
VisionPreprocessorOutcome::AutoSet(expected.clone())
);
assert_eq!(config.vision_preprocessor_provider, Some(expected));
}
#[test]
fn vision_preprocessor_unchanged_none_when_list_has_no_vl() {
let mut config = blank_config();
let models = vec![vl_model_entry("moonshotai/Kimi-K2-Instruct")];
let info = run_register(&mut config, models);
assert_eq!(info.vision_preprocessor, VisionPreprocessorOutcome::UnchangedNone);
assert_eq!(config.vision_preprocessor_provider, None);
}
#[test]
fn vision_preprocessor_overwrites_stale_atomgit_value() {
let mut config = blank_config();
// Simulate previous /codingplan that set this AtomGit-* key.
config.vision_preprocessor_provider =
Some("AtomGit-Qwen-Qwen2-VL-72B".into());
let models = vec![
vl_model_entry("Kimi-K2-Instruct"),
vl_model_entry("Qwen/Qwen3-VL-32B-Instruct"),
];
let info = run_register(&mut config, models);
let expected = "AtomGit-Qwen-Qwen3-VL-32B-Instruct".to_string();
assert_eq!(
info.vision_preprocessor,
VisionPreprocessorOutcome::AutoSet(expected.clone())
);
assert_eq!(config.vision_preprocessor_provider, Some(expected));
}
#[test]
fn vision_preprocessor_cleared_when_stale_atomgit_and_list_has_no_vl() {
let mut config = blank_config();
config.vision_preprocessor_provider =
Some("AtomGit-Qwen-Qwen2-VL-72B".into());
let models = vec![vl_model_entry("moonshotai/Kimi-K2-Instruct")];
let info = run_register(&mut config, models);
assert_eq!(info.vision_preprocessor, VisionPreprocessorOutcome::Cleared);
assert_eq!(config.vision_preprocessor_provider, None);
}
#[test]
fn vision_preprocessor_preserves_user_set_non_atomgit() {
let mut config = blank_config();
// User has manually configured a SiliconFlow-hosted VL.
config.vision_preprocessor_provider = Some("Qwen3-VL-32B-Instruct".into());
let models = vec![
vl_model_entry("Kimi-K2-Instruct"),
vl_model_entry("Qwen/Qwen3-VL-32B-Instruct"), // would otherwise auto-set
];
let info = run_register(&mut config, models);
assert_eq!(
info.vision_preprocessor,
VisionPreprocessorOutcome::UserSupplied("Qwen3-VL-32B-Instruct".into())
);
// Crucially: config value is unchanged.
assert_eq!(
config.vision_preprocessor_provider.as_deref(),
Some("Qwen3-VL-32B-Instruct")
);
}
#[test]
fn vision_preprocessor_recognises_pure_ocr_model_name() {
// Regression for Task 7: pure OCR names (no VL/vision substring)
// must still be recognized as VL candidates by the heuristic, so
// the auto-set path picks them up.
let mut config = blank_config();
let models = vec![
vl_model_entry("Kimi-K2-Instruct"),
vl_model_entry("PaddleOCR-2.0"),
];
let info = run_register(&mut config, models);
let expected = "AtomGit-PaddleOCR-2.0".to_string();
assert_eq!(
info.vision_preprocessor,
VisionPreprocessorOutcome::AutoSet(expected.clone())
);
assert_eq!(config.vision_preprocessor_provider, Some(expected));
}
- Step 7: Run tests
cd /Users/theo/Documents/workspace/atomcode/
cargo test -p atomcode-core --lib coding_plan
Expected: all coding_plan tests pass — existing ones (which now have the new field in ModelsInfo literals) plus the 6 new ones.
If any existing test fails because a ModelsInfo literal is incomplete, find it and add vision_preprocessor: VisionPreprocessorOutcome::UnchangedNone,.
- Step 8: Run render tests specifically and inspect output
The new render-line code adds output for AutoSet / UserSupplied / Cleared variants. Render tests use UnchangedNone (no-op), so they should still pass without output changes. Verify:
cd /Users/theo/Documents/workspace/atomcode/
cargo test -p atomcode-core --lib coding_plan::setup::tests::render -- --nocapture
Expected: all pass. (The --nocapture is just so you eyeball the output if curious.)
- Step 9: Add a render test for the new line
Append to mod tests:
/// Render exercise: the vision-preprocessor line shows up under
/// `Added N providers` when the auto-set path fires.
#[test]
fn render_includes_vision_preprocessor_auto_set_line() {
let report = SetupReport {
login: StepResult::Skipped("already logged in".into()),
claim: StepResult::Ok(ClaimInfo {
message: String::new(),
duplicate: false,
}),
models: StepResult::Ok(ModelsInfo {
display_names: vec![
"Kimi-K2-Instruct".into(),
"Qwen/Qwen3-VL-32B-Instruct".into(),
],
provider_names: vec![
"AtomGit-Kimi-K2-Instruct".into(),
"AtomGit-Qwen-Qwen3-VL-32B-Instruct".into(),
],
default_provider: "AtomGit-Kimi-K2-Instruct".into(),
vision_preprocessor: VisionPreprocessorOutcome::AutoSet(
"AtomGit-Qwen-Qwen3-VL-32B-Instruct".into(),
),
}),
status: StepResult::Skipped("status check skipped for this test".into()),
};
let out = report.render();
assert!(
out.contains("Vision preprocessor → AtomGit-Qwen-Qwen3-VL-32B-Instruct"),
"render must include the auto-detected line: {out}",
);
assert!(out.contains("(auto-detected)"));
}
#[test]
fn render_includes_vision_preprocessor_cleared_line_when_stale_dropped() {
let report = SetupReport {
login: StepResult::Skipped("already logged in".into()),
claim: StepResult::Ok(ClaimInfo {
message: String::new(),
duplicate: false,
}),
models: StepResult::Ok(ModelsInfo {
display_names: vec!["Kimi-K2-Instruct".into()],
provider_names: vec!["AtomGit-Kimi-K2-Instruct".into()],
default_provider: "AtomGit-Kimi-K2-Instruct".into(),
vision_preprocessor: VisionPreprocessorOutcome::Cleared,
}),
status: StepResult::Skipped("test skip".into()),
};
let out = report.render();
assert!(
out.contains("Vision preprocessor cleared"),
"render must surface the cleared state: {out}",
);
}
#[test]
fn render_includes_vision_preprocessor_user_supplied_line() {
let report = SetupReport {
login: StepResult::Skipped("already logged in".into()),
claim: StepResult::Ok(ClaimInfo {
message: String::new(),
duplicate: false,
}),
models: StepResult::Ok(ModelsInfo {
display_names: vec![
"Kimi-K2-Instruct".into(),
"Qwen/Qwen3-VL-32B-Instruct".into(),
],
provider_names: vec![
"AtomGit-Kimi-K2-Instruct".into(),
"AtomGit-Qwen-Qwen3-VL-32B-Instruct".into(),
],
default_provider: "AtomGit-Kimi-K2-Instruct".into(),
vision_preprocessor: VisionPreprocessorOutcome::UserSupplied(
"Qwen3-VL-32B-Instruct".into(),
),
}),
status: StepResult::Skipped("test skip".into()),
};
let out = report.render();
assert!(out.contains("Vision preprocessor → Qwen3-VL-32B-Instruct"));
assert!(out.contains("(user setting kept)"));
}
#[test]
fn render_omits_vision_preprocessor_line_when_unchanged_none() {
let report = SetupReport {
login: StepResult::Skipped("already logged in".into()),
claim: StepResult::Ok(ClaimInfo {
message: String::new(),
duplicate: false,
}),
models: StepResult::Ok(ModelsInfo {
display_names: vec!["Kimi-K2-Instruct".into()],
provider_names: vec!["AtomGit-Kimi-K2-Instruct".into()],
default_provider: "AtomGit-Kimi-K2-Instruct".into(),
vision_preprocessor: VisionPreprocessorOutcome::UnchangedNone,
}),
status: StepResult::Skipped("test skip".into()),
};
let out = report.render();
// No-op variant must NOT add a line — keeps existing report output
// identical for users who never set/get a VL provider.
assert!(!out.contains("Vision preprocessor"));
}
- Step 10: Re-run all coding_plan tests
cd /Users/theo/Documents/workspace/atomcode/
cargo test -p atomcode-core --lib coding_plan
Expected: all pass.
- Step 11: Workspace clippy + build
cd /Users/theo/Documents/workspace/atomcode/
cargo build --workspace --all-targets 2>&1 | tail -20
cargo clippy -p atomcode-core --lib --all-targets -- -D warnings 2>&1 | tail -30
Expected: build OK, no NEW clippy warnings introduced by this commit.
- Step 12: Commit
cat > /tmp/atomcode-task8-msg.txt <<'EOF'
feat(coding_plan): auto-set vision_preprocessor_provider from model list
When /codingplan installs the AtomGit provider list, scan for the first
vision-capable model (via model_name_suggests_vision) and set
vision_preprocessor_provider to its provider key. Precedence:
- User-supplied non-AtomGit values: preserved unchanged.
- None or stale AtomGit-* (from previous run): replaced or cleared.
The render() output adds one of three new lines (auto-detected,
user setting kept, cleared) so users can see the resulting state.
UnchangedNone is silent to keep output identical for setups without VL.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
EOF
cd /Users/theo/Documents/workspace/atomcode/
git add crates/atomcode-core/src/coding_plan/setup.rs
git commit -F /tmp/atomcode-task8-msg.txt -- crates/atomcode-core/src/coding_plan/setup.rs
Task 9: Workspace verification
This is a verification-only task — no code changes unless verification reveals a regression.
- Step 1: Run full atomcode-core tests
cd /Users/theo/Documents/workspace/atomcode/
cargo test -p atomcode-core --lib 2>&1 | tail -10
Expected: pass count equals or exceeds baseline (after Tasks 1–6 we had 1104 passing). New tests from Tasks 7+8 should add ~10. Pre-existing failures unchanged.
- Step 2: Workspace build
cd /Users/theo/Documents/workspace/atomcode/
cargo build --workspace --all-targets 2>&1 | tail -10
Expected: success.
- Step 3: Workspace clippy
cd /Users/theo/Documents/workspace/atomcode/
cargo clippy --workspace --all-targets -- -D warnings 2>&1 | tail -30
Expected: only pre-existing warnings (same as Task 6 reported).
- Step 4: If any fixups were needed, commit them
If verification surfaced a struct-literal that needs the new vision_preprocessor field initializer (mirror of the daemon fix in commit 4ce8bc0), apply it:
cat > /tmp/atomcode-task9-msg.txt <<'EOF'
fix(coding_plan): missed ModelsInfo literal cleanups
[describe specific fixes here]
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
EOF
cd /Users/theo/Documents/workspace/atomcode/
git add -A
git commit -F /tmp/atomcode-task9-msg.txt
If no fixups needed, skip this step.
Manual Verification (post-merge)
- Save current
~/.atomcode/config.toml. - Edit it to remove the
vision_preprocessor_provider = ...line so the field becomes None. - Run
cargo run -p atomcode-cli --release -- /codingplan(or invoke/codingplanfrom inside the TUI). - Inspect the
/codingplanoutput: expect a✔ Vision preprocessor → AtomGit-... (auto-detected)line if the API returned a VL model in the list. - Confirm
~/.atomcode/config.tomlnow containsvision_preprocessor_provider = "AtomGit-...". - Set the field to your own non-AtomGit value (e.g.
Qwen3-VL-32B-Instructfrom your SiliconFlow setup), re-run /codingplan, and verify it stays untouched + the report says(user setting kept).
Self-Review Checklist (run before handoff)
1. Spec coverage:
- OCR models recognized → Task 7. ✓
- Auto-set on None → Task 8 step 3 + test. ✓
- Auto-overwrite on AtomGit-* stale → Task 8 step 3 + test. ✓
- Cleared on AtomGit-* + no-VL list → Task 8 step 3 + test. ✓
- Preserved on non-AtomGit user value → Task 8 step 3 + test. ✓
- Render line for each outcome → Task 8 steps 4 + 9. ✓
- UnchangedNone silent → Task 8 step 9 (
render_omits_vision_preprocessor_line_when_unchanged_none). ✓
2. Placeholder scan: No "TBD"/"TODO"/"add error handling" in any task. The "[describe specific fixes here]" placeholder in Task 9's optional commit is fine — it's only used IF a fixup is actually needed, and in that case the implementer fills it in.
3. Type consistency:
VisionPreprocessorOutcomedefined once (Task 8 step 1) and used in both production (Task 8 steps 2–4) and tests (Task 8 steps 5, 6, 9). ✓model_name_suggests_vision(free function) used identically across Tasks 7 and 8. ✓is_codingplan_provider_nameused in both wipe step and the precedence check. ✓ModelsInfoliteral updates (Task 8 step 5) cover all 8 existing render tests. ✓