mirror of
https://github.com/langbot-app/LangBot.git
synced 2026-10-27 02:45:24 +08:00
Resolve conflicts in the workspace assistant integration: - DynamicFormItemComponent: keep HEAD's compact model selector and `disabled` field support while adopting master's `sortModelsByCatalog` ordering and `MODEL_SELECT_TRIGGER_CLASS`. - i18n (en-US, ja-JP, zh-Hans): keep both the `assistant` namespace from HEAD and master's `sidebarGuide` / `pipelineMigration` additions. Add 0030_merge_assistant to join the assistant conversations branch with the released chain so the migration graph converges on a single head. Rename it from 0030_merge_assistant_conversations to stay within the 32 character revision limit enforced by test_migrations. Also add assistant button docking (long-press drag, edge collapse to a short blue rail, hover restore) with pure helpers in assistant-dock.ts and unit coverage, plus auto-collapse for finished tool result cards.
133 lines
4.2 KiB
JavaScript
133 lines
4.2 KiB
JavaScript
import assert from 'node:assert/strict';
|
|
import fs from 'node:fs';
|
|
import test from 'node:test';
|
|
import ts from 'typescript';
|
|
|
|
// The floating assistant button docks against a viewport edge, collapses into a
|
|
// short blue rail, and re-expands on hover. The transition rules are pure so
|
|
// they can be pinned here: the race this guards against is a button that drops
|
|
// under the cursor and never becomes visible again.
|
|
const sourcePath = new URL(
|
|
'../../src/app/home/components/assistant-dock.ts',
|
|
import.meta.url,
|
|
);
|
|
assert.ok(fs.existsSync(sourcePath), 'Missing assistant dock module');
|
|
const compiled = ts.transpileModule(fs.readFileSync(sourcePath, 'utf8'), {
|
|
compilerOptions: { module: ts.ModuleKind.CommonJS },
|
|
}).outputText;
|
|
const loaded = { exports: {} };
|
|
new Function('require', 'module', 'exports', compiled)(
|
|
() => {
|
|
throw new Error('assistant-dock must stay dependency free');
|
|
},
|
|
loaded,
|
|
loaded.exports,
|
|
);
|
|
const {
|
|
ASSISTANT_BUTTON_SIZE,
|
|
ASSISTANT_SNAP_THRESHOLD,
|
|
ASSISTANT_RAIL_WIDTH,
|
|
clampAssistantPosition,
|
|
resolveAssistantEdge,
|
|
shouldCollapseRail,
|
|
shouldExpandRail,
|
|
restingRailExpanded,
|
|
} = loaded.exports;
|
|
|
|
const viewport = { width: 1280, height: 800 };
|
|
const maxX = viewport.width - ASSISTANT_BUTTON_SIZE;
|
|
|
|
test('clamping keeps the button fully inside the viewport', () => {
|
|
assert.deepEqual(clampAssistantPosition(-50, -50, viewport), { x: 0, y: 0 });
|
|
assert.deepEqual(clampAssistantPosition(9999, 9999, viewport), {
|
|
x: maxX,
|
|
y: viewport.height - ASSISTANT_BUTTON_SIZE,
|
|
});
|
|
assert.deepEqual(clampAssistantPosition(400, 300, viewport), {
|
|
x: 400,
|
|
y: 300,
|
|
});
|
|
});
|
|
|
|
test('a narrow viewport never produces a negative travel range', () => {
|
|
assert.deepEqual(
|
|
clampAssistantPosition(10, 10, { width: 20, height: 20 }),
|
|
{ x: 0, y: 0 },
|
|
);
|
|
});
|
|
|
|
test('positions within the snap threshold dock to the nearest edge', () => {
|
|
assert.equal(resolveAssistantEdge(0, viewport), 'left');
|
|
assert.equal(resolveAssistantEdge(ASSISTANT_SNAP_THRESHOLD, viewport), 'left');
|
|
assert.equal(resolveAssistantEdge(maxX, viewport), 'right');
|
|
assert.equal(
|
|
resolveAssistantEdge(maxX - ASSISTANT_SNAP_THRESHOLD, viewport),
|
|
'right',
|
|
);
|
|
});
|
|
|
|
test('a centred position stays free floating', () => {
|
|
assert.equal(resolveAssistantEdge(Math.round(maxX / 2), viewport), null);
|
|
assert.equal(
|
|
resolveAssistantEdge(ASSISTANT_SNAP_THRESHOLD + 1, viewport),
|
|
null,
|
|
);
|
|
});
|
|
|
|
test('the left edge wins ties so a centred drop is deterministic', () => {
|
|
// A viewport sized so both gaps are inside the snap threshold at x = 0.
|
|
const tight = { width: ASSISTANT_BUTTON_SIZE, height: 400 };
|
|
assert.equal(resolveAssistantEdge(0, tight), 'left');
|
|
});
|
|
|
|
test('hovering a docked rail expands it, hovering a free button does not', () => {
|
|
assert.equal(
|
|
shouldExpandRail({ dockedEdge: 'right', railExpanded: false, dragging: false }),
|
|
true,
|
|
);
|
|
assert.equal(
|
|
shouldExpandRail({ dockedEdge: null, railExpanded: false, dragging: false }),
|
|
false,
|
|
);
|
|
// Already expanded: nothing to do, so no redundant state churn on every move.
|
|
assert.equal(
|
|
shouldExpandRail({ dockedEdge: 'left', railExpanded: true, dragging: false }),
|
|
false,
|
|
);
|
|
});
|
|
|
|
test('a drag in progress never expands or collapses the rail', () => {
|
|
const dragging = { dockedEdge: 'left', railExpanded: false, dragging: true };
|
|
assert.equal(shouldExpandRail(dragging), false);
|
|
assert.equal(shouldCollapseRail(dragging), false);
|
|
});
|
|
|
|
test('a docked, idle button is the only state that collapses', () => {
|
|
assert.equal(
|
|
shouldCollapseRail({ dockedEdge: 'left', railExpanded: false, dragging: false }),
|
|
true,
|
|
);
|
|
assert.equal(
|
|
shouldCollapseRail({ dockedEdge: null, railExpanded: false, dragging: false }),
|
|
false,
|
|
);
|
|
});
|
|
|
|
test('dropping always starts collapsed so the rail is never stuck open', () => {
|
|
// The pointer is still over the button on drop; the next pointermove expands.
|
|
assert.equal(restingRailExpanded(), false);
|
|
assert.equal(
|
|
shouldExpandRail({
|
|
dockedEdge: 'right',
|
|
railExpanded: restingRailExpanded(),
|
|
dragging: false,
|
|
}),
|
|
true,
|
|
);
|
|
});
|
|
|
|
test('the rail strip stays thinner than the button it replaces', () => {
|
|
assert.ok(ASSISTANT_RAIL_WIDTH > 0);
|
|
assert.ok(ASSISTANT_RAIL_WIDTH < ASSISTANT_BUTTON_SIZE);
|
|
});
|