Skip to content

Commit cce6b6f

Browse files
authored
Merge pull request #1315 from wakatime/develop
Release v2.0.14
2 parents 97f39ce + a0e3a21 commit cce6b6f

File tree

8 files changed

+140
-53
lines changed

8 files changed

+140
-53
lines changed

pkg/ai/ai.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -308,7 +308,7 @@ func aiUserAgent(ctx context.Context, entity string, userAgents map[string]strin
308308
}
309309

310310
if existing != "" {
311-
return heartbeat.UserAgent(ctx, existing+" "+parser)
311+
return heartbeat.UserAgent(ctx, parser+" "+existing)
312312
}
313313

314314
return heartbeat.UserAgent(ctx, parser)

pkg/ai/claude.go

Lines changed: 17 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -251,6 +251,7 @@ func (g Claude) parseTranscript(ctx context.Context, transcript string) (Heartbe
251251
var heartbeats Heartbeats
252252

253253
claudeVersion := ""
254+
sessionEntity := filepath.Base(transcript)
254255

255256
for scanner.Scan() {
256257
if ctx.Err() != nil {
@@ -282,7 +283,7 @@ func (g Claude) parseTranscript(ctx context.Context, transcript string) (Heartbe
282283
continue
283284
}
284285

285-
heartbeats = append(heartbeats, g.claudeHeartbeats(ctx, logLine, claudeVersion)...)
286+
heartbeats = append(heartbeats, g.claudeHeartbeats(ctx, logLine, sessionEntity, claudeVersion)...)
286287
}
287288

288289
if err := scanner.Err(); err != nil {
@@ -292,10 +293,15 @@ func (g Claude) parseTranscript(ctx context.Context, transcript string) (Heartbe
292293
return heartbeats, nil
293294
}
294295

295-
func (g Claude) claudeHeartbeats(ctx context.Context, logLine claudeLogLine, version string) Heartbeats {
296+
func (g Claude) claudeHeartbeats(
297+
ctx context.Context,
298+
logLine claudeLogLine,
299+
sessionEntity string,
300+
version string,
301+
) Heartbeats {
296302
var heartbeats Heartbeats
297303

298-
if heartbeat := g.claudeAppHeartbeat(ctx, logLine, version); heartbeat != nil {
304+
if heartbeat := g.claudeAppHeartbeat(ctx, logLine, sessionEntity, version); heartbeat != nil {
299305
heartbeats = append(heartbeats, *heartbeat)
300306
}
301307

@@ -306,19 +312,23 @@ func (g Claude) claudeHeartbeats(ctx context.Context, logLine claudeLogLine, ver
306312
return heartbeats
307313
}
308314

309-
func (g Claude) claudeAppHeartbeat(ctx context.Context, logLine claudeLogLine, version string) *heartbeat.Heartbeat {
315+
func (g Claude) claudeAppHeartbeat(
316+
ctx context.Context,
317+
logLine claudeLogLine,
318+
sessionEntity string,
319+
version string,
320+
) *heartbeat.Heartbeat {
310321
lineChanges := claudeAppLineChanges(logLine.ToolUseResult)
311322
if lineChanges == 0 {
312323
return nil
313324
}
314325

315-
entity := "ClaudeCode"
316326
h := heartbeat.New(
317327
nil,
318328
"",
319329
heartbeat.AICodingCategory.String(),
320330
nil,
321-
entity,
331+
sessionEntity,
322332
heartbeat.AppType,
323333
nil,
324334
false,
@@ -333,7 +343,7 @@ func (g Claude) claudeAppHeartbeat(ctx context.Context, logLine claudeLogLine, v
333343
"",
334344
"",
335345
float64(logLine.Timestamp.Unix()),
336-
aiUserAgent(ctx, entity, g.UserAgents, g.FallbackUserAgent, claudePlugin(version)),
346+
aiUserAgent(ctx, sessionEntity, g.UserAgents, g.FallbackUserAgent, claudePlugin(version)),
337347
)
338348

339349
return &h

pkg/ai/claude_test.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -75,15 +75,15 @@ func TestClaudeParse(t *testing.T) {
7575
assert.Contains(t, got[0].UserAgent, heartbeat.UserAgent(ctx, "editor/1.2.3"))
7676
assert.Contains(t, got[0].UserAgent, "ClaudeCode/2.1.45")
7777

78-
assert.Equal(t, "ClaudeCode", got[1].Entity)
78+
assert.Equal(t, filepath.Base(transcriptPath), got[1].Entity)
7979
assert.Equal(t, heartbeat.AppType, got[1].EntityType)
8080
assert.Nil(t, got[1].AILineChanges)
8181
require.NotNil(t, got[1].IsWrite)
8282
assert.False(t, *got[1].IsWrite)
8383
assert.Contains(t, got[1].UserAgent, "plugin/0.0.1")
8484
assert.Contains(t, got[1].UserAgent, "ClaudeCode/2.1.45")
8585

86-
assert.Equal(t, "ClaudeCode", got[2].Entity)
86+
assert.Equal(t, filepath.Base(transcriptPath), got[2].Entity)
8787
assert.Equal(t, heartbeat.AppType, got[2].EntityType)
8888
assert.Nil(t, got[2].AILineChanges)
8989
require.NotNil(t, got[2].IsWrite)

pkg/ai/codex.go

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -142,6 +142,7 @@ func (g Codex) parseTranscript(ctx context.Context, transcript string) (Heartbea
142142
}
143143

144144
cwd := ""
145+
sessionEntity := filepath.Base(transcript)
145146
version := ""
146147

147148
if len(firstLine) > 0 {
@@ -215,6 +216,7 @@ func (g Codex) parseTranscript(ctx context.Context, transcript string) (Heartbea
215216
entities := getCodexEntities(
216217
ctx,
217218
logLine.Timestamp,
219+
sessionEntity,
218220
version,
219221
cwd,
220222
g.UserAgents,
@@ -238,6 +240,7 @@ func (g Codex) parseTranscript(ctx context.Context, transcript string) (Heartbea
238240
func getCodexEntities(
239241
ctx context.Context,
240242
timestamp time.Time,
243+
sessionEntity string,
241244
version string,
242245
cwd string,
243246
userAgents map[string]string,
@@ -248,6 +251,7 @@ func getCodexEntities(
248251
if heartbeat := codexMessageHeartbeat(
249252
ctx,
250253
timestamp,
254+
sessionEntity,
251255
version,
252256
userAgents,
253257
fallbackUserAgent,
@@ -333,14 +337,14 @@ func codexPatchHeartbeats(
333337
func codexMessageHeartbeat(
334338
ctx context.Context,
335339
timestamp time.Time,
340+
sessionEntity string,
336341
version string,
337342
userAgents map[string]string,
338343
fallbackUserAgent string,
339344
payload codexPayload,
340345
) *heartbeat.Heartbeat {
341-
entity := "Codex"
342-
343346
var (
347+
entity = sessionEntity
344348
expectedType string
345349
lineChanges int
346350
)

pkg/ai/codex_test.go

Lines changed: 22 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import (
44
"context"
55
"os"
66
"path/filepath"
7+
"strings"
78
"testing"
89
"time"
910

@@ -39,24 +40,34 @@ func TestCodexParse(t *testing.T) {
3940
require.NoError(t, err)
4041
require.Len(t, got, 3)
4142

42-
assert.Equal(t, "Codex", got[0].Entity)
43+
assert.Equal(t, filepath.Base(transcriptPath), got[0].Entity)
4344
assert.Equal(t, heartbeat.AppType, got[0].EntityType)
4445
assert.Equal(t, heartbeat.AICodingCategory.String(), got[0].Category)
4546
assert.Nil(t, got[0].AILineChanges)
4647
require.NotNil(t, got[0].IsWrite)
4748
assert.False(t, *got[0].IsWrite)
4849
assert.Equal(t, float64(time.Date(2026, 3, 28, 11, 33, 14, 289, time.UTC).Unix()), got[0].Time)
49-
assert.Contains(t, got[0].UserAgent, heartbeat.UserAgent(ctx, "plugin/0.0.1"))
5050
assert.Contains(t, got[0].UserAgent, "Codex/0.116.0-alpha.1")
51-
52-
assert.Equal(t, "Codex", got[1].Entity)
51+
assert.True(
52+
t,
53+
strings.Index(got[0].UserAgent, "Codex/0.116.0-alpha.1") <
54+
strings.Index(got[0].UserAgent, "plugin/0.0.1"),
55+
)
56+
assert.Contains(t, got[0].UserAgent, "plugin/0.0.1")
57+
58+
assert.Equal(t, filepath.Base(transcriptPath), got[1].Entity)
5359
assert.Equal(t, heartbeat.AppType, got[1].EntityType)
5460
assert.Nil(t, got[1].AILineChanges)
5561
require.NotNil(t, got[1].IsWrite)
5662
assert.False(t, *got[1].IsWrite)
5763
assert.Equal(t, float64(time.Date(2026, 3, 28, 11, 33, 18, 535, time.UTC).Unix()), got[1].Time)
58-
assert.Contains(t, got[1].UserAgent, heartbeat.UserAgent(ctx, "plugin/0.0.1"))
5964
assert.Contains(t, got[1].UserAgent, "Codex/0.116.0-alpha.1")
65+
assert.True(
66+
t,
67+
strings.Index(got[1].UserAgent, "Codex/0.116.0-alpha.1") <
68+
strings.Index(got[1].UserAgent, "plugin/0.0.1"),
69+
)
70+
assert.Contains(t, got[1].UserAgent, "plugin/0.0.1")
6071

6172
assert.Equal(t, "/home/user/projects/wakatime-cli/pkg/ai/claude.go", got[2].Entity)
6273
assert.Equal(t, heartbeat.FileType, got[2].EntityType)
@@ -66,8 +77,13 @@ func TestCodexParse(t *testing.T) {
6677
require.NotNil(t, got[2].IsWrite)
6778
assert.True(t, *got[2].IsWrite)
6879
assert.Equal(t, float64(time.Date(2026, 3, 28, 11, 33, 34, 952, time.UTC).Unix()), got[2].Time)
69-
assert.Contains(t, got[2].UserAgent, heartbeat.UserAgent(ctx, "editor/1.2.3"))
7080
assert.Contains(t, got[2].UserAgent, "Codex/0.116.0-alpha.1")
81+
assert.True(
82+
t,
83+
strings.Index(got[2].UserAgent, "Codex/0.116.0-alpha.1") <
84+
strings.Index(got[2].UserAgent, "editor/1.2.3"),
85+
)
86+
assert.Contains(t, got[2].UserAgent, "editor/1.2.3")
7187
}
7288

7389
func TestCodexParse_ParsesTranscriptFromPreviousDayFolder(t *testing.T) {

pkg/ai/cursor.go

Lines changed: 31 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -44,13 +44,19 @@ type (
4444
}
4545

4646
cursorLogLine struct {
47+
BubbleID string
4748
CreatedAt time.Time `json:"createdAt"`
4849
Type int `json:"type"`
4950
Text string `json:"text"`
5051
ToolFormerData *cursorToolFormerData `json:"toolFormerData"`
5152
CodeBlocks []cursorCodeBlock `json:"codeBlocks"`
5253
}
5354

55+
cursorLogRow struct {
56+
BubbleID string
57+
Value string
58+
}
59+
5460
cursorEditParams struct {
5561
RelativeWorkspacePath string `json:"relativeWorkspacePath"`
5662
StreamingContent string `json:"streamingContent"`
@@ -97,10 +103,12 @@ func (g Cursor) Parse(ctx context.Context) (Heartbeats, error) {
97103

98104
for _, row := range rows {
99105
var logLine cursorLogLine
100-
if err := json.Unmarshal([]byte(row), &logLine); err != nil {
106+
if err := json.Unmarshal([]byte(row.Value), &logLine); err != nil {
101107
continue
102108
}
103109

110+
logLine.BubbleID = row.BubbleID
111+
104112
if logLine.CreatedAt.IsZero() || logLine.CreatedAt.Before(g.After) {
105113
continue
106114
}
@@ -151,15 +159,15 @@ func stateDBPath(ctx context.Context) (string, error) {
151159
return "", nil
152160
}
153161

154-
func (g Cursor) queryRows(ctx context.Context, dbPath string) ([]string, error) {
162+
func (g Cursor) queryRows(ctx context.Context, dbPath string) ([]cursorLogRow, error) {
155163
db, err := sql.Open("sqlite", dbPath)
156164
if err != nil {
157165
return nil, fmt.Errorf("failed opening cursor sqlite db %q: %s", dbPath, err)
158166
}
159167
defer db.Close() // nolint:errcheck
160168

161169
rows, err := db.QueryContext(ctx, `
162-
SELECT CAST(value AS TEXT)
170+
SELECT key, CAST(value AS TEXT)
163171
FROM cursorDiskKV
164172
WHERE key LIKE 'bubbleId:%'
165173
AND json_extract(CAST(value AS TEXT), '$.createdAt') >= ?
@@ -180,17 +188,28 @@ ORDER BY json_extract(CAST(value AS TEXT), '$.createdAt') ASC;
180188
}
181189
defer rows.Close() // nolint:errcheck
182190

183-
var results []string
191+
var results []cursorLogRow
184192

185193
for rows.Next() {
186-
var row string
187-
if err := rows.Scan(&row); err != nil {
194+
var (
195+
key string
196+
row string
197+
)
198+
if err := rows.Scan(&key, &row); err != nil {
188199
return nil, fmt.Errorf("failed scanning cursor sqlite row: %s", err)
189200
}
190201

191202
row = strings.TrimSpace(row)
192203
if row != "" {
193-
results = append(results, row)
204+
bubbleID := strings.TrimPrefix(key, "bubbleId:")
205+
if idx := strings.Index(bubbleID, ":"); idx != -1 {
206+
bubbleID = bubbleID[:idx]
207+
}
208+
209+
results = append(results, cursorLogRow{
210+
BubbleID: bubbleID,
211+
Value: row,
212+
})
194213
}
195214
}
196215

@@ -228,7 +247,11 @@ func (g Cursor) cursorAppHeartbeat(ctx context.Context, logLine cursorLogLine) *
228247
return nil
229248
}
230249

231-
entity := "Cursor"
250+
entity := logLine.BubbleID
251+
if entity == "" {
252+
entity = "Cursor"
253+
}
254+
232255
h := heartbeat.New(
233256
nil,
234257
"",

pkg/ai/cursor_test.go

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -159,15 +159,16 @@ func TestCursorParse(t *testing.T) {
159159
require.NoError(t, err)
160160
require.Len(t, got, 6)
161161

162-
assert.Equal(t, "Cursor", got[0].Entity)
162+
assert.Equal(t, "composer-1", got[0].Entity)
163163
assert.Equal(t, heartbeat.AppType, got[0].EntityType)
164164
assert.Equal(t, heartbeat.AICodingCategory.String(), got[0].Category)
165165
assert.Nil(t, got[0].AILineChanges)
166166
require.NotNil(t, got[0].IsWrite)
167167
assert.False(t, *got[0].IsWrite)
168168
assert.Equal(t, float64(time.Date(2026, 3, 15, 23, 34, 10, 0, time.UTC).Unix()), got[0].Time)
169-
assert.Contains(t, got[0].UserAgent, heartbeat.UserAgent(ctx, "plugin/0.0.1"))
170169
assert.Contains(t, got[0].UserAgent, "Cursor")
170+
assert.True(t, strings.Index(got[0].UserAgent, "Cursor") < strings.Index(got[0].UserAgent, "plugin/0.0.1"))
171+
assert.Contains(t, got[0].UserAgent, "plugin/0.0.1")
171172

172173
assert.Equal(t, "/tmp/edited.js", got[1].Entity)
173174
assert.Equal(t, heartbeat.FileType, got[1].EntityType)
@@ -177,8 +178,9 @@ func TestCursorParse(t *testing.T) {
177178
require.NotNil(t, got[1].IsWrite)
178179
assert.True(t, *got[1].IsWrite)
179180
assert.Equal(t, float64(time.Date(2026, 3, 15, 23, 34, 39, 0, time.UTC).Unix()), got[1].Time)
180-
assert.Contains(t, got[1].UserAgent, heartbeat.UserAgent(ctx, "editor/1.2.3"))
181181
assert.Contains(t, got[1].UserAgent, "Cursor")
182+
assert.True(t, strings.Index(got[1].UserAgent, "Cursor") < strings.Index(got[1].UserAgent, "editor/1.2.3"))
183+
assert.Contains(t, got[1].UserAgent, "editor/1.2.3")
182184

183185
assert.Equal(t, "/tmp/read.go", got[2].Entity)
184186
require.NotNil(t, got[2].AILineChanges)
@@ -188,7 +190,7 @@ func TestCursorParse(t *testing.T) {
188190
assert.Contains(t, got[2].UserAgent, "plugin/0.0.1")
189191
assert.Contains(t, got[2].UserAgent, "Cursor")
190192

191-
assert.Equal(t, "Cursor", got[3].Entity)
193+
assert.Equal(t, "composer-1", got[3].Entity)
192194
assert.Equal(t, heartbeat.AppType, got[3].EntityType)
193195
assert.Nil(t, got[3].AILineChanges)
194196
require.NotNil(t, got[3].IsWrite)

0 commit comments

Comments
 (0)