gitdone-agent 0.6.16 → 0.6.17

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (2) hide show
  1. package/index.js +77 -23
  2. package/package.json +15 -15
package/index.js CHANGED
@@ -27,7 +27,7 @@ import { randomUUID } from 'node:crypto'
27
27
  // Reported to the server on every sync so the web UI can flag outdated agents.
28
28
  // Keep in lockstep with packages/agent/package.json "version" AND
29
29
  // src/lib/agentVersion.ts LATEST_AGENT_VERSION.
30
- const AGENT_VERSION = '0.6.16'
30
+ const AGENT_VERSION = '0.6.17'
31
31
 
32
32
  const AGENT_DIR = join(homedir(), '.gitdone-agent')
33
33
  const CONFIG_PATH = join(AGENT_DIR, 'config.json')
@@ -759,20 +759,24 @@ async function reportCommandResult(cfg, id, status, result) {
759
759
 
760
760
  // Post a batch of console events (and optional lifecycle status) for an AiRun.
761
761
  // `usage` is sent once on the final (done/error) post so the server can record
762
- // how many tokens the run cost.
763
- async function postRunEvents(cfg, runId, events, status, result, usage) {
762
+ // how many tokens the run cost. `streamingText` / `activityText` (gd-419) carry
763
+ // the live "being typed" preview and the latest thinking snippet — explicit ''
764
+ // clears them; `undefined` leaves them untouched.
765
+ async function postRunEvents(cfg, runId, events, status, result, usage, streamingText, activityText) {
764
766
  await api(cfg, '/api/v1/agent/ai-run/events', {
765
767
  runId,
766
768
  events,
767
769
  ...(status ? { status } : {}),
768
770
  ...(result !== undefined ? { result } : {}),
769
771
  ...(usage ? { usage } : {}),
772
+ ...(streamingText !== undefined ? { streamingText } : {}),
773
+ ...(activityText !== undefined ? { activityText } : {}),
770
774
  }).catch((e) => log(`✗ ai-run events post failed: ${e.message}`))
771
775
  }
772
776
 
773
777
  // Post transcript lines (and optional turn status / Claude session id) for an
774
778
  // interactive AiSession chat turn.
775
- async function postSessionEvents(cfg, sessionId, events, status, claudeSessionId, streamingText) {
779
+ async function postSessionEvents(cfg, sessionId, events, status, claudeSessionId, streamingText, activityText) {
776
780
  await api(cfg, '/api/v1/agent/ai-session/events', {
777
781
  sessionId,
778
782
  events,
@@ -780,6 +784,8 @@ async function postSessionEvents(cfg, sessionId, events, status, claudeSessionId
780
784
  ...(claudeSessionId ? { claudeSessionId } : {}),
781
785
  // Explicit '' clears the live preview; `undefined` leaves it untouched.
782
786
  ...(streamingText !== undefined ? { streamingText } : {}),
787
+ // Same semantics for the live thinking snippet (gd-419).
788
+ ...(activityText !== undefined ? { activityText } : {}),
783
789
  }).catch((e) => log(`✗ ai-session events post failed: ${e.message}`))
784
790
  }
785
791
 
@@ -810,13 +816,18 @@ function parseStreamLine(line, push, onInit, onDelta, onMeta) {
810
816
  }
811
817
  return
812
818
  }
813
- // Partial-message stream: only the model's visible text deltas feed the live
814
- // preview. Tool-input JSON deltas and thinking deltas are ignored here — the
815
- // full block still arrives as a normal `assistant` event below.
819
+ // Partial-message stream: the model's visible text deltas feed the live
820
+ // preview, its thinking deltas feed the gray "какво прави АИ-то" snippet
821
+ // (gd-419). Tool-input JSON deltas stay ignored the full block still
822
+ // arrives as a normal `assistant` event below.
816
823
  if (ev.type === 'stream_event' && typeof onDelta === 'function') {
817
824
  const e = ev.event
818
- if (e?.type === 'content_block_delta' && e.delta?.type === 'text_delta' && e.delta.text) {
819
- onDelta(e.delta.text)
825
+ if (e?.type === 'content_block_delta') {
826
+ if (e.delta?.type === 'text_delta' && e.delta.text) onDelta(e.delta.text, 'text')
827
+ else if (e.delta?.type === 'thinking_delta' && e.delta.thinking) onDelta(e.delta.thinking, 'thinking')
828
+ } else if (e?.type === 'content_block_start' && e.content_block?.type === 'thinking') {
829
+ // A fresh thought begins — reset the snippet so old and new don't blend.
830
+ onDelta('', 'thinking-start')
820
831
  }
821
832
  return
822
833
  }
@@ -1043,6 +1054,9 @@ function runAiCommand(cfg, cmd, repoPath) {
1043
1054
  '-p',
1044
1055
  '--output-format', 'stream-json',
1045
1056
  '--verbose',
1057
+ // Stream text token-by-token + thinking deltas so the terminal shows the
1058
+ // reply being written live, ред по ред, not in whole-block batches (gd-419).
1059
+ '--include-partial-messages',
1046
1060
  // Model chosen in gitDone (project default / task); omitted → machine default (gd-354).
1047
1061
  ...(model ? ['--model', model] : []),
1048
1062
  '--permission-mode', 'acceptEdits',
@@ -1060,15 +1074,39 @@ function runAiCommand(cfg, cmd, repoPath) {
1060
1074
  // Batch events on a timer so we don't hammer the server per token/line.
1061
1075
  let pending = []
1062
1076
  let flushing = false
1077
+ let liveText = '' // in-progress text of the current assistant block (live preview)
1078
+ let sentLive = '' // last streamingText we posted — only push on change
1079
+ let liveActivity = '' // latest thinking snippet — "какво прави АИ-то" (gd-419)
1080
+ let sentActivity = ''
1063
1081
  const flush = async () => {
1064
- if (flushing || pending.length === 0) return
1082
+ if (flushing) return
1083
+ const liveChanged = liveText !== sentLive
1084
+ const activityChanged = liveActivity !== sentActivity
1085
+ if (pending.length === 0 && !liveChanged && !activityChanged) return
1065
1086
  flushing = true
1066
1087
  const batch = pending; pending = []
1067
- await postRunEvents(cfg, runId, batch)
1088
+ const streamingText = liveChanged ? liveText : undefined
1089
+ const activityText = activityChanged ? liveActivity : undefined
1090
+ sentLive = liveText
1091
+ sentActivity = liveActivity
1092
+ await postRunEvents(cfg, runId, batch, undefined, undefined, undefined, streamingText, activityText)
1068
1093
  flushing = false
1069
1094
  }
1070
- const push = (kind, text) => { if (text != null && String(text) !== '') pending.push({ kind, text: String(text) }) }
1071
- const timer = setInterval(flush, 800)
1095
+ const push = (kind, text) => {
1096
+ if (text == null || String(text) === '') return
1097
+ // A completed text block becomes a real event — drop its live preview; any
1098
+ // new event also supersedes the last thinking snippet.
1099
+ if (kind === 'TEXT') liveText = ''
1100
+ liveActivity = ''
1101
+ pending.push({ kind, text: String(text) })
1102
+ }
1103
+ const onDelta = (chunk, type) => {
1104
+ if (type === 'thinking-start') { liveActivity = ''; return }
1105
+ // Keep only the tail — the freshest thought is what the gray line shows.
1106
+ if (type === 'thinking') { liveActivity = (liveActivity + chunk).slice(-4000); return }
1107
+ liveText += chunk
1108
+ }
1109
+ const timer = setInterval(flush, 500)
1072
1110
 
1073
1111
  // Accumulate model + token usage across the stream (model from init, usage
1074
1112
  // from the final result event) to report once on exit (gd-334).
@@ -1107,14 +1145,16 @@ function runAiCommand(cfg, cmd, repoPath) {
1107
1145
  while ((nl = buf.indexOf('\n')) >= 0) {
1108
1146
  const line = buf.slice(0, nl).trim()
1109
1147
  buf = buf.slice(nl + 1)
1110
- if (line) parseStreamLine(line, push, undefined, undefined, onMeta)
1148
+ if (line) parseStreamLine(line, push, undefined, onDelta, onMeta)
1111
1149
  }
1112
1150
  })
1113
1151
  child.stderr.on('data', (d) => { const s = d.toString().trim(); if (s) push('SYSTEM', s) })
1114
1152
  child.on('error', (err) => push('SYSTEM', `Процесна грешка: ${err.message}`))
1115
1153
  child.on('close', async (code) => {
1116
1154
  clearInterval(timer)
1117
- if (buf.trim()) parseStreamLine(buf.trim(), push, undefined, undefined, onMeta)
1155
+ if (buf.trim()) parseStreamLine(buf.trim(), push, undefined, onDelta, onMeta)
1156
+ liveText = '' // run is over — drop any lingering live preview / thought
1157
+ liveActivity = ''
1118
1158
  await flush()
1119
1159
  const ok = code === 0
1120
1160
  // Build the usage payload + a console summary line from what we saw.
@@ -1238,32 +1278,44 @@ async function runAiChat(cfg, cmd, repoPath) {
1238
1278
  let flushing = false
1239
1279
  let liveText = '' // in-progress text of the current assistant block (live preview)
1240
1280
  let sentLive = '' // last streamingText we posted — so we only push on change
1281
+ let liveActivity = '' // latest thinking snippet — "какво прави АИ-то" (gd-419)
1282
+ let sentActivity = ''
1241
1283
  let lastPostAt = 0 // Date.now() of the last post, for the keep-alive heartbeat
1242
1284
  const flush = async () => {
1243
1285
  if (flushing) return
1244
1286
  const hasEvents = pending.length > 0
1245
1287
  const liveChanged = liveText !== sentLive
1288
+ const activityChanged = liveActivity !== sentActivity
1246
1289
  // Heartbeat: with nothing new for a while, still post (bumps lastActivityAt)
1247
1290
  // so the console shows honest "still working" and can spot a real stall.
1248
- const heartbeat = !hasEvents && !liveChanged && Date.now() - lastPostAt > 2500
1249
- if (!hasEvents && !liveChanged && !heartbeat) return
1291
+ const heartbeat = !hasEvents && !liveChanged && !activityChanged && Date.now() - lastPostAt > 2500
1292
+ if (!hasEvents && !liveChanged && !activityChanged && !heartbeat) return
1250
1293
  flushing = true
1251
1294
  const batch = pending.map((e) => ({ role: roleFor(e.kind), text: e.text })); pending = []
1252
1295
  const streamingText = liveChanged ? liveText : undefined
1296
+ const activityText = activityChanged ? liveActivity : undefined
1253
1297
  sentLive = liveText
1254
- await postSessionEvents(cfg, sessionId, batch, 'running', capturedSession || undefined, streamingText)
1298
+ sentActivity = liveActivity
1299
+ await postSessionEvents(cfg, sessionId, batch, 'running', capturedSession || undefined, streamingText, activityText)
1255
1300
  lastPostAt = Date.now()
1256
1301
  flushing = false
1257
1302
  }
1258
1303
  const push = (kind, text) => {
1259
1304
  if (text == null || String(text) === '') return
1260
1305
  // A completed text block becomes a real event — drop its live preview so the
1261
- // finalised bubble and the cleared preview swap in on the same flush.
1306
+ // finalised bubble and the cleared preview swap in on the same flush. Any
1307
+ // new event also supersedes the last thinking snippet (gd-419).
1262
1308
  if (kind === 'TEXT') liveText = ''
1309
+ liveActivity = ''
1263
1310
  pending.push({ kind, text: String(text) })
1264
1311
  }
1265
1312
  const onInit = (sid) => { if (sid && !claudeSessionId) capturedSession = sid }
1266
- const onDelta = (chunk) => { liveText += chunk }
1313
+ const onDelta = (chunk, type) => {
1314
+ if (type === 'thinking-start') { liveActivity = ''; return }
1315
+ // Keep only the tail — the freshest thought is what the gray line shows.
1316
+ if (type === 'thinking') { liveActivity = (liveActivity + chunk).slice(-4000); return }
1317
+ liveText += chunk
1318
+ }
1267
1319
  const timer = setInterval(flush, 500)
1268
1320
 
1269
1321
  // Fetch any attached images locally and fold their paths into the prompt so
@@ -1319,7 +1371,8 @@ async function runAiChat(cfg, cmd, repoPath) {
1319
1371
  chatChildren.delete(sessionId)
1320
1372
  if (imgDir) { try { rmSync(imgDir, { recursive: true, force: true }) } catch { /* best-effort */ } }
1321
1373
  if (buf.trim()) parseStreamLine(buf.trim(), push, onInit, onDelta)
1322
- liveText = '' // turn is over — drop any lingering live preview
1374
+ liveText = '' // turn is over — drop any lingering live preview / thought
1375
+ liveActivity = ''
1323
1376
  await flush()
1324
1377
  // User-initiated stop (gd-303): land on idle with a friendly note, not an
1325
1378
  // "exited with code N" error — the kill's non-zero code is expected here.
@@ -1327,7 +1380,7 @@ async function runAiChat(cfg, cmd, repoPath) {
1327
1380
  await postSessionEvents(
1328
1381
  cfg, sessionId,
1329
1382
  [{ role: 'SYSTEM', text: '⏹ Спряно. Напиши още нещо, за да продължим разговора.' }],
1330
- 'idle', capturedSession || undefined, '',
1383
+ 'idle', capturedSession || undefined, '', '',
1331
1384
  )
1332
1385
  reportCommandResult(cfg, cmd.id, 'done', 'stopped by user')
1333
1386
  log(`⏹ ai_chat ${sessionId} спряно от потребителя`)
@@ -1340,6 +1393,7 @@ async function runAiChat(cfg, cmd, repoPath) {
1340
1393
  ok ? 'idle' : 'error',
1341
1394
  capturedSession || undefined,
1342
1395
  '', // clear the live preview on the terminal post
1396
+ '', // …and the thinking snippet (gd-419)
1343
1397
  )
1344
1398
  reportCommandResult(cfg, cmd.id, ok ? 'done' : 'error', `exit ${code}`)
1345
1399
  log(`■ ai_chat ${sessionId} приключи (code ${code})`)
@@ -1354,7 +1408,7 @@ function stopAiChat(cfg, cmd) {
1354
1408
  const sessionId = cmd.payload?.sessionId
1355
1409
  const child = sessionId ? chatChildren.get(sessionId) : null
1356
1410
  if (!child) {
1357
- if (sessionId) postSessionEvents(cfg, sessionId, [], 'idle', undefined, '')
1411
+ if (sessionId) postSessionEvents(cfg, sessionId, [], 'idle', undefined, '', '')
1358
1412
  reportCommandResult(cfg, cmd.id, 'done', 'no active turn')
1359
1413
  return
1360
1414
  }
package/package.json CHANGED
@@ -1,15 +1,15 @@
1
- {
2
- "name": "gitdone-agent",
3
- "version": "0.6.16",
4
- "description": "Local git agent for gitdone — watches a local repo and sends snapshots to gitdone.eu",
5
- "type": "module",
6
- "bin": {
7
- "gitdone-agent": "index.js"
8
- },
9
- "scripts": {
10
- "start": "node index.js"
11
- },
12
- "engines": {
13
- "node": "\u003e=18"
14
- }
15
- }
1
+ {
2
+ "name": "gitdone-agent",
3
+ "version": "0.6.17",
4
+ "description": "Local git agent for gitdone — watches a local repo and sends snapshots to gitdone.eu",
5
+ "type": "module",
6
+ "bin": {
7
+ "gitdone-agent": "index.js"
8
+ },
9
+ "scripts": {
10
+ "start": "node index.js"
11
+ },
12
+ "engines": {
13
+ "node": "\u003e=18"
14
+ }
15
+ }