@azatakmyradov/opencode-recap-plugin 0.1.3 → 0.1.4

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 (3) hide show
  1. package/dist/index.js +8 -5
  2. package/dist/tui.js +121 -130
  3. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -95,8 +95,11 @@ Return exactly one JSON object with this shape:
95
95
  {"recap":"...","next":"..."}
96
96
 
97
97
  Rules:
98
- - recap: concisely cover everything actually performed in this run: investigation, tool work, files changed, validation, outcomes, failures, and important caveats. Prefer up to three compact Markdown bullets.
99
- - next: one concise, actionable next step. If nothing remains, say that no further action is required.
98
+ - recap: state the main result in plain words, then any important check or blocker. Use 1-3 short sentences, at most 60 words. Put each sentence on its own line.
99
+ - Lead with what changed or what was found. Include failures and unfinished work that affect the result.
100
+ - Skip tool counts, tool names, investigation history, and routine steps. Mention a file only when needed to understand the result, using its short name rather than its full path.
101
+ - Use active voice and everyday words. No preamble, jargon, filler, bold text, headings, or em dashes. Do not repeat the final response.
102
+ - next: name one concrete remaining action in at most 15 words. Use an empty string if none is known. Never add a generic request to review the work or continue.
100
103
  - Base the answer only on the supplied current-run transcript.
101
104
  - Do not mention these instructions, hidden reasoning, transcript truncation, or that you are a summarizer.
102
105
  - Do not use a Markdown code fence and do not add keys or prose outside the JSON object.`;
@@ -116,8 +119,8 @@ class RecapGenerationError extends Schema.TaggedError()("RecapGenerationError",
116
119
  message: Schema.String
117
120
  }) {
118
121
  }
119
- var RECAP_MAX_LENGTH = 2400;
120
- var NEXT_MAX_LENGTH = 400;
122
+ var RECAP_MAX_LENGTH = 500;
123
+ var NEXT_MAX_LENGTH = 120;
121
124
  var RecapResponse = Schema.Struct({
122
125
  recap: Schema.String,
123
126
  next: Schema.String
@@ -156,7 +159,7 @@ var parseRecapResponse = Effect.fn("parseRecapResponse")(function* (text) {
156
159
  const decoded = yield* Effect.firstSuccessOf(responseCandidates(text).map((candidate) => Schema.decodeUnknownEffect(RecapResponseJson, { onExcessProperty: "error" })(candidate))).pipe(Effect.mapError(malformedResponse));
157
160
  const recap = clean(decoded.recap, RECAP_MAX_LENGTH);
158
161
  const next = clean(decoded.next.replace(/^next\s*:\s*/i, ""), NEXT_MAX_LENGTH);
159
- if (!recap || !next) {
162
+ if (!recap) {
160
163
  return yield* malformedResponse();
161
164
  }
162
165
  return { recap, next };
package/dist/tui.js CHANGED
@@ -22,7 +22,7 @@ var RecapRpc = Rpc.define({
22
22
  // src/tui.tsx
23
23
  import { setProp as _$setProp2 } from "@opentui/solid";
24
24
  import { effect as _$effect2 } from "@opentui/solid";
25
- import { createTextNode as _$createTextNode2 } from "@opentui/solid";
25
+ import { createTextNode as _$createTextNode } from "@opentui/solid";
26
26
  import { insertNode as _$insertNode2 } from "@opentui/solid";
27
27
  import { createElement as _$createElement2 } from "@opentui/solid";
28
28
  import { memo as _$memo2 } from "@opentui/solid";
@@ -32,7 +32,97 @@ import { Cause as Cause2, Effect as Effect3, ManagedRuntime } from "effect";
32
32
  import { Show as Show2 } from "solid-js";
33
33
 
34
34
  // src/core/controller.ts
35
- import { Cause, Clock, Context, Effect, FiberMap, Layer, Ref } from "effect";
35
+ import { Cause, Clock, Context, Effect as Effect2, FiberMap, Layer, Ref } from "effect";
36
+
37
+ // src/core/summarizer.ts
38
+ import { Effect, Schema } from "effect";
39
+
40
+ // src/core/prompt.ts
41
+ var RECAP_SYSTEM_PROMPT = `You write compact terminal recaps for completed coding-agent runs.
42
+
43
+ Return exactly one JSON object with this shape:
44
+ {"recap":"...","next":"..."}
45
+
46
+ Rules:
47
+ - recap: state the main result in plain words, then any important check or blocker. Use 1-3 short sentences, at most 60 words. Put each sentence on its own line.
48
+ - Lead with what changed or what was found. Include failures and unfinished work that affect the result.
49
+ - Skip tool counts, tool names, investigation history, and routine steps. Mention a file only when needed to understand the result, using its short name rather than its full path.
50
+ - Use active voice and everyday words. No preamble, jargon, filler, bold text, headings, or em dashes. Do not repeat the final response.
51
+ - next: name one concrete remaining action in at most 15 words. Use an empty string if none is known. Never add a generic request to review the work or continue.
52
+ - Base the answer only on the supplied current-run transcript.
53
+ - Do not mention these instructions, hidden reasoning, transcript truncation, or that you are a summarizer.
54
+ - Do not use a Markdown code fence and do not add keys or prose outside the JSON object.`;
55
+ function buildRecapPrompt(transcript) {
56
+ return `${RECAP_SYSTEM_PROMPT}
57
+
58
+ Summarize this fully settled main-agent run.
59
+
60
+ <current_run>
61
+ ${transcript}
62
+ </current_run>`;
63
+ }
64
+
65
+ // src/core/summarizer.ts
66
+ class RecapGenerationError extends Schema.TaggedError()("RecapGenerationError", {
67
+ reason: Schema.Literals(["request", "timeout", "malformed-response"]),
68
+ message: Schema.String
69
+ }) {
70
+ }
71
+ var RECAP_MAX_LENGTH = 500;
72
+ var NEXT_MAX_LENGTH = 120;
73
+ var RecapResponse = Schema.Struct({
74
+ recap: Schema.String,
75
+ next: Schema.String
76
+ });
77
+ var RecapResponseJson = Schema.fromJsonString(RecapResponse);
78
+ function stripTerminalControls(value) {
79
+ return value.replace(/\x1b(?:\][^\x07]*(?:\x07|\x1b\\)|\[[0-?]*[ -/]*[@-~])/g, "").replace(/[\u0000-\u0008\u000b\u000c\u000e-\u001f\u007f-\u009f]/g, "");
80
+ }
81
+ function clean(value, limit) {
82
+ const result = stripTerminalControls(value).trim();
83
+ if (result.length <= limit) {
84
+ return result;
85
+ }
86
+ return `${result.slice(0, limit - 3).trimEnd()}...`;
87
+ }
88
+ function responseCandidates(text) {
89
+ const trimmed = text.trim();
90
+ const candidates = [trimmed];
91
+ for (const match of trimmed.matchAll(/```(?:json)?\s*([\s\S]*?)```/gi)) {
92
+ if (match[1]) {
93
+ candidates.push(match[1].trim());
94
+ }
95
+ }
96
+ const first = trimmed.indexOf("{");
97
+ const last = trimmed.lastIndexOf("}");
98
+ if (first >= 0 && last > first) {
99
+ candidates.push(trimmed.slice(first, last + 1));
100
+ }
101
+ return [...new Set(candidates)];
102
+ }
103
+ var malformedResponse = () => new RecapGenerationError({
104
+ reason: "malformed-response",
105
+ message: "The recap model did not return valid recap JSON."
106
+ });
107
+ var parseRecapResponse = Effect.fn("parseRecapResponse")(function* (text) {
108
+ const decoded = yield* Effect.firstSuccessOf(responseCandidates(text).map((candidate) => Schema.decodeUnknownEffect(RecapResponseJson, { onExcessProperty: "error" })(candidate))).pipe(Effect.mapError(malformedResponse));
109
+ const recap = clean(decoded.recap, RECAP_MAX_LENGTH);
110
+ const next = clean(decoded.next.replace(/^next\s*:\s*/i, ""), NEXT_MAX_LENGTH);
111
+ if (!recap) {
112
+ return yield* malformedResponse();
113
+ }
114
+ return { recap, next };
115
+ });
116
+ var summarizeRun = Effect.fn("summarizeRun")(function* (options) {
117
+ const text = yield* options.generate({
118
+ prompt: buildRecapPrompt(options.transcript),
119
+ model: options.model
120
+ }).pipe(Effect.timeout(options.timeoutMs ?? 45000), Effect.mapError((error) => error._tag === "TimeoutError" ? new RecapGenerationError({
121
+ reason: "timeout",
122
+ message: "The recap model request timed out."
123
+ }) : error));
124
+ return yield* parseRecapResponse(text);
125
+ });
36
126
 
37
127
  // src/core/transcript.ts
38
128
  var TOOL_ARGUMENT_MAX_BYTES = 2000;
@@ -172,31 +262,24 @@ function serializeRunTranscript(messages, detail, maxBytes = TRANSCRIPT_MAX_BYTE
172
262
  return `${head}${marker}${tail}`;
173
263
  }
174
264
  function buildFallbackRecap(messages, outcome = "completed") {
175
- const tools = [];
176
265
  let final = "";
177
266
  for (const message of messages) {
178
267
  if (message.type !== "assistant") {
179
268
  continue;
180
269
  }
181
270
  for (const part of message.content) {
182
- if (part.type === "tool") {
183
- tools.push(part.name);
184
- }
185
271
  if (part.type === "text" && part.text.trim()) {
186
272
  final = redactSecrets(part.text.trim());
187
273
  }
188
274
  }
189
275
  }
190
- const names = [...new Set(tools)];
191
- let activity = "";
192
- if (names.length) {
193
- const toolCallLabel = tools.length === 1 ? "tool call" : "tool calls";
194
- activity = ` The run used ${tools.length} ${toolCallLabel} across ${names.join(", ")}.`;
195
- }
196
- const result = final ? ` ${capped(final.replace(/\s+/g, " "), 700, "final response capped")}` : "";
276
+ const excerpt = stripTerminalControls(final).split(/\n\s*\n/)[0].replace(/\*\*|`/g, "").trim();
277
+ const result = excerpt.length <= 360 ? excerpt : `${excerpt.slice(0, 357).replace(/\s+\S*$/, "").trimEnd()}...`;
278
+ const status = `Run ${outcome}.`;
197
279
  return {
198
- recap: `The main-agent run ${outcome}.${activity}${result}`.trim(),
199
- next: "Review the completed work above and continue if anything remains."
280
+ recap: result ? outcome === "completed" || outcome === "succeeded" ? result : `${status}
281
+ ${result}` : status,
282
+ next: ""
200
283
  };
201
284
  }
202
285
  function selectRunMessages(messages, baseline) {
@@ -206,13 +289,13 @@ function selectRunMessages(messages, baseline) {
206
289
  // src/core/controller.ts
207
290
  class RecapControllerService extends Context.Service()("opencode-recap-plugin/RecapController") {
208
291
  }
209
- var createRecapController = Effect.fn("createRecapController")(function* (deps) {
292
+ var createRecapController = Effect2.fn("createRecapController")(function* (deps) {
210
293
  const state = yield* Ref.make({
211
294
  inboxBaselines: new Map,
212
295
  runs: new Map
213
296
  });
214
297
  const active = yield* FiberMap.make();
215
- const invalidate = Effect.fn("RecapController.invalidate")(function* (sessionID) {
298
+ const invalidate = Effect2.fn("RecapController.invalidate")(function* (sessionID) {
216
299
  yield* FiberMap.remove(active, sessionID);
217
300
  yield* Ref.modify(state, (current) => {
218
301
  const runs = new Map(current.runs);
@@ -221,7 +304,7 @@ var createRecapController = Effect.fn("createRecapController")(function* (deps)
221
304
  });
222
305
  yield* deps.persist(undefined, sessionID);
223
306
  });
224
- const inbox = Effect.fn("RecapController.inbox")(function* (sessionID, user) {
307
+ const inbox = Effect2.fn("RecapController.inbox")(function* (sessionID, user) {
225
308
  if (!user || deps.session(sessionID)?.parentID) {
226
309
  return;
227
310
  }
@@ -236,7 +319,7 @@ var createRecapController = Effect.fn("createRecapController")(function* (deps)
236
319
  yield* FiberMap.remove(active, sessionID);
237
320
  yield* deps.persist(undefined, sessionID);
238
321
  });
239
- const started = Effect.fn("RecapController.started")(function* (sessionID) {
322
+ const started = Effect2.fn("RecapController.started")(function* (sessionID) {
240
323
  if (deps.session(sessionID)?.parentID) {
241
324
  return;
242
325
  }
@@ -253,7 +336,7 @@ var createRecapController = Effect.fn("createRecapController")(function* (deps)
253
336
  });
254
337
  yield* deps.persist(undefined, sessionID);
255
338
  });
256
- const terminal = Effect.fn("RecapController.terminal")(function* (input) {
339
+ const terminal = Effect2.fn("RecapController.terminal")(function* (input) {
257
340
  const baseline = yield* Ref.modify(state, (current) => {
258
341
  const runs = new Map(current.runs);
259
342
  const boundary = runs.get(input.sessionID);
@@ -263,7 +346,7 @@ var createRecapController = Effect.fn("createRecapController")(function* (deps)
263
346
  if (!baseline) {
264
347
  return;
265
348
  }
266
- const generation = Effect.gen(function* () {
349
+ const generation = Effect2.gen(function* () {
267
350
  yield* deps.syncMessages(input.sessionID);
268
351
  const messages = selectRunMessages(deps.messages(input.sessionID), baseline);
269
352
  if (messages.length === 0) {
@@ -275,7 +358,7 @@ var createRecapController = Effect.fn("createRecapController")(function* (deps)
275
358
  eventID: input.eventID,
276
359
  transcript: serializeRunTranscript(messages, input.detail),
277
360
  model
278
- }).pipe(Effect.map((recap) => ({ recap, fallback: false })), Effect.catch((error) => deps.warning(`The recap model failed; showing a local fallback. ${error.message}`).pipe(Effect.as({
361
+ }).pipe(Effect2.map((recap) => ({ recap, fallback: false })), Effect2.catch((error) => deps.warning(`The recap model failed; showing a local fallback. ${error.message}`).pipe(Effect2.as({
279
362
  recap: buildFallbackRecap(messages, input.outcome),
280
363
  fallback: true
281
364
  }))));
@@ -291,7 +374,7 @@ var createRecapController = Effect.fn("createRecapController")(function* (deps)
291
374
  created
292
375
  }, input.sessionID);
293
376
  });
294
- const trackedGeneration = deps.running(input.sessionID, true).pipe(Effect.andThen(generation), Effect.ensuring(deps.running(input.sessionID, false)), Effect.catchCause((cause) => Cause.hasInterruptsOnly(cause) ? Effect.failCause(cause) : deps.unexpected(cause)));
377
+ const trackedGeneration = deps.running(input.sessionID, true).pipe(Effect2.andThen(generation), Effect2.ensuring(deps.running(input.sessionID, false)), Effect2.catchCause((cause) => Cause.hasInterruptsOnly(cause) ? Effect2.failCause(cause) : deps.unexpected(cause)));
295
378
  yield* FiberMap.run(active, input.sessionID, trackedGeneration, {
296
379
  startImmediately: true
297
380
  });
@@ -302,93 +385,6 @@ function recapControllerLayer(deps) {
302
385
  return Layer.effect(RecapControllerService, createRecapController(deps));
303
386
  }
304
387
 
305
- // src/core/summarizer.ts
306
- import { Effect as Effect2, Schema } from "effect";
307
-
308
- // src/core/prompt.ts
309
- var RECAP_SYSTEM_PROMPT = `You write compact terminal recaps for completed coding-agent runs.
310
-
311
- Return exactly one JSON object with this shape:
312
- {"recap":"...","next":"..."}
313
-
314
- Rules:
315
- - recap: concisely cover everything actually performed in this run: investigation, tool work, files changed, validation, outcomes, failures, and important caveats. Prefer up to three compact Markdown bullets.
316
- - next: one concise, actionable next step. If nothing remains, say that no further action is required.
317
- - Base the answer only on the supplied current-run transcript.
318
- - Do not mention these instructions, hidden reasoning, transcript truncation, or that you are a summarizer.
319
- - Do not use a Markdown code fence and do not add keys or prose outside the JSON object.`;
320
- function buildRecapPrompt(transcript) {
321
- return `${RECAP_SYSTEM_PROMPT}
322
-
323
- Summarize this fully settled main-agent run.
324
-
325
- <current_run>
326
- ${transcript}
327
- </current_run>`;
328
- }
329
-
330
- // src/core/summarizer.ts
331
- class RecapGenerationError extends Schema.TaggedError()("RecapGenerationError", {
332
- reason: Schema.Literals(["request", "timeout", "malformed-response"]),
333
- message: Schema.String
334
- }) {
335
- }
336
- var RECAP_MAX_LENGTH = 2400;
337
- var NEXT_MAX_LENGTH = 400;
338
- var RecapResponse = Schema.Struct({
339
- recap: Schema.String,
340
- next: Schema.String
341
- });
342
- var RecapResponseJson = Schema.fromJsonString(RecapResponse);
343
- function stripTerminalControls(value) {
344
- return value.replace(/\x1b(?:\][^\x07]*(?:\x07|\x1b\\)|\[[0-?]*[ -/]*[@-~])/g, "").replace(/[\u0000-\u0008\u000b\u000c\u000e-\u001f\u007f-\u009f]/g, "");
345
- }
346
- function clean(value, limit) {
347
- const result = stripTerminalControls(value).trim();
348
- if (result.length <= limit) {
349
- return result;
350
- }
351
- return `${result.slice(0, limit - 3).trimEnd()}...`;
352
- }
353
- function responseCandidates(text) {
354
- const trimmed = text.trim();
355
- const candidates = [trimmed];
356
- for (const match of trimmed.matchAll(/```(?:json)?\s*([\s\S]*?)```/gi)) {
357
- if (match[1]) {
358
- candidates.push(match[1].trim());
359
- }
360
- }
361
- const first = trimmed.indexOf("{");
362
- const last = trimmed.lastIndexOf("}");
363
- if (first >= 0 && last > first) {
364
- candidates.push(trimmed.slice(first, last + 1));
365
- }
366
- return [...new Set(candidates)];
367
- }
368
- var malformedResponse = () => new RecapGenerationError({
369
- reason: "malformed-response",
370
- message: "The recap model did not return valid recap JSON."
371
- });
372
- var parseRecapResponse = Effect2.fn("parseRecapResponse")(function* (text) {
373
- const decoded = yield* Effect2.firstSuccessOf(responseCandidates(text).map((candidate) => Schema.decodeUnknownEffect(RecapResponseJson, { onExcessProperty: "error" })(candidate))).pipe(Effect2.mapError(malformedResponse));
374
- const recap = clean(decoded.recap, RECAP_MAX_LENGTH);
375
- const next = clean(decoded.next.replace(/^next\s*:\s*/i, ""), NEXT_MAX_LENGTH);
376
- if (!recap || !next) {
377
- return yield* malformedResponse();
378
- }
379
- return { recap, next };
380
- });
381
- var summarizeRun = Effect2.fn("summarizeRun")(function* (options) {
382
- const text = yield* options.generate({
383
- prompt: buildRecapPrompt(options.transcript),
384
- model: options.model
385
- }).pipe(Effect2.timeout(options.timeoutMs ?? 45000), Effect2.mapError((error) => error._tag === "TimeoutError" ? new RecapGenerationError({
386
- reason: "timeout",
387
- message: "The recap model request timed out."
388
- }) : error));
389
- return yield* parseRecapResponse(text);
390
- });
391
-
392
388
  // src/tui/inline.tsx
393
389
  import { createComponent as _$createComponent } from "@opentui/solid";
394
390
  import { isRenderable } from "@opentui/core";
@@ -397,44 +393,39 @@ import { createSignal, onCleanup, onMount, Show } from "solid-js";
397
393
 
398
394
  // src/tui/card.tsx
399
395
  import { effect as _$effect } from "@opentui/solid";
400
- import { memo as _$memo } from "@opentui/solid";
401
- import { insert as _$insert } from "@opentui/solid";
402
- import { createTextNode as _$createTextNode } from "@opentui/solid";
403
396
  import { insertNode as _$insertNode } from "@opentui/solid";
397
+ import { insert as _$insert } from "@opentui/solid";
398
+ import { memo as _$memo } from "@opentui/solid";
404
399
  import { setProp as _$setProp } from "@opentui/solid";
405
400
  import { createElement as _$createElement } from "@opentui/solid";
406
401
  function RecapCard(props) {
407
402
  return (() => {
408
- var _el$ = _$createElement("box"), _el$2 = _$createElement("text"), _el$3 = _$createElement("b"), _el$5 = _$createElement("text"), _el$6 = _$createElement("text");
403
+ var _el$ = _$createElement("box"), _el$2 = _$createElement("text"), _el$3 = _$createElement("b"), _el$4 = _$createElement("text");
409
404
  _$insertNode(_el$, _el$2);
410
- _$insertNode(_el$, _el$5);
411
- _$insertNode(_el$, _el$6);
405
+ _$insertNode(_el$, _el$4);
412
406
  _$setProp(_el$, "flexDirection", "column");
413
407
  _$setProp(_el$, "marginTop", 1);
414
408
  _$setProp(_el$, "paddingLeft", 3);
415
409
  _$insertNode(_el$2, _el$3);
416
- _$insertNode(_el$3, _$createTextNode(`Summary:`));
417
- _$insert(_el$5, () => props.recap.recap);
418
- _$insert(_el$6, () => `Next: ${props.recap.next}`);
410
+ _$insert(_el$3, () => props.recap.fallback ? "Recap \xB7 excerpt" : "Recap");
411
+ _$insert(_el$4, () => props.recap.recap);
419
412
  _$insert(_el$, (() => {
420
- var _c$ = _$memo(() => !!props.recap.fallback);
413
+ var _c$ = _$memo(() => !!props.recap.next);
421
414
  return () => _c$() ? (() => {
422
- var _el$7 = _$createElement("text");
423
- _$insertNode(_el$7, _$createTextNode(`Local fallback`));
424
- _$effect((_$p) => _$setProp(_el$7, "fg", props.theme.text.subdued, _$p));
425
- return _el$7;
415
+ var _el$5 = _$createElement("text");
416
+ _$insert(_el$5, () => `Next: ${props.recap.next}`);
417
+ _$effect((_$p) => _$setProp(_el$5, "fg", props.theme.text.subdued, _$p));
418
+ return _el$5;
426
419
  })() : null;
427
420
  })(), null);
428
421
  _$effect((_p$) => {
429
- var _v$ = props.theme.text.subdued, _v$2 = props.theme.text.default, _v$3 = props.theme.text.subdued;
422
+ var _v$ = props.theme.text.subdued, _v$2 = props.theme.text.default;
430
423
  _v$ !== _p$.e && (_p$.e = _$setProp(_el$2, "fg", _v$, _p$.e));
431
- _v$2 !== _p$.t && (_p$.t = _$setProp(_el$5, "fg", _v$2, _p$.t));
432
- _v$3 !== _p$.a && (_p$.a = _$setProp(_el$6, "fg", _v$3, _p$.a));
424
+ _v$2 !== _p$.t && (_p$.t = _$setProp(_el$4, "fg", _v$2, _p$.t));
433
425
  return _p$;
434
426
  }, {
435
427
  e: undefined,
436
- t: undefined,
437
- a: undefined
428
+ t: undefined
438
429
  });
439
430
  return _el$;
440
431
  })();
@@ -781,7 +772,7 @@ ${event.data.reason}`
781
772
  },
782
773
  get children() {
783
774
  var _el$ = _$createElement2("text");
784
- _$insertNode2(_el$, _$createTextNode2(`\u25CF generating run recap...`));
775
+ _$insertNode2(_el$, _$createTextNode(`\u25CF generating run recap...`));
785
776
  _$effect2((_$p) => _$setProp2(_el$, "fg", context.theme.text.status.running, _$p));
786
777
  return _el$;
787
778
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@azatakmyradov/opencode-recap-plugin",
3
- "version": "0.1.3",
3
+ "version": "0.1.4",
4
4
  "description": "Run recaps and suggested next steps for the OpenCode V2 TUI",
5
5
  "keywords": [
6
6
  "opencode",