f99010fae1
Desktop Artifacts / Desktop Build (Linux) (push) Waiting to run
Desktop Artifacts / Desktop Build (Windows) (push) Waiting to run
Desktop Artifacts / Desktop Build (Linux (arm64)) (push) Waiting to run
Desktop Artifacts (macOS) / Desktop Build (macOS (aarch64)) (push) Waiting to run
Desktop Artifacts (macOS) / Desktop Build (macOS (x86_64)) (push) Waiting to run
CI / lint (push) Failing after 1s
CI / frontend (push) Failing after 1s
CI / scripts (push) Failing after 1s
CI / Go Test (ubuntu-latest) (push) Failing after 0s
CI / frontend-node-25 (push) Failing after 1s
CI / docs (push) Failing after 0s
CI / coverage (push) Failing after 0s
CI / e2e (push) Failing after 0s
Docker / build-and-push (push) Failing after 1s
CI / integration (push) Failing after 4m43s
CI / Go Test (windows-latest) (push) Has been cancelled
CI / Desktop Unit Tests (Windows) (push) Has been cancelled
346 lines
7.6 KiB
Go
346 lines
7.6 KiB
Go
package parser
|
|
|
|
import (
|
|
"encoding/json"
|
|
"fmt"
|
|
"os"
|
|
"path/filepath"
|
|
"strconv"
|
|
"strings"
|
|
"time"
|
|
|
|
"github.com/tidwall/gjson"
|
|
)
|
|
|
|
type commandCodeMeta struct {
|
|
Title string `json:"title"`
|
|
UserRenamed bool `json:"userRenamed"`
|
|
ProjectPath string `json:"projectPath"`
|
|
Cwd string `json:"cwd"`
|
|
}
|
|
|
|
// parseSession parses a Command Code JSONL transcript.
|
|
func (p *commandCodeProvider) parseSession(
|
|
path, machine string,
|
|
) (*ParsedSession, []ParsedMessage, error) {
|
|
info, err := os.Stat(path)
|
|
if err != nil {
|
|
return nil, nil, fmt.Errorf("stat %s: %w", path, err)
|
|
}
|
|
|
|
f, err := os.Open(path)
|
|
if err != nil {
|
|
return nil, nil, fmt.Errorf("open %s: %w", path, err)
|
|
}
|
|
defer f.Close()
|
|
|
|
lr := newLineReader(f, maxLineSize)
|
|
defer releaseLineReader(lr)
|
|
var (
|
|
sessionID string
|
|
cwd string
|
|
gitBranch string
|
|
firstMessage string
|
|
startedAt time.Time
|
|
endedAt time.Time
|
|
ordinal int
|
|
userCount int
|
|
malformedLines int
|
|
messages []ParsedMessage
|
|
)
|
|
|
|
for {
|
|
line, ok := lr.next()
|
|
if !ok {
|
|
break
|
|
}
|
|
if !gjson.Valid(line) {
|
|
malformedLines++
|
|
continue
|
|
}
|
|
|
|
root := gjson.Parse(line)
|
|
if sessionID == "" {
|
|
sessionID = root.Get("sessionId").Str
|
|
}
|
|
if cwd == "" {
|
|
cwd = commandCodeCwd(root)
|
|
}
|
|
if gitBranch == "" {
|
|
gitBranch = root.Get("gitBranch").Str
|
|
}
|
|
|
|
ts := parseTimestamp(root.Get("timestamp").Str)
|
|
if !ts.IsZero() {
|
|
if startedAt.IsZero() || ts.Before(startedAt) {
|
|
startedAt = ts
|
|
}
|
|
if endedAt.IsZero() || ts.After(endedAt) {
|
|
endedAt = ts
|
|
}
|
|
}
|
|
|
|
role := root.Get("role").Str
|
|
content := root.Get("content")
|
|
text, thinking, hasThinking, hasToolUse, toolCalls, toolResults :=
|
|
extractCommandCodeContent(content)
|
|
text = strings.TrimSpace(text)
|
|
|
|
switch role {
|
|
case "user":
|
|
if text == "" && len(toolResults) == 0 {
|
|
continue
|
|
}
|
|
if firstMessage == "" && text != "" {
|
|
firstMessage = truncate(
|
|
strings.ReplaceAll(text, "\n", " "),
|
|
300,
|
|
)
|
|
}
|
|
messages = append(messages, ParsedMessage{
|
|
Ordinal: ordinal,
|
|
Role: RoleUser,
|
|
Content: text,
|
|
ThinkingText: thinking,
|
|
Timestamp: ts,
|
|
HasThinking: hasThinking,
|
|
HasToolUse: hasToolUse,
|
|
ContentLength: len(text),
|
|
ToolCalls: toolCalls,
|
|
ToolResults: toolResults,
|
|
})
|
|
ordinal++
|
|
if text != "" {
|
|
userCount++
|
|
}
|
|
|
|
case "assistant":
|
|
if text == "" && !hasThinking &&
|
|
len(toolCalls) == 0 && len(toolResults) == 0 {
|
|
continue
|
|
}
|
|
messages = append(messages, ParsedMessage{
|
|
Ordinal: ordinal,
|
|
Role: RoleAssistant,
|
|
Content: text,
|
|
ThinkingText: thinking,
|
|
Timestamp: ts,
|
|
HasThinking: hasThinking,
|
|
HasToolUse: hasToolUse,
|
|
ContentLength: len(text),
|
|
ToolCalls: toolCalls,
|
|
ToolResults: toolResults,
|
|
})
|
|
ordinal++
|
|
|
|
case "tool":
|
|
if len(toolResults) == 0 {
|
|
continue
|
|
}
|
|
messages = append(messages, ParsedMessage{
|
|
Ordinal: ordinal,
|
|
Role: RoleUser,
|
|
Timestamp: ts,
|
|
ContentLength: 0,
|
|
ToolResults: toolResults,
|
|
})
|
|
ordinal++
|
|
}
|
|
}
|
|
|
|
if err := lr.Err(); err != nil {
|
|
return nil, nil, fmt.Errorf("reading command-code %s: %w", path, err)
|
|
}
|
|
if len(messages) == 0 {
|
|
return nil, nil, nil
|
|
}
|
|
|
|
meta := loadCommandCodeMeta(path)
|
|
if meta != nil {
|
|
if cwd == "" {
|
|
cwd = commandCodeFirstNonEmpty(meta.Cwd, meta.ProjectPath)
|
|
}
|
|
if firstMessage == "" {
|
|
firstMessage = meta.Title
|
|
}
|
|
}
|
|
if sessionID == "" {
|
|
sessionID = strings.TrimSuffix(filepath.Base(path), ".jsonl")
|
|
}
|
|
|
|
project := ExtractProjectFromCwd(cwd)
|
|
if project == "" {
|
|
project = NormalizeName(filepath.Base(filepath.Dir(path)))
|
|
}
|
|
|
|
sessionName := ""
|
|
if meta != nil {
|
|
sessionName = meta.Title
|
|
}
|
|
|
|
sess := &ParsedSession{
|
|
ID: "commandcode:" + sessionID,
|
|
Project: project,
|
|
Machine: machine,
|
|
Agent: AgentCommandCode,
|
|
Cwd: cwd,
|
|
GitBranch: gitBranch,
|
|
SourceVersion: "2",
|
|
MalformedLines: malformedLines,
|
|
FirstMessage: firstMessage,
|
|
SessionName: sessionName,
|
|
StartedAt: startedAt,
|
|
EndedAt: endedAt,
|
|
MessageCount: len(messages),
|
|
UserMessageCount: userCount,
|
|
File: FileInfo{
|
|
Path: path,
|
|
Size: info.Size(),
|
|
Mtime: info.ModTime().UnixNano(),
|
|
},
|
|
}
|
|
return sess, messages, nil
|
|
}
|
|
|
|
func loadCommandCodeMeta(path string) *commandCodeMeta {
|
|
metaPath := strings.TrimSuffix(path, ".jsonl") + ".meta.json"
|
|
data, err := os.ReadFile(metaPath)
|
|
if err != nil {
|
|
return nil
|
|
}
|
|
var meta commandCodeMeta
|
|
if err := json.Unmarshal(data, &meta); err != nil {
|
|
return nil
|
|
}
|
|
return &meta
|
|
}
|
|
|
|
func commandCodeCwd(root gjson.Result) string {
|
|
for _, key := range []string{
|
|
"metadata.cwd",
|
|
"metadata.projectPath",
|
|
"metadata.context.cwd",
|
|
"cwd",
|
|
} {
|
|
if v := root.Get(key).Str; v != "" {
|
|
return v
|
|
}
|
|
}
|
|
return ""
|
|
}
|
|
|
|
func extractCommandCodeContent(
|
|
content gjson.Result,
|
|
) (string, string, bool, bool, []ParsedToolCall, []ParsedToolResult) {
|
|
if content.Type == gjson.String {
|
|
return content.Str, "", false, false, nil, nil
|
|
}
|
|
if !content.IsArray() {
|
|
return "", "", false, false, nil, nil
|
|
}
|
|
|
|
var (
|
|
textParts []string
|
|
thinkingParts []string
|
|
toolCalls []ParsedToolCall
|
|
toolResults []ParsedToolResult
|
|
hasThinking bool
|
|
hasToolUse bool
|
|
)
|
|
|
|
content.ForEach(func(_, block gjson.Result) bool {
|
|
switch block.Get("type").Str {
|
|
case "text":
|
|
if text := block.Get("text").Str; text != "" {
|
|
textParts = append(textParts, text)
|
|
}
|
|
case "reasoning", "thinking":
|
|
thinking := commandCodeFirstNonEmpty(
|
|
block.Get("text").Str,
|
|
block.Get("thinking").Str,
|
|
)
|
|
if thinking != "" {
|
|
hasThinking = true
|
|
thinkingParts = append(thinkingParts, thinking)
|
|
}
|
|
case "tool-call", "tool_use":
|
|
toolName := commandCodeFirstNonEmpty(
|
|
block.Get("toolName").Str,
|
|
block.Get("name").Str,
|
|
)
|
|
if toolName == "" {
|
|
return true
|
|
}
|
|
hasToolUse = true
|
|
input := block.Get("input")
|
|
inputJSON := input.Raw
|
|
if inputJSON == "" {
|
|
inputJSON = "{}"
|
|
}
|
|
toolCalls = append(toolCalls, ParsedToolCall{
|
|
ToolUseID: blockID(block, "toolCallId", "id"),
|
|
ToolName: toolName,
|
|
Category: NormalizeToolCategory(toolName),
|
|
InputJSON: inputJSON,
|
|
})
|
|
case "tool-result", "tool_result":
|
|
toolUseID := blockID(block, "toolCallId", "tool_use_id")
|
|
contentRaw, contentLen := commandCodeToolResultContent(block)
|
|
if toolUseID == "" || contentRaw == "" {
|
|
return true
|
|
}
|
|
toolResults = append(toolResults, ParsedToolResult{
|
|
ToolUseID: toolUseID,
|
|
ContentLength: contentLen,
|
|
ContentRaw: contentRaw,
|
|
})
|
|
}
|
|
return true
|
|
})
|
|
|
|
return strings.Join(textParts, "\n"),
|
|
strings.Join(thinkingParts, "\n\n"),
|
|
hasThinking, hasToolUse, toolCalls, toolResults
|
|
}
|
|
|
|
func commandCodeToolResultContent(block gjson.Result) (string, int) {
|
|
output := block.Get("output")
|
|
if !output.Exists() {
|
|
output = block.Get("content")
|
|
}
|
|
if output.Exists() {
|
|
if output.IsObject() {
|
|
if value := output.Get("value"); value.Exists() &&
|
|
value.Type == gjson.String {
|
|
return strconv.Quote(value.Str), len(value.Str)
|
|
}
|
|
}
|
|
return output.Raw, toolResultContentLength(output)
|
|
}
|
|
|
|
for _, key := range []string{"text", "error", "value"} {
|
|
if text := block.Get(key).Str; text != "" {
|
|
return strconv.Quote(text), len(text)
|
|
}
|
|
}
|
|
return "", 0
|
|
}
|
|
|
|
func blockID(block gjson.Result, keys ...string) string {
|
|
for _, key := range keys {
|
|
if value := block.Get(key).Str; value != "" {
|
|
return value
|
|
}
|
|
}
|
|
return ""
|
|
}
|
|
|
|
func commandCodeFirstNonEmpty(values ...string) string {
|
|
for _, value := range values {
|
|
if value != "" {
|
|
return value
|
|
}
|
|
}
|
|
return ""
|
|
}
|