Files
wehub-resource-sync 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
chore: import upstream snapshot with attribution
2026-07-13 12:30:36 +08:00

295 lines
7.6 KiB
Go

package parser
import (
"encoding/json"
"fmt"
"os"
"path/filepath"
"slices"
"sort"
"strings"
)
const qoderIDPrefix = "qoder:"
type qoderSessionMeta struct {
Title string `json:"title"`
ParentSessionID string `json:"parent_session_id"`
ForkFrom string `json:"fork_from"`
WorkingDir string `json:"working_dir"`
}
func DiscoverQoderSessions(projectsDir string) []DiscoveredFile {
if projectsDir == "" {
return nil
}
projects, err := os.ReadDir(projectsDir)
if err != nil {
return nil
}
var files []DiscoveredFile
for _, projectEntry := range projects {
if !isDirOrSymlink(projectEntry, projectsDir) {
continue
}
projectDir := filepath.Join(projectsDir, projectEntry.Name())
project := DecodeQoderProjectDir(projectEntry.Name())
entries, err := os.ReadDir(projectDir)
if err != nil {
continue
}
for _, entry := range entries {
name := entry.Name()
if !entry.IsDir() && strings.HasSuffix(name, ".jsonl") {
stem := strings.TrimSuffix(name, ".jsonl")
if strings.HasPrefix(stem, "agent-") ||
!IsValidSessionID(stem) {
continue
}
files = append(files, DiscoveredFile{
Path: filepath.Join(projectDir, name),
Project: project,
Agent: AgentQoder,
})
continue
}
if !isDirOrSymlink(entry, projectDir) || !IsValidSessionID(name) {
continue
}
subagentsDir := filepath.Join(projectDir, name, "subagents")
subagents, err := os.ReadDir(subagentsDir)
if err != nil {
continue
}
for _, sub := range subagents {
if sub.IsDir() || !strings.HasSuffix(sub.Name(), ".jsonl") {
continue
}
stem := strings.TrimSuffix(sub.Name(), ".jsonl")
if !strings.HasPrefix(stem, "agent-") ||
!IsValidSessionID(stem) {
continue
}
files = append(files, DiscoveredFile{
Path: filepath.Join(subagentsDir, sub.Name()),
Project: project,
Agent: AgentQoder,
})
}
}
}
sort.Slice(files, func(i, j int) bool {
return files[i].Path < files[j].Path
})
return files
}
func FindQoderSourceFile(projectsDir, rawID string) string {
if projectsDir == "" {
return ""
}
rawID = strings.TrimPrefix(rawID, qoderIDPrefix)
sessionID, subagentID, hasSubagent := strings.Cut(rawID, ":subagent:")
if !IsValidSessionID(sessionID) {
return ""
}
if hasSubagent &&
(!strings.HasPrefix(subagentID, "agent-") || !IsValidSessionID(subagentID)) {
return ""
}
projects, err := os.ReadDir(projectsDir)
if err != nil {
return ""
}
for _, projectEntry := range projects {
if !isDirOrSymlink(projectEntry, projectsDir) {
continue
}
projectDir := filepath.Join(projectsDir, projectEntry.Name())
candidate := filepath.Join(projectDir, sessionID+".jsonl")
if hasSubagent {
candidate = filepath.Join(projectDir, sessionID, "subagents", subagentID+".jsonl")
}
if _, err := os.Stat(candidate); err == nil {
return candidate
}
}
return ""
}
func ParseQoderSession(path, project, machine string) ([]ParseResult, error) {
results, _, err := ParseQoderSessionWithExclusions(path, project, machine)
return results, err
}
func ParseQoderSessionWithExclusions(
path, project, machine string,
) ([]ParseResult, []string, error) {
results, excluded, err := claudeParseWithExclusions(
path, project, machine,
)
if err != nil {
return nil, nil, fmt.Errorf("qoder parse %s: %w", path, err)
}
meta := readQoderSessionMeta(path)
parentID, subagentID, isSubagent := qoderPathIDs(path, project)
fileStem := strings.TrimSuffix(filepath.Base(path), ".jsonl")
for i := range results {
retagQoderResult(&results[i], fileStem, parentID, subagentID, isSubagent)
if !isSubagent {
applyQoderMeta(&results[i].Session, meta)
}
}
for i := range excluded {
excluded[i] = qoderExcludedID(
excluded[i], fileStem, parentID, subagentID, isSubagent,
)
}
InferRelationshipTypes(results)
return results, excluded, nil
}
func DecodeQoderProjectDir(encoded string) string {
if !strings.HasPrefix(encoded, "-") {
return NormalizeName(encoded)
}
parts := strings.Split(encoded, "-")
for i := 0; i < len(parts)-1; i++ {
if isQoderProjectParentDir(parts[i]) {
project := strings.Join(parts[i+1:], "-")
if project != "" {
return NormalizeName(project)
}
}
}
for _, v := range slices.Backward(parts) {
if v != "" {
return NormalizeName(v)
}
}
return NormalizeName(encoded)
}
func isQoderProjectParentDir(part string) bool {
switch strings.ToLower(part) {
case "code", "coding", "dev", "development", "projects", "repos", "src", "work", "workspace":
return true
default:
return false
}
}
func retagQoderResult(
result *ParseResult,
fileStem, parentID, subagentID string,
isSubagent bool,
) {
rawID := strings.TrimPrefix(result.Session.ID, qoderIDPrefix)
if isSubagent {
suffix := strings.TrimPrefix(rawID, fileStem)
result.Session.ID = qoderSubagentID(parentID, subagentID+suffix)
result.Session.ParentSessionID = qoderPrefixID(parentID)
result.Session.RelationshipType = RelSubagent
} else {
result.Session.ID = qoderPrefixID(rawID)
result.Session.ParentSessionID = qoderPrefixMaybe(result.Session.ParentSessionID)
}
result.Session.Agent = AgentQoder
toolCallParentID := result.Session.ID
if !isSubagent {
toolCallParentID = qoderPrefixID(fileStem)
}
retagQoderToolCalls(result.Messages, toolCallParentID)
}
func retagQoderToolCalls(messages []ParsedMessage, sessionID string) {
for i := range messages {
for j := range messages[i].ToolCalls {
subagentID := messages[i].ToolCalls[j].SubagentSessionID
if strings.HasPrefix(subagentID, "agent-") {
messages[i].ToolCalls[j].SubagentSessionID =
sessionID + ":subagent:" + subagentID
}
}
}
}
func applyQoderMeta(sess *ParsedSession, meta qoderSessionMeta) {
if meta.Title != "" {
sess.SessionName = meta.Title
}
if sess.Cwd == "" && meta.WorkingDir != "" {
sess.Cwd = meta.WorkingDir
}
if meta.ForkFrom != "" && !hasParserDiscoveredForkParent(sess) {
sess.ParentSessionID = qoderPrefixID(meta.ForkFrom)
sess.RelationshipType = RelFork
} else if sess.ParentSessionID == "" && meta.ParentSessionID != "" {
sess.ParentSessionID = qoderPrefixID(meta.ParentSessionID)
}
}
func hasParserDiscoveredForkParent(sess *ParsedSession) bool {
return sess.RelationshipType == RelFork && sess.ParentSessionID != ""
}
func readQoderSessionMeta(path string) qoderSessionMeta {
metaPath := strings.TrimSuffix(path, ".jsonl") + "-session.json"
data, err := os.ReadFile(metaPath)
if err != nil {
return qoderSessionMeta{}
}
var meta qoderSessionMeta
if err := json.Unmarshal(data, &meta); err != nil {
return qoderSessionMeta{}
}
return meta
}
func qoderPathIDs(path, _ string) (parentID, subagentID string, isSubagent bool) {
if filepath.Base(filepath.Dir(path)) != "subagents" {
return "", "", false
}
parent := filepath.Base(filepath.Dir(filepath.Dir(path)))
if !IsValidSessionID(parent) {
return "", "", false
}
stem := strings.TrimSuffix(filepath.Base(path), ".jsonl")
if !strings.HasPrefix(stem, "agent-") || !IsValidSessionID(stem) {
return "", "", false
}
return parent, stem, true
}
func qoderPrefixID(id string) string {
id = strings.TrimPrefix(id, qoderIDPrefix)
return qoderIDPrefix + id
}
func qoderPrefixMaybe(id string) string {
if id == "" {
return ""
}
return qoderPrefixID(id)
}
func qoderSubagentID(parentID, subagentID string) string {
return qoderPrefixID(parentID) + ":subagent:" + subagentID
}
func qoderExcludedID(
id, fileStem, parentID, subagentID string,
isSubagent bool,
) string {
rawID := strings.TrimPrefix(id, qoderIDPrefix)
if isSubagent {
suffix := strings.TrimPrefix(rawID, fileStem)
return qoderSubagentID(parentID, subagentID+suffix)
}
return qoderPrefixID(rawID)
}