Skip to content

Multi-file Projects

Most real apps don't fit in one file. Whittl handles multi-file projects as a first-class case: the AI generates them with explicit file boundaries, the editor shows each file in its own tab, and smart routing keeps token costs bounded as your project grows.

How the AI marks multiple files

When the AI generates a multi-file project, it emits explicit file boundaries using a ### FILE: marker:

### FILE: main.py
```python
from ui.window import MainWindow
...

FILE: ui/window.py

from PySide6.QtWidgets import QMainWindow
...

FILE: managers/database.py

import sqlite3
...
Whittl's parser scans the streamed output for these markers and routes each block to the corresponding tab in the preview panel. Tabs appear as they're written — the first file auto-focuses, then releases so you can click around while the rest finishes.

### Tab appearance during streaming

As the AI writes multi-file output:

1. First `### FILE:` marker hits → Whittl creates a tab named `main.py` and starts filling it
2. Second `### FILE:` marker → second tab appears, first tab stops streaming
3. Third, fourth, etc. → new tabs appear in order
4. Final `###` / end of stream → all tabs stop streaming

You can watch the AI work across all files in real time instead of waiting for everything to appear at once.

!!! tip "Duplicate-marker regression protection"

    Some models (grok-4.1-fast notably) occasionally emit a stray second `### FILE: main.py` marker for a file they've already finished. Whittl's parser detects this and keeps the larger valid block rather than letting a 125-char orphan fragment overwrite a 5,900-char real file. Fixed in v2.3. If you were hit by IndentationErrors on older versions, upgrade.

## Creating a multi-file project

There's no special "create multi-file project" step. Just ask the AI for something modular:
Build me a desktop password manager with a login screen, main vault view, and settings dialog. Split the code into separate files: main.py as entry point, views/ for the UI screens, and core/ for the data model.
Whittl generates the project with that structure. The AI picks the import paths, file names, and module boundaries based on your prompt and on [skills](skills.md) about Python project layout.

You can also **convert** an existing single-file project to multi-file by prompting:
Split this into multiple files. Put the UI classes in a ui/ folder, data model in core/, and leave main.py as just the entry point. ```

That's a structural refactor — Whittl's planner recognizes it as such and uses full regeneration rather than surgical edits (surgical editing doesn't handle "reorganize everything" gracefully).

Smart file routing

Large projects (8+ files, or 2+ if you enable the "Smart Routing for All Projects" toggle) use smart routing automatically. Instead of sending all files to the AI on every request, Whittl:

  1. Pre-analyzes your request with a fast keyword + AI-assisted scan
  2. Identifies which files are relevant to what you're asking
  3. Sends only those files in the system prompt
  4. Protects imports — warns the AI which symbols are exported from the files you didn't send so it doesn't rename them

The status bar shows routing status: Smart routing: 3 of 28 files means 3 files were selected and 25 were excluded for that request.

Why this matters for cost

A 30-file project that would otherwise send ~100K tokens per request now sends ~10-15K. That's 4-15× fewer tokens per modification. On Claude Sonnet, that's the difference between $0.30 and $0.03 per edit.

When routing gets it wrong

Smart routing is a heuristic. Occasionally it misses a file that's actually needed. You'll see a symptom like:

  • The AI modifies views/login.py but references a signal that lives in views/main.py which wasn't in the prompt
  • The modification fails on run because of the missing context

Workarounds:

  • Edit → Send Active File Only (max control — just the currently selected tab goes)
  • Edit → Send All Files (send everything — slow + expensive, but works)
  • Be more specific in your prompt — mention the specific files to include ("also include views/main.py")

Per-file generation on Test Run

When you click Test Run on a multi-file project, Whittl:

  1. Writes every file in the project to the project folder (preserving directory structure — ui/main.py goes to ui/main.py, not ui_main.py)
  2. Runs main.py from the project directory so relative imports work
  3. Installs any pip dependencies detected from imports

Don't touch the data/ folder

If your project has a data/ subfolder, Whittl preserves it across generations and doesn't let the AI overwrite files there. This is protection for user-generated runtime data (settings, saved state, databases). If you need the AI to modify something, move it out of data/.

Imports across files

The AI is instructed to use explicit relative or package-rooted imports:

  • from ui.window import MainWindow
  • from .window import MainWindow (relative)
  • from window import MainWindow (ambiguous, fails when run from elsewhere)

Whittl's auto-fix catches most bad imports before they hit disk, but the AI occasionally invents circular imports or forgets to add __init__.py to a new subfolder. Both get flagged on the first Test Run.

Local module protection

When the AI writes import database and there's a database.py in the project folder, Whittl recognizes it as a LOCAL import and doesn't try to pip-install the database package from PyPI. This prevents a whole class of "pip installed some random package that shadows my local module" bugs.

The list of folders Whittl treats as always-local: core/, ui/, views/, models/, utils/, helpers/, data/, assets/, handlers/, services/, managers/, api/, lib/, components/, widgets/. If your project uses a non-standard folder name, add it explicitly to project.json or just rename the folder.

Multi-file search (Ctrl+Shift+F)

Press Ctrl+Shift+F to open the multi-file search dialog. Search across all files in the current project — useful for:

  • "Where is this signal emitted?"
  • "Find every file that imports QTimer"
  • "What files mention the old color token?"

Results show with line numbers and a file badge. Click a result to jump to that line.

File tabs with line counts

Each tab shows the filename and live line count: ui/main.py (147). The count updates on save. A colored dot (●) indicates unsaved changes (red = unsaved, green = saved).

Right-click a tab for:

  • Rename — rename the file on disk (Whittl updates imports too, so from ui.main import X still works after main.py becomes main_window.py)
  • Close Tab — removes from the open tabs but keeps the file on disk
  • Delete File — removes the file from disk entirely (irreversible)

The files dropdown (☰)

The small hamburger button next to the tab bar opens a dropdown showing all files in the project — both open tabs and closed ones. Click a closed file to reopen it.

Particularly useful for projects with 10+ files where the tab bar scrolls horizontally.

Streaming indicator on tabs

During a multi-file generation, the active tab (the one currently being written) shows a subtle "Streaming..." indicator. Coming in v2.4 as part of the UX polish pass.

What's next