import: share per-turn splitting across line-based importers · Entire

import: share per-turn splitting across line-based importers

4eae85f→main·

computermode·3w ago·7 files·+168 added/-250 removed

Add splitLineTurns (linesplit.go): the per-turn scaffolding every JSONL importer repeated — scan for user-prompt starts, bound each turn by the next, truncate to the [0,end) buffer the agents' token helpers consume, and assemble
the Turn. Each importer now passes an isPrompt predicate and a build callback
that fills the agent-specific fields by calling that agent's own parsing
(CalculateTokenUsage/CalculateTotalTokenUsage, ExtractModel, ExtractUserContent,
pijsonl, etc.); the duplicated loops are gone.

Routes claude, cursor, pi, factory, codex, and copilot through it. Net ~125\nfewer lines across the importers. gemini stays per-session (single JSON\ndocument, not newline-delimited).

The per-format prompt detectors remain: per-turn import needs each prompt's\nline offset (to bound token usage in the agent's native offset space), which no\nagent method exposes — so detection still scans raw lines. Pure refactor;\nbehavior unchanged, verified against the importtest fixtures and existing tests.

Co-Authored-By: Claude Opus 4.8 noreply@anthropic.com\

Sessions\


873ca152414fView transcript
\

Changes\


7
\

42 unmodified lines\
\
```\n
// start a turn.\nfunc (claudeImporter) SplitTurns(sf SessionFile, full []byte) ([]Turn, error) {\n\nrawLines := splitRawLines(full)\n\n// Identify user-prompt turn starts in raw-line space.\nvar starts []int\n\nfor i, raw := range rawLines {\nif isUserPromptLine(raw) {\nstarts = append(starts, i)\n}\n}\n\nag := &claudecode.ClaudeCodeAgent{}\nturns := make([]Turn, 0, len(starts))\nfor k, start := range starts {\nend := len(rawLines)\nif k+1 < len(starts) {\nend = starts[k+1]\n}\n\n// Bound token usage to [start, end): truncate to the first `end` lines,\n// then let the agent helper slice from `start`.\ntruncated := joinLines(rawLines[:end])\ntokens, err := ag.CalculateTotalTokenUsage(truncated, start, subagentsDir)\nif err != nil {\nreturn nil, fmt.Errorf("token usage for turn %d: %w", k, err)\n}\n\nvar rec struct {\nUUID string `json:"uuid"`\nMessage json.RawMessage `json:"message"`\nTimestamp string `json:"timestamp"`\n}\nif err := json.Unmarshal(rawLines[start], &rec); err != nil {\n// Already validated as a user-prompt line in isUserPromptLine; skip\n// defensively if it somehow fails to parse here.\ncontinue\n}\nts, parseErr := time.Parse(time.RFC3339, rec.Timestamp)\nif parseErr != nil {\nts = time.Time{}\n}\n\nturns = append(turns, Turn{\nLineStart: start,\nLineEnd: end,\nUUID: rec.UUID,\nPrompt: transcript.ExtractUserContent(rec.Message),\nModel: modelInRange(rawLines, start, end),\nCreatedAt: ts,\nTokens: tokens,\n})\n}\nreturn splitLineTurns(splitRawLines(full), isUserPromptLine,\nfunc(rawLines [][]byte, start, end int, truncated []byte) (*Turn, error) {\ntokens, err := ag.CalculateTotalTokenUsage(truncated, start, subagentsDir)\nif err != nil {\nreturn nil, fmt.Errorf("token usage: %w", err)\n}\nvar rec struct {\nUUID string `json:"uuid"`\nMessage json.RawMessage `json:"message"`\nTimestamp string `json:"timestamp"`\n}\nif err := json.Unmarshal(rawLines[start], &rec); err != nil {\n//nolint:nilerr // skip defensively; the line already parsed in isUserPromptLine\nreturn nil, nil\n}\nts, parseErr := time.Parse(time.RFC3339, rec.Timestamp)\nif parseErr != nil {\nts = time.Time{}\n}\nreturn &Turn{\nUUID: rec.UUID,\nPrompt: transcript.ExtractUserContent(rec.Message),\nModel: modelInRange(rawLines, start, end),\nCreatedAt: ts,\nTokens: tokens,\n}, nil\n})\n}\n// claudeExtraFields are line fields not modeled by transcript.Line.\n```\n
Mcmd/entire/cli/agentimport/claude.go+26/-50\n\n```\n111 unmodified lines\n\n```\n// its (append-only) start line index. Token usage is delegated to the Codex\n// agent, which computes the cumulative-usage delta for the line range.\nfunc (codexImporter) SplitTurns(_ SessionFile, full []byte) ([]Turn, error) {\n// ...<truncated for brevity>...\n// piPromptText reports whether a raw Pi JSONL line is a user-prompt message and\n```