## Summary Adds the **Stash memory backend** (`memory.backendType=stash`) that connects Fusion's agent memory to the [Stash](https://github.com/Fergana-Labs/stash) product — *knowledge bases for the agent era* ([product site: joinstash.ai](https://joinstash.ai)). Fusion becomes a first-class Stash client: complete chat sessions and finished tasks are captured into Stash, memory is recalled during chat, and Stash sessions are kept in sync with the dashboard (including deletes and archival). **Product:** <https://github.com/Fergana-Labs/stash> · [joinstash.ai](https://joinstash.ai) ## What's included ### 1. Stash memory backend (RUFU-068 / RUFU-121) - New `StashMemoryBackend` (`memory.backendType=stash`) with `stashUrl` / `stashApiKey` settings (global secrets-store `stash-api-key` + per-project override). - **Complete-chat-session capture** keyed by ChatSession id. - Sessions are classified into **per-project folders** (get-or-create, `external_key fusion-<projectId>`, 1h per-process cache) and **soft-deleted with their chat** via `DELETE /api/chat/sessions/:id`. - Per-conversation **memory-focus** read-time scoping (new `0066_chat_session_memory_focus.sql` migration — sequence renumbered 0059→0060→0061→0065→0066 as origin/main claimed the lower numbers); event metadata enriched with `project` / `project_name` / `chat_title`. - Recall queries normalized to single-keyword / explicit-OR ASCII (≤100 chars); shared normalizer export reused by per-turn recall. ### 2. Per-task executor transcript capture (RUFU-122) Finished or failed tasks upload their executor transcript (`agent-log.jsonl`) to Stash as a task session. ### 3. Bulk archive Stash sync (RUFU-125) Archived task-planner chats soft-delete their Stash sessions on bulk archival (paged). The snapshot of doomed session ids is taken *before* the local bulk delete, and the Stash sync runs fire-and-forget so a Stash stall can never delay local archival. ### 4. Per-chat "Preserve to Stash" backfill (RUFU-136) A per-chat action that backfills a chat's full history into Stash, with client-side idempotency and a pre-check that skips already-uploaded content (fail-closed, no duplicate upload on transport failure). - **Session-folder naming fix:** the first project folder is now named "Fusion — <project name>" instead of the bare "Fusion" fallback (the backfill now resolves the central-registry project name, best-effort, never blocking the upload). ### 5. Opt-in semantic (vector) recall (RUFU-126) `stashVectorSearch` setting (default `false` — **zero behavior change until enabled**). For multi-word queries the backend tries Stash's semantic-search endpoint first, then falls back byte-identically to the keyword path. Definitive 404/405/501/503 responses are negatively cached per process. Requires a patched Stash server (new endpoint + `sentence-transformers` + embedding backfill); unpatched servers are transparently bypassed after the first 404. ## Safety - **Opt-in / inert by default:** the default backend remains `qmd`; the Stash backend is inert until `memoryBackendType=stash` + `stashUrl` are set. - All Stash I/O is **best-effort, fail-closed, and non-blocking** — a Stash outage never blocks chat, task completion, or archival. No run-audit content is emitted. ## Testing - Backfill + delete-sync suites (20/20), Stash backend suite (68/68), executor memory / session capture suites, `memory-focus-recalling`, description-guard — all green. - `tsc` clean across core / engine / dashboard. - Live verification: bulk backfill of 21/24 chats completed; the "Preserve to Stash" action is idempotent on re-run. ## Changesets - `@runfusion/fusion` **minor** — Stash memory backend + capture (RUFU-068/121), per-task transcript (RUFU-122), bulk archive sync (RUFU-125), per-chat backfill (RUFU-136), opt-in vector search (RUFU-126) - `@runfusion/fusion` **patch** — backfill session-folder naming fix ## Rebase Note (2026-08-23) Rebased onto `origin/main` `3f448f7292` (v0.77.0-beta.7). Conflicts resolved additively: - `packages/core/src/postgres/schema-applier.ts` + test — upstream's 0062-0065 migrations (task/subtask splitting removal, AI merge review reconciliation, task repository scope, FN-149 review convergence) unioned with this PR's `chat_sessions.memory_focus` migration, which is **renumbered 0065 → 0066** (upstream's FN-149 shipped 0065 canonically on origin/main); `SCHEMA_BASELINE_VERSION` advances to `0066`. - `packages/dashboard/app/components/ChatView.tsx` — upstream's docked chat sidebar resize handlers unioned with the RUFU-136 "Preserve to Stash" backfill handler. - New commit: `settings.memory.*` stash-backend i18n keys added to all 6 secondary locales (RUFU-121/122 parity fix; `pnpm i18n:status` no longer reports any violation introduced by this PR). **Deploy note (operator environments that already ran a pre-rebase build of this PR):** the memory-focus SQL may already be in the schema under ledger row `0065`. Remap that row to `0066` (`UPDATE fusion_schema_migrations SET version = '0066' WHERE version = '0065';`) *before* first boot of a 0066-ceiling binary — otherwise the fresh upstream `0065_fn_149_review_convergence_stage.sql` would be skipped as "already applied". Clean databases (no prior memory-focus row) need no action. **CI note — Lint (lifecycle-column census) is red on the merge base:** `pnpm check:lifecycle-columns --strict` fails identically on pure `origin/main` `3f448f7292` with `packages/core/src/db/legacy-adoption.ts: 0 -> 3` (3 column guards in the U9b legacy-adoption table without a baseline entry or `DELIBERATE-LITERAL` marker). Verified by running the census on a clean origin/main checkout — inherited from the base, not introduced by this PR. Fix belongs upstream; tracked separately. <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit * **New Features** * Added Stash memory integration with project configuration and optional semantic search. * Added per-chat memory focus controls and a `/focus` command. * Added “Preserve to Stash” for uploading complete chat history. * Added automatic chat, task transcript, and completion-event capture. * Added project-specific Stash session folders and archive/delete synchronization. * **Bug Fixes** * Improved Stash folder naming and handling of missing branches during no-commit tasks. * **Documentation** * Added setup, configuration, integration, vector-search, and performance guidance. <!-- end of auto-generated comment: release notes by coderabbit.ai --> --------- Co-authored-by: Fusion <noreply@runfusion.ai> Co-authored-by: gsxdsm <gsxdsm@users.noreply.github.com>
391 lines
12 KiB
CSS
391 lines
12 KiB
CSS
.task-planner-chat {
|
|
position: relative;
|
|
display: flex;
|
|
flex: 1 1 auto;
|
|
flex-direction: column;
|
|
gap: var(--space-md);
|
|
min-height: 0;
|
|
}
|
|
|
|
/*
|
|
FNXC:ChatMessageLayout 2026-08-18-20:27:
|
|
Planner Chat reuses StandardChatSurface message markup, so its root modifier is the only task-specific hook needed to remove the shared horizontal bubble split while preserving the planner transcript, controls, and empty/loading states.
|
|
*/
|
|
.task-planner-chat--full-width .chat-message,
|
|
.task-planner-chat--full-width .chat-message--user,
|
|
.task-planner-chat--full-width .chat-message--assistant,
|
|
.task-planner-chat--full-width .chat-message--streaming,
|
|
.task-planner-chat--full-width .chat-message--failure {
|
|
width: 100%;
|
|
max-width: 100%;
|
|
align-self: stretch;
|
|
}
|
|
|
|
/*
|
|
FNXC:TaskDetailPlannerChat 2026-07-02-00:28:
|
|
Planner Chat should not render a redundant in-panel title/header. The transcript starts at the top of the chat area, the expand button floats over the view, and the planning model/provider icon appears only as a small empty-state affordance.
|
|
*/
|
|
.task-planner-chat-expand-toggle {
|
|
flex: 0 0 auto;
|
|
min-inline-size: calc(var(--space-2xl) - var(--space-xs));
|
|
min-block-size: calc(var(--space-2xl) - var(--space-xs));
|
|
padding: 0;
|
|
}
|
|
|
|
/*
|
|
FNXC:TaskDetailPlannerChat 2026-07-02-00:16:
|
|
Planner Chat expansion should use the same in-view floating affordance as Activity Live/Feed, not a header action that shifts layout. Anchor it to the chat panel so it remains visible while the transcript scrolls.
|
|
*/
|
|
.task-planner-chat-expand-toggle--overlay {
|
|
position: absolute;
|
|
top: var(--space-sm);
|
|
right: var(--space-sm);
|
|
z-index: 3;
|
|
background: var(--surface);
|
|
border-color: var(--border);
|
|
box-shadow: var(--shadow-sm);
|
|
}
|
|
|
|
.task-planner-chat-empty-model {
|
|
position: absolute;
|
|
top: 0;
|
|
left: 0;
|
|
display: inline-flex;
|
|
flex: 0 0 auto;
|
|
align-items: center;
|
|
justify-content: center;
|
|
inline-size: calc(var(--space-2xl) - var(--space-xs));
|
|
block-size: calc(var(--space-2xl) - var(--space-xs));
|
|
border: var(--btn-border-width) solid var(--border);
|
|
border-radius: var(--radius);
|
|
padding: 0;
|
|
color: var(--text-muted);
|
|
background: var(--surface-subtle);
|
|
}
|
|
|
|
.task-planner-chat-error {
|
|
border: var(--btn-border-width) solid var(--color-error);
|
|
border-radius: var(--radius-md);
|
|
padding: var(--space-sm) var(--space-md);
|
|
color: var(--color-error);
|
|
background: var(--surface-subtle);
|
|
}
|
|
|
|
.task-planner-chat-transcript {
|
|
flex: 1 1 auto;
|
|
min-height: 0;
|
|
overflow: auto;
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: var(--space-md);
|
|
border: var(--btn-border-width) solid var(--border);
|
|
border-radius: var(--radius-lg);
|
|
padding: var(--space-md);
|
|
background: var(--surface);
|
|
}
|
|
|
|
.task-planner-chat-state,
|
|
.task-planner-chat-empty {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: var(--space-sm);
|
|
color: var(--text-muted);
|
|
text-align: center;
|
|
}
|
|
|
|
.task-planner-chat-state {
|
|
margin: auto;
|
|
}
|
|
|
|
.task-planner-chat-empty {
|
|
position: relative;
|
|
flex-direction: column;
|
|
width: 100%;
|
|
max-width: 42rem;
|
|
margin: 0 auto auto;
|
|
gap: var(--space-lg);
|
|
padding-top: var(--space-md);
|
|
}
|
|
|
|
/*
|
|
FNXC:TaskDetailPlannerChat 2026-07-02-00:41:
|
|
An empty Planner Chat transcript should enter at the top of the chat surface instead of auto-centering or inheriting the message auto-scroll-to-bottom behavior. Once real messages exist, message streaming still scrolls to the latest entry.
|
|
*/
|
|
|
|
.task-planner-chat-empty-copy {
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: var(--space-xs);
|
|
}
|
|
|
|
.task-planner-chat-empty-copy h5,
|
|
.task-planner-chat-empty-copy p {
|
|
margin: 0;
|
|
}
|
|
|
|
.task-planner-chat-empty-copy h5 {
|
|
color: var(--text);
|
|
font-size: var(--font-size-lg);
|
|
}
|
|
|
|
.task-planner-chat-starters {
|
|
display: grid;
|
|
grid-template-columns: repeat(2, minmax(0, 1fr));
|
|
gap: var(--space-sm);
|
|
width: 100%;
|
|
}
|
|
|
|
.task-planner-chat-starter {
|
|
align-items: flex-start;
|
|
justify-content: flex-start;
|
|
min-width: 0;
|
|
height: auto;
|
|
white-space: normal;
|
|
text-align: left;
|
|
}
|
|
|
|
.task-planner-chat-starter-label,
|
|
.task-planner-chat-starter-description {
|
|
display: block;
|
|
}
|
|
|
|
.task-planner-chat-starter-label {
|
|
color: var(--text);
|
|
font-weight: 600;
|
|
}
|
|
|
|
.task-planner-chat-starter-description {
|
|
margin-top: var(--space-xs);
|
|
color: var(--text-muted);
|
|
font-size: var(--font-size-xs);
|
|
line-height: var(--line-height-tight);
|
|
}
|
|
|
|
.task-planner-chat-steering-confirmation {
|
|
margin-top: var(--space-sm);
|
|
border: var(--btn-border-width) solid var(--color-success);
|
|
border-radius: var(--radius-md);
|
|
padding: var(--space-sm);
|
|
color: var(--text);
|
|
background: var(--surface);
|
|
}
|
|
|
|
.task-planner-chat-steering-confirmation--pending {
|
|
border-color: var(--color-warning);
|
|
}
|
|
|
|
.task-planner-chat-steering-confirmation--error {
|
|
border-color: var(--color-error);
|
|
color: var(--color-error);
|
|
}
|
|
|
|
.task-planner-chat-steering-confirmation strong,
|
|
.task-planner-chat-steering-confirmation p {
|
|
margin: 0;
|
|
}
|
|
|
|
.task-planner-chat-steering-confirmation p {
|
|
margin-top: var(--space-xs);
|
|
color: var(--text-muted);
|
|
}
|
|
|
|
.task-planner-chat .chat-question-response {
|
|
max-width: 100%;
|
|
overflow-wrap: anywhere;
|
|
}
|
|
|
|
/*
|
|
FNXC:TaskDetailPlannerChat 2026-06-30-00:00:
|
|
The Planner Chat composer should behave like a compact input/button row on desktop: match the textarea's default height to the Activity chat input and let flex wrapping, not a forced column, decide when the controls no longer fit inline.
|
|
|
|
FNXC:TaskDetailPlannerChat 2026-06-30-23:58:
|
|
Keep the composer outside the transcript scroller and make the panel fill the modal body. Loading, errors, starters, history, and streaming content scroll inside `.task-planner-chat-transcript`, while the input row remains pinned and reachable in normal and mobile-expanded detail layouts.
|
|
|
|
FNXC:TaskDetailPlannerChat 2026-06-30-20:06:
|
|
Planner Chat must keep visual height parity with Activity chat: desktop textareas use the Activity compact min-height token, and the stacked mobile composer switches to the Activity mobile touch-height token without hardcoded dimensions.
|
|
|
|
FNXC:TaskDetailPlannerChat 2026-07-01-23:54:
|
|
Mobile Planner Chat should match regular task chat: keep the composer as a single input row with a one-line textarea and a square send affordance on the right instead of stacking the labeled button below the input.
|
|
|
|
FNXC:TaskDetailPlannerChat 2026-07-07-00:00:
|
|
The Planner Chat streaming Stop button must occupy the same width footprint as the Send button it replaces (no shift/shrink on swap) and mirror the regular Chat view's stop-button sizing (`.chat-input-row`'s `--chat-input-control-size` in ChatView.css). The shared `.chat-input-send` / `.chat-input-stop` classes read that custom property from `.chat-input-row`, which the Planner composer never renders inside of, so the property was undefined here and `width` fell back to `auto`, sizing each button from its own content only. Declare the same control-size formula scoped to `.task-planner-chat-composer` and give `.task-planner-chat-send` (present on both the send and stop button variants) a matching `min-inline-size` floor so neither button can render narrower than the other on desktop, without touching ChatView.css's own token.
|
|
*/
|
|
/*
|
|
FNXC:TaskChatDefaultModel 2026-08-19-12:12:
|
|
Task Chat keeps model and thinking controls reachable beside the composer, reusing the Direct Chat primitives instead of adding a task-only selector style. The compact control row wraps on narrow layouts so the task transcript and send affordance remain usable.
|
|
*/
|
|
.task-planner-chat-target-controls {
|
|
display: flex;
|
|
flex: 0 1 auto;
|
|
align-items: center;
|
|
gap: var(--space-xs);
|
|
min-width: 0;
|
|
}
|
|
|
|
.task-planner-chat-target-controls .model-combobox {
|
|
min-width: 0;
|
|
max-width: calc(var(--space-2xl) * 8);
|
|
}
|
|
|
|
.task-planner-chat-target-controls .model-combobox-trigger {
|
|
max-width: 100%;
|
|
}
|
|
|
|
.task-planner-chat-target-controls .chat-thinking-level-root {
|
|
flex: 0 0 auto;
|
|
}
|
|
|
|
/*
|
|
FNXC:ChatMemoryFocus 2026-08-13:
|
|
Spacing row holding the per-conversation memory focus chip above the planner
|
|
composer. The chip inherits its control-size token from the composer below so
|
|
it aligns with the send/stop buttons.
|
|
*/
|
|
.task-planner-chat-focus-row {
|
|
display: flex;
|
|
flex: 0 0 auto;
|
|
align-items: center;
|
|
justify-content: flex-start;
|
|
padding-inline: var(--space-xs);
|
|
padding-bottom: var(--space-xs);
|
|
}
|
|
|
|
.task-planner-chat-composer {
|
|
/* FN-7634: same formula as ChatView.css's `.chat-input-row { --chat-input-control-size: … }` so the Planner stop/send buttons share the regular Chat view's control-size floor. */
|
|
--chat-input-control-size: calc(var(--space-lg) * 2.5);
|
|
|
|
display: flex;
|
|
flex: 0 0 auto;
|
|
flex-wrap: wrap;
|
|
align-items: stretch;
|
|
gap: var(--space-sm);
|
|
}
|
|
|
|
/*
|
|
FNXC:ChatComposer 2026-08-20-19:25:
|
|
FN-076 requires Planner Chat to grow automatically through five rendered lines, internally scroll excess content, and shrink after deletion. Native mouse resizing is disabled and CSS leaves the controller's measured cap authoritative.
|
|
*/
|
|
.task-planner-chat-input {
|
|
box-sizing: border-box;
|
|
flex: 1 1 calc(var(--space-2xl) * 8);
|
|
min-width: min(100%, calc(var(--space-2xl) * 8));
|
|
min-height: calc(var(--space-2xl) + var(--space-sm));
|
|
max-height: none;
|
|
padding: var(--btn-padding);
|
|
overflow-y: hidden;
|
|
resize: none;
|
|
}
|
|
|
|
/*
|
|
FNXC:TaskDetailPlannerChat 2026-07-20-12:00:
|
|
FN-8421 extends the existing desktop/mobile composer parity to tablet, which
|
|
inherits these base rules. Send and streaming Stop share this class, so pin
|
|
its border-box block size and clear button padding to the textarea's compact
|
|
height rather than relying on the global button minimum.
|
|
*/
|
|
.task-planner-chat-send {
|
|
flex: 0 0 auto;
|
|
block-size: calc(var(--space-2xl) + var(--space-sm));
|
|
min-block-size: calc(var(--space-2xl) + var(--space-sm));
|
|
box-sizing: border-box;
|
|
padding: 0;
|
|
/* FN-7634: floor both the Send and Stop button variants (both carry this class) at the same width so the streaming swap never narrows or widens the control. */
|
|
min-inline-size: var(--chat-input-control-size);
|
|
}
|
|
|
|
@media (max-width: 768px) {
|
|
.task-planner-chat {
|
|
min-height: 0;
|
|
}
|
|
|
|
.task-planner-chat-expand-toggle {
|
|
min-inline-size: var(--space-2xl);
|
|
min-block-size: var(--space-2xl);
|
|
}
|
|
|
|
.task-planner-chat-expand-toggle--overlay {
|
|
top: var(--space-sm);
|
|
right: var(--space-sm);
|
|
}
|
|
|
|
.task-planner-chat-composer {
|
|
flex-direction: row;
|
|
flex-wrap: wrap;
|
|
align-items: flex-end;
|
|
gap: var(--space-xs);
|
|
}
|
|
|
|
.task-planner-chat-target-controls {
|
|
flex: 1 1 100%;
|
|
order: 0;
|
|
justify-content: flex-end;
|
|
}
|
|
|
|
.task-planner-chat-target-controls .model-combobox {
|
|
flex: 1 1 auto;
|
|
max-width: none;
|
|
}
|
|
|
|
.task-planner-chat-input {
|
|
flex: 1 1 auto;
|
|
min-width: 0;
|
|
min-height: calc(var(--space-2xl) + var(--space-lg));
|
|
max-height: none;
|
|
resize: none;
|
|
}
|
|
|
|
.task-planner-chat-starters {
|
|
grid-template-columns: 1fr;
|
|
}
|
|
|
|
.task-planner-chat-send {
|
|
--btn-icon-size: calc(var(--space-2xl) + var(--space-sm));
|
|
flex: 0 0 auto;
|
|
justify-content: center;
|
|
inline-size: calc(var(--space-2xl) + var(--space-lg));
|
|
min-inline-size: calc(var(--space-2xl) + var(--space-lg));
|
|
block-size: calc(var(--space-2xl) + var(--space-lg));
|
|
min-block-size: calc(var(--space-2xl) + var(--space-lg));
|
|
padding: 0;
|
|
}
|
|
|
|
/*
|
|
FNXC:TaskDetailPlannerChat 2026-07-01-08:09:
|
|
Mobile Planner Chat hides compact action text labels, but the shared thinking/streaming stop affordance draws its icon with `.chat-input-stop-icon` on a span. Exclude that icon span so the stop button never becomes an empty-looking shell while preserving the first-tap send/stop button contract.
|
|
|
|
FNXC:TaskDetailPlannerChat 2026-07-08-00:00:
|
|
FN-7685 removed the idle send button's visible "Send" text span (it now renders icon-only,
|
|
matching regular chat), so this rule no longer needs to hide send text. Kept as-is: it is
|
|
still the defensive guard governing any non-icon span on the shared `.task-planner-chat-send`
|
|
class, and it remains load-bearing for the streaming Stop button's FN-7594 icon-visibility
|
|
contract exercised by the FN-7634 parity tests.
|
|
*/
|
|
.task-planner-chat-send span:not(.chat-input-stop-icon) {
|
|
position: absolute;
|
|
width: 1px;
|
|
height: 1px;
|
|
padding: 0;
|
|
margin: -1px;
|
|
overflow: hidden;
|
|
clip: rect(0 0 0 0);
|
|
white-space: nowrap;
|
|
border: 0;
|
|
}
|
|
|
|
.task-planner-chat .chat-message {
|
|
max-width: 100%;
|
|
}
|
|
|
|
.task-planner-chat .chat-question-response {
|
|
margin-inline: 0;
|
|
}
|
|
}
|
|
|
|
@media (max-width: 768px) {
|
|
.task-planner-chat--full-width .chat-message {
|
|
width: 100%;
|
|
max-width: 100%;
|
|
align-self: stretch;
|
|
}
|
|
}
|