Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
18 commits
Select commit Hold shift + click to select a range
daf8229
fix(desktop,ui): re-key the plate's live quotes and drop stale footer…
ggbdpq Sep 27, 2026
14a3685
Merge upstream/main into fix/desktop-revision-structured-context
ggbdpq Sep 28, 2026
f531f73
fix(desktop,ui): preserve the quote plate across the in-flight revisi…
ggbdpq Sep 28, 2026
ddd4c95
fix(desktop,ui): keep quotes added during a cancelled edit
ggbdpq Sep 28, 2026
e39891b
fix(ui): treat negative owned counts as fully user-owned
ggbdpq Sep 28, 2026
436e435
Merge upstream/main (8648258d0)
ggbdpq Sep 29, 2026
b97145c
fix(desktop): assemble the revision staged context outside the frozen…
ggbdpq Sep 29, 2026
15564fb
Merge upstream/main (28cc4e647)
ggbdpq Sep 30, 2026
a727c09
Merge upstream/main (0aa2707b5)
ggbdpq Sep 30, 2026
bf77764
Merge upstream/main (d6876d708)
ggbdpq Sep 30, 2026
94bbff1
Merge upstream/main (d7dffca98)
ggbdpq Sep 30, 2026
ae47a65
Merge upstream/main (c838e1fa7)
ggbdpq Sep 30, 2026
d8cf8bd
Merge upstream/main (1e80e3b88) into fix/desktop-revision-structured-…
ggbdpq Oct 1, 2026
4e93a8e
Merge upstream/main (c7fa6bb6a) into fix/desktop-revision-structured-…
ggbdpq Oct 2, 2026
868ed95
Merge upstream/main (255ae23ae) into fix/desktop-revision-structured-…
ggbdpq Oct 3, 2026
95bd2c4
Merge upstream/main (229e1b466) into fix/desktop-revision-structured-…
ggbdpq Oct 3, 2026
54babb2
Merge upstream/main (f7633c3d7) into fix/desktop-revision-structured-…
ggbdpq Oct 3, 2026
3a3c2e3
Merge upstream/main (7c90bac2d) into fix/desktop-revision-structured-…
ggbdpq Oct 3, 2026
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
16 changes: 8 additions & 8 deletions apps/desktop/renderer-architecture.json
Original file line number Diff line number Diff line change
Expand Up @@ -504,7 +504,7 @@
"legacyAppShell": {
"files": {
"src/renderer/app-shell-chrome-actions.tsx": {
"importDeclarations": 1,
"importDeclarations": 0,
"bridgePaths": {},
"environmentCapabilities": {},
"hookCalls": {
Expand All @@ -519,7 +519,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 @@ -609,7 +609,7 @@
"nonTriviaTokens": 843
},
"src/renderer/app-shell-overlays.tsx": {
"importDeclarations": 5,
"importDeclarations": 4,
"bridgePaths": {},
"environmentCapabilities": {
"window.addEventListener": 1,
Expand All @@ -635,11 +635,11 @@
"@maka/ui": 1,
"react": 1
},
"importSpecifiers": 8,
"importSpecifiers": 7,
"nonTriviaTokens": 858
},
"src/renderer/app-shell.tsx": {
"importDeclarations": 35,
"importDeclarations": 34,
"bridgePaths": {},
"environmentCapabilities": {
"window.requestAnimationFrame": 3
Expand Down Expand Up @@ -726,11 +726,11 @@
"@maka/ui": 1,
"react": 1
},
"importSpecifiers": 52,
"importSpecifiers": 43,
"nonTriviaTokens": 7333
},
"src/renderer/use-app-shell-session-list.ts": {
"importDeclarations": 4,
"importDeclarations": 3,
"bridgePaths": {},
"environmentCapabilities": {},
"hookCalls": {
Expand All @@ -753,7 +753,7 @@
"@maka/ui": 1,
"react": 1
},
"importSpecifiers": 5,
"importSpecifiers": 4,
"nonTriviaTokens": 479
},
"src/renderer/use-app-shell-session-workspace.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 FEATURE_PUBLIC_ENTRY = /^src\/renderer\/features\/[^/]+\/index\.(?:(?:c|m)?(?:js|ts)x?)$/u;
const ROOT_SYMBOL_ZONES = ['appShell', 'bootstrap', 'composition'];
const CAPABILITY_DEBT_METRICS = [
Expand Down Expand Up @@ -3951,9 +3955,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 @@ -3558,6 +3558,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
76 changes: 71 additions & 5 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 = createRevisionActions({
services: windowSubmissionServices(),
Expand All @@ -71,7 +77,24 @@ function createActions(input: { messages: StoredMessage[]; failRefresh?: boolean
} as never,
},
readMessages: () => input.messages,
hasPendingAttachments: () => false,
staging: {
captureSubmission: () => ({ hasPendingContext: 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);
return staged.quotes.splice(0, staged.quotes.length);
},
}),
},
openSessionInChat: (sessionId: string) => {
selectionRevision += 1;
activeIdRef.current = sessionId;
Expand All @@ -92,10 +115,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 @@ -135,7 +166,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 @@ -154,7 +189,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 @@ -264,7 +322,15 @@ describe('revision draft lifecycle over a prepared send', () => {
},
},
readMessages: () => [userMessage('turn-1', 'original message')],
hasPendingAttachments: () => false,
staging: {
captureSubmission: () => ({ hasPendingContext: false }),
stagedContext: () => ({
quotes: [],
attachments: [],
restoreQuotes: (_ownerKey: string, _quotes: unknown[]) => {},
clearQuotes: (_ownerKey: string) => [],
}),
},
openSessionInChat: (sessionId: string) => {
selectionRevision += 1;
activeIdRef.current = sessionId;
Expand Down
14 changes: 11 additions & 3 deletions apps/desktop/src/main/__tests__/app-shell-revision-resend.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,8 +20,8 @@
/**
* Unchanged-text edit-and-resend regression (PR #5815 follow-up).
*
* The production fix removed the `text.trim() === revision.originalText.trim()`
* early return from the composer's submit path. The existing
* The production fix removed the unchanged-text early return from the
* revision send path. The existing
* `app-shell-revision-actions.test.ts` suite only calls `prepareRevisionSend()`
* directly, so it cannot tell whether the real submit path still short-circuits
* unchanged text. This suite mounts the real ChatComposerRegion, wires its
Expand Down Expand Up @@ -214,7 +214,15 @@ async function mountRevisionWorld(): Promise<RevisionWorld> {
},
composerRef: composer,
readMessages: () => [userMessage('turn-1', ORIGINAL_TEXT)],
hasPendingAttachments: () => false,
staging: {
captureSubmission: () => ({ hasPendingContext: false }),
stagedContext: () => ({
quotes: [],
attachments: [],
restoreQuotes: () => {},
clearQuotes: () => [],
}),
},
openSessionInChat: (sessionId: string) => {
selectionRevision += 1;
activeIdRef.current = sessionId;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -371,7 +371,7 @@ for (const context of ['attachment', 'directory'] as const) {
services: windowSubmissionServices(),
uiLocale: 'en', activeIdRef, captureSelection: () => () => true, composerRef: view.composer,
readMessages: () => [{ type: 'user', id: 'message', turnId: `guard-${context}`, text: 'original', ts: 1 }],
hasPendingAttachments: () => view.commands.captureSubmission().hasPendingContext,
staging: view.commands,
openSessionInChat() {}, refreshSessions: async () => [],
commitRevisionDraft: (draft) => { revisionDraftRef.current = draft; }, revisionDraftRef,
toastApi: { info() {}, error: () => assert.fail('unexpected revision error') },
Expand Down
Loading