diff --git a/02-context-conversations/README.md b/02-context-conversations/README.md
index 56a8222d..411d0bbc 100644
--- a/02-context-conversations/README.md
+++ b/02-context-conversations/README.md
@@ -82,7 +82,7 @@ copilot
### Basic @ Patterns
| Pattern | What It Does | Example Use |
-|---------|--------------|-------------|
+| --- | --- | --- |
| `@file.py` | Reference a single file | `Review @samples/book-app-project/books.py` |
| `@folder/` | Reference all files in a directory | `Review @samples/book-app-project/` |
| `@file1.py @file2.py` | Reference multiple files | `Compare @samples/book-app-project/book_app.py @samples/book-app-project/books.py` |
@@ -475,10 +475,10 @@ No re-explaining. No re-reading files. Just continue working.
-These topics build on the essentials above. **Pick what interests you, or skip ahead to [Practice](#practice).**
+These topics build on the essentials above. **Pick what interests you, or skip ahead to** [**Practice**](#practice)**.**
| I want to learn about... | Jump to |
-|---|---|
+| --- | --- |
| Wildcard patterns and advanced session commands | [Additional @ Patterns & Session Commands](#additional-patterns) |
| Building on context across multiple prompts | [Context-Aware Conversations](#context-aware-conversations) |
| Token limits and `/compact` | [Understanding Context Windows](#understanding-context-windows) |
@@ -486,7 +486,8 @@ These topics build on the essentials above. **Pick what interests you, or skip a
| Analyzing screenshots and mockups | [Working with Images](#working-with-images) |
-Additional @ Patterns & Session Commands
+Additional @ Patterns & Session Commands
+
### Additional @ Patterns
@@ -494,7 +495,7 @@ These topics build on the essentials above. **Pick what interests you, or skip a
For power users, Copilot CLI supports wildcard patterns and image references:
| Pattern | What It Does |
-|---------|--------------|
+| --- | --- |
| `@folder/*.py` | All .py files in folder |
| `@**/test_*.py` | Recursive wildcard: find all test files anywhere |
| `@image.png` | Image file for UI review |
@@ -537,6 +538,7 @@ copilot
Context-Aware Conversations
+
### Context-Aware Conversations
@@ -577,6 +579,7 @@ Notice how each prompt builds on the previous work. This is the power of context
Understanding Context Windows
+
### Understanding Context Windows
@@ -637,7 +640,7 @@ copilot
#### Context Efficiency Tips
| Situation | Action | Why |
-|-----------|--------|-----|
+| --- | --- | --- |
| Starting new topic | `/clear` | Removes irrelevant context |
| Went down wrong path | `/rewind` | Roll back conversation (and optionally restore files) to any earlier point |
| Long conversation | `/compact` | Summarizes history, frees tokens |
@@ -656,6 +659,7 @@ copilot
Choosing What to Reference
+
### Choosing What to Reference
@@ -665,13 +669,14 @@ Not all files are equal when it comes to context. Here's how to choose wisely:
#### File Size Considerations
| File Size | Approximate [Tokens](../GLOSSARY.md#token) | Strategy |
-|-----------|-------------------|----------|
+| --- | --- | --- |
| Small (<100 lines) | ~500-1,500 tokens | Reference freely |
| Medium (100-500 lines) | ~1,500-7,500 tokens | Reference specific files |
| Large (500+ lines) | 7,500+ tokens | Be selective, use specific files |
| Very Large (1000+ lines) | 15,000+ tokens | Consider splitting or targeting sections |
**Concrete examples:**
+
- The book app's 4 Python files combined ≈ 2,000-3,000 tokens
- A typical Python module (200 lines) ≈ 3,000 tokens
- A Flask API file (400 lines) ≈ 6,000 tokens
@@ -683,6 +688,7 @@ Not all files are equal when it comes to context. Here's how to choose wisely:
#### What to Include vs. Exclude
**High value** (include these):
+
- Entry points (`book_app.py`, `main.py`, `app.py`)
- The specific files you're asking about
- Files directly imported by your target file
@@ -690,6 +696,7 @@ Not all files are equal when it comes to context. Here's how to choose wisely:
- Data models or dataclasses
**Lower value** (consider excluding):
+
- Generated files (compiled output, bundled assets)
- Node modules or vendor directories
- Large data files or fixtures
@@ -706,11 +713,13 @@ Less specific ──────────────────────
```
**When to go broad** (`@samples/book-app-project/`):
+
- Initial codebase exploration
- Finding patterns across many files
- Architecture reviews
**When to go specific** (`@samples/book-app-project/books.py`):
+
- Debugging a particular issue
- Code review of a specific file
- Asking about a single function
@@ -739,6 +748,7 @@ This staged approach keeps context focused and efficient.
Working with Images
+
### Working with Images
@@ -847,6 +857,7 @@ The hands-on examples focused on code quality reviews and input validation. Now
💡 Hints (click to expand)
**Getting started:**
+
```bash
cd /path/to/copilot-cli-for-beginners
copilot
@@ -859,6 +870,7 @@ copilot
Then resume with: `copilot --continue`
**Useful commands:**
+
- `@file.py` - Reference a single file
- `@folder/` - Reference all files in a folder (note the trailing `/`)
- `/context` - Check how much context you're using
@@ -877,12 +889,12 @@ Then resume with: `copilot --continue`
---
-🔧 Common Mistakes & Troubleshooting (click to expand)
+🔧 Common Mistakes & Troubleshooting (click to expand)
### Common Mistakes
| Mistake | What Happens | Fix |
-|---------|--------------|-----|
+| --- | --- | --- |
| Forgetting `@` before filenames | Copilot CLI treats "books.py" as plain text | Use `@samples/book-app-project/books.py` to reference files |
| Expecting sessions to persist automatically | Starting `copilot` fresh loses all previous context | Use `--continue` (last session) or `--resume` (pick a session) |
| Referencing files outside current directory | "Permission denied" or "File not found" errors | Use `/add-dir /path/to/directory` to grant access |
@@ -912,6 +924,7 @@ copilot --add-dir /path/to/directory
```
**Context fills up too quickly**:
+
- Be more specific with file references
- Use `/clear` between different topics
- Split work across multiple sessions
@@ -942,7 +955,7 @@ copilot --add-dir /path/to/directory
Now that you can give Copilot CLI context, let's put it to work on real development tasks. The context techniques you just learned (file references, cross-file analysis, and session management) are the foundation for the powerful workflows in the next chapter.
-In **[Chapter 03: Development Workflows](../03-development-workflows/README.md)**, you'll learn:
+In [**Chapter 03: Development Workflows**](../03-development-workflows/README.md), you'll learn:
- Code review workflows
- Refactoring patterns
@@ -952,4 +965,4 @@ In **[Chapter 03: Development Workflows](../03-development-workflows/README.md)*
---
-**[← Back to Chapter 01](../01-setup-and-first-steps/README.md)** | **[Continue to Chapter 03 →](../03-development-workflows/README.md)**
+[**← Back to Chapter 01**](../01-setup-and-first-steps/README.md) | [**Continue to Chapter 03 →**](../03-development-workflows/README.md)
diff --git a/samples/book-app-project-cs/Services/BookCollection.cs b/samples/book-app-project-cs/Services/BookCollection.cs
index b219ba53..a2c543e7 100644
--- a/samples/book-app-project-cs/Services/BookCollection.cs
+++ b/samples/book-app-project-cs/Services/BookCollection.cs
@@ -27,7 +27,8 @@ private void LoadBooks()
try
{
var json = File.ReadAllText(_dataFile);
- _books = JsonSerializer.Deserialize>(json, JsonOptions) ?? [];
+ var books = JsonSerializer.Deserialize>(json, JsonOptions);
+ _books = books ?? [];
}
catch (FileNotFoundException)
{
@@ -42,13 +43,31 @@ private void LoadBooks()
private void SaveBooks()
{
+ var directory = Path.GetDirectoryName(_dataFile);
+ if (!string.IsNullOrEmpty(directory) && !Directory.Exists(directory))
+ {
+ Directory.CreateDirectory(directory);
+ }
+
var json = JsonSerializer.Serialize(_books, JsonOptions);
File.WriteAllText(_dataFile, json);
}
public Book AddBook(string title, string author, int year)
{
- var book = new Book { Title = title, Author = author, Year = year };
+ var cleanTitle = title.Trim();
+ var cleanAuthor = author.Trim();
+ if (string.IsNullOrWhiteSpace(cleanTitle) || string.IsNullOrWhiteSpace(cleanAuthor))
+ {
+ throw new ArgumentException("Title and author are required.");
+ }
+
+ if (year <= 0)
+ {
+ throw new ArgumentOutOfRangeException(nameof(year), "Year must be a positive integer.");
+ }
+
+ var book = new Book { Title = cleanTitle, Author = cleanAuthor, Year = year };
_books.Add(book);
SaveBooks();
return book;
@@ -58,7 +77,7 @@ public Book AddBook(string title, string author, int year)
public Book? FindBookByTitle(string title)
{
- return _books.Find(b => b.Title.Equals(title, StringComparison.OrdinalIgnoreCase));
+ return _books.Find(b => b.Title.Equals(title.Trim(), StringComparison.OrdinalIgnoreCase));
}
public bool MarkAsRead(string title)
@@ -81,8 +100,14 @@ public bool RemoveBook(string title)
public List FindByAuthor(string author)
{
+ var normalized = author.Trim();
+ if (string.IsNullOrWhiteSpace(normalized))
+ {
+ return [];
+ }
+
return _books
- .Where(b => b.Author.Equals(author, StringComparison.OrdinalIgnoreCase))
+ .Where(b => b.Author.Contains(normalized, StringComparison.OrdinalIgnoreCase))
.ToList();
}
}
diff --git a/samples/book-app-project-js/books.js b/samples/book-app-project-js/books.js
index 85020b4b..99f8aed6 100644
--- a/samples/book-app-project-js/books.js
+++ b/samples/book-app-project-js/books.js
@@ -13,8 +13,8 @@ class Book {
}
class BookCollection {
- constructor(dataFile) {
- this.dataFile = dataFile || DATA_FILE;
+ constructor(dataFile = DATA_FILE) {
+ this.dataFile = dataFile;
this.books = [];
this.loadBooks();
}
@@ -23,7 +23,13 @@ class BookCollection {
try {
const raw = fs.readFileSync(this.dataFile, "utf-8");
const data = JSON.parse(raw);
- this.books = data.map((b) => new Book(b.title, b.author, b.year, b.read));
+ if (!Array.isArray(data)) {
+ this.books = [];
+ return;
+ }
+ this.books = data
+ .filter((item) => item && typeof item === "object")
+ .map((b) => new Book(b.title, b.author, b.year, !!b.read));
} catch (err) {
if (err.code === "ENOENT") {
this.books = [];
@@ -37,6 +43,10 @@ class BookCollection {
}
saveBooks() {
+ const dir = path.dirname(this.dataFile);
+ if (dir && !fs.existsSync(dir)) {
+ fs.mkdirSync(dir, { recursive: true });
+ }
const data = this.books.map((b) => ({
title: b.title,
author: b.author,
@@ -47,7 +57,18 @@ class BookCollection {
}
addBook(title, author, year) {
- const book = new Book(title, author, year);
+ const cleanTitle = String(title).trim();
+ const cleanAuthor = String(author).trim();
+ if (!cleanTitle || !cleanAuthor) {
+ throw new Error("Title and author are required.");
+ }
+
+ const numericYear = Number.parseInt(year, 10);
+ if (Number.isNaN(numericYear) || numericYear <= 0) {
+ throw new Error("Year must be a positive integer.");
+ }
+
+ const book = new Book(cleanTitle, cleanAuthor, numericYear);
this.books.push(book);
this.saveBooks();
return book;
@@ -58,7 +79,8 @@ class BookCollection {
}
findBookByTitle(title) {
- return this.books.find((b) => b.title.toLowerCase() === title.toLowerCase()) || null;
+ const normalizedTitle = String(title).trim().toLowerCase();
+ return this.books.find((b) => b.title.trim().toLowerCase() === normalizedTitle) || null;
}
markAsRead(title) {
@@ -82,7 +104,11 @@ class BookCollection {
}
findByAuthor(author) {
- return this.books.filter((b) => b.author.toLowerCase() === author.toLowerCase());
+ const normalizedAuthor = String(author).trim().toLowerCase();
+ if (!normalizedAuthor) {
+ return [];
+ }
+ return this.books.filter((b) => b.author.trim().toLowerCase().includes(normalizedAuthor));
}
}
diff --git a/samples/book-app-project-js/tests/test_books.js b/samples/book-app-project-js/tests/test_books.js
index 446f68cb..328695b3 100644
--- a/samples/book-app-project-js/tests/test_books.js
+++ b/samples/book-app-project-js/tests/test_books.js
@@ -55,4 +55,21 @@ describe("BookCollection", () => {
const result = collection.removeBook("Nonexistent Book");
assert.equal(result, false);
});
+
+ it("should find books by partial author match", () => {
+ const collection = new BookCollection(tempFile);
+ collection.addBook("Dune", "Frank Herbert", 1965);
+ collection.addBook("The Hobbit", "J.R.R. Tolkien", 1937);
+
+ const matches = collection.findByAuthor("tolkien");
+
+ assert.deepEqual(matches.map((book) => book.title), ["The Hobbit"]);
+ });
+
+ it("should validate required data before adding a book", () => {
+ const collection = new BookCollection(tempFile);
+
+ assert.throws(() => collection.addBook(" ", "George Orwell", 1949), /Title and author are required/);
+ assert.throws(() => collection.addBook("1984", "George Orwell", 0), /Year must be a positive integer/);
+ });
});
diff --git a/samples/book-app-project/books.py b/samples/book-app-project/books.py
index 2110689f..88e67872 100644
--- a/samples/book-app-project/books.py
+++ b/samples/book-app-project/books.py
@@ -1,6 +1,8 @@
import json
+import os
from dataclasses import dataclass, asdict
-from typing import List, Optional
+from pathlib import Path
+from typing import List, Optional, Union
DATA_FILE = "data.json"
@@ -14,29 +16,57 @@ class Book:
class BookCollection:
- def __init__(self):
+ def __init__(self, data_file: Optional[Union[str, Path]] = None):
+ self.data_file = str(data_file) if data_file is not None else DATA_FILE
self.books: List[Book] = []
self.load_books()
- def load_books(self):
+ def load_books(self) -> None:
"""Load books from the JSON file if it exists."""
try:
- with open(DATA_FILE, "r") as f:
+ with open(self.data_file, "r", encoding="utf-8") as f:
data = json.load(f)
- self.books = [Book(**b) for b in data]
+ if not isinstance(data, list):
+ self.books = []
+ return
+
+ self.books = []
+ for item in data:
+ if not isinstance(item, dict):
+ continue
+ try:
+ self.books.append(Book(**item))
+ except TypeError:
+ continue
except FileNotFoundError:
self.books = []
except json.JSONDecodeError:
print("Warning: data.json is corrupted. Starting with empty collection.")
self.books = []
- def save_books(self):
+ def save_books(self) -> None:
"""Save the current book collection to JSON."""
- with open(DATA_FILE, "w") as f:
+ directory = os.path.dirname(self.data_file)
+ if directory:
+ os.makedirs(directory, exist_ok=True)
+ with open(self.data_file, "w", encoding="utf-8") as f:
json.dump([asdict(b) for b in self.books], f, indent=2)
def add_book(self, title: str, author: str, year: int) -> Book:
- book = Book(title=title, author=author, year=year)
+ cleaned_title = title.strip()
+ cleaned_author = author.strip()
+ if not cleaned_title or not cleaned_author:
+ raise ValueError("Title and author are required.")
+
+ try:
+ year_value = int(year)
+ except (TypeError, ValueError) as exc:
+ raise ValueError("Year must be an integer.") from exc
+
+ if year_value <= 0:
+ raise ValueError("Year must be a positive integer.")
+
+ book = Book(title=cleaned_title, author=cleaned_author, year=year_value)
self.books.append(book)
self.save_books()
return book
@@ -45,8 +75,9 @@ def list_books(self) -> List[Book]:
return self.books
def find_book_by_title(self, title: str) -> Optional[Book]:
+ normalized_title = title.strip().lower()
for book in self.books:
- if book.title.lower() == title.lower():
+ if book.title.strip().lower() == normalized_title:
return book
return None
@@ -68,5 +99,11 @@ def remove_book(self, title: str) -> bool:
return False
def find_by_author(self, author: str) -> List[Book]:
- """Find all books by a given author."""
- return [b for b in self.books if b.author.lower() == author.lower()]
+ """Find all books by a given author, including partial matches."""
+ normalized_author = author.strip().lower()
+ if not normalized_author:
+ return []
+ return [
+ book for book in self.books
+ if normalized_author in book.author.strip().lower()
+ ]
diff --git a/samples/book-app-project/tests/test_books.py b/samples/book-app-project/tests/test_books.py
index 061149c5..2962e17b 100644
--- a/samples/book-app-project/tests/test_books.py
+++ b/samples/book-app-project/tests/test_books.py
@@ -51,3 +51,23 @@ def test_remove_book_invalid():
collection = BookCollection()
result = collection.remove_book("Nonexistent Book")
assert result is False
+
+
+def test_find_by_author_partial_match():
+ collection = BookCollection()
+ collection.add_book("Dune", "Frank Herbert", 1965)
+ collection.add_book("The Hobbit", "J.R.R. Tolkien", 1937)
+
+ matches = collection.find_by_author("tolkien")
+
+ assert [book.title for book in matches] == ["The Hobbit"]
+
+
+def test_add_book_validates_required_fields():
+ collection = BookCollection()
+
+ with pytest.raises(ValueError, match="Title and author are required"):
+ collection.add_book(" ", "George Orwell", 1949)
+
+ with pytest.raises(ValueError, match="Year must be a positive integer"):
+ collection.add_book("1984", "George Orwell", 0)