Skip to content
Open
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
34 changes: 17 additions & 17 deletions apps/desktop/renderer-architecture.json
Original file line number Diff line number Diff line change
Expand Up @@ -291,7 +291,7 @@
"legacyAppShell": {
"files": {
"src/renderer/app-shell-chat-actions.ts": {
"importDeclarations": 7,
"importDeclarations": 6,
"bridgePaths": {
"window.maka.newTasks.create": 1,
"window.maka.sessions.remove": 1,
Expand All @@ -317,11 +317,11 @@
"@maka/core/session-name": 1,
"@maka/ui": 1
},
"importSpecifiers": 10,
"importSpecifiers": 9,
"nonTriviaTokens": 3525
},
"src/renderer/app-shell-chrome-actions.tsx": {
"importDeclarations": 1,
"importDeclarations": 0,
"bridgePaths": {},
"environmentCapabilities": {},
"hookCalls": {
Expand All @@ -336,7 +336,7 @@
"./shell/window-titlebar": 1,
"@maka/ui": 1
},
"importSpecifiers": 1,
"importSpecifiers": 0,
"nonTriviaTokens": 122
},
"src/renderer/app-shell-command-actions.ts": {
Expand Down Expand Up @@ -488,7 +488,7 @@
"nonTriviaTokens": 3494
},
"src/renderer/app-shell-overlays.tsx": {
"importDeclarations": 5,
"importDeclarations": 4,
"bridgePaths": {},
"environmentCapabilities": {
"window.addEventListener": 1,
Expand All @@ -514,7 +514,7 @@
"@maka/ui": 1,
"react": 1
},
"importSpecifiers": 8,
"importSpecifiers": 7,
"nonTriviaTokens": 858
},
"src/renderer/app-shell-project-actions.ts": {
Expand Down Expand Up @@ -548,7 +548,7 @@
"nonTriviaTokens": 2157
},
"src/renderer/app-shell-revision-actions.ts": {
"importDeclarations": 3,
"importDeclarations": 2,
"bridgePaths": {
"window.maka.sessions.abandonSessionCopy": 2,
"window.maka.sessions.reviseBeforeTurn": 1
Expand All @@ -565,13 +565,13 @@
"./locales/shell-copy.js": 1,
"./session-copy-attempt.js": 1,
"./session-workspace-errors.js": 1,
"@maka/core/session": 1
"@maka/ui": 1
},
"importSpecifiers": 7,
"nonTriviaTokens": 2028
"importSpecifiers": 3,
"nonTriviaTokens": 482
},
"src/renderer/app-shell-session-events.ts": {
"importDeclarations": 2,
"importDeclarations": 1,
"bridgePaths": {},
"environmentCapabilities": {
"window.setTimeout": 1
Expand All @@ -590,7 +590,7 @@
"./model-connection-errors.js": 1,
"@maka/ui": 1
},
"importSpecifiers": 7,
"importSpecifiers": 1,
"nonTriviaTokens": 2557
},
"src/renderer/app-shell-session-ui-state.ts": {
Expand Down Expand Up @@ -648,7 +648,7 @@
"nonTriviaTokens": 571
},
"src/renderer/app-shell.tsx": {
"importDeclarations": 55,
"importDeclarations": 54,
"bridgePaths": {
"window.maka.attachments": 1,
"window.maka.attachments.readBytes": 1,
Expand Down Expand Up @@ -782,11 +782,11 @@
"@maka/ui": 1,
"react": 1
},
"importSpecifiers": 91,
"nonTriviaTokens": 12127
"importSpecifiers": 79,
"nonTriviaTokens": 12111
},
"src/renderer/use-app-shell-session-list.ts": {
"importDeclarations": 4,
"importDeclarations": 3,
"bridgePaths": {
"window.maka.sessions.list": 1
},
Expand All @@ -811,7 +811,7 @@
"@maka/ui": 1,
"react": 1
},
"importSpecifiers": 5,
"importSpecifiers": 4,
"nonTriviaTokens": 481
},
"src/renderer/use-app-shell-session-ui-reads.ts": {
Expand Down
25 changes: 24 additions & 1 deletion apps/desktop/scripts/check-renderer-architecture.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -108,6 +108,10 @@ const RENDERER_VITE_CONFIG = 'vite.config.ts';
const RENDERER_BUILD_SCRIPT =
'vite build && node scripts/check-renderer-entry-output.mjs && node ../../scripts/check-third-party-notices.mjs';
const DESKTOP_SELF_PREFIX = '@maka/desktop/';
// The package renderer ownership is migrating into. Shell debt is defined to
// shrink by moving onto it, so depending on the destination is the opposite
// of debt and its edges are sanctioned for shell importers.
const MIGRATION_TARGET_PACKAGE = '@maka/ui';
const CAPABILITY_DEBT_METRICS = [
'actionFactories',
'bridgePaths',
Expand Down Expand Up @@ -3099,9 +3103,28 @@ function withoutSanctionedDependencies(desktopRoot, section, importerPath, depen
return filtered;
}

function isMigrationTargetPackageSpecifier(dependency) {
const specifier = dependency.split(/[?#]/u, 1)[0];
return (
specifier === MIGRATION_TARGET_PACKAGE ||
specifier.startsWith(`${MIGRATION_TARGET_PACKAGE}/`)
);
}

function isSanctionedDependencyTarget(desktopRoot, section, importerPath, dependency) {
const target = resolveDependency(desktopRoot, resolve(desktopRoot, importerPath), dependency);
if (!target) return false;
if (!target) {
// Bare package specifiers resolve to nothing inside the desktop tree.
// The migration destination is the one free among them: a shell importer
// depending on @maka/ui sheds ownership the shell is defined to lose,
// the same way validated copy catalogs take bare-package imports for
// free. Root entries stay fully priced: they are meant to become thin
// mounts.
return (
(section === 'legacyAppShell' || section === 'legacyAppShellClosure') &&
isMigrationTargetPackageSpecifier(dependency)
);
}
const targetRelative = normalizePath(relative(desktopRoot, target));
if (isValidatedCopyCatalog(desktopRoot, targetRelative)) return true;
// Root entries are meant to become thin mounts; only catalogs are free for them.
Expand Down
79 changes: 79 additions & 0 deletions apps/desktop/scripts/check-renderer-architecture.test.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -3457,6 +3457,85 @@ describe('renderer architecture base-tree derivation (git fixtures)', () => {
});
});

it('sanctions a shell file migrating onto @maka/ui relative to the derived base tree', async () => {
// Only files named app-shell* enter the legacyAppShell ledger section,
// and every section file needs an ownership entry to pass validation.
const LEGACY_SHELL_WIDGET = 'src/renderer/app-shell-widget.ts';
const seed = architectureConfig({
rootDebt: { [RENDERER_ENTRY_PATH]: emptyDebt() },
ownership: [
{
capability: 'fixture-root',
targetZone: 'bootstrap',
legacyPaths: [RENDERER_ENTRY_PATH],
},
{
capability: 'fixture-shell-widget',
targetZone: 'shell',
legacyPaths: [LEGACY_SHELL_WIDGET],
},
],
});
await withGitFixture(async (fixture) => {
// The base file carries more debt than the head ever will: the migration
// edge must be the only delta under test, so every priced metric shrinks.
await fixture.writeFiles({
[LEGACY_SHELL_WIDGET]: `
import { existsSync } from 'node:fs';
import { join } from 'node:path';

const widgetSlots = ['header', 'body', 'footer'];
const resolveWidgetPath = (root: string, name: string) =>
existsSync(join(root, name)) ? join(root, name) : root;

export const legacyWidget = {
name: 'legacy-widget',
slots: widgetSlots,
resolve: resolveWidgetPath,
};
`,
});
await fixture.writeLedger(seed);
const base = fixture.commit('base');

await fixture.writeFiles({
[LEGACY_SHELL_WIDGET]: `
import { revisionStage } from '@maka/ui';

export const legacyWidget = { name: 'legacy-widget', stage: revisionStage };
`,
});
await fixture.writeLedger(seed);
fixture.commit('migrate a legacy shell file onto @maka/ui');

for (const args of [['--base', base], ['--base', base, '--strict-base']]) {
assertPassed(fixture.runChecker(args), base, args.join(' '));
}
});
});

it('keeps pricing an @maka/ui edge gained by a root debt entry under --strict-base', async () => {
await withGitFixture(async (fixture) => {
await fixture.writeLedger();
const base = fixture.commit('base');
await fixture.writeFiles({
[RENDERER_ENTRY_PATH]: `
import { revisionStage } from '@maka/ui';
export const main = revisionStage;
`,
});
await fixture.writeLedger();
fixture.commit('point the root entry at @maka/ui');

const result = fixture.runChecker(['--base', base, '--strict-base']);
assert.notEqual(result.status, 0);
assert.match(
result.stderr,
/^- src\/renderer\/main\.tsx: new dependency debt @maka\/ui/mu,
);
});
});

it('does not wedge on a base ledger that under-reports its own tree (#4250)', async () => {
await withGitFixture(async (fixture) => {
// The base ledger only knows one legacy file while the base *tree*
Expand Down
68 changes: 65 additions & 3 deletions apps/desktop/src/main/__tests__/app-shell-revision-actions.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,12 @@ function createActions(input: { messages: StoredMessage[]; failRefresh?: boolean
let composerText = '';
let selectionRevision = 0;
const activeIdRef: { current: string | undefined } = { current: SESSION_1 };
const staged: {
quotes: unknown[];
restoredQuotes: unknown[][];
restoredAttachments: unknown[][];
clearedKeys: string[];
} = { quotes: [], restoredQuotes: [], restoredAttachments: [], clearedKeys: [] };
const revisionDraftRef: { current: unknown } = { current: null };
const actions = createAppShellRevisionActions({
uiLocale: 'en' as never,
Expand All @@ -71,6 +77,21 @@ function createActions(input: { messages: StoredMessage[]; failRefresh?: boolean
},
messages: input.messages,
hasPendingAttachments: () => false,
stagedContext: () => ({
quotes: staged.quotes,
attachments: [],
restoreQuotes: (_ownerKey: string, quotes: unknown[]) => {
staged.restoredQuotes.push(quotes);
staged.quotes.push(...quotes);
},
restoreAttachments: (_ownerKey: string, attachments: unknown[]) => {
staged.restoredAttachments.push(attachments);
},
clearQuotes: (ownerKey: string) => {
staged.clearedKeys.push(ownerKey);
staged.quotes.length = 0;
},
}),
openSessionInChat: (sessionId: string) => {
selectionRevision += 1;
activeIdRef.current = sessionId;
Expand All @@ -91,10 +112,18 @@ function createActions(input: { messages: StoredMessage[]; failRefresh?: boolean
} as never);
return Object.assign(actions, {
drafts,
staged,
errors,
infos,
activeIdRef,
composerState: { get text(): string { return composerText; } },
composerState: {
get text(): string {
return composerText;
},
get attachments(): unknown[] {
return staged.restoredAttachments.at(-1) ?? [];
},
},
});
}

Expand Down Expand Up @@ -134,7 +163,11 @@ describe('app-shell revision actions with structured context (#5109)', () => {
assert.equal(h.composerState.text, 'plain follow-up');
});

it('rejects a source message that itself carries attachments', () => {
it('refuses editing a message that carries attachments (#5274 review)', () => {
// Attachment ownership does not follow a revision copy — the copied
// transcript stops before the selected turn, so no target-owned refs
// exist client-side to restage. The edit refuses rather than silently
// dropping the files.
const h = createActions({
messages: [
userMessage('turn-1', 'with image', {
Expand All @@ -153,7 +186,30 @@ describe('app-shell revision actions with structured context (#5109)', () => {

h.beginEditUserMessage('turn-1');

assert.equal(h.drafts.at(-1), undefined, 'attachment-bearing sources stay explicitly rejected');
assert.equal(
h.drafts.at(-1),
undefined,
'a revision copy excludes the revised turn, so no target-owned attachment rewrite exists to restage',
);
assert.equal(h.composerState.text, '', 'the composer stays untouched');
});

it('stages a source message quotes into the composer', () => {
const quote = { text: 'a large pasted excerpt', sourceTurnId: 'turn-0' };
const h = createActions({
messages: [userMessage('turn-1', 'explain this', { quotes: [quote] })],
});

h.beginEditUserMessage('turn-1');

const draft = h.drafts.at(-1) as { originalQuotes?: unknown[] } | undefined;
assert.ok(draft, 'a quote-carrying source message is editable now');
assert.deepEqual(draft?.originalQuotes, [quote]);
assert.deepEqual(
h.staged.restoredQuotes.at(-1),
[quote],
'the source quotes stage into the composer verbatim',
);
});
});

Expand Down Expand Up @@ -263,6 +319,12 @@ describe('revision draft lifecycle over a prepared send', () => {
},
messages: [userMessage('turn-1', 'original message')],
hasPendingAttachments: () => false,
stagedContext: () => ({
quotes: [],
attachments: [],
restoreQuotes: (_ownerKey: string, _quotes: unknown[]) => {},
clearQuotes: (_ownerKey: string) => {},
}),
openSessionInChat: (sessionId: string) => {
selectionRevision += 1;
activeIdRef.current = sessionId;
Expand Down
Loading