gitdone-agent 0.6.2 → 0.6.3

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 +48 -9
  2. package/package.json +1 -1
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.2'
30
+ const AGENT_VERSION = '0.6.3'
31
31
 
32
32
  const AGENT_DIR = join(homedir(), '.gitdone-agent')
33
33
  const CONFIG_PATH = join(AGENT_DIR, 'config.json')
@@ -521,19 +521,24 @@ async function postRunEvents(cfg, runId, events, status, result) {
521
521
 
522
522
  // Post transcript lines (and optional turn status / Claude session id) for an
523
523
  // interactive AiSession chat turn.
524
- async function postSessionEvents(cfg, sessionId, events, status, claudeSessionId) {
524
+ async function postSessionEvents(cfg, sessionId, events, status, claudeSessionId, streamingText) {
525
525
  await api(cfg, '/api/v1/agent/ai-session/events', {
526
526
  sessionId,
527
527
  events,
528
528
  ...(status ? { status } : {}),
529
529
  ...(claudeSessionId ? { claudeSessionId } : {}),
530
+ // Explicit '' clears the live preview; `undefined` leaves it untouched.
531
+ ...(streamingText !== undefined ? { streamingText } : {}),
530
532
  }).catch((e) => log(`✗ ai-session events post failed: ${e.message}`))
531
533
  }
532
534
 
533
535
  // Turn one stream-json line from `claude -p` into console events. Phase 1 shows
534
536
  // what the model writes (TEXT) and which tools it calls (TOOL_CALL); raw tool
535
537
  // results (Bash stdout etc.) come in Phase 2 via a PostToolUse hook.
536
- function parseStreamLine(line, push, onInit) {
538
+ // With `--include-partial-messages`, Claude also emits `stream_event` lines
539
+ // carrying incremental text deltas — onDelta gets those so chat sessions can
540
+ // show the reply being written live (gd-302).
541
+ function parseStreamLine(line, push, onInit, onDelta) {
537
542
  let ev
538
543
  try { ev = JSON.parse(line) } catch { return }
539
544
  if (ev.type === 'system') {
@@ -544,6 +549,16 @@ function parseStreamLine(line, push, onInit) {
544
549
  }
545
550
  return
546
551
  }
552
+ // Partial-message stream: only the model's visible text deltas feed the live
553
+ // preview. Tool-input JSON deltas and thinking deltas are ignored here — the
554
+ // full block still arrives as a normal `assistant` event below.
555
+ if (ev.type === 'stream_event' && typeof onDelta === 'function') {
556
+ const e = ev.event
557
+ if (e?.type === 'content_block_delta' && e.delta?.type === 'text_delta' && e.delta.text) {
558
+ onDelta(e.delta.text)
559
+ }
560
+ return
561
+ }
547
562
  if (ev.type === 'assistant' && ev.message?.content) {
548
563
  for (const block of ev.message.content) {
549
564
  if (block.type === 'text' && block.text?.trim()) {
@@ -822,6 +837,9 @@ async function runAiChat(cfg, cmd, repoPath) {
822
837
  '-p',
823
838
  '--output-format', 'stream-json',
824
839
  '--verbose',
840
+ // Stream the reply token-by-token so the console can show it being written
841
+ // live, not only once each block is complete (gd-302).
842
+ '--include-partial-messages',
825
843
  '--permission-mode', 'acceptEdits',
826
844
  '--allowedTools', 'Read,Edit,Write,Bash,mcp__gitdone__*',
827
845
  // Block git commit/push unless this repo opted in (per-repo aiAutoCommit).
@@ -837,16 +855,35 @@ async function runAiChat(cfg, cmd, repoPath) {
837
855
  let pending = []
838
856
  let capturedSession = null // Claude's session id, sent (idempotently) for --resume
839
857
  let flushing = false
858
+ let liveText = '' // in-progress text of the current assistant block (live preview)
859
+ let sentLive = '' // last streamingText we posted — so we only push on change
860
+ let lastPostAt = 0 // Date.now() of the last post, for the keep-alive heartbeat
840
861
  const flush = async () => {
841
- if (flushing || pending.length === 0) return
862
+ if (flushing) return
863
+ const hasEvents = pending.length > 0
864
+ const liveChanged = liveText !== sentLive
865
+ // Heartbeat: with nothing new for a while, still post (bumps lastActivityAt)
866
+ // so the console shows honest "still working" and can spot a real stall.
867
+ const heartbeat = !hasEvents && !liveChanged && Date.now() - lastPostAt > 2500
868
+ if (!hasEvents && !liveChanged && !heartbeat) return
842
869
  flushing = true
843
870
  const batch = pending.map((e) => ({ role: roleFor(e.kind), text: e.text })); pending = []
844
- await postSessionEvents(cfg, sessionId, batch, 'running', capturedSession || undefined)
871
+ const streamingText = liveChanged ? liveText : undefined
872
+ sentLive = liveText
873
+ await postSessionEvents(cfg, sessionId, batch, 'running', capturedSession || undefined, streamingText)
874
+ lastPostAt = Date.now()
845
875
  flushing = false
846
876
  }
847
- const push = (kind, text) => { if (text != null && String(text) !== '') pending.push({ kind, text: String(text) }) }
877
+ const push = (kind, text) => {
878
+ if (text == null || String(text) === '') return
879
+ // A completed text block becomes a real event — drop its live preview so the
880
+ // finalised bubble and the cleared preview swap in on the same flush.
881
+ if (kind === 'TEXT') liveText = ''
882
+ pending.push({ kind, text: String(text) })
883
+ }
848
884
  const onInit = (sid) => { if (sid && !claudeSessionId) capturedSession = sid }
849
- const timer = setInterval(flush, 800)
885
+ const onDelta = (chunk) => { liveText += chunk }
886
+ const timer = setInterval(flush, 500)
850
887
 
851
888
  // Fetch any attached images locally and fold their paths into the prompt so
852
889
  // Claude reads them with its Read tool (URLs on stdin don't render).
@@ -888,7 +925,7 @@ async function runAiChat(cfg, cmd, repoPath) {
888
925
  while ((nl = buf.indexOf('\n')) >= 0) {
889
926
  const line = buf.slice(0, nl).trim()
890
927
  buf = buf.slice(nl + 1)
891
- if (line) parseStreamLine(line, push, onInit)
928
+ if (line) parseStreamLine(line, push, onInit, onDelta)
892
929
  }
893
930
  })
894
931
  child.stderr.on('data', (d) => { const s = d.toString().trim(); if (s) push('SYSTEM', s) })
@@ -896,7 +933,8 @@ async function runAiChat(cfg, cmd, repoPath) {
896
933
  child.on('close', async (code) => {
897
934
  clearInterval(timer)
898
935
  if (imgDir) { try { rmSync(imgDir, { recursive: true, force: true }) } catch { /* best-effort */ } }
899
- if (buf.trim()) parseStreamLine(buf.trim(), push, onInit)
936
+ if (buf.trim()) parseStreamLine(buf.trim(), push, onInit, onDelta)
937
+ liveText = '' // turn is over — drop any lingering live preview
900
938
  await flush()
901
939
  const ok = code === 0
902
940
  await postSessionEvents(
@@ -904,6 +942,7 @@ async function runAiChat(cfg, cmd, repoPath) {
904
942
  ok ? [] : [{ role: 'SYSTEM', text: `✗ Ходът приключи с код ${code}.` }],
905
943
  ok ? 'idle' : 'error',
906
944
  capturedSession || undefined,
945
+ '', // clear the live preview on the terminal post
907
946
  )
908
947
  reportCommandResult(cfg, cmd.id, ok ? 'done' : 'error', `exit ${code}`)
909
948
  log(`■ ai_chat ${sessionId} приключи (code ${code})`)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "gitdone-agent",
3
- "version": "0.6.2",
3
+ "version": "0.6.3",
4
4
  "description": "Local git agent for gitdone — watches a local repo and sends snapshots to gitdone.eu",
5
5
  "type": "module",
6
6
  "bin": {