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
5 changes: 4 additions & 1 deletion apps/desktop/src/main/__tests__/workbar-boundary.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,10 @@ describe('Workbar feature boundary', () => {
});

it('is consumed outside the feature only through public entries', () => {
const allowed = /\/features\/workbar\/(?:index|testing)(?:\.js)?$/;
// `stories` joins `index` and `testing` as a public entry: the surface it
// exposes is only resolvable by a bundler, so Storybook can import it and
// the node suites behind `testing` cannot.
const allowed = /\/features\/workbar\/(?:index|testing|stories)(?:\.js)?$/;
const violations: string[] = [];
for (const root of [join(desktopRoot, 'src'), join(desktopRoot, 'stories')]) {
for (const path of sourceFiles(root)) {
Expand Down
18 changes: 8 additions & 10 deletions apps/desktop/src/renderer/chat-message-surface.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -166,13 +166,10 @@ export function ChatMessageSurface({
seedRevision: liveContentSeedRevision,
initialLiveContent: liveContentSeedRevision > 0 ? captureLiveContent(liveTurn) : undefined,
}));
if (activation.sessionId !== activeSessionId) {
setActivation({
sessionId: activeSessionId,
seedRevision: liveContentSeedRevision,
initialLiveContent: liveContentSeedRevision > 0 ? captureLiveContent(liveTurn) : undefined,
});
} else if (activation.seedRevision !== liveContentSeedRevision) {
if (
activation.sessionId !== activeSessionId
|| activation.seedRevision !== liveContentSeedRevision
) {
setActivation({
sessionId: activeSessionId,
seedRevision: liveContentSeedRevision,
Expand Down Expand Up @@ -240,9 +237,10 @@ export function ChatMessageSurface({
<ChatView
{...chatViewRest}
liveTurn={seededLiveTurn}
initialLiveContentSnapshot={activation.sessionId === activeSessionId
? activation.initialLiveContent
: liveContentSeedRevision > 0 ? captureLiveContent(liveTurn) : undefined}
// Every branch above reseeds `sessionId` to `activeSessionId`, and a
// render-phase setState re-runs this body before anything commits, so
// the activation reaching the DOM is always this session's.
initialLiveContentSnapshot={activation.initialLiveContent}
shellRunUpdates={shellRunUpdates}
deepResearchRun={deepResearchRun}
emptyOverride={emptyOverride}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,8 +29,7 @@ import {
import type { QuoteRef } from '@maka/core/events';
import type { SessionSummary } from '@maka/core/session';
import { Composer, useUiLocale } from '@maka/ui';
import type { ChatModelChoice } from '@maka/ui';
import type { ComposerProps } from '../../../../../../../packages/ui/dist/composer.d.ts';
import type { ChatModelChoice, ComposerProps } from '@maka/ui';
import { safeLocalStorageGet, safeLocalStorageSet } from '../../../browser-storage.js';
import { getDesktopConversationCopy } from '../../../locales/conversation-copy.js';
import { localizedShellErrorMessage } from '../../../locales/shell-copy.js';
Expand Down
6 changes: 5 additions & 1 deletion apps/desktop/src/renderer/features/workbar/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,8 +17,12 @@
* under the License.
*/

// `WorkbarSurface` is deliberately absent: `workbar-host` reaches it through
// `lazy(() => import('./workbar-surface'))`, and re-exporting it here would
// pull the surface and its five nested tool panels back into the eager chunk
// for every importer of this barrel. Stories reach it through `stories`,
// which nothing shipped imports.
export { WorkbarHost } from './ui/workbar-host';
export { WorkbarSurface } from './ui/workbar-surface';
export { WorkbarTitlebarActions } from './ui/workbar-toggle';
export { WorkbarServicesProvider } from './services-context';
export { useWorkbarController } from './controller/use-workbar-controller';
Expand Down
33 changes: 33 additions & 0 deletions apps/desktop/src/renderer/features/workbar/stories.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/

/**
* Storybook-only entry, separate from `testing` for a reason the module graph
* enforces: `testing` is loaded by `node --test` against tsc output, and
* `workbar-surface` and its tool panels use extensionless relative specifiers
* that only a bundler resolves. Stories run through Vite, so they can reach
* the surface; the node suites cannot, and must not be made to.
*
* The production entry omits `WorkbarSurface` on top of that: `workbar-host`
* reaches it through `lazy()`, and a static re-export beside `WorkbarHost`
* would pull the surface and its five tool panels back into the eager chunk.
* Nothing shipped imports this module either.
*/

export { WorkbarSurface } from './ui/workbar-surface.js';
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ import {
type ChatModelChoice,
type ComposerHandle,
} from '@maka/ui';
import type { ComposerProps } from '../../../../../../../../packages/ui/dist/composer.d.ts';
import type { ComposerProps } from '@maka/ui';
import type { SessionSummary } from '@maka/core/session';
import { useQuoteCompanion } from './use-quote-companion';
import { useComposerAttachments } from '../../../../use-composer-attachments';
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ import { Card } from '@astryxdesign/core/Card';
import { ResizeHandle, type ResizableProps } from '@astryxdesign/core/Resizable';
import { Spinner } from '@astryxdesign/core/Spinner';
import { Composer, useUiLocale } from '@maka/ui';
import type { ComposerProps } from '../../../../../../../packages/ui/dist/composer.d.ts';
import type { ComposerProps } from '@maka/ui';
import type { ChatModelChoice } from '@maka/core/chat-model-choice';
import type { SessionSummary } from '@maka/core/session';
import { getShellCopy } from '../../../locales/shell-copy';
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@ import {
useUiLocale,
type ChatModelChoice,
} from '@maka/ui';
import type { ComposerProps } from '../../../../../../../packages/ui/dist/composer.d.ts';
import type { ComposerProps } from '@maka/ui';
import {
ICON_SIZE,
Activity,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,9 +22,6 @@ import { useRef, useState } from 'react';
import { expect, fn, userEvent, waitFor, within } from 'storybook/test';
import type { ArtifactDescriptor } from '@maka/core/artifacts';
import { ToastProvider } from '@maka/ui';
import {
WorkbarSurface,
} from '../src/renderer/features/workbar';
import {
createFakeWorkbarServices,
createSessionWorkbarPanelsState,
Expand All @@ -34,6 +31,7 @@ import {
WorkbarServicesProvider,
type WorkbarServices,
} from '../src/renderer/features/workbar/testing';
import { WorkbarSurface } from '../src/renderer/features/workbar/stories';
import { RemoteProjectDirectoryDialog } from '../src/renderer/remote-project-directory-dialog';
import { RuntimeHostSshTerminalDialog } from '../src/renderer/settings/runtime-host-ssh-terminal-dialog';
import { withScopedMakaBridge } from './maka-bridge';
Expand Down
6 changes: 2 additions & 4 deletions apps/desktop/stories/session-workbar.stories.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -28,10 +28,8 @@ import type { Task } from '@maka/core/task-ledger';
import type { SessionTrace } from '@maka/core/session-trace';
import type { ContextDiagnosticsResult } from '@maka/runtime-host/protocol';
import { ToastProvider } from '@maka/ui';
import {
WorkbarServicesProvider,
WorkbarSurface,
} from '../src/renderer/features/workbar';
import { WorkbarServicesProvider } from '../src/renderer/features/workbar';
import { WorkbarSurface } from '../src/renderer/features/workbar/stories';
import {
createFakeWorkbarServices,
createSessionWorkbarPanelsState,
Expand Down
171 changes: 171 additions & 0 deletions packages/ui/src/__tests__/stream-delta.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,171 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/

/**
* The shared pipeline directly, not through `applyAssistantDelta` /
* `applyThinkingDelta`. Those wrappers only ever exercise one `recovery`
* direction each, so a change that broke the other direction — or that made
* the two agree where they must differ — would not fail there first.
*
* Markers here are short ASCII stand-ins so every expectation can name the
* exact resulting string rather than assert a shape.
*/

import { strict as assert } from 'node:assert';
import { describe, it } from 'node:test';
import {
applyStreamComplete,
applyStreamDelta,
type StreamDeltaSpec,
} from '../stream-delta.js';

const CHUNK = '[C]';
const TOTAL = '[T]';

function spec(overrides: Partial<StreamDeltaSpec> = {}): StreamDeltaSpec {
return {
maxDeltaChars: 1024,
maxTotalChars: 8,
recovery: 'head',
chunkMarker: CHUNK,
totalMarker: TOTAL,
...overrides,
};
}

describe('applyStreamDelta — the two recovery directions', () => {
it('head-keeps the prefix and tail-keeps the most recent, from one input', () => {
const head = applyStreamDelta('', 'abcdefghij', spec({ recovery: 'head' }));
const tail = applyStreamDelta('', 'abcdefghij', spec({ recovery: 'tail' }));

// maxTotalChars 8 minus a 3-char marker leaves 5 characters of content.
assert.equal(head.text, `abcde${TOTAL}`);
assert.equal(tail.text, `${TOTAL}fghij`);
assert.equal(head.text.length, 8);
assert.equal(tail.text.length, 8);
assert.equal(head.truncated, true);
assert.equal(tail.truncated, true);
});

it('drops the carried state only where head-keep cut the suffix it describes', () => {
const headCut = applyStreamDelta('', 'abcdefghij', spec({ recovery: 'head' }));
const tailCut = applyStreamDelta('', 'abcdefghij', spec({ recovery: 'tail' }));
const headUncut = applyStreamDelta('', 'abc', spec({ recovery: 'head' }));

assert.equal('redactionState' in headCut, false);
assert.notEqual(tailCut.redactionState, undefined);
assert.notEqual(headUncut.redactionState, undefined);
assert.equal(headUncut.truncated, false);
});

it('freezes a full head-kept buffer and keeps a tail-kept window sliding', () => {
const frozen = `abcde${TOTAL}`;
const dropped = applyStreamDelta(frozen, 'more', spec({ recovery: 'head' }));
assert.deepEqual(dropped, { text: frozen, redacted: false, truncated: true });

const sliding = `${TOTAL}fghij`;
const advanced = applyStreamDelta(sliding, 'KL', spec({ recovery: 'tail' }));
assert.equal(advanced.text, `${TOTAL}hijKL`);
assert.equal(advanced.truncated, true);
});

it('caps an oversize single delta the same way in both directions', () => {
// Well under the total cap, so only the per-delta gate can fire.
const perDelta = spec({ maxDeltaChars: 6, maxTotalChars: 1024 });
const expected = `xy${CHUNK}fgh`;

assert.equal(
applyStreamDelta('xy', 'abcdefgh', { ...perDelta, recovery: 'head' }).text,
expected,
);
assert.equal(
applyStreamDelta('xy', 'abcdefgh', { ...perDelta, recovery: 'tail' }).text,
expected,
);
});
});

describe('applyStreamDelta — defensive guard', () => {
it('drops a non-string delta without claiming redaction, in both directions', () => {
for (const recovery of ['head', 'tail'] as const) {
assert.deepEqual(
applyStreamDelta('so far', undefined as unknown as string, spec({ recovery })),
{ text: 'so far', redacted: false, truncated: false },
);
}
assert.equal(
applyStreamDelta(undefined as unknown as string, 42 as unknown as string, spec()).text,
'',
);
});

it('passes the caller state straight back through the guard', () => {
const seeded = applyStreamDelta('', 'seed', spec({ maxTotalChars: 1024 }));
const carried = seeded.redactionState;
assert.notEqual(carried, undefined);

const guarded = applyStreamDelta('seed', null as unknown as string, {
...spec({ maxTotalChars: 1024 }),
redactionState: carried,
});
assert.equal(guarded.redactionState, carried);
});
});

describe('applyStreamComplete', () => {
it('applies the total cap in the direction it was given', () => {
assert.equal(
applyStreamComplete('abcdefghij', {
maxTotalChars: 8,
recovery: 'head',
totalMarker: TOTAL,
}).text,
`abcde${TOTAL}`,
);
assert.equal(
applyStreamComplete('abcdefghij', {
maxTotalChars: 8,
recovery: 'tail',
totalMarker: TOTAL,
}).text,
`${TOTAL}fghij`,
);
});

it('redacts before the cap and reports it', () => {
const result = applyStreamComplete(
'Authorization: Bearer sk-secret123ABCDEFGHIJKLMNOP',
{ maxTotalChars: 1024, recovery: 'head', totalMarker: TOTAL },
);
assert.equal(result.redacted, true);
assert.equal(result.truncated, false);
assert.equal(result.text.includes('sk-secret123ABCDEFGHIJKLMNOP'), false);
});

it('returns empty for a non-string payload', () => {
assert.deepEqual(
applyStreamComplete(undefined as unknown as string, {
maxTotalChars: 8,
recovery: 'tail',
totalMarker: TOTAL,
}),
{ text: '', redacted: false, truncated: false },
);
});
});
Loading