- Add ClaudeWebParser to file structure and parser system section - Add summary-builder.ts to generators section - Update format detection to include Claude web paste date-stamp detection - Update parser registry order with ClaudeWebParser - Update title generation from 4-strategy to 5-strategy chain with assistant topic as highest priority - Replace Key Info Extractor with Summary Builder in note generation pipeline (questions asked, topics covered, key takeaways, references) - Mark key-info-extractor as legacy
66 KiB
Chat Splitter - Architecture Document
Status: v1.0 -- Current Plugin ID:
chat-splitterPlatform: Obsidian (Desktop & Mobile) Last Updated: 2026-02-28
This document is the complete technical specification for Chat Splitter.
Table of Contents
- Overview
- Architecture Overview & Data Flow
- File & Folder Structure
- Core Data Model
- Parser System
- Segmentation Engine
- Note Generation Pipeline
- UI/UX Specification
- Settings Schema
- Implementation Roadmap
- Risk Register
- Decision Log
- Verification Plan
Overview
Users accumulate long, multi-topic AI chat sessions (ChatGPT, Claude.ai) containing valuable planning notes, decisions, research, and code snippets trapped in monolithic transcripts. Existing plugins import conversations as-is (1 conversation = 1 note). Chat Splitter's differentiator is intelligent decomposition: breaking a single sprawling chat -- or any long document -- into multiple organized, topic-specific Obsidian notes with bidirectional links, tags, and an index note.
Key Design Principles
- Both copy-paste and file import equally important as input methods
- Offline-first heuristic segmentation as primary, with optional Ollama support (API keys deferred to future work)
- Per-import configuration since users' vault conventions are still evolving
- Smart defaults that auto-create notes, with opt-in preview/adjust mode
- Callout-style speaker formatting as the default rendering
Scope & Limitations (v1.0)
In scope:
- Paste import (ChatGPT and Claude copy-paste text)
- File import (ChatGPT JSON export, Claude JSON export, ZIP files, markdown files)
- Document import (non-chat text with headings or paragraph structure)
- ChatGPT browser extension markdown (via expanded generic parser)
- Heuristic segmentation (offline) with optional Ollama enhancement
- Desktop and mobile support
Deferred to post-v1.0:
- Shared conversation URL import (fetching from ChatGPT/Claude share links)
- API key providers (OpenAI, Anthropic) for segmentation
- Batch import of multiple conversations
- Vault-aware linking suggestions to existing notes
- Non-English transition phrase and reintroduction signal patterns
Known limitations:
- Segmentation signal quality is optimized for English conversations. Four of six signals (domain shift, vocabulary shift, temporal gap, self-contained) are language-agnostic and work for any language. Two signals (transition phrases, reintroduction) use English patterns only.
- Tool call messages (Code Interpreter, web browsing, Claude tool use) are stripped during parsing. Only the final assistant response using tool output is retained.
Architecture Overview & Data Flow
┌─────────────────────────────────────────────────────┐
│ main.ts │
│ Plugin lifecycle, commands, ribbon, settings tab │
└──────────┬──────────────────────────┬───────────────┘
│ │
┌─────▼─────┐ ┌───────▼────────┐
│ UI Layer │ │ Settings/Data │
│ │ │ │
│ ImportModal│ │ settings.ts │
│ PreviewModal │ import-config │
│ SettingsTab│ └────────────────┘
└──────┬─────┘
│
┌────────▼─────────┐
│ Parser Registry │ ← Format auto-detection
│ │
│ ChatGPT paste/JSON│
│ Claude paste/JSON │
│ Generic markdown │
└────────┬──────────┘
│ ParsedConversation
┌────────▼──────────┐
│ Segmentation Engine│ ← Heuristic (primary) or Ollama (optional)
│ │
│ 6 signal detectors │
│ Weighted scorer │
│ Greedy selector │
└────────┬───────────┘
│ Segment[]
┌────────▼──────────┐
│ Note Generator │ → vault.create()
│ │
│ Frontmatter builder│
│ Content formatter │
│ Index note builder │
│ Link resolver │
└────────────────────┘
Data Flow Summary
- Input — User pastes text or selects a file (JSON, ZIP, Markdown)
- Detection —
format-detector.tsidentifies the source platform and input format - Parsing — The appropriate parser converts raw input into a
ParsedConversation - Segmentation — The heuristic engine (or optionally Ollama) splits messages into topic-based
Segment[] - Generation — Each segment becomes a Markdown note with frontmatter, callout-formatted messages, and wikilinks
- Output — Notes are written to the vault via
vault.create(), including an index/MOC note linking all segments
File & Folder Structure
obsidian-chat-analyze-plugin/
├── src/
│ ├── main.ts # Plugin entry, commands, ribbon icon
│ ├── types/
│ │ ├── index.ts # Barrel re-exports
│ │ ├── conversation.ts # ParsedConversation, Message, ContentBlock
│ │ ├── segment.ts # Segment, SegmentBoundary, SegmentationConfig
│ │ ├── generated-note.ts # GeneratedNote, NoteFrontmatter, NoteLink
│ │ ├── settings.ts # ChatSplitterSettings, DEFAULT_SETTINGS
│ │ └── import-config.ts # Per-import ImportConfig
│ ├── parsers/
│ │ ├── index.ts # ParserRegistry bootstrap + parseInput()
│ │ ├── parser-interface.ts # IChatParser, InputFormat, ParseOptions
│ │ ├── format-detector.ts # Auto-detection from content
│ │ ├── code-block-guard.ts # Protects code blocks during speaker detection
│ │ ├── content-block-parser.ts # Shared text/code block extraction from raw message text
│ │ ├── chatgpt-paste-parser.ts
│ │ ├── chatgpt-json-parser.ts # Includes tree walker for mapping structure
│ │ ├── claude-paste-parser.ts
│ │ ├── claude-web-parser.ts # Date-stamped web copy-paste (no speaker labels)
│ │ ├── claude-json-parser.ts
│ │ └── markdown-parser.ts # Generic fallback
│ ├── segmentation/
│ │ ├── index.ts # Barrel exports
│ │ ├── segmenter.ts # Orchestrator: runs signals, applies scorer
│ │ ├── scorer.ts # Weighted composite scoring
│ │ ├── signals/
│ │ │ ├── transition-phrases.ts # "let's move on to..." detection
│ │ │ ├── domain-shift.ts # Jaccard similarity on domain tokens
│ │ │ ├── vocabulary-shift.ts # TF-vector cosine similarity (TextTiling)
│ │ │ ├── temporal-gap.ts # Timestamp gap detection
│ │ │ ├── self-contained.ts # Long deliverable message detection
│ │ │ └── reintroduction.ts # "I have a question about..." detection
│ │ ├── title-generator.ts # Topic title extraction
│ │ ├── tag-generator.ts # Auto-tag generation
│ │ ├── segment-utils.ts # merge/split/rename helpers for preview
│ │ └── ollama/
│ │ ├── client.ts # Health check, model listing
│ │ ├── ollama-segmenter.ts # LLM-based segmentation
│ │ ├── prompts.ts # Prompt templates
│ │ └── chunker.ts # Context window chunking
│ ├── generators/
│ │ ├── index.ts
│ │ ├── note-generator.ts # Segment → markdown note
│ │ ├── index-note-generator.ts # MOC/index note creation
│ │ ├── frontmatter-builder.ts # YAML frontmatter
│ │ ├── content-formatter.ts # Speaker callouts, code blocks
│ │ ├── link-resolver.ts # Prev/next/parent wikilinks
│ │ ├── key-info-extractor.ts # Extracts summary, key points, links (legacy, still exported)
│ │ ├── summary-builder.ts # Rich summary block: questions, topics, takeaways, links
│ │ └── sanitize.ts # Filename sanitization
│ ├── ui/
│ │ ├── import-modal.ts # Main 2-step import wizard
│ │ ├── preview-modal.ts # Segment review/edit modal
│ │ ├── settings-tab.ts # Plugin settings
│ │ └── folder-suggest.ts # Folder autocomplete component
│ └── utils/
│ ├── templates.ts # {{variable}} template rendering
│ ├── stop-words.ts # English stop word list
│ └── debug-log.ts # Gated console logging with [Chat Splitter] prefix
├── styles.css # Custom callout types + modal styles
├── manifest.json
├── package.json
├── tsconfig.json
├── esbuild.config.mjs
├── .gitignore
└── version-bump.mjs
Module Responsibilities
| Module | Responsibility |
|---|---|
main.ts |
Plugin lifecycle (onload/onunload), registers commands ("Split Chat", "Import Chat File"), adds ribbon icon, initializes settings tab |
types/ |
All TypeScript interfaces and type definitions; no runtime logic |
parsers/ |
Converts raw input (paste text or file content) into a normalized ParsedConversation structure |
segmentation/ |
Analyzes a ParsedConversation and produces Segment[] — groups of messages representing distinct topics |
generators/ |
Transforms Segment[] into Obsidian-ready Markdown files with frontmatter, formatted content, and wikilinks |
ui/ |
All Obsidian modal and settings tab UI components |
utils/ |
Shared utilities (template rendering, stop words) |
Core Data Model
ParsedConversation
The unified output of all parsers. Every parser, regardless of input format, must produce this structure.
type ContentType = 'chat' | 'document';
interface ParsedConversation {
id: string; // UUID from source or generated
title: string; // Fallback chain: source title, first user message (50 chars), "Untitled Chat"
source: 'chatgpt' | 'claude' | 'markdown';
contentType: ContentType; // 'chat' for conversations, 'document' for non-chat text
inputMethod: 'paste' | 'file-json' | 'file-zip' | 'file-markdown';
createdAt: Date | null;
updatedAt: Date | null;
messages: Message[];
messageCount: number;
parseWarnings: string[]; // Non-fatal issues encountered during parsing
sourceMetadata: {
originalId?: string; // Platform-specific conversation ID
defaultModel?: string; // e.g., "gpt-4", "claude-3-opus"
sourceUrl?: string; // URL if available
};
}
Message
interface Message {
id: string; // UUID from source or generated
index: number; // 0-based position in conversation
role: 'user' | 'assistant' | 'system' | 'tool';
contentBlocks: ContentBlock[]; // Structured: text, code, thinking, artifact, etc.
plainText: string; // Flattened text for search/segmentation
timestamp: Date | null;
metadata: {
model?: string; // Model used for this specific message
attachments?: Attachment[];
};
}
ContentBlock (Discriminated Union)
type ContentBlock =
| TextBlock
| CodeBlock
| ThinkingBlock
| ArtifactBlock
| ToolUseBlock
| ImageBlock;
interface TextBlock {
type: 'text';
content: string;
}
interface CodeBlock {
type: 'code';
language: string;
content: string;
filename?: string;
}
interface ThinkingBlock {
type: 'thinking';
content: string;
}
interface ArtifactBlock {
type: 'artifact';
title: string;
artifactType: string; // e.g., "code", "document", "svg"
language?: string;
content: string;
}
interface ToolUseBlock {
type: 'tool_use';
toolName: string;
input: string;
output?: string;
}
interface ImageBlock {
type: 'image';
altText: string;
url?: string; // External URL or omitted if inline
}
interface Attachment {
name: string;
mimeType?: string;
size?: number;
}
Segment
interface Segment {
id: string; // Generated UUID
title: string; // Auto-generated topic title
summary: string; // 1-2 sentence summary
tags: string[]; // Auto-generated tags (e.g., ["coding", "sql"])
messages: Message[]; // References to messages in this segment
startIndex: number; // Index of first message (inclusive)
endIndex: number; // Index of last message (inclusive)
confidence: number; // Composite score of the boundary that starts this segment (1.0 for first segment)
method: 'heuristic' | 'ollama' | 'manual';
}
interface SegmentBoundary {
beforeIndex: number; // Split occurs before message at this index
score: number; // Composite signal score
signals: SignalResult[]; // Individual signal contributions
}
interface SignalResult {
signal: string; // Signal name (e.g., "domain-shift")
score: number; // 0-1 raw score
weight: number; // Weight applied
detail?: string; // Human-readable explanation
}
GeneratedNote
interface GeneratedNote {
path: string; // Full vault path (e.g., "AI Chats/My Chat/topic.md")
filename: string; // Just the filename
content: string; // Complete Markdown content including frontmatter
frontmatter: NoteFrontmatter;
isIndex: boolean; // True for the index/MOC note
segmentId?: string; // Links back to the segment
}
interface NoteFrontmatter {
cssclasses: string[];
source: string;
conversation_id: string;
conversation_title: string;
segment?: number;
segment_total?: number;
topic?: string;
date: string; // ISO date (YYYY-MM-DD)
date_imported: string; // ISO datetime
tags: string[];
message_count: number;
prev?: string; // Wikilink to previous segment note
next?: string; // Wikilink to next segment note
parent?: string; // Wikilink to index note
}
interface NoteLink {
type: 'prev' | 'next' | 'parent' | 'child';
target: string; // Note filename (without .md)
display?: string; // Display text for wikilink
}
SegmentationConfig
interface SegmentationConfig {
granularity: 'coarse' | 'medium' | 'fine';
method: 'heuristic' | 'ollama';
signalWeights: Record<string, number>;
thresholds: GranularityThresholds;
}
interface GranularityThresholds {
confidenceThreshold: number; // Minimum score to accept a split
minMessages: number; // Minimum messages per segment
minWords: number; // Minimum words per segment
}
// Preset thresholds:
// coarse: { confidenceThreshold: 0.70, minMessages: 8, minWords: 500 }
// medium: { confidenceThreshold: 0.50, minMessages: 4, minWords: 200 }
// fine: { confidenceThreshold: 0.35, minMessages: 2, minWords: 80 }
Parser System
Format Detection
format-detector.ts examines raw input content and returns an InputFormat discriminant:
type InputFormat =
| { source: 'chatgpt'; method: 'paste' }
| { source: 'chatgpt'; method: 'file-json' }
| { source: 'claude'; method: 'paste' }
| { source: 'claude'; method: 'file-json' }
| { source: 'markdown'; method: 'paste' }
| { source: 'markdown'; method: 'file-markdown' };
Detection heuristics (in priority order):
- JSON structure check — If input parses as JSON:
- Has
mappingkey with nestedmessageobjects → ChatGPT JSON - Has
chat_messagesarray withsenderfield → Claude JSON
- Has
- Paste pattern matching — If input is plain text:
- Lines matching
^You said:or^ChatGPT said:→ ChatGPT paste - Lines matching
^Human:at line start or Claude-style turn markers → Claude paste - 2+ date-stamp lines (
^Jan 1$,^Feb 23$, etc.) withoutHuman:/Assistant:labels → Claude web paste
- Lines matching
- Generic fallback — Treat as generic Markdown conversation
Parser Interface
interface IChatParser {
readonly format: InputFormat;
canParse(input: string): boolean;
parse(input: string, options?: ParseOptions): ParsedConversation;
}
interface ParseOptions {
conversationId?: string; // For multi-conversation JSON files
branchStrategy?: 'current' | 'longest'; // ChatGPT tree traversal
}
Parser Registry
// parsers/index.ts
function parseInput(input: string, options?: ParseOptions): ParsedConversation {
const format = detectFormat(input);
const parser = registry.get(format);
return parser.parse(input, options);
}
The registry is a simple ordered list: ChatGPTPasteParser → ClaudePasteParser → ClaudeWebParser → MarkdownParser. detectFormat() runs detection heuristics and returns the first match. If no specific parser matches, the generic Markdown parser is used.
Code Block Guard
code-block-guard.ts addresses a critical edge case: code blocks may contain text that looks like speaker labels (e.g., "You said:" inside a code example).
Algorithm:
- Find all fenced code blocks (
```delimited) in the raw input - Replace their contents with unique placeholder tokens (e.g.,
__CODE_BLOCK_0__) - Run speaker detection / line-based parsing on the sanitized input
- After parsing, restore code block content from the placeholder map
This ensures speaker detection regex patterns never match inside code fences.
ChatGPT JSON Parser
ChatGPT exports use a tree structure where conversations can branch. Each node in the mapping object has:
id: Node UUIDmessage: Message content (may be null for root nodes)parent: Parent node IDchildren: Array of child node IDs
Tree traversal strategy:
- Default (
current): Follow thecurrent_nodepointer backward to root, then reverse for chronological order - Alternative (
longest): Walk the longest path from root
Edge case handling:
- Skip nodes where
messageis null (root/system nodes) - Skip nodes where
message.content.partsis empty - Add parse warnings for skipped nodes
- Handle
content_typevariations:text,code,execution_output,tether_browsing_display_result,multimodal_text - Filter out messages where
author.roleistoolorsystem-- only retainuserandassistantmessages
Claude JSON Parser
Claude exports contain a chat_messages array with simpler flat structure:
uuid: Message IDsender:"human"or"assistant"text: Message content (may contain Markdown)created_at: ISO timestampattachments: Array of file attachmentscontent: Array of content blocks (for newer exports with thinking/artifacts)- Filter out messages where
senderis nothumanorassistant
Paste Parsers (ChatGPT & Claude)
Line-based parsers that split on speaker label patterns:
ChatGPT paste patterns:
You said:→ user message (content starts on next line)ChatGPT said:→ assistant message (content starts on next line)- Variant:
You:/ChatGPT:(older format)
Claude paste patterns:
Human:at line start → user message (content follows on same line or next)Assistant:orClaude:at line start → assistant message
Common handling:
- Code block guard applied before speaker detection
- Consecutive lines without a speaker label are appended to the current message
- Leading/trailing whitespace trimmed from each message
- Empty messages filtered out with parse warning
- Tool-role messages (Code Interpreter results, browsing results) are filtered out during parsing; only user and assistant messages are retained
Claude Web Paste Parser
claude-web-parser.ts handles conversations copied from the Claude.ai web interface where no Human:/Assistant: labels are present. The only structural markers are date stamps on their own lines (e.g., Jan 1, Feb 23).
Detection (canParse):
- Reject if
Human:/Assistant:/Claude:labels are present (those belong toClaudePasteParser) - Mask code blocks to prevent false matches on dates inside code
- Count lines matching
^(Jan|Feb|Mar|...|Dec)\s+\d{1,2}$ - Require 2+ date-stamp lines
Parsing algorithm:
- Mask code blocks, find all date-stamp line positions
- Split text into sections at date boundaries
- Section 0 (before first date) = first user message
- Each date section: backward-scan from end to find trailing user message. A paragraph is "user-like" if: < 300 chars, no code fences, no headings, no lists, no bold. If the scan would consume the entire section, treat it all as assistant.
- Final section (after last date, no following date) = assistant message only
- Returns
contentType: 'chat',source: 'claude',inputMethod: 'paste'
Generic Markdown Parser
Fallback parser for unrecognized formats. First attempts speaker detection based on common patterns:
## User/## Assistantheadings#### You:/#### ChatGPT:headings (common browser extension format)#### Human:/#### Assistant:headings (extension variant)**User:**/**Assistant:**bold labels>blockquote alternation- Frontmatter with conversation metadata (title, date, model fields)
If a speaker pattern is detected, sets contentType: 'chat'. If no speaker pattern is detected, the parser attempts document splitting:
- Heading-based splitting: Strip frontmatter, scan for
#{1-6}headings (code blocks already masked), split at heading boundaries. Each heading section becomes aMessagewithrole: 'user'. Sections with fewer than 10 words are skipped. - Paragraph-group fallback: If no headings found, group consecutive paragraphs (groups of ~3, separated by double-newlines). Requires at least 4 paragraphs to attempt.
- Single-message fallback: If neither produces 2+ sections, the entire input becomes a single message.
When 2+ sections are produced, sets contentType: 'document'; otherwise contentType: 'chat'.
Segmentation Engine
Signal-Based Heuristic Engine (Primary)
The heuristic engine evaluates every user-message boundary as a potential split point using 6 weighted signals. Split points only occur immediately before user messages — never in the middle of an assistant response.
Signal Definitions
| # | Signal | File | What It Detects | Weight (all granularities) |
|---|---|---|---|---|
| 1 | Transition phrases | transition-phrases.ts |
Explicit topic change markers like "let's move on to...", "switching topics...", "now let's talk about...", "on a different note..." | 0.25 |
| 2 | Domain shift | domain-shift.ts |
Jaccard similarity drop on domain-specific tokens (non-stop-word tokens) between a window of messages before and after the candidate boundary | 0.20 |
| 3 | Vocabulary shift | vocabulary-shift.ts |
Cosine similarity of term-frequency vectors between sliding windows (TextTiling approach) | 0.20 |
| 4 | Reintroduction | reintroduction.ts |
New-topic phrasing: "I have a question about...", "Can you help with...", "I need help with...", "What is..." at the start of a user message | 0.15 |
| 5 | Temporal gap | temporal-gap.ts |
Gap of >30 minutes between consecutive messages (only when timestamps are available; contributes 0.0 otherwise) | 0.10 |
| 6 | Self-contained | self-contained.ts |
Long structured assistant response (>500 words with headings/lists, suggesting a "deliverable") immediately followed by a short new user question | 0.10 |
Signal weights are constant across all granularity levels. Granularity is controlled exclusively through the confidence threshold and minimum segment size, not through weight adjustments.
Document signal weights: When contentType === 'document', the segmenter overrides signal weights with a document-specific profile that zeroes out chat-only signals:
| Signal | Document Weight |
|---|---|
| transition-phrases | 0.00 |
| domain-shift | 0.50 |
| vocabulary-shift | 0.50 |
| reintroduction | 0.00 |
| temporal-gap | 0.00 |
| self-contained | 0.00 |
This redistribution ensures domain and vocabulary shifts -- the only signals meaningful for documents -- can reach sufficient composite scores to trigger splits.
Scoring Algorithm
For each candidate boundary (before user message at index i):
1. Compute each signal's raw score (0.0 to 1.0)
2. Multiply by signal weight
3. Sum weighted scores → composite score for this boundary
Implementation detail (scorer.ts):
interface ScoredBoundary {
beforeIndex: number;
compositeScore: number;
signals: SignalResult[];
}
function scoreBoundaries(
messages: Message[],
config: SegmentationConfig
): ScoredBoundary[];
Greedy Selection Algorithm
1. Score all candidate split points
2. Sort by composite score (descending)
3. Filter: remove candidates below confidence threshold
4. Greedy select:
a. Take the highest-scoring unselected boundary
b. Check if adding this split would create any segment smaller than minimum size
c. If valid, accept; if not, skip
d. Repeat until no more valid candidates
5. Sort accepted boundaries by message index (chronological order)
6. Produce Segment[] from the boundary list
Key design principle: Better to under-split than create garbage. A single large segment with mixed topics is more useful than three tiny segments with broken context.
Signal Implementation Details
Transition Phrases (transition-phrases.ts):
- Regex patterns matched against the first 200 characters of user messages
- Pattern categories: explicit transitions, context switches, topic introductions
- Score: 1.0 for strong match, 0.5 for weak match, 0.0 for no match
- Example patterns:
/^(let'?s|can we|i want to)\s+(move on|switch|change|talk about|discuss)/i
Domain Shift (domain-shift.ts):
- Extract domain tokens: lowercase, remove stop words, keep tokens >2 chars
- Window size: 3 messages before and 3 messages after the boundary
- Compute Jaccard similarity:
|A ∩ B| / |A ∪ B| - Score:
1.0 - jaccard_similarity(high score = low similarity = likely topic change) - Minimum token count: if either window has <5 tokens, score is 0.0
Vocabulary Shift (vocabulary-shift.ts):
- Build term-frequency vectors for windows before and after boundary
- Window size: 3 messages (configurable)
- Compute cosine similarity between TF vectors
- Score:
1.0 - cosine_similarity - Uses the TextTiling approach adapted for chat messages
Temporal Gap (temporal-gap.ts):
- Only fires when both adjacent messages have timestamps
- Gap threshold: 30 minutes (1800 seconds)
- Score:
min(1.0, gap_minutes / 120)— scales linearly, maxes at 2 hours - Score is 0.0 when timestamps are unavailable
Self-Contained (self-contained.ts):
- Checks the assistant message immediately before the boundary
- "Deliverable" heuristic: word count >500 AND (contains 2+ headings OR 5+ list items)
- Also checks that the user message after the boundary is "short" (<100 words, suggesting a new topic rather than a follow-up)
- Score: 1.0 if both conditions met, 0.0 otherwise
Reintroduction (reintroduction.ts):
- Regex patterns matched against user messages at the boundary
- Patterns detect: new questions, help requests, fresh topic introductions
- Score: 1.0 for strong match, 0.5 for moderate match, 0.0 for no match
- Example:
/^(i have a question|can you help|i need help|could you explain|what('?s| is))/i
Title Generation (title-generator.ts)
For each segment, generate a short topic title using a 5-strategy priority chain. The first strategy to produce a non-null result wins:
- Assistant topic (headings or opening statement) -- Scan assistant messages for markdown headings (
##-####), score by level and frequency. If short, combine with user topic kernel for context (e.g., heading "Health" + user topic "Draven" = "Draven Character Health"). If no headings, extract the first meaningful sentence from the first assistant response, stripping greeting prefixes ("Sure!", "Great question!", etc.) and pattern phrases ("Here is a breakdown of..."). This is the highest-priority strategy because assistant content is the most reliable signal for what the conversation actually covers. - Comparison detection -- Match patterns like "X vs Y", "X or Y", "difference between X and Y", "compare X and Y". Extracts both sides, cleans them, and produces a "Side A vs Side B" title.
- Entity + topic kernel -- Extract capitalized proper nouns from all messages (skipping words in a
CAPITALIZED_EXCLUSIONSset of ~200 common English words). User messages are weighted 3x. Multi-word entities are merged. Fuzzy dedup handles misspellings (e.g., "carriibbean" vs "Caribbean") via duplicate-char normalization and prefix + 75% character overlap matching. Generic category nouns (countries, options, types, etc.) are stripped from the kernel. The topic kernel is extracted from the first user message's first verb phrase after stripping filler prefixes and action verb patterns, capped at 40 chars. - Cleaned first sentence -- Strip question prefixes ("Can you", "Please", etc.) and action verb patterns from the first user message's first sentence, title-case it, cap at 72 chars.
- Keyword frequency fallback -- Top 3 non-stop-word tokens by frequency across all messages in the segment, title-cased.
Tag Generation (tag-generator.ts)
Auto-generate tags for each segment based on domain detection patterns:
- Predefined domain patterns (regex to tag mapping):
- Code-related terms (language names, syntax patterns) →
coding - Language-specific terms (python, javascript, etc.) →
coding/{{language}} - Database terms (sql, postgres, schema, etc.) →
database - API/web terms (api endpoint, rest, graphql, etc.) →
web - Design terms (figma, wireframe, ui/ux, etc.) →
design - Writing/content terms (essay, blog, proofread, etc.) →
writing - Real estate terms (property, mortgage, rental, etc.) →
real-estate - Finance terms (investment, portfolio, stock, etc.) →
finance - Immigration terms (citizenship, visa, residency, etc.) →
immigration - Travel terms (flight, hotel, itinerary, etc.) →
travel - Health terms (healthcare, medical, insurance, etc.) →
health - AI/ML terms (machine learning, neural network, LLM, etc.) →
ai-ml
- Code-related terms (language names, syntax patterns) →
- Tags are prefixed with the user's configured tag prefix (default:
ai-chat/) - Maximum 5 tags per segment
The tag prefix should not include a trailing slash. The generator always appends / between the prefix and tag name. If the user enters a trailing slash in settings, it is trimmed automatically.
Document tag generation: For documents, tags are generated once from the full text (all messages combined) and applied to every segment. This ensures domain patterns have enough keyword occurrences to meet minMatches thresholds, since individual document segments may contain a keyword only once. Chat segments continue to generate tags per-segment.
Ollama Integration (Optional Enhancement)
Located in src/segmentation/ollama/.
Client (client.ts)
interface OllamaClient {
healthCheck(): Promise<boolean>; // GET / with 3s timeout
listModels(): Promise<string[]>; // GET /api/tags
generate(prompt: string, model: string): Promise<string>; // POST /api/generate
}
- Uses Obsidian's
requestUrl()for all HTTP calls (ensures CORS compatibility) - Default endpoint:
http://localhost:11434 - Connection timeout: 3 seconds for health check, 60 seconds for generation
Ollama Segmenter (ollama-segmenter.ts)
- Chunk the conversation at ~12,000 characters with 4-message overlap between chunks
- For each chunk, send a structured prompt requesting:
- Split points (by message index)
- Confidence score (0-1) for each split
- Suggested topic title for each resulting segment
- Parse the JSON response; merge split points across chunks (dedup overlapping boundaries)
- Validate: ensure all split points reference valid message indices
- Automatic fallback to heuristic if:
- Ollama is unreachable (health check fails)
- Response is not valid JSON
- Response references invalid message indices
- Generation takes >60 seconds
Partial failure behavior: If Ollama succeeds for some chunks but fails for others, the entire Ollama segmentation is discarded and the system falls back to heuristic segmentation for the full conversation. Mixing heuristic and Ollama boundaries could produce inconsistent segment quality.
Prompt Templates (prompts.ts)
Structured prompt that:
- Provides conversation messages in a numbered format
- Requests JSON output with specific schema
- Includes examples of expected output format
- Instructs the model to only split at user message boundaries
Chunker (chunker.ts)
- Target chunk size: 12,000 characters (~3,000 tokens)
- Overlap: 4 messages repeated between adjacent chunks
- Never splits in the middle of a message
- Chunk boundaries always align to message boundaries
Note Generation Pipeline
Note Generator (note-generator.ts)
Orchestrates the conversion from Segment[] to GeneratedNote[]:
function generateNotes(
conversation: ParsedConversation,
segments: Segment[],
config: ImportConfig,
customFrontmatter?: string
): GeneratedNote[];
Steps:
- Resolve all note filenames (using naming template + sanitization)
- Build frontmatter for each segment note (merging custom frontmatter if provided)
- Format message content for each segment
- Build rich summary block (questions, topics, takeaways, links) for each segment
- Build navigation links (prev/next/parent)
- Generate index note
- Return array of
GeneratedNoteobjects ready forvault.create()
Summary Builder (summary-builder.ts)
Extracts structured summary information from a segment's messages and renders it as a multi-section callout block above the message transcript.
function buildSummaryBlock(messages: Message[], segmentSummary: string, tags: string[]): string;
function extractQuestions(messages: Message[]): string[];
function extractTopics(messages: Message[]): string[];
function extractTakeaways(messages: Message[]): string[];
extractQuestions -- Extracts cleaned first sentences from user messages. Strips filler prefixes ("can you", "ok great", etc.) and action verb patterns. Deduplicates via normalized string comparison. Maximum 8 items.
extractTopics -- Scans assistant messages for headings (##-####), strips numbering prefixes ("1.", "Step 1:"). Falls back to the first meaningful sentence from each assistant response (stripping greeting prefixes). Deduplicates. Maximum 8 items.
extractTakeaways -- Scans assistant messages for recommendation/conclusion patterns: "recommend", "suggest", "should consider", "key takeaway", "in summary", "in conclusion", "most important", "the best option", "to summarize", "bottom line", "ultimately", "my advice". Also extracts bold phrases containing actionable language. Falls back to the first sentence of the last assistant message's final paragraph as a conclusion. Deduplicates. Maximum 6 items.
buildSummaryBlock renders as Obsidian callouts (empty sections omitted):
> [!abstract] Summary-- one-liner segment summary with tag pills (always rendered)> [!question] Questions Asked-- numbered list of cleaned user questions> [!list] Topics Covered-- bulleted list of topics from headings or assistant openings> [!tip] Key Takeaways-- bulleted list of recommendations/conclusions> [!link] References-- bulleted list of URLs (tracking params stripped, formatted as[domain](url))
Link extraction reuses extractLinks from key-info-extractor.ts.
Key Info Extractor (key-info-extractor.ts) (Legacy)
Still exported from the generators barrel for backward compatibility but no longer used by note-generator.ts. The summary builder supersedes its functionality with richer extraction.
Note Structure Order
Each segment note is assembled in this order:
- YAML frontmatter
# Titleheading> [!info]metadata callout (segment N of M, source, date, message/section count)- Rich summary block (summary, questions asked, topics covered, key takeaways, references)
---horizontal rule separator- Formatted messages (using configured speaker style)
- Navigation footer (prev/next/parent wikilinks)
For documents, the info callout uses "Section" instead of "Segment" and "Sections" instead of "Messages".
Frontmatter Builder (frontmatter-builder.ts)
Produces YAML frontmatter from NoteFrontmatter interface. Key behaviors:
- Dates formatted as
YYYY-MM-DDfordate, ISO 8601 fordate_imported - Wikilinks in frontmatter are quoted strings:
"[[Note Name]]" - Tags array uses the configured prefix
cssclassesincludeschat-segmentordocument-segmentfor segment notes (based oncontentType),chat-indexfor index notes,chat-transcriptordocument-transcriptfor full transcript notes
Content Formatter (content-formatter.ts)
Formats messages according to the user's chosen speaker style:
Callout style (default):
> [!user] User
> Message content here
> [!assistant] Assistant
> Response content here
- Messages over 800 words use collapsible callouts:
> [!assistant]- - Code blocks within messages are preserved with their original language tags
- Thinking blocks rendered as collapsed callouts:
> [!thinking]- Thinking - Artifact blocks rendered with title:
> [!artifact] Artifact: Title
Blockquote style (alternative):
**User:**
> Message content
**Assistant:**
> Response content
Bold style (minimal):
**User:** Message content
**Assistant:** Response content
Plain style (no labels):
Message content here
Another section of content
Renders content blocks directly with no speaker labels, callouts, or wrapping. Automatically selected for document imports. If timestamps are present, they are rendered as <small> below the content.
ContentBlock rendering rules (all styles):
TextBlock: Rendered inline as markdown textCodeBlock: Rendered as fenced code blocks with language tag preservedThinkingBlock: Rendered as collapsed callout:> [!thinking]- Thinking(always collapsed regardless of length)ArtifactBlock: Rendered as callout:> [!note] Artifact: {title}with content inside, language-tagged if codeToolUseBlock: Stripped during parsing (not rendered -- see Scope & Limitations)ImageBlock: Rendered asif URL available, or[Image: {altText}]placeholder if no URL
Role rendering:
usermessages: Rendered with configured speaker styleassistantmessages: Rendered with configured speaker stylesystemmessages: Filtered out of note output (rarely useful to end users)toolmessages: Filtered out during parsing (see Scope & Limitations)
Index Note Generator (index-note-generator.ts)
Creates one Map of Content (MOC) note per imported conversation:
---
cssclasses: [chat-index]
source: chatgpt
conversation_id: "abc-123"
conversation_title: "Building a REST API"
date: 2025-01-15
date_imported: 2025-01-20T14:30:00
tags: [ai-chat, ai-chat/index]
segment_count: 7
total_messages: 84
---
# Building a REST API
> [!info] Chat Import Summary
> **Source:** ChatGPT | **Date:** 2025-01-15 | **Segments:** 7 | **Messages:** 84
## Topics
| # | Topic | Messages | Tags |
|---|-------|----------|------|
| 1 | [[...Project Setup]] | 8 | `coding`, `nodejs` |
| 2 | [[...Authentication]] | 14 | `coding`, `auth` |
| 3 | [[...Database Schema]] | 12 | `coding`, `sql` |
| ... | ... | ... | ... |
## Segment Summaries
### 1. [[...Project Setup]]
Setting up the Node.js project with Express, discussing folder structure and dependency choices.
### 2. [[...Authentication]]
Implementing JWT-based authentication with refresh tokens and middleware.
...
Full Transcript Note (Optional)
When keepFullTranscript is enabled, an additional note is generated containing the entire unsplit conversation:
- Filename:
{{date}} - {{conversation_title}} - Full Transcript - Placement: Same folder as segment notes
- Frontmatter: Same as segment notes but with
cssclasses: [chat-transcript], nosegment/segment_total/prev/nextfields,parentlinks to index note - Content: All messages formatted with the configured speaker style, no segment headers or navigation footers
- Index note link: The index note includes a "Full Transcript" section linking to this note
Link Resolver (link-resolver.ts)
Generates wikilinks for navigation between notes:
prev: Points to the previous segment note (null for first segment)next: Points to the next segment note (null for last segment)parent: Points to the index note (always present for segment notes)
Links are stored both in frontmatter (for Dataview queries) and rendered in a navigation footer block.
Navigation footer:
---
> [!info] Navigation
> Previous: [[...Authentication]] | [[...Index|Back to Index]] | Next: [[...Deployment]]
Filename Sanitization (sanitize.ts)
Ensures filenames are valid across all platforms:
- Replace
/\:*?"<>|with- - Collapse multiple spaces/dashes into single
- Trim to 200 characters (leaving room for
.mdextension) - Handle collision: if path exists, append
(2),(3), etc. - Check existence via
vault.getAbstractFileByPath()
Naming Convention
Default template: {{topic}}
{{date}}: Conversation date formatted asYYYY-MM-DD{{conversation_title}}: Original conversation title (sanitized){{topic}}: Segment topic title (sanitized)- Index note uses
Indexas the topic value
Template is configurable in settings via {{variable}} syntax.
Folder Structure
Default: AI Chats/{{conversation_title}}/ (nested, one folder per conversation)
Configurable options:
- Nested (default):
{{base_folder}}/{{conversation_title}}/— each conversation gets its own folder - Flat:
{{base_folder}}/— all notes in one folder, differentiated by filename
Template Rendering (utils/templates.ts)
Simple {{variable}} template engine for naming templates:
function renderTemplate(template: string, variables: Record<string, string>): string;
Available variables:
{{date}}— Conversation date{{conversation_title}}— Full conversation title{{topic}}— Segment topic title{{source}}— Source platform (chatgpt, claude, markdown){{segment}}— Segment number{{segment_total}}— Total segments
UI/UX Specification
Import Modal (2-Step Wizard)
src/ui/import-modal.ts — Extends Obsidian's Modal class.
Step 1: Input
Layout:
- Tab-style toggle at top: "Paste" | "File"
- Paste mode: Large
<textarea>(min 300px height) with monospace font- Real-time format auto-detection badge below textarea (e.g., "Detected: ChatGPT paste format")
- Badge updates on input with 300ms debounce
- File mode: "Choose File" button (styled) triggering hidden
<input type="file">- Accepts:
.json,.zip,.md - Shows selected filename and detected format after selection
- For multi-conversation JSON files (ChatGPT full export), shows a dropdown to select which conversation
- Accepts:
- Quick settings (between input area and button):
- Target folder: Text input with folder autocomplete (same as Step 2; values carry through)
- Tag prefix: Text input (same as Step 2; values carry through)
- "Analyze" button at bottom (disabled until input provided and format detected)
- Clicking "Analyze" triggers parsing + segmentation, then transitions to Step 2
- For documents: badge shows "Document" instead of format details; after analysis, speaker style auto-sets to "Plain"
Step 2: Configure & Create
Layout:
- Header: "Import Chat - Step 2: Configure" (or "Import Document" for documents)
- Summary card at top: "Found X topics in Y messages" (or "Y sections" for documents) with segment titles listed
- Per-import settings (overriding plugin defaults for this import):
- Target folder: Text input with folder autocomplete (
folder-suggest.ts) - Tag prefix: Text input (default from settings)
- Granularity: 3-way toggle (Coarse / Medium / Fine)
- Changing granularity re-runs segmentation inline and updates the summary card
- Changing granularity discards any manual segment edits made in the preview modal (since segments are fully regenerated). If the user has previously edited segments via preview, a confirmation is shown before re-segmenting.
- Speaker style: Dropdown (Callouts / Blockquotes / Bold / Plain)
- Keep full transcript: Toggle — if on, also creates a single note with the complete unsplit conversation
- Use Ollama: Toggle (only visible if Ollama enabled in settings and healthy)
- Target folder: Text input with folder autocomplete (
- "Create N Notes" button (primary CTA) — creates notes with current configuration
- "Preview segments..." link — opens PreviewModal for manual adjustment
- This link is always visible but can be made the default action via settings ("Always preview before creating")
Behavior Notes
- Modal width: 600px (constrained for readability)
- Step transition is animated (simple fade/slide)
- Error states: parse failures show inline error message with details
- Progress: during note creation, button shows "Creating... (3/7)" with progress
Preview Modal (Opt-In)
src/ui/preview-modal.ts — Extends Obsidian's Modal class. Opened from Import Modal Step 2.
Layout:
- Scrollable vertical list of segment cards
- Each card shows:
- Editable title (inline text input)
- Message count and confidence score
- Tag pills (removable, with "+" to add)
- Expandable message preview — first 3 messages shown, "Show all" to expand
- Controls between cards:
- Merge up button (combines this segment with previous)
- Split here button (splits segment at a message boundary, with message-level click-to-split)
- Bottom bar:
- Segment count summary
- "Create Notes" button (finalizes from preview state)
Interaction Details
- Merge: Combines two adjacent segments. Title defaults to the first segment's title. Tags are unioned.
- Split: Opens an inline message list with clickable boundaries. Clicking between two messages creates a new split point. The new segment gets an auto-generated title.
- Reorder: Not supported (segments are chronological, reordering would break context).
- Undo: Single-level undo for merge/split operations (stores previous state).
Settings Tab
src/ui/settings-tab.ts — Extends Obsidian's PluginSettingTab class.
Organized into 5 collapsible sections:
1. General
| Setting | Type | Default | Description |
|---|---|---|---|
| Default folder | Text + folder suggest | AI Chats |
Base folder for imported conversations |
| Naming template | Text | {{topic}} |
Template for note filenames |
| Tag prefix | Text | ai-chat |
Prefix for auto-generated tags |
| Folder structure | Dropdown | Nested |
Nested (per-conversation folder) or Flat |
2. Segmentation
| Setting | Type | Default | Description |
|---|---|---|---|
| Default granularity | Dropdown | Medium |
Coarse / Medium / Fine |
| Min segment messages | Number | 4 |
Minimum messages per segment |
| Min segment words | Number | 200 |
Minimum words per segment |
| Always preview | Toggle | false |
Open preview modal by default |
3. Formatting
| Setting | Type | Default | Description |
|---|---|---|---|
| Speaker style | Dropdown | Callouts |
Callouts / Blockquotes / Bold / Plain |
| Show timestamps | Toggle | true |
Include timestamps in messages (when available) |
| Collapse long messages | Toggle | true |
Auto-collapse messages >800 words |
| Collapse threshold | Number | 800 |
Word count threshold for collapsing |
| Keep full transcript | Toggle | false |
Also create unsplit transcript note |
4. AI Enhancement
| Setting | Type | Default | Description |
|---|---|---|---|
| Enable Ollama | Toggle | false |
Use Ollama for segmentation |
| Ollama endpoint | Text | http://localhost:11434 |
Ollama API endpoint |
| Ollama model | Dropdown | (auto-populated) | Model to use for segmentation |
| Test connection | Button | — | Checks Ollama connectivity and shows result |
5. Advanced
| Setting | Type | Default | Description |
|---|---|---|---|
| Custom frontmatter | Textarea | (empty) | Additional YAML fields added to every note. Validated as valid YAML on save; invalid input shows an error. |
| Debug logging | Toggle | false |
Log detailed segmentation info to console |
Folder Suggest (folder-suggest.ts)
Autocomplete component for folder path inputs:
- Extends Obsidian's
AbstractInputSuggest - Queries
vault.getAllLoadedFiles()filtered toTFolderinstances - Filters suggestions as user types
- Selecting a suggestion fills the input
Settings Schema
ChatSplitterSettings
interface ChatSplitterSettings {
// General
defaultFolder: string;
namingTemplate: string;
tagPrefix: string;
folderStructure: 'nested' | 'flat';
// Segmentation
defaultGranularity: 'coarse' | 'medium' | 'fine';
minSegmentMessages: number;
minSegmentWords: number;
alwaysPreview: boolean;
// Formatting
speakerStyle: 'callouts' | 'blockquotes' | 'bold' | 'plain';
showTimestamps: boolean;
collapseLongMessages: boolean;
collapseThreshold: number;
keepFullTranscript: boolean;
// AI Enhancement
enableOllama: boolean;
ollamaEndpoint: string;
ollamaModel: string;
// Advanced
customFrontmatter: string;
debugLogging: boolean;
}
const DEFAULT_SETTINGS: ChatSplitterSettings = {
defaultFolder: 'AI Chats',
namingTemplate: '{{topic}}',
tagPrefix: 'ai-chat',
folderStructure: 'nested',
defaultGranularity: 'medium',
minSegmentMessages: 4,
minSegmentWords: 200,
alwaysPreview: false,
speakerStyle: 'callouts',
showTimestamps: true,
collapseLongMessages: true,
collapseThreshold: 800,
keepFullTranscript: false,
enableOllama: false,
ollamaEndpoint: 'http://localhost:11434',
ollamaModel: '',
customFrontmatter: '',
debugLogging: false,
};
ImportConfig (Per-Import Overrides)
interface ImportConfig {
targetFolder: string;
tagPrefix: string;
granularity: 'coarse' | 'medium' | 'fine';
speakerStyle: 'callouts' | 'blockquotes' | 'bold' | 'plain';
keepFullTranscript: boolean;
useOllama: boolean;
namingTemplate: string;
folderStructure: 'nested' | 'flat';
}
Initialized from ChatSplitterSettings defaults, then user may override per-import in the Import Modal (folder and tag prefix available in Step 1; all settings available in Step 2). For document imports, speakerStyle is auto-set to 'plain' after parsing.
Implementation Roadmap
| Phase | Description | Size | Key Files | Dependencies |
|---|---|---|---|---|
| 1 | Project scaffolding | S | manifest.json, package.json, tsconfig.json, esbuild.config.mjs, .gitignore, empty main.ts |
— |
| 2 | Core types & interfaces | S | src/types/*.ts |
Phase 1 |
| 3 | Paste parsers + format detection | M | src/parsers/format-detector.ts, *-paste-parser.ts, code-block-guard.ts |
Phase 2 |
| 4 | Heuristic segmentation engine | L | src/segmentation/segmenter.ts, scorer.ts, signals/*.ts, title-generator.ts, tag-generator.ts |
Phase 2 |
| 5 | Note generation pipeline | M | src/generators/*.ts, src/utils/*.ts |
Phase 2 |
| 6 | Import modal UI + plugin wiring | L | src/ui/import-modal.ts, folder-suggest.ts, styles.css, main.ts |
Phases 3, 4, 5 |
| 7 | Preview modal UI | L | src/ui/preview-modal.ts, segment-utils.ts |
Phase 6 |
| 8 | Settings tab | M | src/ui/settings-tab.ts |
Phase 6 |
| 9 | JSON file parsers (ChatGPT + Claude) | M | chatgpt-json-parser.ts, claude-json-parser.ts |
Phase 3 |
| 10 | Ollama integration | M | src/segmentation/ollama/*.ts |
Phase 4 |
| 11 | Polish & edge cases | M | All files | All phases |
| 12 | Document splitting + folder/tag in Step 1 | M | types/*.ts, markdown-parser.ts, segmenter.ts, content-formatter.ts, note-generator.ts, import-modal.ts, settings-tab.ts |
Phase 11 |
Dependency Graph
Phase 1 → Phase 2 → Phase 3 → Phase 9
↘ Phase 4 → Phase 10
↘ Phase 5
↘
Phases 3,4,5 → Phase 6 → Phase 7
→ Phase 8
All → Phase 11 → Phase 12
External Dependencies
jszip— Required for ChatGPT/Claude ZIP file imports (Phase 9)- No other external runtime dependencies
- Build tooling:
esbuild,typescript,@types/node,obsidian(dev dependencies)
Risk Register
| # | Risk | Impact | Likelihood | Mitigation |
|---|---|---|---|---|
| 1 | Paste format fragility — platforms change their copy format | Parsers break for affected platform | Medium | Support multiple regex variants per platform; generic fallback parser ensures some functionality always works |
| 2 | Code blocks containing fake speaker labels (e.g., "You said:" in a code example) |
Mis-parsed messages, broken conversation structure | High | code-block-guard.ts: replace code block content with placeholders before speaker detection, restore after |
| 3 | ChatGPT tree structure complexity (branching conversations) | Lost messages, incorrect ordering, crashes | Medium | Default to current_node branch traversal; skip null/empty nodes with parse warnings; add branchStrategy option |
| 4 | Segmentation quality without LLM | Poor topic boundaries, segments that don't match user expectations | Medium | Conservative defaults (under-split preferred); 3 granularity levels; preview mode for manual adjustment; Ollama as optional enhancement |
| 5 | Very large conversations (1000+ messages) | UI freeze, slow processing | Low | Segmentation is O(n*w) where w is small window size; note generation is sequential but each note is fast; progress indicators throughout |
| 6 | Note naming collisions | Overwritten files, data loss | Medium | Check vault.getAbstractFileByPath() before creating; append numeric suffix (2), (3) on collision |
| 7 | Re-import of same conversation | Duplicate notes cluttering vault | Medium | Check for existing notes with matching conversation_id in frontmatter; prompt user: overwrite / skip / import as new |
| 8 | Ollama unavailable or returns invalid output | Segmentation fails silently or produces garbage | Low | Automatic fallback to heuristic engine; validate JSON response schema; timeout at 60 seconds |
| 9 | Custom frontmatter causes invalid YAML | Note creation fails, frontmatter unreadable | Low | Validate custom frontmatter as valid YAML before merging; show error in settings if invalid |
| 10 | Platform-specific file path issues | Failures on Windows/Linux/mobile | Low | Use Obsidian's normalizePath() for all paths; sanitize filenames per sanitize.ts; test on multiple platforms |
Decision Log
| # | Decision | Rationale | Alternatives Considered |
|---|---|---|---|
| 1 | Split only before user messages | Splitting mid-assistant-response creates broken context. User messages are natural conversation boundaries. | Split at any message boundary — rejected because assistant responses often span multiple messages that form a coherent unit |
| 2 | Callouts as default speaker style | Obsidian's callout syntax provides visual distinction, collapsibility for long messages, and custom styling via CSS. Most visually polished option. | Blockquotes (less visual distinction), Bold labels (minimal but fast to scan), HTML (not portable) |
| 3 | Heuristic-first, Ollama-optional | Offline-first respects privacy and works without setup. Most users won't have Ollama. Heuristic is deterministic and fast. | LLM-only (requires setup, non-deterministic), Hybrid always-on (complex, slower) |
| 4 | Per-import configuration | User's vault conventions are evolving. Different conversations may need different settings (e.g., work vs personal chats in different folders). | Global-only settings — rejected because it's too rigid; users want flexibility per import |
| 5 | Smart defaults with opt-in preview | Most imports should "just work" with good defaults. Preview is powerful but adds friction. Default to auto-create, let power users opt into preview. | Always-preview (too much friction), No preview (no way to adjust) |
| 6 | Auto-create notes (not auto-preview) | Reduces friction to minimum. One click after configuration creates everything. Preview is opt-in. | Mandatory preview — rejected because it adds a step most users don't need |
| 7 | Index/MOC note per conversation | Provides a natural entry point and overview. Enables Dataview queries. Supports bidirectional navigation. | No index note (harder to navigate), Single note with embeds (doesn't scale) |
| 8 | jszip as only external dependency |
ZIP support is necessary for full exports. jszip is well-maintained, small, and widely used. | Node.js built-in zlib (not available in Obsidian's runtime), Manual ZIP parsing (too complex, error-prone) |
| 9 | ContentBlock discriminated union | Preserves structural information from source (code blocks, thinking blocks, artifacts) instead of flattening to plain text. Enables richer formatting. | Plain text only — simpler but loses valuable structure |
| 10 | TextTiling-inspired vocabulary shift | Well-established algorithm for topic segmentation in text. Adapted for chat messages with smaller windows. Provides meaningful signal without ML. | LDA topic modeling (too heavy, needs training), Simple keyword overlap (too naive) |
| 11 | 6 weighted signals with configurable granularity | Multiple weak signals combined produce robust segmentation. Granularity presets make it accessible. Individual weights are internal (not user-facing). | Single heuristic (too brittle), User-configurable weights (too complex for most users) |
| 12 | Segment confidence scores | Enables quality-based filtering and helps users understand why splits were made. Useful in preview mode. | Binary split/no-split — loses nuance |
| 13 | Navigation links in both frontmatter and footer | Frontmatter enables Dataview queries and graph view connections. Footer provides in-note navigation. | Frontmatter only (requires Dataview to navigate), Footer only (not queryable) |
| 14 | Strip tool messages rather than rendering them | Simplifies both parsing and rendering. Tool call details (Code Interpreter execution, browsing steps) add noise to the output notes. Users care about the final assistant response, not the intermediate tool interactions. | Render as collapsed callouts (adds complexity), Defer entirely (loses context about tool use existence) |
| 15 | Desktop and mobile support from v1.0 | HTML file input works on both platforms. Paste works everywhere. Shipping mobile-compatible from the start avoids a painful retrofit and reaches a larger audience. | Desktop-only first (simpler but limits reach) |
| 16 | English-only segmentation signals in v1.0 | Four of six signals are language-agnostic. The two English-only signals (transition phrases, reintroduction) gracefully degrade to 0.0 for non-English, leaving the other four signals to handle segmentation. Full i18n is deferred. | Add 2-3 extra languages (scope creep for v1.0), Auto-detect language and skip (adds complexity) |
| 17 | Defer shared URL import to post-v1.0 | URL fetching introduces CORS complexity, HTML parsing fragility, and authentication concerns. Paste and file import cover the primary use cases. URLs can be added later. | Add as Phase 12 (adds scope and risk) |
| 18 | Title fallback chain: source, first message, "Untitled Chat" | Every conversation needs a title for filenames and index notes. The fallback chain ensures no empty titles while preferring the most meaningful option. | Always prompt user for title (adds friction) |
| 19 | Document splitting via headings then paragraph groups | Headings are the most reliable structural signal in documents. Paragraph groups provide a reasonable fallback for unstructured text. Both produce role: 'user' messages that flow through the existing segmentation pipeline without changes to the scorer. |
Sentence-level splitting (too granular), fixed character count (ignores structure), require headings only (misses plain documents) |
| 20 | Document signal weights: 50/50 domain-shift + vocabulary-shift | Chat-only signals (transition phrases, reintroduction, temporal gap, self-contained) score 0.0 on documents since there are no speaker turns. Domain and vocabulary shifts are the only meaningful signals for documents. Equal weighting lets either signal drive a split. | Keep chat weights (max composite score ~0.40, below all thresholds), add new document-specific signals (over-engineering for v1.0) |
| 21 | Full-text tag generation for documents | Per-segment tag generation fails for documents because individual segments have too little text for domain patterns to reach minMatches thresholds (2-3 occurrences). Generating tags from the full document text and applying to all segments matches the chat experience where repeated keywords across turns satisfy the threshold. |
Lower minMatches for documents (reduces precision), add document-specific patterns (maintenance burden) |
| 22 | Plain speaker style for documents | Documents don't have speaker roles, so callout/blockquote/bold labels ("User:", "Assistant:") are meaningless noise. Plain style renders content directly. Auto-selected for documents but available for chats too. | Always use callouts (confusing labels on documents), strip labels only for documents (special-case logic in formatter) |
| 23 | Folder and tag prefix in Step 1 | These are the most commonly changed per-import settings. Surfacing them in Step 1 reduces friction -- users can set them before analysis rather than going back from Step 2. Values are shared state with Step 2. | Step 2 only (original design, extra click for common case), all settings in Step 1 (clutters the input step) |
Verification Plan
These verification steps apply when implementation begins after architecture approval:
Build Verification
npm install && npm run buildproducesmain.jswithout errors
Plugin Load Test
- Copy built files into test vault's
.obsidian/plugins/chat-splitter/ - Enable plugin in Obsidian settings
- Verify no console errors on load
- Verify ribbon icon appears
- Verify commands registered in command palette
Paste Import End-to-End
- Open import modal via command or ribbon
- Paste a real ChatGPT conversation
- Verify format auto-detected and badge shown
- Click "Analyze", verify segmentation summary
- Configure settings (folder, tags, granularity)
- Click "Create Notes"
- Verify: notes created in correct folder, index note links work, frontmatter is valid YAML, callouts render correctly, navigation links work
File Import End-to-End
- Import a ChatGPT JSON export file
- Select a conversation from the dropdown (if multi-conversation)
- Verify same flow and results as paste import
Segmentation Quality
- Import a known multi-topic conversation (prepared test fixture)
- Test all three granularity levels:
- Coarse: Few large segments, only the clearest topic changes
- Medium: Balanced segmentation matching intuitive topic boundaries
- Fine: Many smaller segments, catching subtle topic shifts
- Verify segments align with actual topic boundaries
Edge Cases
| Test Case | Expected Behavior |
|---|---|
| Empty paste | Error message: "No content to analyze" |
| 2-message conversation | Single segment (no split possible below minimum) |
| 500+ message conversation | Completes without UI freeze; progress shown |
| Code blocks containing "You said:" | Speaker label inside code block not treated as message boundary |
| Single-topic conversation | Single segment with appropriate title |
| Conversation with no timestamps | Temporal gap signal contributes 0.0; other signals still work |
| Duplicate import | Prompted to overwrite/skip/import as new |
| Document with headings | Badge shows "Document", heading-based splitting, plain formatting, document-aware labels |
| Document without headings (4+ paragraphs) | Paragraph-group fallback, plain formatting |
| Document without headings (<4 paragraphs) | Single-message fallback, treated as chat |
| Document segments with domain keywords | Tags generated from full text, applied to all segments |
Settings Persistence
- Change every setting in the settings tab
- Reload Obsidian
- Verify all values persisted correctly
Preview Mode
- Enable "Always preview" in settings
- Import a conversation
- In preview modal: rename a segment, merge two segments, split a segment
- Click "Create Notes"
- Verify final notes reflect all preview changes