mirror of
https://github.com/Ark0N/Codeman.git
synced 2026-10-20 09:09:35 +02:00
fix(copilot): agent read keeps shell prompts and ruled paragraphs inside an answer
A submitted prompt is only a prompt at column 0 or 1, so an indented shell prompt in a code block stays answer text. Only the last rule, composer row, rule triple is the composer, so a paragraph between two rules in an answer is no longer dropped. A test for each.
This commit is contained in:
1 parent
6cca5465a9
commit
e37f1f1483
2 files changed
+64
-4
No files matched your search
@@ -28,6 +28,12 @@ interface PaneDialect {
|
|||||||
status: RegExp[];
|
status: RegExp[];
|
||||||
/** Rows that carry nothing (the empty composer), dropped and treated as a block boundary. */
|
/** Rows that carry nothing (the empty composer), dropped and treated as a block boundary. */
|
||||||
ignore: RegExp[];
|
ignore: RegExp[];
|
||||||
|
/**
|
||||||
|
* The composer row, the one between two rules at the bottom of the screen. Only the LAST rule,
|
||||||
|
* composer row, rule triple counts: an answer can hold a rule-delimited paragraph or a quoted
|
||||||
|
* prompt of its own, and that is answer text.
|
||||||
|
*/
|
||||||
|
composer: RegExp;
|
||||||
/** Furniture drawn at the end of a row (a scrollbar gutter), removed before anything else reads it. */
|
/** Furniture drawn at the end of a row (a scrollbar gutter), removed before anything else reads it. */
|
||||||
lineTrailer?: RegExp;
|
lineTrailer?: RegExp;
|
||||||
/**
|
/**
|
||||||
@@ -44,7 +50,9 @@ const PANE_DIALECTS: Record<string, PaneDialect> = {
|
|||||||
// rules, and a `← open sidebar · … · / commands · ? help` footer. Tool-call rows are not
|
// rules, and a `← open sidebar · … · / commands · ? help` footer. Tool-call rows are not
|
||||||
// modelled: they open with the same bullet and read as part of the answer.
|
// modelled: they open with the same bullet and read as part of the answer.
|
||||||
copilot: {
|
copilot: {
|
||||||
prompt: /^\s*❯\s+(\S.*)$/,
|
// Copilot draws a submitted prompt at column 0 or 1; the continuation rows of an answer are
|
||||||
|
// indented three spaces, so a shell prompt inside a code block (` ❯ npm test`) is not a prompt.
|
||||||
|
prompt: /^ ?❯\s+(\S.*)$/,
|
||||||
promptTrailer: /\s{2,}\d{1,2}:\d{2}\s*$/,
|
promptTrailer: /\s{2,}\d{1,2}:\d{2}\s*$/,
|
||||||
responseBullet: /^\s*●\s+/,
|
responseBullet: /^\s*●\s+/,
|
||||||
status: [
|
status: [
|
||||||
@@ -55,7 +63,8 @@ const PANE_DIALECTS: Record<string, PaneDialect> = {
|
|||||||
/\/ commands · \? help/,
|
/\/ commands · \? help/,
|
||||||
/^\s*[◉◎]\s+Working\b/,
|
/^\s*[◉◎]\s+Working\b/,
|
||||||
],
|
],
|
||||||
ignore: [/^\s*❯\s*$/],
|
ignore: [/^ ?❯\s*$/],
|
||||||
|
composer: /^ ?❯/,
|
||||||
// The right-hand scrollbar gutter, `┃` on a row (or alone on a blank one).
|
// The right-hand scrollbar gutter, `┃` on a row (or alone on a blank one).
|
||||||
lineTrailer: /\s*[┃▐]\s*$/,
|
lineTrailer: /\s*[┃▐]\s*$/,
|
||||||
startup: [/Copilot v\d+\.\d+\.\d+ uses AI/, /Confirm folder trust/, /Do you trust the files in this folder/],
|
startup: [/Copilot v\d+\.\d+\.\d+ uses AI/, /Confirm folder trust/, /Do you trust the files in this folder/],
|
||||||
@@ -339,8 +348,18 @@ function parseDialectTranscript(cleaned: string, dialect: PaneDialect): Response
|
|||||||
const rows = cleaned.split('\n').map((row) => (dialect.lineTrailer ? row.replace(dialect.lineTrailer, '') : row));
|
const rows = cleaned.split('\n').map((row) => (dialect.lineTrailer ? row.replace(dialect.lineTrailer, '') : row));
|
||||||
// The composer sits between two rules and holds whatever is typed but not sent. That is not a
|
// The composer sits between two rules and holds whatever is typed but not sent. That is not a
|
||||||
// prompt row: otherwise typing under a tall answer would make the answer's tail read as banner.
|
// prompt row: otherwise typing under a tall answer would make the answer's tail read as banner.
|
||||||
const isComposerRow = (i: number) =>
|
let composerIndex = -1;
|
||||||
i > 0 && i < rows.length - 1 && isDividerOnlyLine(rows[i - 1] ?? '') && isDividerOnlyLine(rows[i + 1] ?? '');
|
for (let i = rows.length - 2; i > 0; i -= 1) {
|
||||||
|
if (
|
||||||
|
dialect.composer.test(rows[i] ?? '') &&
|
||||||
|
isDividerOnlyLine(rows[i - 1] ?? '') &&
|
||||||
|
isDividerOnlyLine(rows[i + 1] ?? '')
|
||||||
|
) {
|
||||||
|
composerIndex = i;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const isComposerRow = (i: number) => i === composerIndex;
|
||||||
const hasPrompt = rows.some((row, i) => dialect.prompt.test(row) && !isComposerRow(i));
|
const hasPrompt = rows.some((row, i) => dialect.prompt.test(row) && !isComposerRow(i));
|
||||||
const startupVisible = rows.some((row) => dialect.startup.some((re) => re.test(row)));
|
const startupVisible = rows.some((row) => dialect.startup.some((re) => re.test(row)));
|
||||||
let sawPrompt = !hasPrompt && !startupVisible;
|
let sawPrompt = !hasPrompt && !startupVisible;
|
||||||
|
|||||||
@@ -514,6 +514,47 @@ describe('GitHub Copilot pane dialect', () => {
|
|||||||
expect(first.text).not.toContain('┃');
|
expect(first.text).not.toContain('┃');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// The footer and composer every screen below ends with.
|
||||||
|
const tail = [
|
||||||
|
' /tmp/work/.../scratch-2 Session: credits unavailable',
|
||||||
|
rule,
|
||||||
|
'❯',
|
||||||
|
rule,
|
||||||
|
' ← open sidebar · Interactive · Manual Approval · / commands · ? help · tab next tab mock',
|
||||||
|
];
|
||||||
|
|
||||||
|
it("keeps a shell prompt inside an answer's code block as answer text, not a new prompt", () => {
|
||||||
|
const screen = [
|
||||||
|
' ❯ how do I run the tests 11:20',
|
||||||
|
' ● Run this from the repo root:',
|
||||||
|
'',
|
||||||
|
' ❯ npm test',
|
||||||
|
'',
|
||||||
|
' Then read the summary line.',
|
||||||
|
...tail,
|
||||||
|
].join('\n');
|
||||||
|
const blocks = parseExternalCliTranscript(screen, 'copilot');
|
||||||
|
expect(blocks.filter((b) => b.kind === 'prompt').map((b) => b.text)).toEqual(['how do I run the tests']);
|
||||||
|
const answer = getLastTranscriptResponse(blocks);
|
||||||
|
expect(answer).toContain('Run this from the repo root:');
|
||||||
|
expect(answer).toContain('❯ npm test');
|
||||||
|
expect(answer).toContain('Then read the summary line.');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('keeps a paragraph between two rules inside an answer, and treats only the last triple as the composer', () => {
|
||||||
|
const screen = [
|
||||||
|
' ❯ quote the passage 11:21',
|
||||||
|
' ● Here it is:',
|
||||||
|
rule,
|
||||||
|
' Quoted passage between two rules.',
|
||||||
|
rule,
|
||||||
|
...tail,
|
||||||
|
].join('\n');
|
||||||
|
expect(getLastTranscriptResponse(parseExternalCliTranscript(screen, 'copilot'))).toBe(
|
||||||
|
'Quoted passage between two rules.'
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
it('answers nothing for a session that has only drawn its banner or a trust dialog', () => {
|
it('answers nothing for a session that has only drawn its banner or a trust dialog', () => {
|
||||||
const banner = pane.split('\n').slice(0, 9).join('\n');
|
const banner = pane.split('\n').slice(0, 9).join('\n');
|
||||||
expect(getLastTranscriptResponse(parseExternalCliTranscript(banner, 'copilot'))).toBe('');
|
expect(getLastTranscriptResponse(parseExternalCliTranscript(banner, 'copilot'))).toBe('');
|
||||||
|
|||||||
Reference in new issue
Block a user