mirror of
https://github.com/nexu-io/open-design.git
synced 2026-09-20 06:15:06 +08:00
3a21392c46
* Keep chat typing and web CSS resolution responsive Cache the normalized mention index by entity array and scan only concrete @ positions so the composer does not rescan every file/plugin/skill token on each draft update. Link the shared components package at the root importer so Turbopack/PostCSS can resolve the exported component stylesheet from the web CSS entrypoint. Constraint: Chat composer typing updates run on every keystroke in Electron, and tools-dev/Next evaluates global CSS from the workspace root. Rejected: Debouncing textarea state updates | would make text entry feel stale and would not remove the expensive synchronous parser path. Rejected: Relative-importing package source CSS from apps/web | would bypass the package export contract and hardcode package layout into the app stylesheet. Confidence: high Scope-risk: narrow Directive: Keep inline mention parsing proportional to draft mention markers, and keep package CSS imports resolvable through workspace package links. Tested: pnpm install Tested: pnpm --filter @open-design/web test -- tests/utils/inlineMentions.test.ts Tested: pnpm --filter @open-design/web typecheck Tested: pnpm --filter @open-design/web build Tested: pnpm guard Not-tested: Full web Vitest remains blocked by unrelated CSS selector tests observed before this commit. * Keep composer typing off the render path Move keystroke-time draft updates into the textarea/ref path while deferring React draft rendering, mention overlay work, picker filtering, and localStorage persistence so Electron input echo stays within frame budget. Constraint: Electron chat input was visibly lagging for plain text and mention text while the right iframe stayed responsive. Rejected: Keep the textarea fully controlled | every keystroke still synchronizes the largest ChatComposer render path. Confidence: high Scope-risk: moderate Directive: Keep prompt submission, slash commands, and mention insertion reading draftRef when draft rendering is deferred. Tested: pnpm guard; pnpm --filter @open-design/web typecheck; pnpm --filter @open-design/web build; pnpm exec vitest run -c vitest.config.ts tests/components/ChatComposer.infinite-render.test.tsx; pnpm exec vitest run -c vitest.config.ts tests/components/ChatComposer.context-pickers.test.tsx; pnpm exec vitest run -c vitest.config.ts tests/utils/inlineMentions.test.ts; Electron inspect eval plain text avg 0.03ms max 0.5ms; Electron inspect eval @ path avg 0.03ms max 0.1ms; HTTP smoke status 200 with no components CSS resolve error. Not-tested: Full web Vitest suite and Playwright UI suite. * Keep Electron chat input responsive under heavy projects Constraint: Electron project views can carry large chat/file DOM and Chromium can throttle or defer visible renderer work under tab/window visibility edge cases. Rejected: Re-rendering the composer draft on every plain keystroke | it couples textarea input to mention overlay, resize, storage, and project layout work. Confidence: medium Scope-risk: moderate Directive: Keep plain text input on the textarea/ref path; only commit React draft state immediately for mention, slash, chip, or imperative draft mutations. Tested: pnpm guard; pnpm --filter @open-design/web typecheck; pnpm exec vitest run -c vitest.config.ts tests/components/ChatComposer.infinite-render.test.tsx; pnpm --filter @open-design/desktop test; pnpm --filter @open-design/desktop typecheck; desktop inspect actual project composer with OS SendKeys. Not-tested: Push/PR remains blocked by GitHub 403 for mimicryluden on nexu-io/open-design. * Keep heavy project typing isolated from preview layout Constraint: Electron project panes can carry large DOM histories and preview frames that make textarea paint sensitive to parent layout. Rejected: Further debouncing ChatComposer input state | app-side handler costs were already low while native textarea updates still lagged. Confidence: high Scope-risk: moderate Directive: Keep the composer mounted through the fixed layer/slot pair so future layout changes do not pull typing back into the split-pane flex reflow path. Tested: pnpm --filter @open-design/web typecheck; pnpm exec vitest run -c vitest.config.ts tests/components/ChatComposer.infinite-render.test.tsx tests/components/ChatComposer.context-pickers.test.tsx; pnpm guard; pnpm --filter @open-design/web build; desktop inspect fixed-layer DOM and SendKeys probe avg beforeinput->input 1.4ms max 2.5ms Not-tested: PR publish blocked by GitHub repository permissions if remote still rejects mimicryluden * Keep stale run recovery from hiding generated previews Constraint: Completed daemon runs can outlive the in-memory run registry after a restart. Rejected: Replaying succeeded rows with empty producedFiles | It turns completed messages into failed messages when the registry is gone. Confidence: high Scope-risk: narrow Directive: Only active run statuses should enter daemon reattach recovery. Tested: vitest generation-preview; vitest ProjectView reattach restore; web typecheck; pnpm guard; web build; desktop DOM eval failureText=false iframeCount=2 Not-tested: Fresh provider generation against an external model * Keep composer contracts aligned with latest main Rebase conflict resolution restored the current composer contract while leaving the lower-risk latency fixes in separate layers. Constraint: The branch was rebased over newer workspace/session context composer contracts. Rejected: Keeping the older composer conflict side because it removed current ChatComposer props and broke web typecheck. Confidence: high Scope-risk: narrow Directive: Preserve upstream ChatComposer contracts when rebasing input-latency work. Tested: web typecheck; targeted ChatComposer, inlineMentions, ProjectView generation-preview tests; desktop window-chrome test Not-tested: Full browser interaction after fork push * Fix daemon Nix workspace symlink pruning --------- Co-authored-by: Siri-Ray <2667192167@qq.com>
112 lines
3.1 KiB
TypeScript
112 lines
3.1 KiB
TypeScript
import { describe, expect, it } from 'vitest';
|
|
|
|
import { buildInlineMentionParts, type InlineMentionEntity } from '../../src/utils/inlineMentions';
|
|
|
|
describe('buildInlineMentionParts', () => {
|
|
it('skips entity matching when plain text has no mention marker', () => {
|
|
const entities: InlineMentionEntity[] = Array.from({ length: 1_000 }, (_, index) => ({
|
|
id: `file-${index}`,
|
|
kind: 'file',
|
|
label: `file-${index}.html`,
|
|
token: `@file-${index}.html`,
|
|
}));
|
|
|
|
expect(buildInlineMentionParts('typing ordinary Chinese text without mentions', entities)).toBeNull();
|
|
});
|
|
|
|
it('does not normalize entities on plain text drafts', () => {
|
|
const entity = {
|
|
id: 'index.html',
|
|
kind: 'file',
|
|
label: 'index.html',
|
|
get token() {
|
|
throw new Error('token should not be read for plain text');
|
|
},
|
|
} as InlineMentionEntity;
|
|
|
|
expect(buildInlineMentionParts('plain text only', [entity])).toBeNull();
|
|
});
|
|
|
|
it('still highlights known mentions when the draft contains a marker', () => {
|
|
const parts = buildInlineMentionParts('Review @index.html', [
|
|
{ id: 'index.html', kind: 'file', label: 'index.html' },
|
|
]);
|
|
|
|
expect(parts).toEqual([
|
|
{ kind: 'text', text: 'Review ' },
|
|
{
|
|
kind: 'mention',
|
|
text: '@index.html',
|
|
entity: {
|
|
id: 'index.html',
|
|
kind: 'file',
|
|
label: 'index.html',
|
|
token: '@index.html',
|
|
},
|
|
},
|
|
]);
|
|
});
|
|
|
|
it('reuses the normalized mention index across draft updates', () => {
|
|
let tokenReads = 0;
|
|
const entities: InlineMentionEntity[] = Array.from({ length: 1_000 }, (_, index) => ({
|
|
id: `file-${index}`,
|
|
kind: 'file',
|
|
label: `file-${index}.html`,
|
|
get token() {
|
|
tokenReads += 1;
|
|
return `@file-${index}.html`;
|
|
},
|
|
}));
|
|
|
|
expect(buildInlineMentionParts('@missing-one', entities)).toEqual([
|
|
{
|
|
kind: 'mention',
|
|
text: '@missing-one',
|
|
entity: {
|
|
id: 'unknown:@missing-one',
|
|
kind: 'unknown',
|
|
label: 'missing-one',
|
|
token: '@missing-one',
|
|
title: '@missing-one',
|
|
},
|
|
},
|
|
]);
|
|
expect(buildInlineMentionParts('@missing-two', entities)).toEqual([
|
|
{
|
|
kind: 'mention',
|
|
text: '@missing-two',
|
|
entity: {
|
|
id: 'unknown:@missing-two',
|
|
kind: 'unknown',
|
|
label: 'missing-two',
|
|
token: '@missing-two',
|
|
title: '@missing-two',
|
|
},
|
|
},
|
|
]);
|
|
expect(tokenReads).toBe(entities.length);
|
|
});
|
|
|
|
it('preserves longest known mentions that contain spaces', () => {
|
|
const parts = buildInlineMentionParts('Open @docs/read me.md now', [
|
|
{ id: 'docs/read me.md', kind: 'file', label: 'docs/read me.md' },
|
|
]);
|
|
|
|
expect(parts).toEqual([
|
|
{ kind: 'text', text: 'Open ' },
|
|
{
|
|
kind: 'mention',
|
|
text: '@docs/read me.md',
|
|
entity: {
|
|
id: 'docs/read me.md',
|
|
kind: 'file',
|
|
label: 'docs/read me.md',
|
|
token: '@docs/read me.md',
|
|
},
|
|
},
|
|
{ kind: 'text', text: ' now' },
|
|
]);
|
|
});
|
|
});
|