@moxxy/plugin-telegram 0.44.0 → 0.45.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.
- package/dist/channel/callback-handler.d.ts +4 -0
- package/dist/channel/callback-handler.d.ts.map +1 -1
- package/dist/channel/callback-handler.js +8 -0
- package/dist/channel/callback-handler.js.map +1 -1
- package/dist/channel/frame-pump.d.ts +3 -1
- package/dist/channel/frame-pump.d.ts.map +1 -1
- package/dist/channel/frame-pump.js +3 -2
- package/dist/channel/frame-pump.js.map +1 -1
- package/dist/channel/mirror.d.ts +1 -1
- package/dist/channel/mirror.d.ts.map +1 -1
- package/dist/channel/mirror.js +1 -0
- package/dist/channel/mirror.js.map +1 -1
- package/dist/channel/question-cards.d.ts +37 -0
- package/dist/channel/question-cards.d.ts.map +1 -0
- package/dist/channel/question-cards.js +46 -0
- package/dist/channel/question-cards.js.map +1 -0
- package/dist/channel/send-images.d.ts +31 -0
- package/dist/channel/send-images.d.ts.map +1 -0
- package/dist/channel/send-images.js +58 -0
- package/dist/channel/send-images.js.map +1 -0
- package/dist/channel/slash-handler.js +1 -1
- package/dist/channel/slash-handler.js.map +1 -1
- package/dist/channel/text-handler.d.ts +4 -1
- package/dist/channel/text-handler.d.ts.map +1 -1
- package/dist/channel/text-handler.js +5 -1
- package/dist/channel/text-handler.js.map +1 -1
- package/dist/channel/turn-runner.d.ts.map +1 -1
- package/dist/channel/turn-runner.js +4 -0
- package/dist/channel/turn-runner.js.map +1 -1
- package/dist/channel.d.ts +3 -0
- package/dist/channel.d.ts.map +1 -1
- package/dist/channel.js +18 -1
- package/dist/channel.js.map +1 -1
- package/dist/permission.d.ts +1 -1
- package/dist/permission.d.ts.map +1 -1
- package/dist/permission.js +5 -3
- package/dist/permission.js.map +1 -1
- package/dist/render.d.ts +17 -5
- package/dist/render.d.ts.map +1 -1
- package/dist/render.js +51 -29
- package/dist/render.js.map +1 -1
- package/package.json +9 -9
- package/src/channel/callback-handler.test.ts +37 -0
- package/src/channel/callback-handler.ts +10 -0
- package/src/channel/frame-pump.ts +5 -2
- package/src/channel/mirror.ts +2 -1
- package/src/channel/question-cards.test.ts +61 -0
- package/src/channel/question-cards.ts +70 -0
- package/src/channel/send-images.test.ts +66 -0
- package/src/channel/send-images.ts +75 -0
- package/src/channel/slash-handler.ts +1 -1
- package/src/channel/text-handler.test.ts +34 -0
- package/src/channel/text-handler.ts +8 -2
- package/src/channel/turn-runner.test.ts +26 -3
- package/src/channel/turn-runner.ts +3 -0
- package/src/channel.ts +19 -0
- package/src/permission.test.ts +19 -0
- package/src/permission.ts +5 -3
- package/src/render.test.ts +100 -4
- package/src/render.ts +66 -32
package/src/render.test.ts
CHANGED
|
@@ -151,6 +151,23 @@ describe('TurnRenderer', () => {
|
|
|
151
151
|
expect(frame.diffHtml).not.toContain('<Foo');
|
|
152
152
|
});
|
|
153
153
|
|
|
154
|
+
it("says in the footer that the agent's limit is used up and when it resets, as the desktop does", () => {
|
|
155
|
+
const r = new TurnRenderer();
|
|
156
|
+
const out = r.accept(
|
|
157
|
+
baseEvent(
|
|
158
|
+
{
|
|
159
|
+
type: 'plugin_event',
|
|
160
|
+
source: 'system',
|
|
161
|
+
pluginId: asPluginId('@moxxy/agent-connection'),
|
|
162
|
+
subtype: 'account_limit',
|
|
163
|
+
payload: { provider: 'codex-agent', agent: 'Codex', limit: { state: 'reached', windowMinutes: 10080 } },
|
|
164
|
+
},
|
|
165
|
+
0,
|
|
166
|
+
),
|
|
167
|
+
);
|
|
168
|
+
expect(out.errorHtml).toBe("⛔ <b>Codex's weekly limit is used up</b>");
|
|
169
|
+
});
|
|
170
|
+
|
|
154
171
|
it('appends an error footer when an error event arrives', () => {
|
|
155
172
|
const r = new TurnRenderer();
|
|
156
173
|
const out = r.accept(baseEvent({ type: 'error', kind: 'fatal', message: 'boom', source: 'system' }, 0));
|
|
@@ -176,9 +193,88 @@ describe('TurnRenderer', () => {
|
|
|
176
193
|
return r;
|
|
177
194
|
};
|
|
178
195
|
|
|
196
|
+
const stepList = (steps: ReadonlyArray<{ text: string; status: string }>) => ({
|
|
197
|
+
forModel: 'list updated',
|
|
198
|
+
display: { kind: 'step-list', steps },
|
|
199
|
+
});
|
|
200
|
+
|
|
201
|
+
it('folds every step-list tool result into one list of the latest steps', () => {
|
|
202
|
+
const r = new TurnRenderer();
|
|
203
|
+
const first = asToolCallId('s1');
|
|
204
|
+
const second = asToolCallId('s2');
|
|
205
|
+
r.accept(baseEvent({ type: 'tool_call_requested', callId: first, name: 'TodoWrite', input: {}, source: 'model' }, 0));
|
|
206
|
+
r.accept(baseEvent({
|
|
207
|
+
type: 'tool_result', callId: first, ok: true, source: 'tool',
|
|
208
|
+
output: stepList([{ text: 'Read the logs', status: 'in_progress' }, { text: 'Fix <the> bug', status: 'pending' }]),
|
|
209
|
+
}, 1));
|
|
210
|
+
r.accept(baseEvent({ type: 'tool_call_requested', callId: second, name: 'TodoWrite', input: {}, source: 'model' }, 2));
|
|
211
|
+
const frame = r.accept(baseEvent({
|
|
212
|
+
type: 'tool_result', callId: second, ok: true, source: 'tool',
|
|
213
|
+
output: stepList([
|
|
214
|
+
{ text: 'Read the logs', status: 'completed' },
|
|
215
|
+
{ text: 'Fix <the> bug', status: 'in_progress' },
|
|
216
|
+
{ text: 'Run the tests', status: 'pending' },
|
|
217
|
+
{ text: 'Ask for review', status: 'cancelled' },
|
|
218
|
+
]),
|
|
219
|
+
}, 3));
|
|
220
|
+
|
|
221
|
+
expect(frame.activityHtml).toContain('<b>Steps · 2 of 4 done</b>');
|
|
222
|
+
expect(frame.activityHtml).toContain('✓ Read the logs');
|
|
223
|
+
expect(frame.activityHtml).toContain('● <b>Fix <the> bug</b>');
|
|
224
|
+
expect(frame.activityHtml).toContain('○ Run the tests');
|
|
225
|
+
expect(frame.activityHtml).toContain('✗ <s>Ask for review</s>');
|
|
226
|
+
expect(frame.activityHtml.match(/Steps ·/g)).toHaveLength(1);
|
|
227
|
+
expect(frame.activityHtml).not.toContain('<code>TodoWrite</code>');
|
|
228
|
+
});
|
|
229
|
+
|
|
230
|
+
it('does not count step-list calls as steps of the collapsed trace', () => {
|
|
231
|
+
const r = withTools(4);
|
|
232
|
+
const id = asToolCallId('list');
|
|
233
|
+
r.accept(baseEvent({ type: 'tool_call_requested', callId: id, name: 'update_plan', input: {}, source: 'model' }, 20));
|
|
234
|
+
r.accept(baseEvent({
|
|
235
|
+
type: 'tool_result', callId: id, ok: true, source: 'tool',
|
|
236
|
+
output: stepList([{ text: 'One', status: 'completed' }]),
|
|
237
|
+
}, 21));
|
|
238
|
+
expect(r.snapshot({ final: true }).activityHtml).toContain('🔧 <b>4 steps</b>');
|
|
239
|
+
});
|
|
240
|
+
|
|
241
|
+
it('names how many images the tools returned and keeps each image once', () => {
|
|
242
|
+
const r = new TurnRenderer();
|
|
243
|
+
const shot = { mediaType: 'image/png', base64: 'AAAA' };
|
|
244
|
+
for (const [i, output] of [
|
|
245
|
+
{ forModel: 'screen', ...shot, moreImages: [{ mediaType: 'image/jpeg', base64: 'BBBB' }] },
|
|
246
|
+
{ forModel: 'screen again', ...shot },
|
|
247
|
+
].entries()) {
|
|
248
|
+
const id = asToolCallId(`shot${i}`);
|
|
249
|
+
r.accept(baseEvent({ type: 'tool_call_requested', callId: id, name: 'screenshot', input: {}, source: 'model' }, i * 2));
|
|
250
|
+
r.accept(baseEvent({ type: 'tool_result', callId: id, ok: true, output, source: 'tool' }, i * 2 + 1));
|
|
251
|
+
}
|
|
252
|
+
expect(r.images()).toEqual([shot, { mediaType: 'image/jpeg', base64: 'BBBB' }]);
|
|
253
|
+
expect(r.snapshot().activityHtml).toContain('🖼 2 images came back');
|
|
254
|
+
});
|
|
255
|
+
|
|
256
|
+
it('says while the turn runs that the agent of a connection is using the computer, and how to stop it', () => {
|
|
257
|
+
const r = new TurnRenderer({ agentName: () => 'Codex' });
|
|
258
|
+
const frame = r.accept(baseEvent({
|
|
259
|
+
type: 'tool_call_requested', callId: c1, name: 'mcp__computer-use__click', input: { app: 'Arc' },
|
|
260
|
+
executedBy: 'provider', source: 'model',
|
|
261
|
+
}, 0));
|
|
262
|
+
expect(frame.activityHtml).toContain('🖥 <b>Codex is using your computer · Arc</b> — send /cancel to stop');
|
|
263
|
+
// The turn is over on the final frame: nothing is using the computer any more.
|
|
264
|
+
expect(r.snapshot({ final: true }).activityHtml).not.toContain('using your computer');
|
|
265
|
+
});
|
|
266
|
+
|
|
267
|
+
it('does not report Moxxy’s own computer tools as an agent using the computer', () => {
|
|
268
|
+
const r = new TurnRenderer({ agentName: () => 'Codex' });
|
|
269
|
+
const frame = r.accept(baseEvent({
|
|
270
|
+
type: 'tool_call_requested', callId: c1, name: 'mcp__computer-use__click', input: { app: 'Arc' }, source: 'model',
|
|
271
|
+
}, 0));
|
|
272
|
+
expect(frame.activityHtml).not.toContain('using your computer');
|
|
273
|
+
});
|
|
274
|
+
|
|
179
275
|
it('folds a long activity trace into an expandable box with a summary on the final frame', () => {
|
|
180
276
|
const r = withTools(4);
|
|
181
|
-
const snap = r.snapshot({
|
|
277
|
+
const snap = r.snapshot({ final: true });
|
|
182
278
|
expect(snap.activityHtml).toContain('<blockquote expandable>');
|
|
183
279
|
expect(snap.activityHtml).toContain('🔧 <b>4 steps</b>');
|
|
184
280
|
// The tool lines are still in the (collapsed) box.
|
|
@@ -187,7 +283,7 @@ describe('TurnRenderer', () => {
|
|
|
187
283
|
|
|
188
284
|
it('keeps the activity open (no expandable) while streaming', () => {
|
|
189
285
|
const r = withTools(5);
|
|
190
|
-
const snap = r.snapshot(); //
|
|
286
|
+
const snap = r.snapshot(); // streaming frame
|
|
191
287
|
expect(snap.activityHtml).toContain('<blockquote>');
|
|
192
288
|
expect(snap.activityHtml).not.toContain('expandable');
|
|
193
289
|
expect(snap.activityHtml).not.toContain('🔧');
|
|
@@ -195,7 +291,7 @@ describe('TurnRenderer', () => {
|
|
|
195
291
|
|
|
196
292
|
it('keeps a short activity trace inline even on the final frame', () => {
|
|
197
293
|
const r = withTools(2); // below the collapse threshold
|
|
198
|
-
const snap = r.snapshot({
|
|
294
|
+
const snap = r.snapshot({ final: true });
|
|
199
295
|
expect(snap.activityHtml).toContain('<blockquote>');
|
|
200
296
|
expect(snap.activityHtml).not.toContain('expandable');
|
|
201
297
|
});
|
|
@@ -210,7 +306,7 @@ describe('TurnRenderer', () => {
|
|
|
210
306
|
r.accept(baseEvent({ type: 'tool_result', callId: id, ok: true, output: 'ok', source: 'tool' }, 2));
|
|
211
307
|
r.accept(baseEvent({ type: 'skill_created', name: 's1', source: 'system' }, 3));
|
|
212
308
|
r.accept(baseEvent({ type: 'skill_created', name: 's2', source: 'system' }, 4));
|
|
213
|
-
const snap = r.snapshot({
|
|
309
|
+
const snap = r.snapshot({ final: true });
|
|
214
310
|
expect(snap.activityHtml).toContain('<blockquote expandable>');
|
|
215
311
|
expect(snap.activityHtml).toContain('🔧 <b>1 step</b>');
|
|
216
312
|
});
|
package/src/render.ts
CHANGED
|
@@ -1,11 +1,21 @@
|
|
|
1
|
-
import type { MoxxyEvent } from '@moxxy/sdk';
|
|
1
|
+
import type { MoxxyEvent, StepListDisplay } from '@moxxy/sdk';
|
|
2
2
|
import {
|
|
3
3
|
assertDefined,
|
|
4
4
|
fileDiffSummary,
|
|
5
5
|
fileDiffVerb,
|
|
6
6
|
isFileDiffDisplay,
|
|
7
7
|
type FileDiffDisplay,
|
|
8
|
+
type ToolImage,
|
|
8
9
|
} from '@moxxy/sdk';
|
|
10
|
+
import {
|
|
11
|
+
STEP_MARKS,
|
|
12
|
+
TurnAgentWork,
|
|
13
|
+
agentComputerText,
|
|
14
|
+
imagesLine,
|
|
15
|
+
stepListHeading,
|
|
16
|
+
unifiedDiffText,
|
|
17
|
+
usedUpLimitText,
|
|
18
|
+
} from '@moxxy/channel-kit';
|
|
9
19
|
|
|
10
20
|
const TELEGRAM_MESSAGE_LIMIT = 4000; // 4096 minus a safety margin
|
|
11
21
|
|
|
@@ -74,6 +84,11 @@ interface SkillEntry {
|
|
|
74
84
|
readonly toolCount: number;
|
|
75
85
|
}
|
|
76
86
|
|
|
87
|
+
export interface TurnRendererOptions {
|
|
88
|
+
/** The name of the agent of the session's active agent connection ("Codex"), read when a frame is drawn. */
|
|
89
|
+
readonly agentName?: () => string | undefined;
|
|
90
|
+
}
|
|
91
|
+
|
|
77
92
|
export class TurnRenderer {
|
|
78
93
|
private chunks: string[] = [];
|
|
79
94
|
/** In-flight + completed tool calls keyed by callId, in arrival order. */
|
|
@@ -84,9 +99,17 @@ export class TurnRenderer {
|
|
|
84
99
|
private errorLine: string | null = null;
|
|
85
100
|
/** Pre-formatted HTML diff blocks, one per file-diff tool result, in arrival order. */
|
|
86
101
|
private diffBlocks: string[] = [];
|
|
102
|
+
/** The step list, images and computer use of the turn, as every channel reads them. */
|
|
103
|
+
private readonly work = new TurnAgentWork();
|
|
104
|
+
private readonly agentName: () => string | undefined;
|
|
87
105
|
private lastFrame = '';
|
|
88
106
|
|
|
107
|
+
constructor(opts: TurnRendererOptions = {}) {
|
|
108
|
+
this.agentName = opts.agentName ?? (() => undefined);
|
|
109
|
+
}
|
|
110
|
+
|
|
89
111
|
accept(event: MoxxyEvent): RenderedFrame {
|
|
112
|
+
this.work.accept(event);
|
|
90
113
|
switch (event.type) {
|
|
91
114
|
case 'assistant_chunk':
|
|
92
115
|
this.chunks.push(event.delta);
|
|
@@ -143,13 +166,17 @@ export class TurnRenderer {
|
|
|
143
166
|
case 'skill_created':
|
|
144
167
|
this.notices.push(`💡 created skill: <b>${escape(event.name)}</b>`);
|
|
145
168
|
break;
|
|
146
|
-
case 'plugin_event':
|
|
169
|
+
case 'plugin_event': {
|
|
147
170
|
if (event.pluginId === '@moxxy/subagents' && event.subtype === 'subagent_started') {
|
|
148
171
|
const payload = (event.payload ?? {}) as Record<string, unknown>;
|
|
149
172
|
const label = typeof payload.label === 'string' ? payload.label : 'agent';
|
|
150
173
|
this.notices.push(`🤖 spawned agent <b>${escape(label)}</b>`);
|
|
151
174
|
}
|
|
175
|
+
// The agent's used-up account limit ends the turn in place of an error.
|
|
176
|
+
const limit = usedUpLimitText(event);
|
|
177
|
+
if (limit) this.errorLine = `⛔ <b>${escape(limit)}</b>`;
|
|
152
178
|
break;
|
|
179
|
+
}
|
|
153
180
|
case 'error':
|
|
154
181
|
this.errorLine = `❗ <b>${escape(event.kind)}</b>: ${escape(event.message)}`;
|
|
155
182
|
break;
|
|
@@ -164,13 +191,14 @@ export class TurnRenderer {
|
|
|
164
191
|
}
|
|
165
192
|
|
|
166
193
|
/**
|
|
167
|
-
* Compose the current frame. `
|
|
168
|
-
* long tool-activity trace into an expandable box so the
|
|
169
|
-
* leads with the answer
|
|
194
|
+
* Compose the current frame. `final` (set on the last flush, once the turn is
|
|
195
|
+
* over) folds a long tool-activity trace into an expandable box so the
|
|
196
|
+
* finished message leads with the answer, and drops what is only true while
|
|
197
|
+
* the turn runs (the agent using the computer); during streaming the activity
|
|
170
198
|
* stays fully visible while work is in flight.
|
|
171
199
|
*/
|
|
172
|
-
snapshot(opts: {
|
|
173
|
-
const activityHtml = this.renderActivity(opts.
|
|
200
|
+
snapshot(opts: { final?: boolean } = {}): RenderedFrame {
|
|
201
|
+
const activityHtml = this.renderActivity(opts.final ?? false) ?? '';
|
|
174
202
|
const body = this.finalAssistant ?? this.chunks.join('');
|
|
175
203
|
const diffHtml = this.diffBlocks.join('\n\n');
|
|
176
204
|
const errorHtml = this.errorLine ?? '';
|
|
@@ -187,18 +215,25 @@ export class TurnRenderer {
|
|
|
187
215
|
* the fence/inline placeholder dance). Use `__inert__` markers so
|
|
188
216
|
* already-formed HTML doesn't get re-escaped.
|
|
189
217
|
*
|
|
190
|
-
*
|
|
218
|
+
* On the final frame, when the trace ran long, fold it
|
|
191
219
|
* into a `<blockquote expandable>` topped by a one-line summary — the
|
|
192
220
|
* tool noise becomes a tap-to-open box and the assistant's answer leads.
|
|
193
221
|
*/
|
|
194
|
-
private renderActivity(
|
|
222
|
+
private renderActivity(final: boolean): string | null {
|
|
195
223
|
const lines: string[] = [];
|
|
224
|
+
const computer = this.work.computerUse;
|
|
225
|
+
if (computer && !final) {
|
|
226
|
+
lines.push(`🖥 <b>${escape(agentComputerText(computer, this.agentName()))}</b> — send /cancel to stop`);
|
|
227
|
+
}
|
|
196
228
|
if (this.skillBanner) {
|
|
197
229
|
lines.push(`💡 <b>${escape(this.skillBanner.name)}</b>`);
|
|
198
230
|
}
|
|
231
|
+
const steps = this.work.stepList;
|
|
232
|
+
if (steps) lines.push(...stepListLines(steps));
|
|
199
233
|
// Show last N tool entries to keep the message compact when a turn
|
|
200
234
|
// makes many calls. Most-recent at the bottom (matches chronology).
|
|
201
|
-
|
|
235
|
+
// A call that changed the list of steps shows in the list, not here.
|
|
236
|
+
const tools = [...this.tools.values()].filter((t) => !this.work.isStepListCall(t.id));
|
|
202
237
|
const visible = tools.slice(-10);
|
|
203
238
|
const hidden = tools.length - visible.length;
|
|
204
239
|
if (hidden > 0) {
|
|
@@ -207,9 +242,11 @@ export class TurnRenderer {
|
|
|
207
242
|
for (const t of visible) {
|
|
208
243
|
lines.push(`• <code>${escape(t.name)}</code>(${escape(t.inputPreview)}) ${statusBadge(t.status)}`);
|
|
209
244
|
}
|
|
245
|
+
const images = this.work.images.length;
|
|
246
|
+
if (images > 0) lines.push(`🖼 ${imagesLine(images)}`);
|
|
210
247
|
for (const n of this.notices.slice(-5)) lines.push(n);
|
|
211
248
|
if (lines.length === 0) return null;
|
|
212
|
-
if (
|
|
249
|
+
if (final && lines.length >= COLLAPSE_ACTIVITY_LINES) {
|
|
213
250
|
const summary = activitySummary(tools.length);
|
|
214
251
|
const inner = summary ? `${summary}\n${lines.join('\n')}` : lines.join('\n');
|
|
215
252
|
return `<blockquote expandable>${inner}</blockquote>`;
|
|
@@ -217,7 +254,13 @@ export class TurnRenderer {
|
|
|
217
254
|
return `<blockquote>${lines.join('\n')}</blockquote>`;
|
|
218
255
|
}
|
|
219
256
|
|
|
257
|
+
/** The images tools returned this turn, each once, for the channel to send after the reply. */
|
|
258
|
+
images(): ReadonlyArray<ToolImage> {
|
|
259
|
+
return this.work.images;
|
|
260
|
+
}
|
|
261
|
+
|
|
220
262
|
reset(): void {
|
|
263
|
+
this.work.reset();
|
|
221
264
|
this.chunks = [];
|
|
222
265
|
this.tools.clear();
|
|
223
266
|
this.skillBanner = null;
|
|
@@ -239,6 +282,18 @@ function activitySummary(toolCount: number): string {
|
|
|
239
282
|
return `🔧 <b>${toolCount} step${toolCount === 1 ? '' : 's'}</b>`;
|
|
240
283
|
}
|
|
241
284
|
|
|
285
|
+
/** An agent's list of steps: its heading, an optional reason it changed, then one marked line per step. */
|
|
286
|
+
function stepListLines(list: StepListDisplay): string[] {
|
|
287
|
+
const lines = [`📋 <b>${escape(stepListHeading(list))}</b>`];
|
|
288
|
+
if (list.explanation) lines.push(`<i>${escape(list.explanation)}</i>`);
|
|
289
|
+
for (const step of list.steps) {
|
|
290
|
+
const text = escape(step.text);
|
|
291
|
+
const shown = step.status === 'in_progress' ? `<b>${text}</b>` : step.status === 'cancelled' ? `<s>${text}</s>` : text;
|
|
292
|
+
lines.push(`${STEP_MARKS[step.status]} ${shown}`);
|
|
293
|
+
}
|
|
294
|
+
return lines;
|
|
295
|
+
}
|
|
296
|
+
|
|
242
297
|
function statusBadge(status: ToolStatus): string {
|
|
243
298
|
// Pair the glyph with a text word ('done'/'error') so the pass/fail
|
|
244
299
|
// distinction survives screen-reader announcement / glyph-stripping —
|
|
@@ -288,27 +343,6 @@ function renderFileDiff(d: FileDiffDisplay): string {
|
|
|
288
343
|
);
|
|
289
344
|
}
|
|
290
345
|
|
|
291
|
-
/**
|
|
292
|
-
* Build a classic unified-diff body from a file-diff display: a
|
|
293
|
-
* `@@ -oldStart,oldLines +newStart,newLines @@` header per hunk, then each
|
|
294
|
-
* line prefixed with ` `/`+`/`-`. Hunks are separated by a blank line.
|
|
295
|
-
* Returns '' when there are no hunks.
|
|
296
|
-
*/
|
|
297
|
-
function unifiedDiffText(d: FileDiffDisplay): string {
|
|
298
|
-
const blocks: string[] = [];
|
|
299
|
-
for (const hunk of d.hunks) {
|
|
300
|
-
const lines: string[] = [
|
|
301
|
-
`@@ -${hunk.oldStart},${hunk.oldLines} +${hunk.newStart},${hunk.newLines} @@`,
|
|
302
|
-
];
|
|
303
|
-
for (const line of hunk.lines) {
|
|
304
|
-
const marker = line.kind === 'add' ? '+' : line.kind === 'del' ? '-' : ' ';
|
|
305
|
-
lines.push(`${marker}${line.text}`);
|
|
306
|
-
}
|
|
307
|
-
blocks.push(lines.join('\n'));
|
|
308
|
-
}
|
|
309
|
-
return blocks.join('\n\n');
|
|
310
|
-
}
|
|
311
|
-
|
|
312
346
|
function truncJson(value: unknown, max = 60): string {
|
|
313
347
|
const s = typeof value === 'string' ? value : JSON.stringify(value) ?? '';
|
|
314
348
|
if (s.length <= max) return s;
|