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
```python
from PySide6.QtWidgets import QMainWindow
...
```
### FILE: managers/database.py
```python
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:
- First
### FILE:marker hits → Whittl creates a tab namedmain.pyand starts filling it - Second
### FILE:marker → second tab appears, first tab stops streaming - Third, fourth, etc. → new tabs appear in order
- 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.
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 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:
- Pre-analyzes your request with a fast keyword + AI-assisted scan
- Identifies which files are relevant to what you're asking
- Sends only those files in the system prompt
- 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.pybut references a signal that lives inviews/main.pywhich wasn't in the prompt - The modification fails on run because of the missing context
Workarounds:
- Edit → Preferences → File Context → Send Active File Only (max control, just the currently selected tab goes)
- Open the files you want included — the focused tab is always sent, and routing is biased toward what is open
- 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:
- Writes every file in the project to the project folder (preserving directory structure —
ui/main.pygoes toui/main.py, notui_main.py) - Runs
main.pyfrom the project directory so relative imports work - 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.
Any folder in the project that holds Python files is local too, whatever its name (tests/, data/, scenes/), on Test Run and in builds. On Test Run, common names such as core, ui, utils, models and widgets are never installed from PyPI, even before the AI has created the folder.
Optional imports¶
An import inside a try whose except ImportError: (or a broader except) lets the app carry on is optional: the app runs without it. Whittl installs it only when it knows the name as a real package (pygame, Pillow and the like), so the feature it enables works. An unknown name is left alone instead of being handed to pip, for example a helper module that another app provides when it launches yours:
try:
import cinebit_app # there only when CineBit starts the app
except ImportError:
cinebit_app = None
If a handler re-raises or exits instead, the module is required and Whittl installs it as usual. To have an optional module installed anyway, add a # REQUIRES: name line to the file; Test Run and desktop builds read it.
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 Xstill works aftermain.pybecomesmain_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. Shipped in v2.4 as part of the UX polish pass.
What's next¶
- Smart Routing — already covered above; Iterating on Generated Code has the follow-up-prompt pattern for multi-file edits
- Project Folder Structure — reference for what every file in a project does
- Auto-fix Rules — how Whittl handles imports, circular deps, and missing modules in generated multi-file code