flowviant 0.21.0 → 0.22.0

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.
@@ -235,10 +235,15 @@ export function humanizeToolUse(name, input = {}, cwd = '') {
235
235
  }
236
236
  }
237
237
 
238
- // Parse ONE line of `--output-format stream-json` NDJSON. Pulls assistant text
239
- // into `out` (so sentinel detection still works) and turns each tool_use into a
240
- // streamed activity line. A line that isn't JSON (a stray warning) is treated as
241
- // raw text so nothing is lost.
238
+ // Collapse whitespace + clip so a narration/thinking snippet is one tidy feed line.
239
+ const oneLine = (s, n = 160) => String(s).replace(/\s+/g, ' ').trim().slice(0, n);
240
+
241
+ // Parse ONE line of `--output-format stream-json` NDJSON into feed activities.
242
+ // Surfaces the WHOLE turn — thinking, narration, AND every tool — so neither the
243
+ // daemon console nor the app cover goes dark while Claude reasons (Opus thinks in
244
+ // bursts before/between tools; emitting only tools left long silent gaps).
245
+ // Assistant text is also folded into `out` so the WIKI_DONE/REGROUND_DONE
246
+ // sentinels still match. A non-JSON line (a stray warning) is kept as raw text.
242
247
  function handleStreamLine(line, { cwd, emit, onActivity, appendText }) {
243
248
  let ev;
244
249
  try {
@@ -248,15 +253,22 @@ function handleStreamLine(line, { cwd, emit, onActivity, appendText }) {
248
253
  emit(line + '\n');
249
254
  return;
250
255
  }
256
+ const push = (a) => {
257
+ if (!a || !a.label) return;
258
+ emit(a.label + '\n');
259
+ onActivity?.(a);
260
+ };
251
261
  if (ev.type === 'assistant' && Array.isArray(ev.message?.content)) {
252
262
  for (const b of ev.message.content) {
253
- if (b.type === 'text' && b.text) appendText(b.text + '\n');
254
- else if (b.type === 'tool_use') {
255
- const a = humanizeToolUse(b.name, b.input || {}, cwd);
256
- if (a) {
257
- emit(a.label + '\n');
258
- onActivity?.(a);
259
- }
263
+ if (b.type === 'thinking' || b.type === 'redacted_thinking') {
264
+ // The `thinking` text is usually redacted (signature only), so emit a
265
+ // marker enough to show Claude is actively reasoning, not hung.
266
+ push({ kind: 'think', label: b.thinking ? `thinking: ${oneLine(b.thinking)}` : 'thinking…' });
267
+ } else if (b.type === 'text' && b.text?.trim()) {
268
+ appendText(b.text + '\n');
269
+ push({ kind: 'say', label: oneLine(b.text) });
270
+ } else if (b.type === 'tool_use') {
271
+ push(humanizeToolUse(b.name, b.input || {}, cwd));
260
272
  }
261
273
  }
262
274
  } else if (ev.type === 'result' && typeof ev.result === 'string') {
@@ -4,7 +4,7 @@ import { readFileSync } from 'node:fs';
4
4
  import { join } from 'node:path';
5
5
  import { homedir } from 'node:os';
6
6
 
7
- export const VERSION = '0.21.0';
7
+ export const VERSION = '0.22.0';
8
8
 
9
9
  // Credential stored by `flowviant login` (device auth) — the no-token,
10
10
  // no-env-var path. An explicit --fleet flag or FLOWVIANT_FLEET env still wins.
package/bin/lib/fleet.mjs CHANGED
@@ -426,7 +426,7 @@ export async function runFleetDaemon() {
426
426
  let lastProgressAt = 0;
427
427
  const postWikiProgress = async (body, force = false) => {
428
428
  const now = Date.now();
429
- if (!force && now - lastProgressAt < 1000) return;
429
+ if (!force && now - lastProgressAt < 600) return;
430
430
  lastProgressAt = now;
431
431
  try {
432
432
  await fetch(WIKI_PROGRESS_URL, {
@@ -486,25 +486,40 @@ export async function runFleetDaemon() {
486
486
  }
487
487
  const task = wikiQueue.shift();
488
488
  const { dir, path: mcpConfig } = mcpConfigFor(token, mcpUrl);
489
- // Live progress for this turn: count the files Claude reads, flip to the
490
- // "writing" phase once it starts emitting nodes, and stream each action
491
- // to the app (throttled). elapsedSec is on the daemon's own clock.
489
+ // Live progress for this turn: a rolling FEED of everything Claude does
490
+ // (thinking, narration, reads, node writes), the file count, and the
491
+ // phase — streamed to the app (throttled; each frame carries the whole
492
+ // recent tail so a dropped POST loses nothing). elapsedSec is the
493
+ // daemon's own clock.
492
494
  const mode = task.type === 'sweep' ? 'sweep' : 'reground';
493
495
  const startedAt = Date.now();
494
496
  let filesRead = 0;
495
497
  let phase = 'reading';
498
+ const feed = [];
499
+ const frame = (extra) => ({
500
+ mode,
501
+ phase,
502
+ activity: feed[feed.length - 1] ?? '',
503
+ recent: feed.slice(-24),
504
+ filesRead,
505
+ elapsedSec: Math.round((Date.now() - startedAt) / 1000),
506
+ ...extra,
507
+ });
496
508
  const onActivity = (a) => {
497
509
  if (a.kind === 'read') filesRead++;
498
510
  if (a.kind === 'write') phase = 'writing';
499
- void postWikiProgress({
500
- mode,
501
- phase,
502
- activity: a.label,
503
- filesRead,
504
- elapsedSec: Math.round((Date.now() - startedAt) / 1000),
505
- });
511
+ // Collapse runs of bare "thinking…" so the feed doesn't fill with it.
512
+ if (!(a.label === 'thinking…' && feed[feed.length - 1] === 'thinking…')) {
513
+ feed.push(a.label);
514
+ if (feed.length > 48) feed.shift();
515
+ }
516
+ void postWikiProgress(frame());
506
517
  };
507
518
  try {
519
+ // Immediate frame so the cover shows the daemon feed right away (the
520
+ // "reading your code" phase), not a static message, while Claude warms up.
521
+ feed.push('starting…');
522
+ await postWikiProgress(frame(), true);
508
523
  if (!existsSync(wikiWt)) {
509
524
  try {
510
525
  git(['worktree', 'add', '--detach', wikiWt, baseRef], repoRoot);
@@ -567,17 +582,7 @@ export async function runFleetDaemon() {
567
582
  } finally {
568
583
  // Terminal frame so the app cover clears promptly (don't wait for the
569
584
  // freshness window to lapse). force-sent past the throttle.
570
- await postWikiProgress(
571
- {
572
- mode,
573
- phase,
574
- activity: '',
575
- filesRead,
576
- elapsedSec: Math.round((Date.now() - startedAt) / 1000),
577
- done: true,
578
- },
579
- true
580
- );
585
+ await postWikiProgress(frame({ done: true }), true);
581
586
  rmSync(dir, { recursive: true, force: true });
582
587
  }
583
588
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "flowviant",
3
- "version": "0.21.0",
3
+ "version": "0.22.0",
4
4
  "description": "Run your own Claude Code as headless build agents for Flowviant — on your own credentials. Claims dispatched work, opens PRs, captures review evidence, and routes questions back to you.",
5
5
  "type": "module",
6
6
  "bin": {