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
86 changes: 86 additions & 0 deletions packages/ui/src/__tests__/session-history-working-mark.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
/*
* 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.
*/

import assert from 'node:assert/strict';
import test from 'node:test';
import { parseHTML } from 'linkedom';
import { renderToStaticMarkup } from 'react-dom/server';
import type { SessionSummary } from '@maka/core/session';
import { LocaleProvider } from '../locale-context.js';
import { SessionHistoryList } from '../session-history-list.js';
import { SessionRailProvider, type SessionRailData } from '../session-rail-context.js';

function Rail(props: Partial<SessionRailData> & { sessions: readonly SessionSummary[] }) {
const data: SessionRailData = {
groupVariant: 'conversation',
onSelectSession: () => undefined,
...props,
};
return (
<LocaleProvider locale="en">
<SessionRailProvider data={data}>
<SessionHistoryList />
</SessionRailProvider>
</LocaleProvider>
);
}

const session: SessionSummary = {
id: 'session-1',
name: 'Release notes',
isFlagged: false,
isArchived: false,
labels: [],
hasUnread: false,
status: 'active',
backend: 'ai-sdk',
llmConnectionSlug: 'test-connection',
connectionLocked: true,
model: 'test-model',
permissionMode: 'ask',
};

test('the responding row’s signal carries the working mark', () => {
const dom = parseHTML(
renderToStaticMarkup(
<Rail sessions={[{ ...session, status: 'running', runningTurnIds: ['turn-1'] }]} />,
),
);
assert.equal(
dom.document.querySelector('.maka-session-row-signal')?.getAttribute('data-working'),
'true',
);
});

test('a persisted running row keeps the working mark without live turns', () => {
const dom = parseHTML(
renderToStaticMarkup(<Rail sessions={[{ ...session, status: 'running' }]} />),
);
assert.equal(
dom.document.querySelector('.maka-session-row-signal')?.getAttribute('data-working'),
'true',
);
});

test('a settled row carries no working mark', () => {
const dom = parseHTML(renderToStaticMarkup(<Rail sessions={[session]} />));
const signal = dom.document.querySelector('.maka-session-row-signal');
assert.ok(signal, 'the signal gutter always renders');
assert.equal(signal.getAttribute('data-working'), null);
});
9 changes: 8 additions & 1 deletion packages/ui/src/session-history-list.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -797,7 +797,14 @@ const SessionNavRow = memo(function SessionNavRow(props: {
// whether or not it has a dot, so state reads as one column down the
// rail instead of a mark that drifts with each title's length.
icon={
<span className="maka-session-row-signal">
<span
className="maka-session-row-signal"
// The working cue's hook: `styles.css` wraps the dot in a spinning
// arc keyed on this attribute (static ring under reduced motion).
// `isPulsing` is exactly "a turn is working" — live responding and
// the persisted running fallback both set it.
data-working={signal?.isPulsing ? 'true' : undefined}
>
{signal ? (
<StatusDot
variant={signal.variant}
Expand Down
29 changes: 29 additions & 0 deletions packages/ui/src/styles.css
Original file line number Diff line number Diff line change
Expand Up @@ -1342,3 +1342,32 @@
.maka-choice-panel:focus-visible { outline: var(--focus-ring-width) solid var(--focus-ring); outline-offset: var(--focus-ring-offset); }

.maka-choice-hint { margin: 8px 0; }

/* ---------------------------------------------------------------------------
Session rail working cue.

The 8px dot's own opacity breathe reads as "settled" in peripheral vision,
and `prefers-reduced-motion` removes even that (apache/maka#5519). While a
turn is working, the signal gutter wraps the dot in a spinning arc — the
rotation is the in-progress cue when motion is fine, and the arc itself is
the non-motion mark when it is not.
--------------------------------------------------------------------------- */

.maka-session-row-signal { display: inline-flex; position: relative; }

.maka-session-row-signal[data-working='true']::after {
content: '';
position: absolute;
inset: -3px;
border-radius: 999px;
border: 1.5px solid transparent;
border-top-color: var(--accent);
animation: maka-spin 0.9s linear infinite;
}

@media (prefers-reduced-motion: reduce) {
.maka-session-row-signal[data-working='true']::after {
animation: none;
border-color: color-mix(in oklch, var(--accent) 55%, transparent);
}
}