Handbook
Teams Web read — dual architecture (LCDL)
Consumer mirror: forge-cockpit-web teams-web-read-dual-architecture.md.
Updated
LCDL owns Playwright read atoms (evaluate, scroll, chunk harvest); Cockpit owns queue, SQLite, sync UI, dedupe, and unified_message; Edge owns operator session on https://teams.microsoft.com/v2/.
No EML / Downloads: Teams has no outlook_download_message_eml, no adopt_download_file, no x-token HTTP export path. List harvest is DOM preview only unless consumer adds a detail task loop.
Discovery strategies (consumer + LCDL)
| Priority | Strategy | Governed task(s) | LCDL status |
|---|---|---|---|
| 1 | Global message search (*, Messages scope, date range, Next, click hit) |
teams_search_messages_chunk |
Production |
| 2 | Sidebar chat inventory | teams_list_chats |
Production |
| 3 | Open chat message pane scroll | teams_list_chat_messages_chunk |
Production |
Run 2 live guided ingest: teams_list_chats returned 0 on operator tab — open-chat scroll alone cannot cover a 7-day window across chats. Global message search (teams_search_messages_chunk) is the production cross-chat discovery path.
Global search (production)
LCDL scripts in teams_readonly.py and connectors/teams_search_chunk.py:
| Step | DOM anchor (Teams v2, observed 2026-06-09) | Evaluate op |
|---|---|---|
| Expand search | [data-tid="title-bar-toggle-search-btn"] |
click via evaluate |
| Query input | input[data-tid="AUTOSUGGEST_INPUT"] |
set *, input events |
| Suggestions popup | [data-tid="ms-searchux-popup"] |
optional; confirm Messages scope |
| Messages scope | Results facet/tab labeled Messages | click (not “Find in chat”) |
| Date filters | Search results date from / to controls | set to consumer date_from / date_to |
| Paginate | Next / more-results control | click until exhausted |
| Open hit | Search result row | click → chat opens, message highlighted |
| Harvest | Highlighted row + pane | teams_harvest_message_rows or dedicated highlight script |
Out of scope for this task: chat-header-find-in-chat-search (per-chat Ctrl+F only).
Cockpit passes date_from / date_to from TeamsHarvestWindow; LCDL does not read Cockpit env for guided ingest.
Governed tasks (v1 + planned)
task_id |
Module | Evaluate scripts | Status |
|---|---|---|---|
teams_probe_health |
tasks/teams_read_v1.py |
teams_page_health |
Production |
teams_list_chats |
tasks/teams_read_v1.py |
teams_harvest_chat_list |
Production |
teams_list_channels |
tasks/teams_read_v1.py |
teams_harvest_channel_list |
Production |
teams_list_chat_messages_chunk |
tasks/teams_read_v1.py |
teams_harvest_message_rows, teams_message_scroll_step, teams_restore_scroll |
Production |
teams_list_channel_posts_chunk |
tasks/teams_read_v1.py |
same harvest + scroll | Production |
teams_read_thread_replies |
tasks/teams_read_v1.py |
thread navigation + harvest | Production |
teams_search_messages_chunk |
production | SERP AAMkAD… id, aria-label parse, click+expand, date filter, next page |
Production |
Scripts: src/forge_lcdl/playwright/scripts/teams_readonly.py. Search chunk engine: connectors/teams_search_chunk.py (orchestrates public atoms in connectors/teams_search_atoms.py; post-click ensure_teams_search_surface + day-scope reapply).
Search ingest atoms
| Atom | Module | Responsibility |
|---|---|---|
teams_ensure_search_surface |
teams_search_atoms |
Wrap ensure_teams_search_surface |
teams_open_messages_search |
teams_search_atoms |
Messages tab + * sync |
teams_apply_single_day_scope |
teams_search_atoms |
Date chip or query scope |
teams_verify_single_day_scope |
teams_search_atoms |
Chip/query + UI state verdict |
teams_guard_single_day_scope |
teams_search_atoms |
Per-page re-apply + card cap |
teams_harvest_serp_cards |
teams_search_atoms |
SERP card harvest + message filter |
teams_advance_serp_next_page |
teams_search_atoms |
Next pagination |
teams_harvest_card_detail |
teams_search_atoms |
Click + expand + rail merge |
teams_restore_serp_after_detail |
teams_search_atoms |
Return to SERP after detail |
teams_probe_day_pagination |
teams_search_atoms |
Discover-only pagination walk |
End-to-end (LCDL slice)
Chat pane chunk (production)
session_ref resolved → PlaywrightPageSession
→ navigate(url) optional (skip_navigate when CDP tab already on chat)
→ evaluate_readonly(teams_page_health) # probe
→ loop: evaluate_readonly(harvest_message_rows)
scroll_readonly(message_scroll_step, direction=up|down)
merge rows → UnifiedItem
checkpoint (scroll_top, seen_item_ids, at_end)
→ wire JSON: items[], next_cursor, chunk.has_more
Global search chunk (production)
session_ref → PlaywrightPageSession
→ keyboard: Ctrl+Alt+E, Messages tab, ``*`` + Enter
→ evaluate: apply date_from / date_to on search filters (keyboard From/To + Apply)
→ loop: harvest search result rows
keyboard: PageDown SERP load; focus Next + Enter for more pages
per hit: focus row + Enter → wait for highlight → harvest_message_rows
after detail: ensure_teams_search_surface + reapply day scope before next card
at_end when all cards known and Next disabled
checkpoint (page_index, seen_item_ids, nav_method)
→ wire JSON: items[], next_cursor, chunk.has_more, warnings[]
Consumer calls via cockpit_server.lcdl_teams_bridge.run_task — LCDL does not import Cockpit.
Responsibility matrix
| Concern | forge-lcdl | Cockpit consumer | Edge |
|---|---|---|---|
| Governed read tasks | teams_read_v1.py + teams_search_messages_chunk |
lcdl_teams_bridge.run_task |
— |
| DOM selectors / evaluate JS | teams_readonly.py |
Must not add page.locator in teams.py |
Teams v2 UI |
| Chunk checkpoint wire | TeamsChunkCheckpoint in teams_chunk.py |
Passed as checkpoint / next_cursor in bridge loop |
— |
| Scroll + virtualization | teams_message_scroll_step (direction up/down), restore_scroll |
scroll_direction, max_scroll_rounds via bridge |
Message pane visible |
| Search UI automation | teams_search_chunk.py, teams_search_keyboard.py |
Orchestration, budget, COCKPIT_TEAMS_NAV_MODE (default keyboard), date window filter; SERP virtualization via PageDown in keyboard mode |
Operator signed in |
| UnifiedItem mapping | teams.py connector + teams_modern.py |
wire_item_to_cockpit_fields |
— |
| HTML snapshot parser (legacy) | connectors/teams.py _TeamsListParser |
Fallback only if COCKPIT_TEAMS_DOM_FALLBACK=1 |
— |
| CDP tab pick | pick_teams_cdp_page_target, teams_urls_equivalent_for_nav |
teams_cdp_attach.py + routine sync |
Existing Teams tab |
| Tab visibility (background ingest) | apply_cdp_focus_emulation on attach |
Same env as Outlook (FORGE_LCDL_CDP_FOCUS_EMULATION, bring-to-front off) |
Chat list must not stay document.hidden |
unified_message, sync_run |
— | connectors/teams.py |
— |
| Sync UI labels | — | SyncProgressPanel.tsx |
— |
| Read-only guarantee | READONLY_POLICY.md, task read_only: true |
audit_read_only_ingestion |
Operator obeys policy |
What LCDL must not do
| Forbidden in LCDL | Belongs in consumer |
|---|---|
unified_message, message_channel |
Cockpit SQLite |
| Ingest budget / channel cap 100 | sync_service.py |
write_capture, files/Teams/ paths |
Cockpit storage.py |
| Checkpoint files on disk | Consumer optional (Teams: in-memory loop today) |
| Plaud, Graph app registration | Cockpit / org MSAL |
TeamsHarvestWindow / guided manifest |
Cockpit teams_harvest_window.py, guided scripts |
See MESSAGING-CONSUMER-BOUNDARY.md.
Preview vs thread detail
| Task | Body depth |
|---|---|
teams_search_messages_chunk |
Search hit preview; fuller text from highlighted pane row when click succeeds |
teams_list_chat_messages_chunk |
body_preview from list row DOM (~500 chars) |
teams_read_thread_replies |
Reply text under opened thread |
No task downloads chat attachments or SharePoint files.
Run status / open questions
| Topic | LCDL status |
|---|---|
| Upward scroll for older chat history | Done (run 2) — direction=up, scrollable ancestor picker |
| Author/body split in harvest rows | Done (run 2) |
teams_search_messages_chunk |
Production — Exchange SERP ids + click-through body harvest |
| @mentions harvest script | Deferred |
| Shared files tab metadata | Deferred |
| In-page REST capture | TBD — consumer network sniff only |
| Channel posts in default sync loop | Deferred — task exists, consumer does not loop it yet |