Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .agents/skills/testing-workflow/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ suites are outside this distribution's verification.
| Sandbox on macOS | `pnpm test:sandbox` |
| Source-sync, workflow and release tools | `pnpm test:release-tools` |
| npm release archive installation | `MCODE_RELEASE_TAG=vX.Y.Z MCODE_RELEASE_ARCHIVE=/path/to/package.tar.gz pnpm verify --profile package` |
| TUI source and test lint | `pnpm lint:tui` |
| Types and standalone build boundary | `pnpm typecheck`, `pnpm build`, `pnpm check:standalone` |
| Published files and generated paths | `pnpm check:source`, `pnpm check:tsconfig` |

Expand Down
129 changes: 129 additions & 0 deletions .eslintrc.cjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,129 @@
// TUI rules adapted from the source repository, without browser/React rules.
// Keep Pi engine style exceptions aligned with the audited fork.
module.exports = {
root: true,
env: {
es2022: true,
node: true,
},
parser: "@typescript-eslint/parser",
parserOptions: {
ecmaVersion: 2022,
sourceType: "module",
},
plugins: ["@typescript-eslint", "import", "prettier"],
extends: [
"airbnb-base",
"plugin:@typescript-eslint/recommended",
"plugin:prettier/recommended",
],
ignorePatterns: [
"dist/",
"node_modules/",
"third_party/",
"packages/tui/test/pi-084-upstream/",
],
rules: {
"prettier/prettier": [
"warn",
{
trailingComma: "all",
tabWidth: 2,
semi: true,
singleQuote: true,
useTabs: false,
bracketSpacing: true,
arrowParens: "always",
jsxSingleQuote: false,
printWidth: 100,
endOfLine: "auto",
embeddedLanguageFormatting: "auto",
proseWrap: "always",
},
],
"import/extensions": "off",
"import/prefer-default-export": "off",
"import/no-extraneous-dependencies": "off",
"import/no-unresolved": "off",
"import/order": "off",
"no-shadow": "off",
"@typescript-eslint/no-shadow": "error",
"no-use-before-define": "off",
"@typescript-eslint/no-use-before-define": "off",
"no-unused-vars": "off",
"@typescript-eslint/no-unused-vars": [
"warn",
{
argsIgnorePattern: "^_",
},
],
"consistent-return": "off",
"no-restricted-syntax": "off",
"class-methods-use-this": "off",
"no-underscore-dangle": "off",
"no-continue": "off",
"no-await-in-loop": "off",
"no-void": "off",
"no-nested-ternary": "off",
"no-plusplus": "off",
"no-bitwise": "off",
"no-cond-assign": "off",
"no-promise-executor-return": "off",
"no-restricted-globals": "off",
"no-duplicate-imports": "off",
"no-multi-assign": "off",
"default-case": "off",
"max-classes-per-file": "off",
radix: "off",
"no-param-reassign": [
"error",
{
props: false,
},
],
"no-control-regex": "off",
"@typescript-eslint/naming-convention": "off",
"@typescript-eslint/no-explicit-any": "warn",
"@typescript-eslint/ban-ts-comment": "off",
"@typescript-eslint/no-non-null-assertion": "warn",
"@typescript-eslint/no-loop-func": "warn",
camelcase: "off",
"lines-between-class-members": "off",
"no-unused-expressions": "off",
"dot-notation": "off",
"no-useless-constructor": "off",
"no-empty-function": "off",
"no-return-await": "off",
"no-throw-literal": "off",
"default-param-last": "off",
"no-loop-func": "off",
eqeqeq: "error",
"no-var": "error",
"prefer-const": "error",
"no-console": [
"warn",
{
allow: ["warn", "error"],
},
],
"no-alert": "warn",
"no-empty": "warn",
"no-unsafe-finally": "warn",
"no-constant-condition": "warn",
"prefer-destructuring": "off",
},
overrides: [
{
files: ["packages/tui/src/tui/engine/**/*.ts"],
excludedFiles: ["packages/tui/src/tui/engine/public.ts"],
rules: {
"@typescript-eslint/no-shadow": "off",
"no-else-return": "off",
"no-lonely-if": "off",
"no-param-reassign": "off",
"one-var": "off",
"prefer-regex-literals": "off",
},
},
],
};
2 changes: 2 additions & 0 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,8 @@ pnpm verify

`pnpm verify` runs the complete gate list in the same order as GitHub CI. Normal PR and main-branch checks use Node.js 24 on Linux and macOS. The Linux job runs the full profile; the macOS job uses `pnpm verify --profile platform`, which omits only the duplicate TypeScript compiler check. Windows runs the focused `pnpm verify --profile windows` contract on PRs; the profile is Windows-only and fails closed elsewhere. It checks source inventory, release tooling, build boundaries, artifacts, and Windows-specific tests without running the full capability suite. Gates that depend on platform behaviour are selected by platform rather than skipped silently; run `pnpm verify --list`, `pnpm verify --profile platform --list`, or `pnpm verify --profile windows --list` to inspect each plan. Individual gates remain available as their own scripts, such as `pnpm typecheck` or `pnpm test:byok`, while you iterate.

`pnpm lint` (or `pnpm lint:tui`) checks TUI source and tests with the source repository's Airbnb, TypeScript, import and Prettier rules. Errors fail verification; formatting, unused variables and explicit `any` remain warnings, matching the source policy. The scripts use `--quiet` to keep existing warning debt out of CI logs; omit it when invoking ESLint directly to inspect warnings. The audited Pi engine retains its narrow style exceptions; its public facade uses the normal rules. Vendored Pi test copies are excluded. Full, platform, archive and Windows verification include this gate; docs and package-installation profiles do not.

CI writes per-gate timing and exit metadata to the Job Summary and a seven-day `verification-<os>-node-<version>-<attempt>` artifact. For a local report, set `MCODE_VERIFY_REPORT_DIR` to a directory outside the repository. Reports distinguish `PASS`, `FAIL`, intentional `SKIP`, and `NOT_RUN` after a failure. JSON is checkpointed before and after each gate; a cancelled run may leave `RUNNING`, which is not a pass. If installation fails before verification starts, no verification report is available. Reports do not collect command output, environment variables, or runtime data; read the corresponding gate's job log for failure details, including the existing bounded BYOK timeout diagnostics. CI jobs have a 15-minute verification limit and a 10-minute release-audit limit.

Existing README files, `CONTRIBUTING.md`, `.github/PULL_REQUEST_TEMPLATE.md`, Markdown under `docs/`, and media directly under `docs/assets/` use the `docs` profile when they are the only changed paths. That profile checks the source inventory and generated paths, exports the committed source, and tests release tooling. History and source-snapshot secret scans still run; platform builds and distribution scans are skipped. Mixed changes, unknown paths, missing comparisons, and any `release/` inventory change get full CI. Documentation-only changes skip the platform matrix entirely. The `verification` aggregate check always runs and rejects failed, cancelled, or unexpectedly skipped jobs. Use it together with `source-history-artifact` as required checks when configuring branch protection; this repository's automation does not change administrative settings.
Expand Down
13 changes: 11 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,9 @@
"test:capabilities": "node scripts/run-vitest-suite.mjs capability",
"test:windows": "node scripts/run-vitest-suite.mjs windows",
"test:artifact": "node --test test/public-artifact.test.mjs",
"test:release-tools": "node --test test/source-sync.test.mjs"
"test:release-tools": "node --test test/source-sync.test.mjs",
"lint": "pnpm run lint:tui",
"lint:tui": "eslint packages/tui/src packages/tui/test --ext .ts,.mjs --quiet"
},
"devDependencies": {
"@types/node": "^20.19.0",
Expand All @@ -40,7 +42,14 @@
"vitest": "4.1.11",
"yaml": "2.9.0",
"tar": "7.5.22",
"vite": "7.3.6"
"vite": "7.3.6",
"eslint": "8.57.1",
"@typescript-eslint/eslint-plugin": "7.18.0",
"@typescript-eslint/parser": "7.18.0",
"eslint-config-airbnb-base": "15.0.0",
"eslint-config-prettier": "10.1.8",
"eslint-plugin-import": "2.32.0",
"eslint-plugin-prettier": "5.5.5"
},
"pnpm": {
"onlyBuiltDependencies": [
Expand Down
4 changes: 2 additions & 2 deletions packages/tui/src/application/assistant-content.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,9 +25,9 @@ export function simplifyAssistantContentForTerminal(content: string): string {
return [presentation.text, formatAssetGroup(presentation.assets)].filter(Boolean).join('\n\n');
}

export function projectAssistantContentForTerminal(content: string): TerminalAssistantContent {
export function projectAssistantContentForTerminal(rawContent: string): TerminalAssistantContent {
// Model content is data. Only the renderer may introduce terminal controls.
content = sanitizeTerminalText(content);
const content = sanitizeTerminalText(rawContent);
if (!content || !RICH_CONTENT_RE.test(content)) return { text: content, assets: [] };

const segments = splitMarkdownProtectedSegments(content);
Expand Down
4 changes: 2 additions & 2 deletions packages/tui/src/observability/incident-reporter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -661,8 +661,8 @@ function minimizeEventLog(value: unknown, eventType: string): string {
// An already minimized unknown thrown value stays unknown after disk replay.
const breadcrumbs = (Array.isArray(log.breadcrumbs) ? log.breadcrumbs : [])
.slice(-MAX_BREADCRUMBS)
.flatMap((value) => {
const crumb = record(value);
.flatMap((breadcrumb) => {
const crumb = record(breadcrumb);
const name = allowedString(crumb.name, BREADCRUMB_NAMES);
const phase = allowedString(record(crumb.details).phase, PHASES);
return name && timestamp(crumb.occurredAtMs)
Expand Down
2 changes: 1 addition & 1 deletion packages/tui/src/tui/commands/plugin-autocomplete.ts
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,7 @@ export class TuiPluginAutocomplete implements AutocompleteProvider {
.map((plugin) => {
const name =
sanitizeTerminalText(plugin.displayName)
.replace(/[\[\]\\]/gu, ' ')
.replace(/[[\]\\]/gu, ' ')
.replace(/\s+/gu, ' ')
.trim()
.slice(0, 256) || plugin.name;
Expand Down
2 changes: 1 addition & 1 deletion packages/tui/test/unit/incident-reporter-privacy.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -307,7 +307,7 @@ describe('TUI automatic incident HTTP privacy boundary', () => {
reporter.capture(input(new Error(privateText)));
await reporter.drain();
expect(requests).toEqual([]);
const name = readdirSync(directory).find((name) => name.startsWith('pending-'))!;
const name = readdirSync(directory).find((filename) => filename.startsWith('pending-'))!;
const path = join(directory, name);
const stored = JSON.parse(readFileSync(path, 'utf8'));
const log = JSON.parse(stored.eventLog);
Expand Down
3 changes: 2 additions & 1 deletion packages/tui/test/unit/tui-image-preview.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { fileURLToPath, pathToFileURL } from 'node:url';
// Test-only packaging infrastructure belongs to the repository root, not a runtime package.
// eslint-disable-next-line import/no-relative-packages
import { packageExportEntries } from '../../../../scripts/lib/package-exports.mjs';
// eslint-disable-next-line import/no-relative-packages -- read the source-only bundle scope
import { readExtraction } from '../../../../scripts/lib/release-metadata.mjs';
// eslint-disable-next-line import/no-relative-packages -- reuse the real bundle module-location contract
import { createTuiBundleModuleLocationConfig } from '../../../../scripts/lib/tui-npm-bundle-profile.mjs';
Expand Down Expand Up @@ -196,7 +197,7 @@ describe('preview file loader', () => {
name: 'preview-test-pi-source',
setup(builder) {
builder.onResolve({ filter: /^@earendil-works\/pi-/ }, ({ path }) => {
const entry = entries.find((entry) => entry.specifier === path);
const entry = entries.find((candidate) => candidate.specifier === path);
if (!entry) return undefined;
return {
path: join(root, entry.file),
Expand Down
11 changes: 5 additions & 6 deletions packages/tui/test/unit/tui-plugin-mentions.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { ConversationApplication } from '../../../local-runtime-v2/src/applicati
import { DirectSendDeliveryService } from '../../../local-runtime-v2/src/application/conversation/direct-send-delivery.js';
import { TuiUserProjection } from '../../src/tui/controller/projection/turn-user-projection.js';
import { TranscriptStore } from '../../src/tui/transcript/store.js';
import { visibleWidth } from '../../src/tui/engine/public.js';
import { visibleWidth, stripTerminalSequences } from '../../src/tui/engine/public.js';
import { TuiInputFlow } from '../../src/tui/controller/interaction/input-flow.js';
import { TuiExternalEditorFlow } from '../../src/tui/controller/interaction/external-editor-flow.js';
import { TuiSessionMutationFlow } from '../../src/tui/controller/product/session-mutation-flow.js';
Expand Down Expand Up @@ -31,7 +31,6 @@ import {
} from '../../src/tui/widgets/editor/editor.js';
import { TuiDraftRecovery } from '../../src/tui/features/composer/draft-recovery.js';
import { createTuiSubmissionSnapshot } from '../../src/tui/features/composer/submission.js';
import { stripTerminalSequences } from '../../src/tui/engine/public.js';

const identity = (value: string) => value;
const makePlugin = (name = 'notes', marketplace: 'local' | 'official' = 'local') => ({
Expand Down Expand Up @@ -525,14 +524,14 @@ describe('Plugin mention review regressions', () => {
workspaceDir: '/workspace',
configuredCommand: 'synthetic-editor',
editDraft: async ({ draft }) =>
rename ? draft.replace('@My Notes', '@Other Notes') : draft + ' tomorrow',
rename ? draft.replace('@My Notes', '@Other Notes') : `${draft} tomorrow`,
isAppStopped: () => false,
append: vi.fn(),
setHint: vi.fn(),
onChanged: vi.fn(),
});
await flow.open();
expect(submittedEditorTransport(editor.captureDraft())).toBe(transport + ' tomorrow');
expect(submittedEditorTransport(editor.captureDraft())).toBe(`${transport} tomorrow`);
editor.handleInput('\x1f');
expect(editor.captureDraft()).toEqual(before);
rename = true;
Expand All @@ -550,7 +549,7 @@ describe('Plugin mention review regressions', () => {
tui: { start: vi.fn(), stop: vi.fn(), requestRender: vi.fn() },
workspaceDir: '/workspace',
configuredCommand: 'synthetic-editor',
editDraft: async ({ draft }) => 'Please ' + draft + ' tomorrow',
editDraft: async ({ draft }) => `Please ${draft} tomorrow`,
isAppStopped: () => false,
append: vi.fn(),
setHint: vi.fn(),
Expand Down Expand Up @@ -670,7 +669,7 @@ describe('Plugin mention review regressions', () => {
editor.handleInput(' tomorrow');
await flow.submitEdit(editor.getText(), [], editor.captureDraft());
expect(editSessionMessage).toHaveBeenCalledWith(
expect.objectContaining({ content: canonical + ' tomorrow' }),
expect.objectContaining({ content: `${canonical} tomorrow` }),
);
},
);
Expand Down
2 changes: 1 addition & 1 deletion packages/tui/test/unit/tui-scrollbar-interaction.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -187,7 +187,7 @@ describe("Scrollbar interaction boundaries", () => {
render: (width: number) =>
Array.from(
{ length: CONTENT_LINES },
() => "x".repeat(width - 2) + "YZ",
() => `${"x".repeat(width - 2)}YZ`,
),
invalidate() {},
};
Expand Down
Loading
Loading