diff --git a/packages/host/app/tools/patch-code.ts b/packages/host/app/tools/patch-code.ts
index 8f4583064a9..0f0b299225d 100644
--- a/packages/host/app/tools/patch-code.ts
+++ b/packages/host/app/tools/patch-code.ts
@@ -66,32 +66,52 @@ export default class PatchCodeTool extends HostBaseTool<
lintIssues = lintResult.lintIssues ?? [];
}
- finalFileIdentifier = await this.determineFinalFileUrl(
- fileUrl,
- fileInfo,
- hasEmptySearchPortion,
- );
+ // An applied patch always changed the pre-lint content, so ending up
+ // back at the original file means the autofix/format pass reverted
+ // the whole edit. Re-patching can never make progress from here —
+ // any further attempt round-trips to this same content — so say so
+ // alongside the lint issues instead of writing a no-op save that
+ // would keep the model trying.
+ let revertedByFormatter =
+ sourceContent !== '' && patchedCode === sourceContent;
+ if (revertedByFormatter) {
+ lintIssues = [
+ ...lintIssues,
+ 'The automatic formatter reverted the applied changes, so the file is unchanged. Reformatting-only patches cannot fix the remaining issues; change the content itself or leave it as is.',
+ ];
+ } else {
+ finalFileIdentifier = await this.determineFinalFileUrl(
+ fileUrl,
+ fileInfo,
+ hasEmptySearchPortion,
+ );
- let clientRequestId = this.toolService.trackAiAssistantCardRequest({
- action: 'patch-code',
- roomId,
- fileUrl: finalFileIdentifier,
- });
+ let clientRequestId = this.toolService.trackAiAssistantCardRequest({
+ action: 'patch-code',
+ roomId,
+ fileUrl: finalFileIdentifier,
+ });
- let savedThroughOpenFile = await this.trySaveThroughOpenFile(
- finalFileIdentifier,
- patchedCode,
- clientRequestId,
- );
- if (!savedThroughOpenFile) {
- this.cardService
- .saveSource(new URL(finalFileIdentifier), patchedCode, 'bot-patch', {
- resetLoader: hasExecutableExtension(finalFileIdentifier),
- clientRequestId,
- })
- .catch((error: unknown) => {
- console.error('PatchCodeTool: failed to save source', error);
- });
+ let savedThroughOpenFile = await this.trySaveThroughOpenFile(
+ finalFileIdentifier,
+ patchedCode,
+ clientRequestId,
+ );
+ if (!savedThroughOpenFile) {
+ this.cardService
+ .saveSource(
+ new URL(finalFileIdentifier),
+ patchedCode,
+ 'bot-patch',
+ {
+ resetLoader: hasExecutableExtension(finalFileIdentifier),
+ clientRequestId,
+ },
+ )
+ .catch((error: unknown) => {
+ console.error('PatchCodeTool: failed to save source', error);
+ });
+ }
}
}
diff --git a/packages/realm-server/tests/realm-endpoints/lint-test.ts b/packages/realm-server/tests/realm-endpoints/lint-test.ts
index 93ad5e08ebc..93a85f8cd9b 100644
--- a/packages/realm-server/tests/realm-endpoints/lint-test.ts
+++ b/packages/realm-server/tests/realm-endpoints/lint-test.ts
@@ -621,6 +621,46 @@ export class MyCard extends CardDef {
);
});
+ test('does not flag the whitespace prettier introduces by wrapping long text', async function (assert) {
+ // Prettier wraps text nodes longer than its print width across
+ // indented lines; no-whitespace-for-layout flags exactly that wrap and
+ // has no autofix, so reporting it would hand back an issue no edit can
+ // clear — the formatter reverts every attempted whitespace fix.
+ let longText =
+ 'WELCOME TO MY HOMEPAGE HOT SITE AWARD WINNER BEST VIEWED IN 800x600 ';
+ let response = await request
+ .post('/_lint')
+ .set(
+ 'Authorization',
+ `Bearer ${createJWT(testRealm, 'john', ['read', 'write'])}`,
+ )
+ .set('X-HTTP-Method-Override', 'QUERY')
+ .set('Accept', 'application/json')
+ .send(`import { CardDef } from '@cardstack/base/card-api';
+export class MyCard extends CardDef {
+}
+
+
+ ${longText}
+
+
+`);
+
+ assert.strictEqual(response.status, 200, 'HTTP 200 status');
+ let responseJson = JSON.parse(response.text);
+ assert.ok(
+ /[^<]*\n[^<]*<\/span>/.test(responseJson.output),
+ 'prettier wrapped the long text node across lines',
+ );
+ let whitespaceMessage = responseJson.messages.find(
+ (m: any) => m.ruleId === 'no-whitespace-for-layout',
+ );
+ assert.notOk(
+ whitespaceMessage,
+ 'no-whitespace-for-layout is not reported for formatter-introduced wrapping',
+ );
+ });
+
test('handles nested template structures correctly', async function (assert) {
let response = await request
.post('/_lint')
diff --git a/packages/runtime-common/tasks/lint.ts b/packages/runtime-common/tasks/lint.ts
index 4505ed47460..21abc922c1a 100644
--- a/packages/runtime-common/tasks/lint.ts
+++ b/packages/runtime-common/tasks/lint.ts
@@ -155,7 +155,24 @@ async function initTemplateLinter(): Promise {
const hostRequire = Module.createRequire(resolve(HOST_PKG, 'package.json'));
const tlModule = hostRequire('ember-template-lint');
const TemplateLinter = (tlModule as any).default ?? tlModule;
- return new TemplateLinter({ workingDir: HOST_PKG });
+ // Host's config, minus rules the rest of this pipeline contradicts.
+ // Prettier (the pass right before template-lint) wraps text nodes longer
+ // than its print width across indented lines; no-whitespace-for-layout
+ // flags exactly that wrap and has no autofix, so for any over-width text
+ // node the pipeline would report an error that no edit can clear — the
+ // formatter reverts every attempted whitespace fix. The wrapped whitespace
+ // collapses at render, so nothing is lost by not flagging it.
+ const hostTemplateLintConfig = hostRequire('./.template-lintrc.js');
+ return new TemplateLinter({
+ workingDir: HOST_PKG,
+ config: {
+ ...hostTemplateLintConfig,
+ rules: {
+ ...hostTemplateLintConfig.rules,
+ 'no-whitespace-for-layout': false,
+ },
+ },
+ });
}
// ---------------------------------------------------------------------------