@crouton-kit/humanloop 0.3.33 → 0.3.35

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 (46) hide show
  1. package/dist/api.d.ts +4 -8
  2. package/dist/api.js +6 -29
  3. package/dist/browser/server.d.ts +3 -3
  4. package/dist/browser/server.js +22 -25
  5. package/dist/cli.js +131 -1114
  6. package/dist/editor/feedback.d.ts +9 -0
  7. package/dist/editor/feedback.js +23 -0
  8. package/dist/editor/review.d.ts +12 -30
  9. package/dist/editor/review.js +109 -119
  10. package/dist/inbox/claim.d.ts +23 -0
  11. package/dist/inbox/claim.js +67 -0
  12. package/dist/inbox/completion.d.ts +4 -0
  13. package/dist/inbox/completion.js +107 -0
  14. package/dist/inbox/controller.d.ts +74 -0
  15. package/dist/inbox/controller.js +457 -0
  16. package/dist/inbox/convention.d.ts +15 -10
  17. package/dist/inbox/convention.js +149 -79
  18. package/dist/inbox/deck-adapter.d.ts +27 -0
  19. package/dist/inbox/deck-adapter.js +57 -0
  20. package/dist/inbox/deck-schema.d.ts +18 -14
  21. package/dist/inbox/deck-schema.js +59 -117
  22. package/dist/inbox/layout.d.ts +8 -0
  23. package/dist/inbox/layout.js +9 -0
  24. package/dist/inbox/registry.d.ts +29 -0
  25. package/dist/inbox/registry.js +97 -0
  26. package/dist/inbox/review-adapter.d.ts +24 -0
  27. package/dist/inbox/review-adapter.js +57 -0
  28. package/dist/inbox/scan.d.ts +3 -2
  29. package/dist/inbox/scan.js +71 -43
  30. package/dist/inbox/tickets.d.ts +63 -0
  31. package/dist/inbox/tickets.js +247 -0
  32. package/dist/inbox/tui.d.ts +3 -6
  33. package/dist/inbox/tui.js +24 -112
  34. package/dist/index.d.ts +12 -3
  35. package/dist/index.js +8 -2
  36. package/dist/render/termrender.d.ts +5 -0
  37. package/dist/render/termrender.js +63 -4
  38. package/dist/summary.d.ts +2 -3
  39. package/dist/summary.js +2 -3
  40. package/dist/surfaces/inbox-popup.d.ts +2 -0
  41. package/dist/surfaces/inbox-popup.js +32 -0
  42. package/dist/tui/app.js +13 -0
  43. package/dist/tui/tmux.d.ts +30 -7
  44. package/dist/tui/tmux.js +190 -66
  45. package/dist/types.d.ts +68 -7
  46. package/package.json +2 -2
package/dist/api.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- import type { Deck, ResolutionEnvelope, GenerateVisual } from './types.js';
1
+ import type { Deck, ResolutionEnvelope } from './types.js';
2
2
  export interface AskOpts {
3
3
  /** Interaction directory. Defaults to a managed temp dir under os.tmpdir(). */
4
4
  dir?: string;
@@ -17,11 +17,7 @@ export declare function notify(title: string, body?: string): Promise<void>;
17
17
  export interface InboxOpts {
18
18
  cols?: number;
19
19
  rows?: number;
20
- generateVisual?: GenerateVisual;
20
+ roots?: string[];
21
21
  }
22
- /**
23
- * List resolve loop across `roots`. Shows pending interactions, lets the
24
- * human pick one, resolves it (writing its `response.json`), then rescans —
25
- * resolved items drop out — until the human quits or nothing is pending.
26
- */
27
- export declare function inbox(roots: string[], opts?: InboxOpts): Promise<void>;
22
+ /** Open the centralized inbox controller in the current human terminal. */
23
+ export declare function openInbox(opts?: InboxOpts): Promise<void>;
package/dist/api.js CHANGED
@@ -2,11 +2,9 @@ import { mkdtempSync, mkdirSync } from 'node:fs';
2
2
  import { tmpdir } from 'node:os';
3
3
  import { join } from 'node:path';
4
4
  import { resolveInteractionDir } from './tui/app.js';
5
- import { scanInbox } from './inbox/scan.js';
6
- import { pickFromInbox } from './inbox/tui.js';
7
- import { deckPath, atomicWriteJson, readJson, stampCanvasNode } from './inbox/convention.js';
5
+ import { InboxController } from './inbox/controller.js';
6
+ import { deckPath, atomicWriteJson, stampCanvasNode } from './inbox/convention.js';
8
7
  import { resolveDeckBodyPaths } from './inbox/deck-schema.js';
9
- import { getTerminalSize } from './tui/terminal.js';
10
8
  import { notifyDeck } from './inbox/deck-factories.js';
11
9
  import { buildSummary } from './summary.js';
12
10
  const RESPONSE_SCHEMA_ID = 'humanloop.response/v2';
@@ -48,29 +46,8 @@ export async function notify(title, body) {
48
46
  const deck = notifyDeck(title, body !== undefined ? { body } : {});
49
47
  await ask(deck, {});
50
48
  }
51
- /**
52
- * List resolve loop across `roots`. Shows pending interactions, lets the
53
- * human pick one, resolves it (writing its `response.json`), then rescans
54
- * resolved items drop out — until the human quits or nothing is pending.
55
- */
56
- export async function inbox(roots, opts = {}) {
57
- for (;;) {
58
- const items = scanInbox(roots);
59
- if (items.length === 0)
60
- return;
61
- const term = getTerminalSize();
62
- const cols = opts.cols ?? term.cols;
63
- const rows = opts.rows ?? term.rows;
64
- const picked = await pickFromInbox(items, { cols, rows });
65
- if (picked === null)
66
- return;
67
- const deck = readJson(deckPath(picked.dir));
68
- if (deck === null)
69
- continue; // raced/removed — rescan
70
- await resolveInteractionDir(picked.dir, deck, {
71
- generateVisual: opts.generateVisual,
72
- cols,
73
- rows,
74
- });
75
- }
49
+ /** Open the centralized inbox controller in the current human terminal. */
50
+ export async function openInbox(opts = {}) {
51
+ const controller = new InboxController({ roots: opts.roots, cols: opts.cols, rows: opts.rows });
52
+ await controller.run();
76
53
  }
@@ -17,15 +17,15 @@ export interface WebServerOpts {
17
17
  onSubmit?: (responses: InteractionResponse[], completedAt: string, responsePath: string) => void;
18
18
  }
19
19
  export interface ReviewWebServerOpts {
20
- /** Shared review job dir — carries review.vim and feedback.json. */
20
+ /** Shared review job dir — carries review.vim and progress.json. */
21
21
  jobDir: string;
22
22
  /** Absolute source file under review. */
23
23
  file: string;
24
- /** Absolute feedback JSON output path. */
24
+ /** Absolute resumable review draft path. */
25
25
  output: string;
26
26
  /** Optional submit sentinel written on final browser submit. */
27
27
  submitFlagPath?: string;
28
- /** Fired after the HTTP submit ack has finished flushing. */
28
+ /** Fired after the HTTP proposal ack has finished flushing. The controller owns finalization. */
29
29
  onSubmit?: (result: FeedbackResult, submittedAt: string, outputPath: string) => void;
30
30
  /** Test-only override for how long `requestTakeBack()` waits for open tabs
31
31
  * to ack a flush before giving up and broadcasting `taken-back` anyway.
@@ -5,7 +5,7 @@ import { join, extname, dirname, basename } from 'node:path';
5
5
  import { fileURLToPath } from 'node:url';
6
6
  import { WebSocketServer } from 'ws';
7
7
  import { deckPath, readJson, writeResponse, clearProgress } from '../inbox/convention.js';
8
- import { buildDraftFeedbackResult, buildFinalFeedbackResult, parseFeedbackComments, readStoredDraftFeedbackResult, serializeFeedbackResult, writeFeedbackResult, writeSubmitFlag, } from '../editor/feedback.js';
8
+ import { buildDraftFeedbackResult, buildFinalFeedbackResult, parseFeedbackComments, readReviewDraft, serializeFeedbackResult, writeReviewDraft, writeSubmitFlag, } from '../editor/feedback.js';
9
9
  // See $CRTR_CONTEXT_DIR/phase1-server-contract.md for the deck HTTP/WS API this
10
10
  // module implements — this file is the reference implementation for the deck
11
11
  // surface, and the review surface reuses the same static/WS skeleton.
@@ -201,20 +201,22 @@ async function startReviewServer(opts) {
201
201
  const { jobDir, file, output, submitFlagPath } = opts;
202
202
  const scaffold = createServerScaffold();
203
203
  let activated = false;
204
- let version = 0;
204
+ const persistedDraft = readReviewDraft(output);
205
+ let version = persistedDraft?.version ?? 0;
205
206
  let ackWaiter = null;
206
207
  const TAKE_BACK_ACK_TIMEOUT_MS = opts.takeBackAckTimeoutMs ?? 2000;
207
- let currentResult = readStoredDraftFeedbackResult(output, file)
208
- ?? buildDraftFeedbackResult(file, [], new Date().toISOString());
208
+ let currentResult = buildDraftFeedbackResult(file, persistedDraft?.comments ?? [], persistedDraft?.savedAt ?? new Date().toISOString());
209
+ let proposed = false;
209
210
  function refreshInitialDraftFromDisk() {
210
- if (version !== 0 || currentResult.submitted)
211
+ if (proposed)
211
212
  return;
212
- const onDisk = readStoredDraftFeedbackResult(output, file);
213
- if (onDisk === null)
213
+ const onDisk = readReviewDraft(output);
214
+ if (onDisk === null || onDisk.version < version)
214
215
  return;
215
- if (serializeFeedbackResult(onDisk) !== serializeFeedbackResult(currentResult)) {
216
- currentResult = onDisk;
217
- version += 1;
216
+ const next = buildDraftFeedbackResult(file, onDisk.comments, onDisk.savedAt);
217
+ if (onDisk.version > version || serializeFeedbackResult(next) !== serializeFeedbackResult(currentResult)) {
218
+ currentResult = next;
219
+ version = onDisk.version;
218
220
  }
219
221
  }
220
222
  function currentSnapshot() {
@@ -297,7 +299,7 @@ async function startReviewServer(opts) {
297
299
  return;
298
300
  }
299
301
  refreshInitialDraftFromDisk();
300
- if (currentResult.submitted) {
302
+ if (proposed) {
301
303
  sendJson(res, 409, { error: 'already_submitted', result: currentSnapshot() });
302
304
  return;
303
305
  }
@@ -317,7 +319,7 @@ async function startReviewServer(opts) {
317
319
  const savedAt = new Date().toISOString();
318
320
  const nextResult = buildDraftFeedbackResult(file, parsedComments.comments, savedAt);
319
321
  try {
320
- writeFeedbackResult(output, nextResult);
322
+ writeReviewDraft(output, parsedComments.comments, version + 1, savedAt);
321
323
  }
322
324
  catch (err) {
323
325
  sendJson(res, 500, { error: 'internal', message: err instanceof Error ? err.message : String(err) });
@@ -343,14 +345,8 @@ async function startReviewServer(opts) {
343
345
  return;
344
346
  }
345
347
  refreshInitialDraftFromDisk();
346
- if (currentResult.submitted) {
347
- sendJson(res, 409, {
348
- ok: false,
349
- error: 'already_submitted',
350
- output,
351
- submittedAt: currentResult.submittedAt,
352
- result: currentSnapshot(),
353
- });
348
+ if (proposed) {
349
+ sendJson(res, 409, { ok: false, error: 'already_submitted', output, result: currentSnapshot() });
354
350
  return;
355
351
  }
356
352
  if (!Number.isInteger(parsed.baseVersion)) {
@@ -369,11 +365,12 @@ async function startReviewServer(opts) {
369
365
  const submittedAt = new Date().toISOString();
370
366
  const nextResult = buildFinalFeedbackResult(file, parsedComments.comments, submittedAt);
371
367
  try {
372
- writeFeedbackResult(output, nextResult);
373
- currentResult = nextResult;
374
- if (submitFlagPath !== undefined && submitFlagPath.length > 0) {
368
+ writeReviewDraft(output, parsedComments.comments, version + 1, submittedAt);
369
+ currentResult = buildDraftFeedbackResult(file, parsedComments.comments, submittedAt);
370
+ version += 1;
371
+ proposed = true;
372
+ if (submitFlagPath !== undefined && submitFlagPath.length > 0)
375
373
  writeSubmitFlag(submitFlagPath);
376
- }
377
374
  }
378
375
  catch (err) {
379
376
  sendJson(res, 500, { error: 'internal', message: err instanceof Error ? err.message : String(err) });
@@ -384,7 +381,7 @@ async function startReviewServer(opts) {
384
381
  // between a lost WS frame and the teardown it precedes.
385
382
  await scaffold.broadcast({ type: 'converged' });
386
383
  res.once('finish', () => {
387
- opts.onSubmit?.(currentSnapshot(), submittedAt, output);
384
+ opts.onSubmit?.(nextResult, submittedAt, output);
388
385
  });
389
386
  sendJson(res, 200, { ok: true, output, submittedAt, result: currentSnapshot() });
390
387
  return;