Files
wehub-resource-sync f99010fae1
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
Desktop Artifacts / Desktop Build (Linux (arm64)) (push) Has been cancelled
Desktop Artifacts / Desktop Build (Linux) (push) Has been cancelled
Desktop Artifacts / Desktop Build (Windows) (push) Has been cancelled
Desktop Artifacts (macOS) / Desktop Build (macOS (aarch64)) (push) Has been cancelled
Desktop Artifacts (macOS) / Desktop Build (macOS (x86_64)) (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 12:30:36 +08:00

950 lines
23 KiB
Go

package parser
import (
"context"
"crypto/sha256"
"database/sql"
"encoding/json"
"fmt"
"io"
"os"
"path/filepath"
"sort"
"strings"
_ "github.com/mattn/go-sqlite3"
)
const (
windsurfStateDBName = "state.vscdb"
// WindsurfStateDBName is the shared SQLite store used by Windsurf workspace chat.
WindsurfStateDBName = windsurfStateDBName
)
var windsurfChatDataKeys = []string{
"workbench.panel.aichat.view.aichat.chatdata",
"aiChat.chatdata",
}
var _ Provider = (*windsurfProvider)(nil)
type windsurfProviderFactory struct {
def AgentDef
}
func newWindsurfProviderFactory(def AgentDef) ProviderFactory {
return windsurfProviderFactory{def: cloneAgentDef(def)}
}
func (f windsurfProviderFactory) Definition() AgentDef {
return cloneAgentDef(f.def)
}
func (f windsurfProviderFactory) Capabilities() Capabilities {
return windsurfProviderCapabilities()
}
func (f windsurfProviderFactory) NewProvider(cfg ProviderConfig) Provider {
cfg = cfg.Clone()
return &windsurfProvider{
ProviderBase: ProviderBase{
Def: cloneAgentDef(f.def),
Caps: windsurfProviderCapabilities(),
Config: cfg,
},
sources: newWindsurfSourceSet(cfg.Roots),
}
}
type windsurfProvider struct {
ProviderBase
sources windsurfSourceSet
}
func (p *windsurfProvider) Discover(ctx context.Context) ([]SourceRef, error) {
return p.sources.Discover(ctx)
}
func (p *windsurfProvider) WatchPlan(ctx context.Context) (WatchPlan, error) {
return p.sources.WatchPlan(ctx)
}
func (p *windsurfProvider) SourcesForChangedPath(
ctx context.Context,
req ChangedPathRequest,
) ([]SourceRef, error) {
return p.sources.SourcesForChangedPath(ctx, req)
}
func (p *windsurfProvider) FindSource(
ctx context.Context,
req FindSourceRequest,
) (SourceRef, bool, error) {
req = ProviderFindRequestWithRawSessionID(p.Def, req)
return p.sources.FindSource(ctx, req)
}
func (p *windsurfProvider) Fingerprint(
ctx context.Context,
source SourceRef,
) (SourceFingerprint, error) {
return p.sources.Fingerprint(ctx, source)
}
func (p *windsurfProvider) Parse(
ctx context.Context,
req ParseRequest,
) (ParseOutcome, error) {
if err := ctx.Err(); err != nil {
return ParseOutcome{}, err
}
src, ok := p.sources.sourceFromRef(req.Source)
if !ok {
return ParseOutcome{}, fmt.Errorf("windsurf source path unavailable")
}
if _, err := os.Stat(src.DBPath); err != nil {
if os.IsNotExist(err) {
return ParseOutcome{
ResultSetComplete: true,
SkipReason: SkipNoSession,
}, nil
}
return ParseOutcome{}, fmt.Errorf("stat %s: %w", src.DBPath, err)
}
machine := firstNonEmptyJSONLString(req.Machine, p.Config.Machine)
sess, msgs, err := parseWindsurfSession(
src.DBPath, src.SessionID, src.Project, machine, src.VirtualPath,
)
if err == sql.ErrNoRows {
return ParseOutcome{
ResultSetComplete: true,
ForceReplace: true,
SkipReason: SkipNoSession,
}, nil
}
if err != nil {
return ParseOutcome{}, err
}
if sess == nil {
return ParseOutcome{
ResultSetComplete: true,
ForceReplace: true,
SkipReason: SkipNoSession,
}, nil
}
if req.Fingerprint.Hash != "" {
sess.File.Hash = req.Fingerprint.Hash
sess.File.Size = req.Fingerprint.Size
sess.File.Mtime = req.Fingerprint.MTimeNS
}
return ParseOutcome{
Results: []ParseResultOutcome{{
Result: ParseResult{
Session: *sess,
Messages: msgs,
UsageEvents: sess.UsageEvents,
},
DataVersion: DataVersionCurrent,
}},
ResultSetComplete: true,
ForceReplace: true,
}, nil
}
type windsurfSource struct {
Root string
DBPath string
SessionID string
Project string
VirtualPath string
}
type windsurfSourceSet struct {
roots []string
}
func newWindsurfSourceSet(roots []string) windsurfSourceSet {
return windsurfSourceSet{roots: cleanJSONLRoots(roots)}
}
func (s windsurfSourceSet) Discover(ctx context.Context) ([]SourceRef, error) {
var sources []SourceRef
seen := make(map[string]struct{})
for _, root := range s.roots {
if err := ctx.Err(); err != nil {
return nil, err
}
dbs := s.workspaceDBs(root)
for _, db := range dbs {
records, err := listWindsurfSessionRecords(db.DBPath)
if err != nil {
return nil, err
}
for _, record := range records {
ref := s.newSourceRef(root, db.DBPath, record.SessionID, db.Project)
ref.DiscoveryMTimeNS = db.MTimeNS
addJSONLSource(ref, &sources, seen)
}
}
}
sortJSONLSources(sources)
return sources, nil
}
func (s windsurfSourceSet) WatchPlan(context.Context) (WatchPlan, error) {
roots := make([]WatchRoot, 0, len(s.roots))
for _, root := range s.roots {
workspace := windsurfWorkspaceRoot(root)
roots = append(roots, WatchRoot{
Path: workspace,
Recursive: true,
IncludeGlobs: []string{windsurfStateDBName, windsurfStateDBName + "-wal", "workspace.json"},
DebounceKey: string(AgentWindsurf) + ":workspace:" + workspace,
})
}
return WatchPlan{Roots: roots}, nil
}
func (s windsurfSourceSet) SourcesForChangedPath(
ctx context.Context,
req ChangedPathRequest,
) ([]SourceRef, error) {
if err := ctx.Err(); err != nil {
return nil, err
}
for _, root := range s.roots {
dbPath, ok := s.dbPathForEvent(root, req)
if !ok {
continue
}
if _, err := os.Stat(dbPath); err != nil {
if os.IsNotExist(err) {
return nil, nil
}
return nil, fmt.Errorf("stat %s: %w", dbPath, err)
}
sources, err := s.sourcesForDB(root, dbPath)
if err != nil {
return nil, err
}
for _, path := range req.StoredSourcePaths {
ref, ok := s.sourceRef(root, path)
if !ok {
continue
}
src := ref.Opaque.(windsurfSource)
if samePath(src.DBPath, dbPath) {
sources = append(sources, ref)
}
}
sortJSONLSources(sources)
return sources, nil
}
return nil, nil
}
func (s windsurfSourceSet) FindSource(
ctx context.Context,
req FindSourceRequest,
) (SourceRef, bool, error) {
if err := ctx.Err(); err != nil {
return SourceRef{}, false, err
}
for _, path := range []string{req.StoredFilePath, req.FingerprintKey} {
if path == "" {
continue
}
for _, root := range s.roots {
ref, ok := s.sourceRef(root, path)
if !ok {
continue
}
if !req.RequireFreshSource {
return ref, true, nil
}
src := ref.Opaque.(windsurfSource)
exists, err := windsurfDBHasSession(src.DBPath, src.SessionID)
if err != nil {
return SourceRef{}, false, err
}
if exists {
return ref, true, nil
}
}
}
if req.RawSessionID == "" {
return SourceRef{}, false, nil
}
for _, root := range s.roots {
for _, db := range s.workspaceDBs(root) {
records, err := listWindsurfSessionRecords(db.DBPath)
if err != nil {
return SourceRef{}, false, err
}
for _, record := range records {
if record.SessionID == req.RawSessionID {
return s.newSourceRef(root, db.DBPath, record.SessionID, db.Project), true, nil
}
}
}
}
return SourceRef{}, false, nil
}
func (s windsurfSourceSet) Fingerprint(
ctx context.Context,
source SourceRef,
) (SourceFingerprint, error) {
if err := ctx.Err(); err != nil {
return SourceFingerprint{}, err
}
src, ok := s.sourceFromRef(source)
if !ok {
return SourceFingerprint{}, fmt.Errorf("windsurf source path unavailable")
}
info, err := os.Stat(src.DBPath)
if err != nil {
if os.IsNotExist(err) {
return SourceFingerprint{
Key: firstNonEmptyJSONLString(
source.FingerprintKey,
source.Key,
src.VirtualPath,
),
}, nil
}
return SourceFingerprint{}, fmt.Errorf("stat %s: %w", src.DBPath, err)
}
workspacePath := windsurfWorkspaceManifestPath(src.DBPath)
combined := antigravityCLICombinedFileInfo(
info,
src.DBPath+"-wal",
workspacePath,
)
hash, err := windsurfSourceHash(src.DBPath, workspacePath)
if err != nil {
return SourceFingerprint{}, err
}
return SourceFingerprint{
Key: firstNonEmptyJSONLString(source.FingerprintKey, source.Key, src.VirtualPath),
Size: combined.Size(),
MTimeNS: combined.ModTime().UnixNano(),
Hash: hash,
}, nil
}
func (s windsurfSourceSet) sourceFromRef(source SourceRef) (windsurfSource, bool) {
switch src := source.Opaque.(type) {
case windsurfSource:
return src, src.DBPath != "" && src.SessionID != ""
case *windsurfSource:
if src != nil && src.DBPath != "" && src.SessionID != "" {
return *src, true
}
}
for _, candidate := range []string{source.DisplayPath, source.FingerprintKey, source.Key} {
for _, root := range s.roots {
if ref, ok := s.sourceRef(root, candidate); ok {
return ref.Opaque.(windsurfSource), true
}
}
}
return windsurfSource{}, false
}
func (s windsurfSourceSet) sourceRef(root, virtualPath string) (SourceRef, bool) {
dbPath, sessionID, ok := splitWindsurfVirtualPath(virtualPath)
if !ok || sessionID == "" {
return SourceRef{}, false
}
if !s.dbBelongsToRoot(root, dbPath) {
return SourceRef{}, false
}
return s.newSourceRef(
root,
dbPath,
sessionID,
windsurfWorkspaceProject(dbPath),
), true
}
func (s windsurfSourceSet) newSourceRef(
root, dbPath, sessionID, project string,
) SourceRef {
virtualPath := windsurfVirtualPath(dbPath, sessionID)
return SourceRef{
Provider: AgentWindsurf,
Key: virtualPath,
DisplayPath: virtualPath,
FingerprintKey: virtualPath,
ProjectHint: project,
Opaque: windsurfSource{
Root: root,
DBPath: dbPath,
SessionID: sessionID,
Project: project,
VirtualPath: virtualPath,
},
}
}
func (s windsurfSourceSet) sourcesForDB(root, dbPath string) ([]SourceRef, error) {
records, err := listWindsurfSessionRecords(dbPath)
if err != nil {
return nil, err
}
project := windsurfWorkspaceProject(dbPath)
sources := make([]SourceRef, 0, len(records))
seen := make(map[string]struct{}, len(records))
for _, record := range records {
addJSONLSource(
s.newSourceRef(root, dbPath, record.SessionID, project),
&sources,
seen,
)
}
return sources, nil
}
func (s windsurfSourceSet) dbPathForEvent(
root string,
req ChangedPathRequest,
) (string, bool) {
if req.WatchRoot != "" {
want := windsurfWorkspaceRoot(root)
if !samePath(req.WatchRoot, want) {
return "", false
}
}
workspaceRoot := windsurfWorkspaceRoot(root)
path := filepath.Clean(req.Path)
rel, ok := relUnder(workspaceRoot, path)
if !ok {
return "", false
}
parts := strings.Split(filepath.ToSlash(rel), "/")
if len(parts) != 2 {
return "", false
}
switch parts[1] {
case windsurfStateDBName, windsurfStateDBName + "-wal", "workspace.json":
return filepath.Join(workspaceRoot, parts[0], windsurfStateDBName), true
default:
return "", false
}
}
func (s windsurfSourceSet) dbBelongsToRoot(root, dbPath string) bool {
workspaceRoot := windsurfWorkspaceRoot(root)
rel, ok := relUnder(workspaceRoot, filepath.Clean(dbPath))
if !ok {
return false
}
parts := strings.Split(filepath.ToSlash(rel), "/")
return len(parts) == 2 && parts[1] == windsurfStateDBName
}
type windsurfWorkspaceDB struct {
DBPath string
Project string
MTimeNS int64
}
func (s windsurfSourceSet) workspaceDBs(root string) []windsurfWorkspaceDB {
workspaceRoot := windsurfWorkspaceRoot(root)
entries, err := os.ReadDir(workspaceRoot)
if err != nil {
return nil
}
dbs := make([]windsurfWorkspaceDB, 0, len(entries))
for _, entry := range entries {
if !entry.IsDir() {
continue
}
dbPath := filepath.Join(workspaceRoot, entry.Name(), windsurfStateDBName)
info, err := os.Stat(dbPath)
if err != nil || info.IsDir() {
continue
}
dbs = append(dbs, windsurfWorkspaceDB{
DBPath: dbPath,
Project: windsurfWorkspaceProject(dbPath),
MTimeNS: info.ModTime().UnixNano(),
})
}
sort.Slice(dbs, func(i, j int) bool {
return dbs[i].DBPath < dbs[j].DBPath
})
return dbs
}
func windsurfWorkspaceRoot(root string) string {
clean := filepath.Clean(root)
if filepath.Base(clean) == "workspaceStorage" {
return clean
}
return filepath.Join(clean, "workspaceStorage")
}
type windsurfSessionRecord struct {
SessionID string
Data []byte
}
type windsurfChatValue struct {
Key string
Value string
}
func listWindsurfSessionRecords(dbPath string) ([]windsurfSessionRecord, error) {
values, err := readWindsurfChatValues(dbPath)
if err != nil {
return nil, err
}
seen := make(map[string]struct{})
var records []windsurfSessionRecord
for _, value := range values {
next, err := windsurfRecordsFromValue(
[]byte(value.Value),
windsurfFallbackSessionID(dbPath),
)
if err != nil {
return nil, err
}
for _, record := range next {
if _, ok := seen[record.SessionID]; ok {
continue
}
seen[record.SessionID] = struct{}{}
records = append(records, record)
}
}
sort.Slice(records, func(i, j int) bool {
return records[i].SessionID < records[j].SessionID
})
return records, nil
}
func readWindsurfChatValues(dbPath string) ([]windsurfChatValue, error) {
if _, err := os.Stat(dbPath); os.IsNotExist(err) {
return nil, nil
}
db, err := openWindsurfDB(dbPath)
if err != nil {
return nil, err
}
defer db.Close()
values := make([]windsurfChatValue, 0, len(windsurfChatDataKeys))
for _, key := range windsurfChatDataKeys {
var value string
err := db.QueryRow(
`SELECT value FROM ItemTable WHERE key = ?`,
key,
).Scan(&value)
if err == sql.ErrNoRows {
continue
}
if err != nil {
return nil, fmt.Errorf("read windsurf chat data: %w", err)
}
values = append(values, windsurfChatValue{
Key: key,
Value: value,
})
}
return values, nil
}
func windsurfDBHasSession(dbPath, sessionID string) (bool, error) {
records, err := listWindsurfSessionRecords(dbPath)
if err != nil {
return false, err
}
for _, record := range records {
if record.SessionID == sessionID {
return true, nil
}
}
return false, nil
}
func parseWindsurfSession(
dbPath, sessionID, project, machine, virtualPath string,
) (*ParsedSession, []ParsedMessage, error) {
record, err := loadWindsurfSessionRecord(dbPath, sessionID)
if err != nil {
return nil, nil, err
}
sess, msgs, err := parseVSCodeCopilotData(
record.Data,
virtualPath,
project,
machine,
)
if err != nil {
return nil, nil, err
}
if sess == nil {
return nil, nil, nil
}
info, err := os.Stat(dbPath)
if err != nil {
return nil, nil, fmt.Errorf("stat %s: %w", dbPath, err)
}
combined := antigravityCLICombinedFileInfo(
info,
dbPath+"-wal",
windsurfWorkspaceManifestPath(dbPath),
)
sess.Agent = AgentWindsurf
sess.ID = "windsurf:" + strings.TrimPrefix(sess.ID, "windsurf:")
sess.File = FileInfo{
Path: virtualPath,
Size: combined.Size(),
Mtime: combined.ModTime().UnixNano(),
}
for i := range sess.UsageEvents {
sess.UsageEvents[i].SessionID = sess.ID
sess.UsageEvents[i].Source = string(AgentWindsurf)
}
return sess, msgs, nil
}
func loadWindsurfSessionRecord(
dbPath, sessionID string,
) (windsurfSessionRecord, error) {
records, err := listWindsurfSessionRecords(dbPath)
if err != nil {
return windsurfSessionRecord{}, err
}
for _, record := range records {
if record.SessionID == sessionID {
return record, nil
}
}
return windsurfSessionRecord{}, sql.ErrNoRows
}
func openWindsurfDB(dbPath string) (*sql.DB, error) {
dsn := "file:" + sqliteURIPath(dbPath) + "?mode=ro&immutable=0&_busy_timeout=3000"
db, err := sql.Open("sqlite3", dsn)
if err != nil {
return nil, fmt.Errorf("open windsurf db %s: %w", dbPath, err)
}
return db, nil
}
func windsurfRecordsFromValue(
data []byte,
fallbackSessionID string,
) ([]windsurfSessionRecord, error) {
var session vscodeCopilotSession
if err := json.Unmarshal(data, &session); err == nil &&
len(session.Requests) > 0 {
id := session.SessionID
if id == "" {
id = fallbackSessionID
session.SessionID = id
payload, err := json.Marshal(session)
if err != nil {
return nil, err
}
data = payload
}
return []windsurfSessionRecord{{
SessionID: id,
Data: append([]byte(nil), data...),
}}, nil
}
var chatData windsurfChatData
if err := json.Unmarshal(data, &chatData); err != nil {
return nil, fmt.Errorf("parse windsurf chatdata: %w", err)
}
records := make([]windsurfSessionRecord, 0, len(chatData.Tabs))
for _, tab := range chatData.Tabs {
session, ok := tab.toVSCodeSession()
if !ok {
continue
}
payload, err := json.Marshal(session)
if err != nil {
return nil, err
}
records = append(records, windsurfSessionRecord{
SessionID: session.SessionID,
Data: payload,
})
}
return records, nil
}
func windsurfFallbackSessionID(dbPath string) string {
workspaceID := strings.TrimSpace(filepath.Base(filepath.Dir(dbPath)))
if workspaceID == "" || workspaceID == "." || workspaceID == string(filepath.Separator) {
hash := sha256.Sum256([]byte(filepath.ToSlash(dbPath)))
return fmt.Sprintf("workspace-%x", hash[:8])
}
return "workspace-" + workspaceID
}
type windsurfChatData struct {
Tabs []windsurfChatTab `json:"tabs"`
}
type windsurfChatTab struct {
TabID string `json:"tabId"`
ChatTitle string `json:"chatTitle"`
Bubbles []windsurfChatBubble `json:"bubbles"`
}
type windsurfChatBubble struct {
Type windsurfBubbleType `json:"type"`
Text string `json:"text"`
RawText string `json:"rawText"`
InitText string `json:"initText"`
}
type windsurfBubbleType string
func (t *windsurfBubbleType) UnmarshalJSON(data []byte) error {
var value string
if err := json.Unmarshal(data, &value); err == nil {
*t = windsurfBubbleType(value)
return nil
}
var number json.Number
if err := json.Unmarshal(data, &number); err == nil {
*t = windsurfBubbleType(number.String())
return nil
}
*t = ""
return nil
}
func (t windsurfChatTab) toVSCodeSession() (vscodeCopilotSession, bool) {
id := strings.TrimSpace(t.TabID)
if id == "" || len(t.Bubbles) == 0 {
return vscodeCopilotSession{}, false
}
session := vscodeCopilotSession{
Version: 1,
SessionID: id,
CustomTitle: t.ChatTitle,
}
var current *vscodeCopilotRequest
flush := func() {
if current == nil {
return
}
session.Requests = append(session.Requests, *current)
current = nil
}
for i, bubble := range t.Bubbles {
content := strings.TrimSpace(bubble.content())
if content == "" {
continue
}
if bubble.isAssistant() {
if current == nil {
current = &vscodeCopilotRequest{
RequestID: fmt.Sprintf("%s-%d", id, i),
}
}
current.Response = append(current.Response, windsurfResponseItem(content))
continue
}
flush()
current = &vscodeCopilotRequest{
RequestID: fmt.Sprintf("%s-%d", id, i),
Message: vscodeCopilotMessage{
Text: content,
},
}
}
flush()
return session, len(session.Requests) > 0
}
func (b windsurfChatBubble) content() string {
for _, value := range []string{b.Text, b.RawText, b.InitText} {
if strings.TrimSpace(value) != "" {
return value
}
}
return ""
}
func (b windsurfChatBubble) isAssistant() bool {
switch strings.ToLower(strings.TrimSpace(string(b.Type))) {
case "2", "ai", "assistant":
return true
default:
return false
}
}
func windsurfResponseItem(content string) json.RawMessage {
data, _ := json.Marshal(map[string]string{"value": content})
return data
}
func windsurfVirtualPath(dbPath, sessionID string) string {
return dbPath + "#" + sessionID
}
func SplitWindsurfVirtualPath(path string) (string, string, bool) {
return splitWindsurfVirtualPath(path)
}
func splitWindsurfVirtualPath(path string) (string, string, bool) {
return ParseVirtualSourcePathForBase(path, windsurfStateDBName)
}
func WriteWindsurfSessionJSON(w io.Writer, dbPath, sessionID string) error {
record, err := loadWindsurfSessionRecord(dbPath, sessionID)
if err == sql.ErrNoRows {
return fmt.Errorf(
"windsurf session %s not found in %s: %w",
sessionID, dbPath, os.ErrNotExist,
)
}
if err != nil {
return err
}
_, err = w.Write(record.Data)
return err
}
func WriteSanitizedWindsurfStateDB(dstPath, dbPath string) error {
values, err := readWindsurfChatValues(dbPath)
if err != nil {
return err
}
if err := os.MkdirAll(filepath.Dir(dstPath), 0o755); err != nil {
return fmt.Errorf("create windsurf export dir: %w", err)
}
if err := os.Remove(dstPath); err != nil && !os.IsNotExist(err) {
return fmt.Errorf("replace windsurf export db %s: %w", dstPath, err)
}
dst, err := sql.Open(
"sqlite3",
"file:"+sqliteURIPath(dstPath)+"?mode=rwc&_busy_timeout=3000",
)
if err != nil {
return fmt.Errorf("open sanitized windsurf db %s: %w", dstPath, err)
}
complete := false
defer func() {
_ = dst.Close()
if !complete {
_ = os.Remove(dstPath)
}
}()
if _, err := dst.Exec(`PRAGMA journal_mode=DELETE`); err != nil {
return fmt.Errorf("configure sanitized windsurf db: %w", err)
}
if _, err := dst.Exec(`CREATE TABLE ItemTable (key TEXT PRIMARY KEY, value TEXT)`); err != nil {
return fmt.Errorf("create sanitized windsurf ItemTable: %w", err)
}
tx, err := dst.Begin()
if err != nil {
return fmt.Errorf("begin sanitized windsurf export: %w", err)
}
stmt, err := tx.Prepare(`INSERT INTO ItemTable (key, value) VALUES (?, ?)`)
if err != nil {
_ = tx.Rollback()
return fmt.Errorf("prepare sanitized windsurf export: %w", err)
}
for _, value := range values {
if _, err := stmt.Exec(value.Key, value.Value); err != nil {
_ = stmt.Close()
_ = tx.Rollback()
return fmt.Errorf("write sanitized windsurf chat key: %w", err)
}
}
if err := stmt.Close(); err != nil {
_ = tx.Rollback()
return fmt.Errorf("close sanitized windsurf export: %w", err)
}
if err := tx.Commit(); err != nil {
return fmt.Errorf("commit sanitized windsurf export: %w", err)
}
if err := dst.Close(); err != nil {
return fmt.Errorf("close sanitized windsurf db: %w", err)
}
complete = true
return nil
}
func windsurfWorkspaceManifestPath(dbPath string) string {
return filepath.Join(filepath.Dir(dbPath), "workspace.json")
}
func windsurfWorkspaceProject(dbPath string) string {
project := readVSCodeWorkspaceManifest(filepath.Dir(dbPath))
if project == "" {
return "unknown"
}
return project
}
func windsurfSourceHash(dbPath, workspacePath string) (string, error) {
h := sha256.New()
if IsRegularFile(dbPath) {
values, err := readWindsurfChatValues(dbPath)
if err != nil {
return "", err
}
for _, value := range values {
_, _ = h.Write([]byte("chat"))
_, _ = h.Write([]byte{0})
_, _ = h.Write([]byte(value.Key))
_, _ = h.Write([]byte{0})
_, _ = h.Write([]byte(value.Value))
_, _ = h.Write([]byte{0})
}
}
if workspacePath != "" && IsRegularFile(workspacePath) {
hash, err := hashJSONLSourceFile(workspacePath)
if err != nil {
return "", err
}
_, _ = h.Write([]byte("workspace"))
_, _ = h.Write([]byte{0})
_, _ = h.Write([]byte(hash))
_, _ = h.Write([]byte{0})
}
return fmt.Sprintf("%x", h.Sum(nil)), nil
}
func windsurfProviderCapabilities() Capabilities {
return Capabilities{
Source: SourceCapabilities{
DiscoverSources: CapabilitySupported,
WatchSources: CapabilitySupported,
ClassifyChangedPath: CapabilitySupported,
StoredSourceHints: CapabilitySupported,
FindSource: CapabilitySupported,
CompositeFingerprint: CapabilitySupported,
IncrementalAppend: CapabilityNotApplicable,
MultiSessionSource: CapabilityNotApplicable,
PerSessionErrors: CapabilityNotApplicable,
ExcludedSessions: CapabilityNotApplicable,
ForceReplaceOnParse: CapabilitySupported,
},
Content: ContentCapabilities{
FirstMessage: CapabilitySupported,
ToolCalls: CapabilitySupported,
ToolResults: CapabilitySupported,
Thinking: CapabilitySupported,
AggregateUsageEvents: CapabilitySupported,
Model: CapabilitySupported,
},
}
}