diff --git a/docs/achievements.md b/docs/achievements.md index 27f4622a..bf2a6001 100644 --- a/docs/achievements.md +++ b/docs/achievements.md @@ -31,3 +31,46 @@ Since Auto Runs can execute in parallel across multiple Maestro sessions, achiev But let's be real — getting to Level 11 is going to take some serious hacking. You'll need a well-orchestrated fleet of agents running around the clock, carefully crafted playbooks that loop indefinitely, and the infrastructure to keep it all humming. It's the ultimate test of your Maestro skills. The achievement panel shows your current rank, progress to the next level, and total accumulated time. Each rank includes flavor text and information about a legendary conductor who exemplifies that level of mastery. + +## Sharing Your Achievements + +Generate a shareable image of your achievements to celebrate milestones or compare progress with other Maestro users. The share image captures **unique statistics not tracked anywhere else** in the app. + +![Achievement Share Image](./screenshots/achievements-share.png) + +**To generate a share image:** +1. Open the Achievements panel +2. Click **Share** in the header +3. Choose **Copy to Clipboard** or **Save as Image** + +### Stats Captured in Share Images + +The share image includes comprehensive usage statistics: + +| Stat | Description | +|------|-------------| +| **Sessions** | Total number of AI sessions created | +| **Total Tokens** | Cumulative tokens processed across all sessions | +| **Total AutoRun** | Cumulative Auto Run execution time | +| **Longest AutoRun** | Your personal record for longest continuous Auto Run | +| **Hands-on Time** | Time spent actively interacting with Maestro | +| **Registered Agents** | Peak number of agents you've configured | +| **Parallel AutoRuns** | Peak simultaneous Auto Runs achieved | +| **Parallel Queries** | Peak simultaneous AI queries in flight | +| **Queue Depth** | Peak message queue depth reached | + +These peak usage stats are tracked automatically and persist across sessions. They represent your high-water marks — evidence of your most intensive Maestro orchestrations. + +## Keyboard Mastery + +Separate from Conductor ranks, Maestro tracks your **keyboard mastery** based on shortcut usage. As you discover and use more keyboard shortcuts, you level up through 5 mastery levels: + +| Level | Title | Shortcuts Used | +|:-----:|-------|----------------| +| 0 | Novice | 0-19% | +| 1 | Apprentice | 20-39% | +| 2 | Journeyman | 40-59% | +| 3 | Expert | 60-79% | +| 4 | Master | 80-100% | + +Your current keyboard mastery level is shown in the status bar. Hover over the keyboard icon to see which shortcuts you've used and which remain to be discovered. See [Keyboard Shortcuts](./keyboard-shortcuts) for the full shortcut reference. diff --git a/docs/autorun-playbooks.md b/docs/autorun-playbooks.md index 6c6c0788..0ac38326 100644 --- a/docs/autorun-playbooks.md +++ b/docs/autorun-playbooks.md @@ -110,6 +110,24 @@ Each completed task is logged to the History panel with: - `Enter` - View full response - `Esc` - Close detail view and return to list +## Expanded Editor View + +For editing complex Auto Run documents, use the **Expanded Editor** — a fullscreen modal that provides more screen real-estate. + +**To open the Expanded Editor:** +- Click the **expand icon** (↗️) in the top-right corner of the Auto Run panel + +![Expanded Auto Run Editor](./screenshots/autorun-expanded.png) + +The Expanded Editor provides: +- **Edit/Preview toggle** — Switch between editing markdown and previewing rendered output +- **Document selector** — Switch between documents without closing the modal +- **Run controls** — Start, stop, and monitor batch runs from the expanded view +- **Task progress** — See "X of Y tasks completed" and token count at the bottom +- **Full toolbar** — Create new documents, refresh, and open folder + +Click **Collapse** or press `Esc` to return to the sidebar panel view. + ## Auto-Save Documents auto-save after 5 seconds of inactivity, and immediately when switching documents. Full undo/redo support with `Cmd+Z` / `Cmd+Shift+Z`. diff --git a/docs/context-management.md b/docs/context-management.md index 491c6201..9126bd48 100644 --- a/docs/context-management.md +++ b/docs/context-management.md @@ -4,6 +4,41 @@ description: Compact, merge, and transfer conversation context between sessions icon: layers --- +## Tab Menu + +Right-click any tab to access the full range of context management options: + +![Tab Menu](./screenshots/tab-menu.png) + +| Action | Description | +|--------|-------------| +| **Copy Session ID** | Copy the Claude Code session ID to clipboard | +| **Star Session** | Bookmark this session for quick access | +| **Rename Tab** | Give the tab a descriptive name | +| **Mark as Unread** | Add unread indicator to the tab | +| **Context: Copy to Clipboard** | Copy the full conversation to clipboard | +| **Context: Compact** | Compress context while preserving key information | +| **Context: Merge Into** | Merge this context into another session | +| **Context: Send to Agent** | Transfer context to a different agent | + +## Tab Export + +Export any tab conversation as a self-contained HTML file: + +1. Right-click the tab → **Context: Copy to Clipboard** +2. Or use **Command Palette** (`Cmd+K`) → "Export tab to HTML" + +The exported HTML file includes: +- **Full conversation history** with all messages +- **Your current theme colors** — the export adopts your active Maestro theme +- **Maestro branding** with links to the website and GitHub +- **Session metadata** — agent type, working directory, timestamps, token usage +- **Rendered markdown** — code blocks, tables, and formatting preserved + +This is useful for sharing conversations, creating documentation, or archiving important sessions. + +--- + Context management lets you combine or transfer conversation history between sessions and agents, enabling powerful workflows where you can: - **Compact & continue** — Compress your context to stay within token limits while preserving key information @@ -33,10 +68,19 @@ When your conversation approaches context limits, you can compress it while pres Combine context from multiple sessions or tabs into one: -1. **Right-click** a tab → **"Merge With..."**, or use **Command Palette** (`Cmd+K` / `Ctrl+K`) → "Merge with another session" -2. Search for or select the target session/tab +1. **Right-click** a tab → **"Context: Merge Into"**, or use **Command Palette** (`Cmd+K` / `Ctrl+K`) → "Merge with another session" +2. Search for or select the target session/tab from the modal 3. Review the merge preview showing estimated token count -4. Click **"Merge Contexts"** +4. Optionally enable **Clean context** to remove duplicates and reduce size +5. Click **"Merge Into"** + +![Merge Modal](./screenshots/tab-merge.png) + +The modal shows: +- **Paste ID** tab — Enter a specific session ID directly +- **Open Tabs** tab — Browse all open tabs across all agents +- **Token estimate** — Shows source size and estimated size after cleaning +- **Agent grouping** — Tabs organized by agent with tab counts The merged context creates a new tab in the target session with conversation history from both sources. Use this to consolidate related conversations or bring context from an older session into a current one. @@ -48,25 +92,35 @@ The merged context creates a new tab in the target session with conversation his - You can merge tabs within the same session or across different sessions - Large merges (100k+ tokens) will show a warning but still proceed - Self-merge (same tab to itself) is prevented +- Enable "Clean context" for large merges to reduce token count ## Sending to Another Agent Transfer your context to a different AI agent: -1. **Right-click** a tab → **"Send to Agent..."**, or use **Command Palette** (`Cmd+K` / `Ctrl+K`) → "Send to another agent" -2. Select the target agent (only available/installed agents are shown) -3. Optionally enable **context grooming** to optimize the context for the target agent -4. A new session opens with the transferred context +1. **Right-click** a tab → **"Context: Send to Agent"**, or use **Command Palette** (`Cmd+K` / `Ctrl+K`) → "Send to another agent" +2. Search for or select the target agent from the list +3. Review the token estimate and cleaning options +4. Click **"Send to Session"** -**Context Grooming:** -When transferring between different agent types, the context can be automatically "groomed" to: -- Remove agent-specific artifacts and formatting -- Condense verbose output while preserving key information -- Optimize for the target agent's capabilities +![Send to Agent Modal](./screenshots/tab-send.png) -Grooming is enabled by default but can be skipped for faster transfers. +The modal shows: +- **Searchable agent list** with status indicators (Idle, Busy, etc.) +- **Agent paths** to distinguish between agents with similar names +- **Token estimate** — Shows source size and estimated size after cleaning +- **Clean context option** — Remove duplicates and reduce size before transfer + +**Context Cleaning:** +When transferring between agents, the context can be automatically cleaned to: +- Remove duplicate messages and verbose output +- Condense while preserving key information +- Optimize token usage for the target session + +Cleaning is enabled by default but can be disabled for verbatim transfers. **Use Cases:** - Start a task in Claude Code, then hand off to Codex for a different perspective - Transfer a debugging session to an agent with different tool access - Move context to an agent pointing at a different project directory +- Share context with a worktree sub-agent working on the same codebase diff --git a/docs/docs.json b/docs/docs.json index 5940092c..a9f78de3 100644 --- a/docs/docs.json +++ b/docs/docs.json @@ -57,9 +57,11 @@ "general-usage", "keyboard-shortcuts", "slash-commands", + "speckit-commands", "git-worktrees", "group-chat", "autorun-playbooks", + "history", "context-management", "achievements" ] diff --git a/docs/general-usage.md b/docs/general-usage.md index 1ae50d1b..783a3e1d 100644 --- a/docs/general-usage.md +++ b/docs/general-usage.md @@ -33,6 +33,25 @@ Browse project files with syntax highlighting, markdown preview, and image viewi ![File viewer](./screenshots/file-viewer.png) +## Prompt Composer + +For complex prompts that need more editing space, use the **Prompt Composer** — a fullscreen editing modal. + +**To open the Prompt Composer:** +- Click the **pencil icon** (✏️) in the bottom-left corner of the AI input box + +![Prompt Composer Button](./screenshots/prompt-composer-button.png) + +The Prompt Composer provides: +- **Full-screen editing space** for complex, multi-paragraph prompts +- **Character and token count** displayed in the footer +- **All input controls** — History toggle, Read-only mode, Thinking toggle, and send shortcut indicator +- **Image attachment support** via the image icon in the footer + +![Prompt Composer](./screenshots/prompt-composer.png) + +When you're done editing, click **Send** or press the displayed shortcut to send your message. The composer closes automatically and your prompt is sent to the AI. + ## Output Filtering Search and filter AI output with include/exclude modes, regex support, and per-response local filters. diff --git a/docs/history.md b/docs/history.md new file mode 100644 index 00000000..3eb2b7dd --- /dev/null +++ b/docs/history.md @@ -0,0 +1,126 @@ +--- +title: History +description: Track all agent activity with searchable, filterable history including Auto Run completions and user annotations. +icon: clock +--- + +The History panel provides a timestamped log of all agent activity — both automated (Auto Run) and manual (user interactions). Use it to review past work, resume sessions, and validate completed tasks. + +![History Panel](./screenshots/history-1.png) + +## Entry Types + +History entries are categorized by source: + +| Type | Label | Description | +|------|-------|-------------| +| **AUTO** | 🤖 AUTO | Entries created by Auto Run task completions | +| **USER** | 👤 USER | Entries created manually by the user | + +### Auto Entries + +Auto entries are created automatically when Auto Run completes a task. Each entry includes: +- **Summary** of what the agent accomplished +- **Session ID** (clickable to jump to that conversation) +- **Duration** and **cost** of the task +- **Timestamp** of completion + +### User Entries + +User entries are created in two ways: + +1. **History Toggle** — Enable the **History** pill in the AI input box. Every prompt-response cycle automatically creates a user history entry. + +2. **`/history` Command** — Run `/history` to create a synopsis entry covering all activity since the last time you ran the command. This is useful for periodic summaries without logging every single interaction. + +**Toggle the default History behavior** in Settings → toggle "Save to History by default". + +## Filtering History + +### By Type + +Use the **AUTO** and **USER** filter buttons at the top of the History panel to show or hide each entry type: +- Click **AUTO** to toggle Auto Run entries +- Click **USER** to toggle user-created entries +- Both can be active simultaneously + +### By Keyword + +Type in the search box to filter entries by keyword. The search matches against: +- Entry summaries +- Session IDs +- Any text content in the entry + +### By Time Range + +The **Graph View** at the top shows activity distribution over time. **Right-click the graph** to change the time range: +- Last 24 hours +- Last 7 days +- Last 30 days +- All time + +The graph bars are clickable — click a time period to jump to entries from that window. + +## Entry Details + +Click any history entry to open the **Detail View**: + +![History Detail View](./screenshots/history-2.png) + +The detail view shows: +- **Full entry header** with type badge, session ID, timestamp, and validation status +- **Context usage** — tokens consumed and context window percentage +- **Token breakdown** — input tokens, output tokens +- **Duration** and **cost** +- **Full summary text** of what was accomplished +- **RESUME button** — Jump directly to the AI session to continue from where Maestro left off + +### Navigation + +- **Prev / Next** buttons to navigate between entries +- **Close** button to return to the list view +- **Delete** button to remove the entry + +## Validating Entries + +The **Validated** flag helps you track which Auto Run tasks have been human-reviewed. + +![Toggling Validated Status](./screenshots/history-3.png) + +**To mark an entry as validated:** +1. Open the entry detail view +2. Click the **VALIDATED** toggle in the header + +![Validated Icon in List](./screenshots/history-4.png) + +Validated entries show a **checkmark icon** (✓✓) in the list view, making it easy to see at a glance which tasks have been reviewed. + +**Workflow tip:** After an Auto Run batch completes, use the History panel to review each task: +1. Open the first AUTO entry +2. Click **RESUME** to jump to the session and verify the work +3. If satisfied, toggle **VALIDATED** +4. Click **Next** to review the next entry +5. Repeat until all entries are validated + +This ensures human oversight of automated work while maintaining the full context needed to continue any task. + +## Resuming Sessions + +Every history entry with a Session ID has a **RESUME** button. Clicking it: +1. Opens the AI Terminal for that agent +2. Loads the exact session where the work was done +3. Positions you to continue the conversation + +This is especially powerful for Auto Run tasks — you can pick up exactly where the agent left off, with full conversation context preserved. + +## Keyboard Navigation + +| Key | Action | +|-----|--------| +| `↑` / `↓` | Navigate between entries | +| `Enter` | Open detail view for selected entry | +| `Esc` | Close detail view, return to list | + +## Storage + +History is stored per-session in `~/Library/Application Support/Maestro/history/`. Each session maintains up to 5,000 entries. History files can be passed to AI agents as context for understanding past work patterns. diff --git a/docs/remote-access.md b/docs/remote-access.md index 9c44ad39..ce3c4264 100644 --- a/docs/remote-access.md +++ b/docs/remote-access.md @@ -38,6 +38,32 @@ To access Maestro from outside your local network (e.g., on mobile data or from 5. Use the Local/Remote pill selector to switch between QR codes 6. The tunnel stays active as long as Maestro is running - no time limits, no account required +## Static Port Configuration + +By default, Maestro assigns a **random port** each time the web server starts. This is a security-by-obscurity measure — attackers can't easily guess which port to target. + +However, if you need a **fixed port** (e.g., for firewall rules, reverse proxies, or persistent tunnel configurations), you can enable static port mode: + +1. Click the **OFFLINE** button to open the Live overlay +2. Toggle **Use Custom Port** to enable static port mode +3. Enter your desired port number (1024-65535) +4. The server restarts automatically on the new port + +**Use cases for static ports:** +- Punching a hole through a firewall or NAT +- Configuring a reverse proxy (nginx, Caddy) +- Setting up persistent SSH tunnels +- Integration with home automation systems + + +**Security Trade-off**: Using a static port removes one layer of security-by-obscurity. The randomized port and auto-generated auth token in the URL work together to protect access. With a static port, you're relying solely on the auth token for security. + +**Recommendations when using static ports:** +- Use Cloudflare tunnel for remote access instead of exposing ports directly +- Ensure your network firewall is properly configured +- Consider additional authentication at the network level + + ## Screenshots ![Mobile chat](./screenshots/mobile-chat.png) diff --git a/docs/screenshots/achievements-share.png b/docs/screenshots/achievements-share.png new file mode 100644 index 00000000..a9b35410 Binary files /dev/null and b/docs/screenshots/achievements-share.png differ diff --git a/docs/screenshots/autorun-expanded.png b/docs/screenshots/autorun-expanded.png new file mode 100644 index 00000000..6f1124d3 Binary files /dev/null and b/docs/screenshots/autorun-expanded.png differ diff --git a/docs/screenshots/history-1.png b/docs/screenshots/history-1.png new file mode 100644 index 00000000..6ae3bb78 Binary files /dev/null and b/docs/screenshots/history-1.png differ diff --git a/docs/screenshots/history-2.png b/docs/screenshots/history-2.png new file mode 100644 index 00000000..71a4d220 Binary files /dev/null and b/docs/screenshots/history-2.png differ diff --git a/docs/screenshots/history-3.png b/docs/screenshots/history-3.png new file mode 100644 index 00000000..e572423e Binary files /dev/null and b/docs/screenshots/history-3.png differ diff --git a/docs/screenshots/history-4.png b/docs/screenshots/history-4.png new file mode 100644 index 00000000..60458cf5 Binary files /dev/null and b/docs/screenshots/history-4.png differ diff --git a/docs/screenshots/prompt-composer-button.png b/docs/screenshots/prompt-composer-button.png new file mode 100644 index 00000000..ef756b2c Binary files /dev/null and b/docs/screenshots/prompt-composer-button.png differ diff --git a/docs/screenshots/prompt-composer.png b/docs/screenshots/prompt-composer.png new file mode 100644 index 00000000..adf8a903 Binary files /dev/null and b/docs/screenshots/prompt-composer.png differ diff --git a/docs/screenshots/speckit-commands.png b/docs/screenshots/speckit-commands.png new file mode 100644 index 00000000..bceca434 Binary files /dev/null and b/docs/screenshots/speckit-commands.png differ diff --git a/docs/screenshots/tab-menu.png b/docs/screenshots/tab-menu.png new file mode 100644 index 00000000..22c8ca70 Binary files /dev/null and b/docs/screenshots/tab-menu.png differ diff --git a/docs/screenshots/tab-merge.png b/docs/screenshots/tab-merge.png new file mode 100644 index 00000000..13315f69 Binary files /dev/null and b/docs/screenshots/tab-merge.png differ diff --git a/docs/screenshots/tab-send.png b/docs/screenshots/tab-send.png new file mode 100644 index 00000000..59cdc121 Binary files /dev/null and b/docs/screenshots/tab-send.png differ diff --git a/docs/speckit-commands.md b/docs/speckit-commands.md new file mode 100644 index 00000000..2b7e937d --- /dev/null +++ b/docs/speckit-commands.md @@ -0,0 +1,118 @@ +--- +title: Spec-Kit Commands +description: Structured specification workflow for AI-assisted development using GitHub's spec-kit methodology. +icon: file-text +--- + +Spec-Kit is a structured specification workflow from [GitHub's spec-kit project](https://github.com/github/spec-kit) that helps teams create clear, actionable specifications before implementation. Maestro bundles these commands and keeps them updated automatically. + +![Spec-Kit Commands in Settings](./screenshots/speckit-commands.png) + +## Spec-Kit vs. Wizard + +Maestro offers two paths to structured development: + +| Feature | Spec-Kit | Onboarding Wizard | +|---------|----------|-------------------| +| **Approach** | Manual, command-driven workflow | Guided, conversational flow | +| **Best For** | Experienced users, complex projects | New users, quick setup | +| **Output** | Constitution, specs, tasks → Auto Run docs | Phase 1 Auto Run document | +| **Control** | Full control at each step | Streamlined, opinionated | +| **Learning Curve** | Moderate | Low | + +**Use Spec-Kit when:** +- You want fine-grained control over specification phases +- You're working on complex features requiring detailed planning +- You prefer explicit command-driven workflows +- You want to create reusable constitutions and specifications + +**Use the Wizard when:** +- You're starting a new project from scratch +- You want to get up and running quickly +- You prefer conversational, guided experiences + +Both approaches ultimately produce markdown documents for Auto Run execution. + +## Viewing & Managing Commands + +Access Spec-Kit commands via **Settings → AI Commands** tab. Here you can: + +- **View all commands** with descriptions +- **Check for Updates** to pull the latest prompts from GitHub +- **Expand commands** to see their full prompts +- **Customize prompts** (modifications are preserved across updates) + +## Core Workflow (Recommended Order) + +### 1. `/speckit.constitution` — Define Project Principles + +Start here to establish your project's foundational values, constraints, and guidelines. The constitution guides all subsequent specifications and ensures consistency across features. + +**Creates:** A constitution document with core principles, technical constraints, and team conventions. + +### 2. `/speckit.specify` — Create Feature Specification + +Define the feature you want to build with clear requirements, acceptance criteria, and boundaries. + +**Creates:** A detailed feature specification with scope, requirements, and success criteria. + +### 3. `/speckit.clarify` — Identify Gaps + +Review your specification for ambiguities, missing details, and edge cases. The AI asks clarifying questions to strengthen the spec before implementation. + +**Tip:** Run `/speckit.clarify` multiple times — each pass catches different gaps. + +### 4. `/speckit.plan` — Implementation Planning + +Convert your specification into a high-level implementation plan with phases and milestones. + +**Creates:** A phased implementation roadmap with dependencies and risk areas. + +### 5. `/speckit.tasks` — Generate Tasks + +Break your plan into specific, actionable tasks with dependencies clearly mapped. + +**Creates:** A dependency-ordered task list ready for execution. + +### 6. `/speckit.implement` — Execute with Auto Run + +**Maestro-specific command.** Converts your tasks into Auto Run documents that Maestro can execute autonomously. This bridges spec-kit's structured approach with Maestro's multi-agent capabilities. + +**Creates:** Markdown documents in `Auto Run Docs/` with task checklists. + +## Analysis & Quality Commands + +### `/speckit.analyze` — Cross-Artifact Analysis + +Verify consistency across your constitution, specifications, and tasks. Catches contradictions and gaps between documents. + +### `/speckit.checklist` — Generate QA Checklist + +Create a custom checklist for your feature based on the specification. Useful for QA, code review, and acceptance testing. + +### `/speckit.taskstoissues` — Export to GitHub Issues + +Convert your tasks directly into GitHub Issues. Requires `gh` CLI to be installed and authenticated. + +## Getting Help + +Run `/speckit.help` to get an overview of the workflow and tips for best results. + +## Auto-Updates + +Spec-Kit prompts are automatically synced from the [GitHub spec-kit repository](https://github.com/github/spec-kit): + +1. Open **Settings → AI Commands** +2. Click **Check for Updates** +3. New commands and prompt improvements are downloaded +4. Your custom modifications are preserved + +The version number and last update date are shown at the top of the Spec Kit Commands section. + +## Tips for Best Results + +- **Start with constitution** — Even for small projects, defining principles helps maintain consistency +- **Iterate on specifications** — Use `/speckit.clarify` multiple times to refine your spec +- **Keep specs focused** — One feature per specification cycle works best +- **Review before implementing** — Use `/speckit.analyze` to catch issues early +- **Leverage parallelism** — With Maestro, run multiple spec-kit workflows simultaneously across different agents diff --git a/package-lock.json b/package-lock.json index d2507f5e..cd6d7952 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "maestro", - "version": "0.12.0", + "version": "0.12.1", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "maestro", - "version": "0.12.0", + "version": "0.12.1", "hasInstallScript": true, "license": "AGPL 3.0", "dependencies": { diff --git a/src/renderer/App.tsx b/src/renderer/App.tsx index cfdd8254..94a260d7 100644 --- a/src/renderer/App.tsx +++ b/src/renderer/App.tsx @@ -9127,6 +9127,33 @@ function MaestroConsoleInner() { }); }); }} + onExportHtml={async (tabId: string) => { + // Export tab conversation as HTML + if (!activeSession) return; + const tab = activeSession.aiTabs.find(t => t.id === tabId); + if (!tab || !tab.logs || tab.logs.length === 0) return; + + try { + const { downloadTabExport } = await import('./utils/tabExport'); + await downloadTabExport( + tab, + { name: activeSession.name, cwd: activeSession.cwd, toolType: activeSession.toolType }, + theme + ); + addToast({ + type: 'success', + title: 'Export Complete', + message: 'Conversation exported as HTML.', + }); + } catch (err) { + console.error('Failed to export tab:', err); + addToast({ + type: 'error', + title: 'Export Failed', + message: 'Failed to export conversation as HTML.', + }); + } + }} // Context warning sash settings (Phase 6) contextWarningsEnabled={contextManagementSettings.contextWarningsEnabled} contextWarningYellowThreshold={contextManagementSettings.contextWarningYellowThreshold} diff --git a/src/renderer/components/MainPanel.tsx b/src/renderer/components/MainPanel.tsx index 997d6edc..d8980a2b 100644 --- a/src/renderer/components/MainPanel.tsx +++ b/src/renderer/components/MainPanel.tsx @@ -194,6 +194,7 @@ interface MainPanelProps { onMergeWith?: (tabId: string) => void; onSendToAgent?: (tabId: string) => void; onCopyContext?: (tabId: string) => void; + onExportHtml?: (tabId: string) => void; // Context warning sash settings (Phase 6) contextWarningsEnabled?: boolean; @@ -250,6 +251,7 @@ export const MainPanel = React.memo(forwardRef( onMergeWith, onSendToAgent, onCopyContext, + onExportHtml, // Context warning sash settings (Phase 6) contextWarningsEnabled = false, contextWarningYellowThreshold = 60, @@ -897,6 +899,7 @@ export const MainPanel = React.memo(forwardRef( onSendToAgent={onSendToAgent} onSummarizeAndContinue={onSummarizeAndContinue} onCopyContext={onCopyContext} + onExportHtml={onExportHtml} showUnreadOnly={showUnreadOnly} onToggleUnreadFilter={onToggleUnreadFilter} onOpenTabSearch={onOpenTabSearch} diff --git a/src/renderer/components/TabBar.tsx b/src/renderer/components/TabBar.tsx index d4543387..c8f9ae19 100644 --- a/src/renderer/components/TabBar.tsx +++ b/src/renderer/components/TabBar.tsx @@ -1,6 +1,6 @@ import React, { useState, useRef, useCallback, useEffect } from 'react'; import { createPortal } from 'react-dom'; -import { X, Plus, Star, Copy, Edit2, Mail, Pencil, Search, GitMerge, ArrowRightCircle, Minimize2, Clipboard } from 'lucide-react'; +import { X, Plus, Star, Copy, Edit2, Mail, Pencil, Search, GitMerge, ArrowRightCircle, Minimize2, Clipboard, Download } from 'lucide-react'; import type { AITab, Theme } from '../types'; import { hasDraft } from '../utils/tabHelpers'; @@ -23,6 +23,8 @@ interface TabBarProps { onSummarizeAndContinue?: (tabId: string) => void; /** Handler to copy conversation context to clipboard */ onCopyContext?: (tabId: string) => void; + /** Handler to export tab as HTML */ + onExportHtml?: (tabId: string) => void; showUnreadOnly?: boolean; onToggleUnreadFilter?: () => void; onOpenTabSearch?: () => void; @@ -53,6 +55,8 @@ interface TabProps { onSummarizeAndContinue?: () => void; /** Handler to copy conversation context to clipboard */ onCopyContext?: () => void; + /** Handler to export tab as HTML */ + onExportHtml?: () => void; shortcutHint?: number | null; registerRef?: (el: HTMLDivElement | null) => void; hasDraft?: boolean; @@ -123,6 +127,7 @@ function Tab({ onSendToAgent, onSummarizeAndContinue, onCopyContext, + onExportHtml, shortcutHint, registerRef, hasDraft @@ -239,6 +244,12 @@ function Tab({ setOverlayOpen(false); }; + const handleExportHtmlClick = (e: React.MouseEvent) => { + e.stopPropagation(); + onExportHtml?.(); + setOverlayOpen(false); + }; + const displayName = getTabDisplayName(tab); // Browser-style tab: all tabs have borders, active tab "connects" to content @@ -455,6 +466,18 @@ function Tab({ Mark as Unread + {/* Export as HTML - only show if tab has logs */} + {(tab.logs?.length ?? 0) >= 1 && onExportHtml && ( + + )} + {/* Context Management Section - divider and grouped options */} {(tab.agentSessionId || (tab.logs?.length ?? 0) >= 1) && (onMergeWith || onSendToAgent || onSummarizeAndContinue || onCopyContext) && (
@@ -535,6 +558,7 @@ export function TabBar({ onSendToAgent, onSummarizeAndContinue, onCopyContext, + onExportHtml, showUnreadOnly: showUnreadOnlyProp, onToggleUnreadFilter, onOpenTabSearch @@ -733,6 +757,7 @@ export function TabBar({ onSendToAgent={onSendToAgent && tab.agentSessionId ? () => onSendToAgent(tab.id) : undefined} onSummarizeAndContinue={onSummarizeAndContinue && (tab.logs?.length ?? 0) >= 5 ? () => onSummarizeAndContinue(tab.id) : undefined} onCopyContext={onCopyContext && (tab.logs?.length ?? 0) >= 1 ? () => onCopyContext(tab.id) : undefined} + onExportHtml={onExportHtml && (tab.logs?.length ?? 0) >= 1 ? () => onExportHtml(tab.id) : undefined} shortcutHint={!showUnreadOnly && originalIndex < 9 ? originalIndex + 1 : null} hasDraft={hasDraft(tab)} registerRef={(el) => { diff --git a/src/renderer/utils/tabExport.ts b/src/renderer/utils/tabExport.ts new file mode 100644 index 00000000..cc7a0367 --- /dev/null +++ b/src/renderer/utils/tabExport.ts @@ -0,0 +1,768 @@ +/** + * @file tabExport.ts + * @description Export utility for AI tab conversations. + * + * Generates a self-contained HTML file with the user's current theme colors + * and properly rendered GitHub Flavored Markdown content using the marked library. + * Based on groupChatExport.ts but adapted for individual tab conversations. + */ + +import { marked } from 'marked'; +import type { AITab, LogEntry, Theme, UsageStats } from '../types'; + +// Configure marked for GFM (tables, strikethrough, etc.) +marked.setOptions({ + gfm: true, + breaks: true, +}); + +/** + * Escape HTML special characters + */ +function escapeHtml(text: string): string { + return text + .replace(/&/g, '&') + .replace(//g, '>') + .replace(/"/g, '"') + .replace(/'/g, '''); +} + +/** + * Format a timestamp for display + */ +function formatTimestamp(timestamp: number): string { + const date = new Date(timestamp); + return date.toLocaleString(); +} + +/** + * Format duration from milliseconds + */ +function formatDuration(logs: LogEntry[]): string { + if (logs.length < 2) return '0m'; + + const firstTimestamp = logs[0].timestamp; + const lastTimestamp = logs[logs.length - 1].timestamp; + const durationMs = lastTimestamp - firstTimestamp; + const durationHours = Math.floor(durationMs / (1000 * 60 * 60)); + const durationMins = Math.floor((durationMs % (1000 * 60 * 60)) / (1000 * 60)); + + return durationHours > 0 + ? `${durationHours}h ${durationMins}m` + : `${durationMins}m`; +} + +/** + * Get color for log entry source + */ +function getSourceColor(source: LogEntry['source'], theme: Theme): string { + switch (source) { + case 'user': + return theme.colors.accent; + case 'ai': + case 'stdout': + return theme.colors.success; + case 'error': + case 'stderr': + return theme.colors.error; + case 'system': + return theme.colors.warning; + case 'thinking': + return theme.colors.textDim; + case 'tool': + return theme.colors.accentDim; + default: + return theme.colors.textMain; + } +} + +/** + * Get display label for log entry source + */ +function getSourceLabel(source: LogEntry['source']): string { + switch (source) { + case 'user': + return 'User'; + case 'ai': + case 'stdout': + return 'AI'; + case 'error': + case 'stderr': + return 'Error'; + case 'system': + return 'System'; + case 'thinking': + return 'Thinking'; + case 'tool': + return 'Tool'; + default: + return source; + } +} + +/** + * Format usage stats for display + */ +function formatUsageStats(stats: UsageStats | undefined): string { + if (!stats) return 'N/A'; + + const parts: string[] = []; + if (stats.inputTokens) parts.push(`${stats.inputTokens.toLocaleString()} input`); + if (stats.outputTokens) parts.push(`${stats.outputTokens.toLocaleString()} output`); + if (stats.totalCostUsd) parts.push(`$${stats.totalCostUsd.toFixed(4)}`); + + return parts.length > 0 ? parts.join(' · ') : 'N/A'; +} + +/** + * Process content to render markdown + */ +function formatContent(content: string): string { + // Render markdown to HTML using marked (synchronous) + const html = marked.parse(content, { async: false }) as string; + return html; +} + +/** + * Generate the HTML export content with theme colors + */ +export function generateTabExportHtml( + tab: AITab, + session: { name: string; cwd: string; toolType: string }, + theme: Theme +): string { + // Filter to only include relevant log entries (user and AI messages) + const relevantLogs = tab.logs.filter( + (log) => ['user', 'ai', 'stdout', 'error', 'stderr', 'system', 'thinking', 'tool'].includes(log.source) + ); + + // Calculate stats + const userMessages = relevantLogs.filter((l) => l.source === 'user').length; + const aiMessages = relevantLogs.filter( + (l) => l.source === 'ai' || l.source === 'stdout' + ).length; + + const stats = { + totalMessages: relevantLogs.length, + userMessages, + aiMessages, + duration: formatDuration(relevantLogs), + }; + + // Generate messages HTML + const messagesHtml = relevantLogs + .map((log) => { + const color = getSourceColor(log.source, theme); + const isUser = log.source === 'user'; + const label = getSourceLabel(log.source); + + // Format content with markdown + const formattedContent = formatContent(log.text); + + return ` +
+
+ ${escapeHtml(label)} + ${formatTimestamp(log.timestamp)} + ${log.readOnly ? 'read-only' : ''} +
+
${formattedContent}
+
`; + }) + .join('\n'); + + // Build HTML document with theme colors + const colors = theme.colors; + + // Maestro app icon as base64 PNG (72x72) - same as groupChatExport + const maestroIconBase64 = 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAEgAAABICAIAAADajyQQAAAAIGNIUk0AAHomAACAhAAA+gAAAIDoAAB1MAAA6mAAADqYAAAXcJy6UTwAAAAGYktHRAD/AP8A/6C9p5MAAAAHdElNRQfpDAEGJCk3BG/2AAAQRElEQVRo3p1bfZBeVXn/Ped+vR/52myiFBJSokz1D8Wx+TIf1U5JUh1HW/6oZDXL1D9sZwokOp0WxBCg7RTb0lEpiIPGKlnUhGmn7ZTEEBIMhGCgltJxKpqQIVVAyO42ZHffvV/n6R/365xzz727eGcnue99z3nO8zvP93PuS3evnkHTRQDbPzKB1JGM8jNzPraalQ0AgdgYnw+mnLBG00bNYKfkKievjHA1rqhYiPOH5XoqEiZ1jYI1fcGcpkKHMiazz6QxqLKrImSFmg63mKgvzgpNV9snLvcuZ6v8ihTaxJUAWBMOg4mzlVibSCoRZRYrq9SFpu1ssV0V7ZI9LvZOkaxbE35dHSxXSYIMTkjbgsbp9cUaJuojyZxlYym7xHxgtEGm4t9SHzINUTmqTWzEblAjOwWuzyrUUgNWX4YV/bbSywawsjwbw9hCUP2GqXrIALONXUXrDBYsVkcVfTZtzJBNs1JValN4Bmofr99zRoE0p6dtDSkT2UKH1MHFDVE1pW5jNRbqrDVxTTbMdYKUo4JuNGRdywRds8ZiQLuN8fx8R+tFdrtCqd6GShtSaLHk9kX1j2K+BObh6+YDeW7K7R/VkNWCli1esQHB/CXZaODFQ0PTaptt/6joJOs6bB1fA8YNCCqPMxfr1CpeMp0BtVMzGCAtLtsduh2YbVDTrvzqBqkmf+1EjOAxb4uoAaNWvmkugUCJznXnZrtn63PjoTXct7qZuirOTyWaRzYHNJvXe0tu+K0oSGsc45pvboJaryuUXFpPe/U6xFKogARIEMDMYGnXQzaMs8aAWyu6lOSEuC1AW5kzs7ACnwBLkAOWc+w0OYgHiAcMsBuQ37dM4UzOavJfgityAF1iKq+/auCyuANCOAOvw9E0gn4bNhIYvMmXvYeu2uwIgfPPpv97mjsLazWZXeDaym6tylPGqqpoqFZJuFZsECujiIgwuMi//XnnN3e6R+6Mnx+TC5ZBJjZKDmYmsPFGZ9tev2Tj6a9Gj92ZdBYKECBZc/caR6QRZDjXDt3WvIENH0n5a5lFAGjwptx6h7vlJt/v0bs/7F58RZ7/ofT7ubYzgShHNT3OWz7nbt0TgElKgMASV65zesP8P/+eegFVpXi99VBj1bl26W0ao8Qg0ioiFUMTGEUTKm8vEM/wB//M3XKzL1OwBAm868PO5Hn56gvsdorkgUAupsd582edrbcFMgE5kCnHA3gdSkJeucbtLeOfPJq6gVLeVlzlzQcu61wiAEKvigrshStnBjPAeb3U9FfqYBVOCUmEhZfTBz/nZdwLF2nCAG39gu/44DRXKhKYvlChEi4A/s5o+OBHZqfHpRtQEvK6P/S33unOTjGJvJuglJVlE0OTXWscY01ClUQLj021AUJU+Y7j4dJreO7bCQCWYAkiAvDsP8ZpSCQAgBwTFTPv3xGePcaTZ/Ct68IMm0yw8Y/8VRspnIIQFXslPDL4Z2vmYbs37JVsg1li9k2wrLC5Ph69JX7+QCxckIBwcOLvoxP3pH4fzBAOpi/wpt0aqrGR8MxR2RtCZyEmzmDfx8KZCSlcAFi1wUkitcHUFmjaex5ksagGG5Mp3B6u3k5uDzLNhwlBQY/+dXf8H/tjgI//TXTsr5PeEAEQAlPjvOlmZ9ueApXkh0fCs0e5v4xkDBCkRGcRhJd3BWXSauMGm20NU9T8T0O8Fi4GF/mj93jvH/H+87vxv3027i4mmQAMCLBEGmPB23HpFXg9oLCrTbucbbfnqGTKYzvCs8e5P4w0huPRzKS8Yg3d8EjgdQVLkOCvf2T2tRfgdfNISHWuTInVLakuMMNJVoGNASQhWGJ4tQAwvFowIwmrVUnADTD9Ovx+DVWay2psZPbscdlflqHCzCSvWEejjwReV6QRk8CxL0a/+BFnOmxubsaP7rpbc8XG/aC8biPIhCCw4NcQzVDeSxHoL4ffw/TrmTDzlo3jEUsWDqYKVCwhHIRT8pHPRGeP5RroeJiZxIp1NHow8DqURuz4dOJL0Yl7ZG8JcVoGMxOboaKu3QBbrLKUFoElHJ+ve8BftcGJZxEsBIDLrxF/fKzj9+jcU+k/3xjniR0DYHIwNc6bdjvb9gQsIRN2fHr+QPLf/5Quf6eIB3B8zExgxVoaPZChguPTk1+OHv+rpD9Eai7G1GBshSTMZk4Rt+aolfN4lcJfgNW/Jbwu9YbIcYklHI8WLBd+n975O06wgGWa8VGg2uVs2xPIFCTg+CRTrP+094E/cS6+Kt0uZiZ4xVrsPOh7XUoidnw8+ZXo6F/G/aXEnEdTznMq1oKZEdgILlg9BMnVyTgEqTe3sjHCw/Qb2L8jWrXeCS/xmhvcpVeJ8Zfkc99OekP08ql0ZpzcAGAIh6Yv8ObdlQ+cfVOyRHeJANPH7+k47uxTX5FXXytGDwRel9IIrk9P3hsd/Yukv1RkxUvFKAMg0lrOpG15Xo9VCS4XWpY/oeKJWfNk+CXcgM6f5JeOJ9GA3/EhZ+lVmDgnn/jbJOiR48HvERjC4akLvGmXu22Pn/vAhMdGwjjEp/8l8HuCGR/9Ymfxyuh9f+B6XUoiuD6eui86elfSHyJOFXXW1abMqIyGMQChHIdw5fTUVrCRXxS4CUwEMPt9LFiO3hAcnwG4HfSGsGA5ggUAWLg8Nc4bdznbbvfThIULmciHPjH7i+f4jR/jmx+fDS9JIsiUt9zoL3ybSGN2fZy8Pzp6R9IfImbOj1FI+7dUuYoZ3UcIg11DoGx+oUFjgEEsIROwpFNfS37+o/TUAwmYZILM6U1d4E03OdtvD2QCxyWZ8EPXh+ee5O4S6izCL3+Mfb83OzMphUNpzDKB49HJ+6PH7kh6Q8QMZlurodbKrzPp5kVX2aTVcydiZbqttZT9LyX8Hs4+zj89HDoe+b28Xp66wBtvcrftrTTwoevDcz9Af1ikMTsewOT4yPNGIuHi5H0Vqurgq6gMS7HkMmtqcjFcu+ejpifavVZwM7wefKKsUeE4mBrnjTc720tUKT+0Y/bcCe4vozTiPF6txw0HAzcQWVw+eV90JNdAYz+tpyu2q7DFeoCes9Nn3SAgS+EBAMIp8kAlY9p//ey5H3B/mNJIyS0OBG4gsiicaWB/iLjokZSeSynJy/+Ys/qrYEH13EzWTrARwmrCs25T+Zfn7AaqHeFLT6C/tEA1wSvWidGDHa8jkgLVkb1Jb4jASCMMJpEFQM1NaJ6dqi4yo/qy8HdvvRPc2ngkF9MXeKOCiiWPjYQvHZdFdouZCV6xPs8tkohdn57+anRkb9wbIgaSCP3l/N5PCK8I7sbqZkvcds4IQFhcuZFY1TsfDa0O4eay2r63QrV/JDx7TPaXUZXdbqDRg50iXtHTD0RH9ib9opZJE952l/f79wYf+lM3GrDwdO7qfQqy8+zOrXLz68Nlnl2TFfPYSHj2cdlfVmogVn6Adn4vkxVcH6e+Fh3Zk1donCCKIRNa+HYBYNFlIg0Rz0A4IKG2EHUpkY1bhlu1GMvH1oze5usrVC6mdLvKa+HHub9M5D5wAis3ZN4iR/XMg9H39yS9JQQgCeH1MXw1xbNwPABwAix/FwULafJllmFVJVi64jaWaoWmuiXNlaU2w8G07gMB3j8SnnmM+8NIEzgeZsaxcgPtPFDJ6pkHo8O3Jb3FxIBM4fUw8rB/+XtFEsLxKPOKScReBy+fSr97Q8Qpqa9xVOxZJVbZmGFO9RZikwa6JipmHvtkeOaxsmqkmQms3IAcVcgVqiWFI5DwunjbbwiA3CBHRQJehwC67D0ir5rr1XATY2ahySa25sgOojy3sGjgUdWueMX6EhXcgJ75eoGqKDyEh6lf4lvXhZdfI6Jp3rzLHV7tvP5ieur+pDtE559NB5N5lZCvbqs2KrYtAZosw+zTSanw91Qa+PCnwjNHOUflF/GqQoUffiM+/PkcVZWJMtwAr/0X//zZNAn5fTvc4dW49ApOfyP1e3B88rqAIjFq1qDyMjIPq6OxAROYHufNu52tX/BlwsIlgMc+OfuzIwaqvMLPUJ3eFx26Neku0TKmUgheD8FChNMgkcmRu4vRWUxpoqGaJ5d6gGabPdWYIIHpiQxVkCYoUIUVqkwD19LogU6F6pvxo7ckvcUEthy4EMApZAJIeuLvkhcPJ09+KSFBMgbqpzOs5hw20TDo7nfMVI/mIWJyMLjI6z/j/O5digbuDH96WInCE1ixFnkULlAd+vO4s5js3OgQ4wGSWXhdeB3zDal5cdqQBM8h4HiA5e+m7Xf6QA1VroG4Yg12qqj2xY/eEndtqCwsMvwugj6xZA1VrVZq2RrUO8FsuVOmEOIBX7lWEGWlJI99avZn35dVzj7BV6yh0YOB3628RYmqvv1kW5sZMuXaYKU/35KvWnr3TadhTTAJLBEP8pFZHnjFGho96Ps9UaCKDt0ad5eYqNhkQ3/3yLrXRc2b1Zu1wfo+VGWL7nzNzlv5EqiE16Xzp2XW+hQujR7o/PoWmvk/HlxELqueSELOZHXo1qSbeQvOWydWrttVy7LhRe/GOExSqQrzqLbhY5UidPDGT/jQngiATCAc2vm9zpUb6bJrMPpIiYpO74sP3VrYlcQcqxSHUo0v27Cyv6U/LKVW99vcfChhmHWWauSJgoPpSd54o7N9b5Am7LiUhCxTVlHldsVm50zpXRTnkGUDs9SLsi5mkxPjhJ3LYM0FzLKvWK8GYKMC9bhTor+Unr43BcLtewMAjkeOT8zQUKm5j6WvVGu21COoDqiRvbKDoIxwjSKTlZzelJiyAKfoD9Opf0iTQbj1dt/vEQCWfOLL8fG7k+4iUi3Tql1UtWeV90nKRllxY62ftNqjrLS44h9zno81XMRgIpCgwUVesgor3i+Ei1dfkG+8iM4iXekJGteNJPVNbR1sDdDGQ1s9xrZ7g0qhYMJBEiKezRPZrL5gzXcphkVsWAmXUiV9liJQrqMuqKpMGsBsmUcTHptXlhKOD7cDACwhZcss/YcEiksjqo8vTqr0KfltoYvVu0flEO1QwpDoW7qKsx2qOQa7tnCeaRtNXDZEU9tlS6fblseU5tf+9tvcmKstpNwlmD/aqXWTqe7EWPMExpSW/ksdbfnVvF9P15i1OUwUxznW7+rL680VRoOUNcMrqKp7ZzjxRlVUL25+SDWTodbx9X3SFJeqn3CozsOYbs2gbEcPmOM9j9YcjouNJyp+olAeYbXPJfsNaQPqZUZDadDQbmp9M6f+nCsqTY2HCtVcjSRUxNreoKWmJ4Z66+NszZyW2Nha2jSiLN3FfI5x5rNEEzWFc2FO4Eq3GY2sqIm4mayzoUasfpUTb8zLLa7YfI2BlVmGzSofa2/mGDtVE11LCGgJgZUfINt4lddGzbM0fZuLbLXQ1BcroyWXm21jyNg7LoRo7F+1LFsIamVJrSnSnmCaZXFxma9DGLHP3CylZCrKvuqNVi1n00VZGC8BXPyyM3vVIjs8JyKdRJ6b2JsiVvswFnXLLEeZa8Y8qtIwYkWtdGFVS5Rn9UB27M8K0ZIYa7unMM16GVKSpZqYtfiva/r/A9X3zV17wf7gAAAAJXRFWHRkYXRlOmNyZWF0ZQAyMDI1LTEyLTAxVDA2OjI4OjM3KzAwOjAwWNgikQAAACV0RVh0ZGF0ZTptb2RpZnkAMjAyNS0xMi0wMVQwNjoyODozNyswMDowMCmFmi0AAAAodEVYdGRhdGU6dGltZXN0YW1wADIwMjUtMTItMDFUMDY6MzY6NDErMDA6MDDIwmiiAAAAAElFTkSuQmCC'; + + // Get tab name or session ID for display + const tabName = tab.name || (tab.agentSessionId ? `Session ${tab.agentSessionId.split('-')[0].toUpperCase()}` : 'New Session'); + + return ` + + + + + ${escapeHtml(tabName)} - Maestro Tab Export + + + +
+ +
+ +
+
+ Maestro +
+
Multi-agent orchestration for AI coding assistants
+ +
+
+ +
+

${escapeHtml(tabName)}

+

Tab Export - ${formatTimestamp(tab.createdAt)}

+
+ +
+
+
${stats.totalMessages}
+
Messages
+
+
+
${stats.userMessages}
+
User
+
+
+
${stats.aiMessages}
+
AI
+
+
+
${stats.duration}
+
Duration
+
+
+ +
+

Details

+
+ ${tab.agentSessionId ? ` + Session ID + ${escapeHtml(tab.agentSessionId)} + ` : ''} + Session Name + ${escapeHtml(session.name)} + Agent + ${escapeHtml(session.toolType)} + Working Directory + ${escapeHtml(session.cwd)} + Created + ${formatTimestamp(tab.createdAt)} + Usage + ${formatUsageStats(tab.usageStats)} +
+
+ +
+

Conversation

+
+ ${messagesHtml} +
+
+ +
+

Exported from Maestro on ${formatTimestamp(Date.now())}

+ +
+
+ +`; +} + +/** + * Download the tab conversation as an HTML file + */ +export async function downloadTabExport( + tab: AITab, + session: { name: string; cwd: string; toolType: string }, + theme: Theme +): Promise { + // Generate HTML + const html = generateTabExportHtml(tab, session, theme); + + // Create blob and download + const blob = new Blob([html], { type: 'text/html;charset=utf-8' }); + const url = URL.createObjectURL(blob); + + // Generate filename from tab name or session ID + const filename = tab.name + ? tab.name.replace(/[^a-z0-9]/gi, '-').toLowerCase() + : tab.agentSessionId + ? tab.agentSessionId.split('-')[0].toLowerCase() + : 'tab'; + + const link = document.createElement('a'); + link.href = url; + link.download = `${filename}-export.html`; + document.body.appendChild(link); + link.click(); + document.body.removeChild(link); + + URL.revokeObjectURL(url); +}