Skip to content

Commit 124fc48

Browse files
Merge pull request #727 from gugu91/fix/pinet-compact-output-default
fix(pinet): default dispatcher output to compact CLI
2 parents 4524e2c + 6492749 commit 124fc48

2 files changed

Lines changed: 221 additions & 11 deletions

File tree

slack-bridge/pinet-tools.test.ts

Lines changed: 138 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -180,6 +180,7 @@ describe("registerPinetTools", () => {
180180
expect(pinet?.promptSnippet).toContain("lanes, ports, reload");
181181
expect(pinet?.promptSnippet).not.toContain("skin");
182182
expect(pinet?.promptSnippet).toContain('args.format="json"');
183+
expect(pinet?.promptSnippet).toContain("fill context quickly");
183184
expect(JSON.stringify(pinet?.parameters)).toContain(
184185
"help, send, read, free, schedule, agents, lanes, ports, reload, or exit",
185186
);
@@ -214,14 +215,19 @@ describe("registerPinetTools", () => {
214215
});
215216
});
216217

217-
it("rejects the removed dispatcher skin action", async () => {
218+
it("rejects the removed dispatcher skin action with compact CLI text by default", async () => {
218219
const tools = registerWithDeps(createDeps());
219220

220221
const result = (await tools.get("pinet")?.execute("tool-call-1", {
221222
action: "skin",
222223
args: { theme: "foundation" },
223-
})) as { details: { status: string; errors: Array<{ message: string }> } };
224+
})) as {
225+
content: Array<{ text: string }>;
226+
details: { status: string; errors: Array<{ message: string }> };
227+
};
224228

229+
expect(result.content[0]?.text).toContain("Pinet failed: Unknown Pinet action: skin");
230+
expect(result.content[0]?.text).not.toContain('"errors"');
225231
expect(result.details.status).toBe("failed");
226232
expect(result.details.errors[0]?.message).toBe("Unknown Pinet action: skin");
227233
});
@@ -501,7 +507,7 @@ describe("registerPinetTools", () => {
501507
expect(envelope.data.full_details).toBeUndefined();
502508
});
503509

504-
it("routes action-dispatched help through the dispatcher", async () => {
510+
it("routes action-dispatched help through the dispatcher with compact CLI text by default", async () => {
505511
const tools = registerWithDeps(createDeps());
506512

507513
const result = (await tools.get("pinet")?.execute("tool-call-dispatch-help", {
@@ -517,6 +523,9 @@ describe("registerPinetTools", () => {
517523
};
518524
};
519525

526+
expect(result.content[0]?.text).toContain("Pinet actions:");
527+
expect(result.content[0]?.text).toContain("send");
528+
expect(result.content[0]?.text).not.toContain('"args_schema"');
520529
expect(result.details.status).toBe("succeeded");
521530
expect(result.details.data.note).toContain("Use args.topic");
522531
expect(result.details.data.actions).toEqual(
@@ -532,6 +541,59 @@ describe("registerPinetTools", () => {
532541
);
533542
});
534543

544+
it("preserves explicit JSON output for action-dispatched help", async () => {
545+
const tools = registerWithDeps(createDeps());
546+
547+
const result = (await tools.get("pinet")?.execute("tool-call-dispatch-help-json", {
548+
action: "help",
549+
args: { format: "json" },
550+
})) as { content: Array<{ text: string }> };
551+
552+
expect(result.content[0]?.text).toContain('"status": "succeeded"');
553+
expect(result.content[0]?.text).toContain('"args_schema"');
554+
});
555+
556+
it("preserves explicit full output for action-dispatched help", async () => {
557+
const tools = registerWithDeps(createDeps());
558+
559+
const result = (await tools.get("pinet")?.execute("tool-call-dispatch-help-full", {
560+
action: "help",
561+
args: { full: true },
562+
})) as { content: Array<{ text: string }> };
563+
564+
expect(result.content[0]?.text).toContain('"status": "succeeded"');
565+
expect(result.content[0]?.text).toContain('"args_schema"');
566+
});
567+
568+
it("preserves valid structured help output flags when a sibling output flag is invalid", async () => {
569+
const tools = registerWithDeps(createDeps());
570+
571+
const jsonResult = (await tools.get("pinet")?.execute("tool-call-help-json-invalid-full", {
572+
action: "help",
573+
args: { format: "json", full: "true" },
574+
})) as { content: Array<{ text: string }> };
575+
const fullResult = (await tools.get("pinet")?.execute("tool-call-help-full-invalid-format", {
576+
action: "help",
577+
args: { format: "yaml", full: true },
578+
})) as { content: Array<{ text: string }> };
579+
580+
expect(jsonResult.content[0]?.text).toContain('"status": "failed"');
581+
expect(jsonResult.content[0]?.text).toContain('"full must be a boolean when provided."');
582+
expect(fullResult.content[0]?.text).toContain('"status": "failed"');
583+
expect(fullResult.content[0]?.text).toContain('"format must be');
584+
});
585+
586+
it("warns compact help topic callers that JSON/full output can fill context quickly", async () => {
587+
const tools = registerWithDeps(createDeps());
588+
589+
const result = (await tools.get("pinet")?.execute("tool-call-dispatch-help-topic", {
590+
action: "help",
591+
args: { topic: "read" },
592+
})) as { content: Array<{ text: string }> };
593+
594+
expect(result.content[0]?.text).toContain("JSON/full output can fill context quickly");
595+
});
596+
535597
it("routes action-dispatched port lease acquire", async () => {
536598
const acquirePortLease = vi.fn(createDeps().acquirePortLease);
537599
const tools = registerWithDeps(createDeps({ acquirePortLease }));
@@ -585,6 +647,79 @@ describe("registerPinetTools", () => {
585647
expect(result.details.errors[0]?.message).toContain("Unknown Pinet action: pinet_send");
586648
});
587649

650+
it("preserves explicit JSON output for dispatcher errors", async () => {
651+
const tools = registerWithDeps(createDeps());
652+
653+
const result = (await tools.get("pinet")?.execute("tool-call-error-json", {
654+
action: "skin",
655+
args: { theme: "foundation", format: "json" },
656+
})) as { content: Array<{ text: string }> };
657+
658+
expect(result.content[0]?.text).toContain('"status": "failed"');
659+
expect(result.content[0]?.text).toContain('"errors"');
660+
});
661+
662+
it("preserves explicit full output for dispatcher errors", async () => {
663+
const tools = registerWithDeps(createDeps());
664+
665+
const result = (await tools.get("pinet")?.execute("tool-call-error-full", {
666+
action: "skin",
667+
args: { theme: "foundation", full: true },
668+
})) as { content: Array<{ text: string }> };
669+
670+
expect(result.content[0]?.text).toContain('"status": "failed"');
671+
expect(result.content[0]?.text).toContain('"errors"');
672+
});
673+
674+
it("preserves explicit full output for action runtime errors", async () => {
675+
const tools = registerWithDeps(createDeps());
676+
677+
const result = (await tools.get("pinet")?.execute("tool-call-runtime-error-full", {
678+
action: "send",
679+
args: { message: "dispatch now", full: true },
680+
})) as { content: Array<{ text: string }> };
681+
682+
expect(result.content[0]?.text).toContain('"status": "failed"');
683+
expect(result.content[0]?.text).toContain('"to is required"');
684+
});
685+
686+
it("preserves valid structured action output flags when a sibling output flag is invalid", async () => {
687+
const tools = registerWithDeps(createDeps());
688+
689+
const jsonResult = (await tools.get("pinet")?.execute("tool-call-send-json-invalid-full", {
690+
action: "send",
691+
args: { to: "alpha", message: "dispatch now", format: "json", full: "true" },
692+
})) as { content: Array<{ text: string }> };
693+
const fullResult = (await tools.get("pinet")?.execute("tool-call-send-full-invalid-format", {
694+
action: "send",
695+
args: { to: "alpha", message: "dispatch now", format: "yaml", full: true },
696+
})) as { content: Array<{ text: string }> };
697+
698+
expect(jsonResult.content[0]?.text).toContain('"status": "failed"');
699+
expect(jsonResult.content[0]?.text).toContain('"full must be a boolean when provided."');
700+
expect(fullResult.content[0]?.text).toContain('"status": "failed"');
701+
expect(fullResult.content[0]?.text).toContain('"format must be');
702+
});
703+
704+
it("reports invalid output options as compact CLI text by default", async () => {
705+
const tools = registerWithDeps(createDeps());
706+
707+
const result = (await tools.get("pinet")?.execute("tool-call-invalid-output", {
708+
action: "send",
709+
args: { to: "alpha", message: "dispatch now", full: "true" },
710+
})) as {
711+
content: Array<{ text: string }>;
712+
details: { status: string; errors: Array<{ message: string }> };
713+
};
714+
715+
expect(result.content[0]?.text).toContain(
716+
"Pinet failed: full must be a boolean when provided.",
717+
);
718+
expect(result.content[0]?.text).not.toContain('"errors"');
719+
expect(result.details.status).toBe("failed");
720+
expect(result.details.errors[0]?.message).toBe("full must be a boolean when provided.");
721+
});
722+
588723
it("routes action-dispatched pinet send", async () => {
589724
const sendPinetAgentMessage = vi.fn(async (_to: string, _message: string) => ({
590725
messageId: 41,

slack-bridge/pinet-tools.ts

Lines changed: 83 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -313,17 +313,87 @@ function buildPinetDispatcherEnvelope(
313313
return { status, data, errors, warnings };
314314
}
315315

316+
function formatPinetHelpCliText(data: Record<string, unknown>): string | null {
317+
const actions = Array.isArray(data.actions)
318+
? data.actions
319+
.map((action) =>
320+
isRecord(action) && typeof action.action === "string" ? action.action : null,
321+
)
322+
.filter((action): action is string => Boolean(action))
323+
: [];
324+
if (actions.length > 0) {
325+
const note = typeof data.note === "string" && data.note.trim() ? ` ${data.note.trim()}` : "";
326+
return `Pinet actions: ${actions.join(", ")}.${note}`;
327+
}
328+
329+
if (typeof data.action === "string") {
330+
const description =
331+
typeof data.description === "string" && data.description.trim()
332+
? ` — ${data.description.trim()}`
333+
: "";
334+
return `Pinet ${data.action}${description}. Use args.format="json" or args.full=true for schema/details; JSON/full output can fill context quickly.`;
335+
}
336+
337+
return null;
338+
}
339+
316340
function getPinetEnvelopeCliText(envelope: PinetDispatcherEnvelope): string {
317341
if (envelope.status === "succeeded" && isRecord(envelope.data)) {
318342
const text = envelope.data.text;
319343
if (typeof text === "string" && text.length > 0) return text;
344+
345+
const helpText = formatPinetHelpCliText(envelope.data);
346+
if (helpText) return helpText;
347+
348+
const action = typeof envelope.data.action === "string" ? ` ${envelope.data.action}` : "";
349+
return `Pinet${action} succeeded. Use args.format="json" or args.full=true for details; JSON/full output can fill context quickly.`;
350+
}
351+
352+
const errors = envelope.errors.map((error) => error.message).filter(Boolean);
353+
const hints = Array.from(
354+
new Set(envelope.errors.map((error) => error.hint).filter((hint) => hint.length > 0)),
355+
);
356+
if (errors.length > 0) {
357+
return `Pinet ${envelope.status}: ${errors.join("; ")}${hints.length > 0 ? ` Hint: ${hints.join(" ")}` : ""}`;
320358
}
321-
return JSON.stringify(envelope, null, 2);
359+
360+
return `Pinet ${envelope.status}. Use args.format="json" or args.full=true for details; JSON/full output can fill context quickly.`;
361+
}
362+
363+
function getTolerantPinetOutputOptions(value: unknown): PinetOutputOptions {
364+
if (!isRecord(value)) return { format: "cli", full: false };
365+
366+
const rawFormat = value.format ?? value.f ?? value["-f"];
367+
const normalizedFormat = rawFormat == null ? "cli" : String(rawFormat).trim().toLowerCase();
368+
const format = normalizedFormat === "json" ? "json" : "cli";
369+
const rawFull = value.full ?? value["--full"];
370+
return { format, full: rawFull === true };
371+
}
372+
373+
function getOptionalPinetOutputOptions(value: unknown): PinetOutputOptions {
374+
if (!isRecord(value)) return { format: "cli", full: false };
375+
try {
376+
return normalizePinetOutputOptions(value);
377+
} catch {
378+
return getTolerantPinetOutputOptions(value);
379+
}
380+
}
381+
382+
function shouldRenderStructuredPinetEnvelope(
383+
envelope: PinetDispatcherEnvelope,
384+
output: PinetOutputOptions,
385+
): boolean {
386+
if (output.format === "json") return true;
387+
if (!output.full) return false;
388+
if (envelope.status === "failed") return true;
389+
if (!isRecord(envelope.data)) return true;
390+
const text = envelope.data.text;
391+
return typeof text !== "string" || text.length === 0;
322392
}
323393

324394
function wrapDispatcherEnvelope(
325395
envelope: PinetDispatcherEnvelope,
326-
output: PinetOutputOptions = { format: "json", full: true },
396+
output: PinetOutputOptions = { format: "cli", full: false },
327397
): {
328398
content: Array<{ type: "text"; text: string }>;
329399
details: PinetDispatcherEnvelope;
@@ -332,10 +402,9 @@ function wrapDispatcherEnvelope(
332402
content: [
333403
{
334404
type: "text",
335-
text:
336-
output.format === "json"
337-
? JSON.stringify(envelope, null, 2)
338-
: getPinetEnvelopeCliText(envelope),
405+
text: shouldRenderStructuredPinetEnvelope(envelope, output)
406+
? JSON.stringify(envelope, null, 2)
407+
: getPinetEnvelopeCliText(envelope),
339408
},
340409
],
341410
details: envelope,
@@ -1381,7 +1450,7 @@ export function registerPinetTools(pi: ExtensionAPI, deps: RegisterPinetToolsDep
13811450
label: "Pinet Dispatcher",
13821451
description: "Dispatch Pinet operations by action with compact help and schema discovery.",
13831452
promptSnippet:
1384-
'Use this compact dispatcher for Pinet actions: send, read, free, schedule, agents, lanes, ports, reload, exit, and help. Use /pinet start, /pinet follow, and /pinet unfollow for TUI lifecycle changes. Defaults to terse CLI text; pass args.format="json" or args.full=true for explicit detail.',
1453+
'Use this compact dispatcher for Pinet actions: send, read, free, schedule, agents, lanes, ports, reload, exit, and help. Use /pinet start, /pinet follow, and /pinet unfollow for TUI lifecycle changes. Defaults to terse CLI text; pass args.format="json" or args.full=true for explicit detail, but avoid JSON/full unless needed because it can fill context quickly.',
13851454
parameters: Type.Object({
13861455
action: Type.String({
13871456
description:
@@ -1390,7 +1459,7 @@ export function registerPinetTools(pi: ExtensionAPI, deps: RegisterPinetToolsDep
13901459
args: Type.Optional(
13911460
Type.Record(Type.String(), Type.Unknown(), {
13921461
description:
1393-
'Action arguments. Add format="cli"|"json" (or f/"-f") and full=true (or "--full": true) for explicit presentation control. Default cli keeps data.details compact; format="json" or full=true exposes full structured details.',
1462+
'Action arguments. Add format="cli"|"json" (or f/"-f") and full=true (or "--full": true) for explicit presentation control. Default cli keeps data.details compact; format="json" or full=true exposes full structured details and can fill context quickly, so use it only when needed.',
13941463
}),
13951464
),
13961465
}),
@@ -1408,6 +1477,7 @@ export function registerPinetTools(pi: ExtensionAPI, deps: RegisterPinetToolsDep
14081477
hint: 'Use action="help" to inspect supported actions.',
14091478
},
14101479
]),
1480+
getOptionalPinetOutputOptions(params.args),
14111481
);
14121482
}
14131483

@@ -1426,6 +1496,7 @@ export function registerPinetTools(pi: ExtensionAPI, deps: RegisterPinetToolsDep
14261496
hint: 'Use format="cli" or format="json" and full as a boolean.',
14271497
},
14281498
]),
1499+
getTolerantPinetOutputOptions(args),
14291500
);
14301501
}
14311502
return wrapDispatcherEnvelope(
@@ -1445,6 +1516,7 @@ export function registerPinetTools(pi: ExtensionAPI, deps: RegisterPinetToolsDep
14451516
hint: 'Use action="help" to inspect supported actions.',
14461517
},
14471518
]),
1519+
getOptionalPinetOutputOptions(params.args),
14481520
);
14491521
}
14501522

@@ -1458,6 +1530,7 @@ export function registerPinetTools(pi: ExtensionAPI, deps: RegisterPinetToolsDep
14581530
hint: "Pass a JSON object as args.",
14591531
},
14601532
]),
1533+
getOptionalPinetOutputOptions(params.args),
14611534
);
14621535
}
14631536

@@ -1474,6 +1547,7 @@ export function registerPinetTools(pi: ExtensionAPI, deps: RegisterPinetToolsDep
14741547
hint: 'Use format="cli" or format="json" and full as a boolean.',
14751548
},
14761549
]),
1550+
getTolerantPinetOutputOptions(params.args),
14771551
);
14781552
}
14791553

@@ -1494,6 +1568,7 @@ export function registerPinetTools(pi: ExtensionAPI, deps: RegisterPinetToolsDep
14941568
const message = getErrorMessage(error);
14951569
return wrapDispatcherEnvelope(
14961570
buildPinetDispatcherEnvelope("failed", null, [classifyPinetError(message)]),
1571+
output,
14971572
);
14981573
}
14991574
},

0 commit comments

Comments
 (0)