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
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
setCollectionsExpanded,
setNodeTypesExpanded,
setAiChatsExpanded,
setPlaysExpanded,
type NavigationItem
} from '$lib/stores/layout.svelte';
import { navigationStore, setActiveTab, addTab } from '$lib/stores/navigation.svelte';
Expand All @@ -20,12 +21,14 @@
import { schemasStore, schemasData } from '$lib/stores/schemas.svelte';
import { aiChatsData } from '$lib/stores/ai-chats.svelte';
import { savedQueriesData } from '$lib/stores/saved-queries.svelte';
import { playsData } from '$lib/stores/plays.svelte';
import { labsFlags } from '$lib/stores/labs-flags.svelte';
import {
clearCollectionRefreshTimer,
clearSchemaRefreshTimer,
clearAiChatRefreshTimer,
clearSavedQueryRefreshTimer
clearSavedQueryRefreshTimer,
clearPlayRefreshTimer
} from '$lib/utils/collection-refresh';
import { aiChatDisplayTitle } from '$lib/utils/ai-chat-title';
import QueryIcon from '$lib/design/icons/components/query-icon.svelte';
Expand Down Expand Up @@ -61,6 +64,8 @@
let nodeTypesExpanded = $derived(layoutStore.state.nodeTypesExpanded);
// AI Chats expanded state from layout store (persisted)
let aiChatsExpanded = $derived(layoutStore.state.aiChatsExpanded);
// Plays expanded state from layout store (persisted)
let playsExpanded = $derived(layoutStore.state.playsExpanded);

// Collections state from collections store (UI-only, not persisted)
let subPanelOpen = $derived(collectionsState.state.subPanelOpen);
Expand Down Expand Up @@ -133,13 +138,20 @@
let createChatBusy = $derived(aiChatsData.createBusy);
let createChatError = $derived(aiChatsData.createError);

// Load collections, schemas, and AI chats from backend on mount
// Plays and the state each row shows, from the global store (reactive:
// reloaded on play node events, and read live for a play open in a viewer)
let plays = $derived(playsData.plays);

const PLAY_STATE_LABELS = { on: 'On', off: 'Off', suspended: 'Suspended' } as const;

// Load collections, schemas, AI chats and plays from backend on mount
onMount(() => {
databaseStore.load();
collectionsData.loadCollections();
schemasData.loadSchemas();
aiChatsData.loadAiChats();
savedQueriesData.loadSavedQueries();
playsData.loadPlays();
});

// Cancel any pending debounced refreshes when the sidebar is destroyed
Expand All @@ -149,6 +161,7 @@
clearSchemaRefreshTimer();
clearAiChatRefreshTimer();
clearSavedQueryRefreshTimer();
clearPlayRefreshTimer();
});

// Element references for click-outside detection
Expand Down Expand Up @@ -262,6 +275,11 @@
}
}

/** Open (or focus) a play in its viewer, where its switch is (ADR-090). */
function handlePlayClick(playId: string, nodeType: string) {
getNavigationService().focusOrOpenNode(playId, { nodeType });
}

/**
* Open (or focus) the Search view. Search is not a node, so it opens a
* singleton non-node tab — mirroring how Settings is opened from the menu —
Expand Down Expand Up @@ -723,6 +741,59 @@
{/if}
{/if}

<!-- Plays section (after AI Chats) - accordion toggle -->
<!-- Gated behind the Labs "Playbooks" toggle (default OFF). The gate hides
this list only: plays run whether or not it shows. -->
{#if labsFlags.playbooksEnabled}
{#if !isCollapsed}
<Collapsible.Root open={playsExpanded} onOpenChange={(open) => setPlaysExpanded(open)}>
<Collapsible.Trigger
class="nav-item"
aria-label={playsExpanded ? 'Collapse Plays' : 'Expand Plays'}
>
{@render playsIcon()}
<span class="nav-label">Plays</span>
</Collapsible.Trigger>

<Collapsible.Content>
<div class="play-list">
{#if plays.length === 0}
<span class="schema-type-empty">No plays yet</span>
{:else}
<!-- A row shows the play's state and opens it. The switch is in
the viewer, so a row writes nothing. -->
{#each plays as play (play.id)}
<button
class="play-item"
data-testid="play-item"
title={play.suspendedMessage}
onclick={() => handlePlayClick(play.id, play.nodeType)}
>
<span class="play-name">{play.title}</span>
<span class="play-state" data-state={play.state}>
{PLAY_STATE_LABELS[play.state]}
</span>
</button>
{/each}
{/if}
</div>
</Collapsible.Content>
</Collapsible.Root>
{:else}
<!-- Collapsed state: just show icon -->
<button
class="nav-item"
title="Plays"
onclick={() => {
toggleSidebar();
setPlaysExpanded(true);
}}
>
{@render playsIcon()}
</button>
{/if}
{/if}

<!-- Remaining nav items (Search, Settings) -->
{#each navItems.slice(1) as item}
{@render navItemButton(item)}
Expand All @@ -749,6 +820,23 @@

</nav>

{#snippet playsIcon()}
<svg
class="nav-icon"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
>
<!-- Two steps joined by a connector: a rule running from trigger to action -->
<rect x="3" y="3" width="8" height="8" rx="2" />
<path d="M7 11v4a2 2 0 0 0 2 2h4" />
<rect x="13" y="13" width="8" height="8" rx="2" />
</svg>
{/snippet}

{#snippet typeRow(schema: SchemaNode)}
{@const views = savedQueriesData.forType(schema.id)}
{@const viewsExpanded = layoutStore.isTypeViewsExpanded(schema.id)}
Expand Down Expand Up @@ -1267,4 +1355,65 @@
background: none;
color: hsl(var(--destructive));
}

/* Plays list (expanded content) - mirrors ai-chat-list */
.play-list {
display: flex;
flex-direction: column;
padding: 0;
margin: 0 -1rem;
overflow-y: auto;
max-height: calc(10 * 28px); /* ~10 items tall before scrolling, matches collection-list */
scrollbar-width: thin;
background: hsl(var(--active-nav-background));
}

.play-list::-webkit-scrollbar {
display: none;
}

/* Play row - mirrors ai-chat-item, with the play's state at its right edge */
.play-item {
display: flex;
align-items: center;
gap: 0.5rem;
padding: 0.4rem 1rem 0.4rem 3.5rem;
background: none;
border: none;
cursor: pointer;
text-align: left;
color: hsl(var(--muted-foreground));
font-size: 0.8125rem;
width: 100%;
flex-shrink: 0;
}

.play-item:hover {
background: hsl(var(--border));
color: hsl(var(--foreground));
}

.play-name {
flex: 1;
min-width: 0;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}

/* State label: the same words and colours the play's viewer uses. The word
carries the state; the colour only repeats it. */
.play-state {
flex-shrink: 0;
font-size: 0.6875rem;
color: hsl(var(--muted-foreground));
}

.play-state[data-state='on'] {
color: hsl(var(--success));
}

.play-state[data-state='suspended'] {
color: hsl(var(--warning));
}
</style>
12 changes: 12 additions & 0 deletions packages/desktop-app/src/lib/components/play/play-node-model.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,13 @@ import type {
Selector,
Trigger
} from '$lib/types/generated';
import type { Node } from '$lib/types/node';
import { isA } from '$lib/types/core-node-types';

/** `node` as the play it is, or `undefined` when it is missing or not a play. */
export function asPlayNode(node: Node | undefined): PlayNode | undefined {
return node && isA(node.nodeType, 'play') ? (node as unknown as PlayNode) : undefined;
}

/** A play's state as its header shows it. */
export type PlayState = 'on' | 'off' | 'suspended';
Expand All @@ -26,6 +33,11 @@ export function playState(play: Pick<PlayNode, 'enabled' | 'suspendedAt'>): Play
return play.suspendedAt ? 'suspended' : 'on';
}

/** The name a play is shown by: its title, or a placeholder while it has none. */
export function playTitle(play: Pick<PlayNode, 'title' | 'content'>): string {
return play.title || play.content || 'Untitled play';
}

/** The noun a selector's nodes are named by: `task`, `node the saved query selects`. */
function selectorNoun(select: Selector): string {
if ('query_id' in select) return 'node the saved query selects';
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -46,8 +46,8 @@
/>
</div>
<p class="text-muted-foreground m-0 text-sm leading-relaxed">
Show the Playbooks settings, where you install and manage playbooks. Playbooks you have
already installed keep running whether or not this is on.
Show the Plays section in the sidebar, which lists your plays and whether each is on.
Plays keep running whether or not this is on.
</p>
</CardHeader>
</Card>
Expand Down

This file was deleted.

Original file line number Diff line number Diff line change
Expand Up @@ -30,16 +30,14 @@ export interface CoreSettingsCategory {

/**
* Core's categories in display order. "AI Models" is gated behind the Labs "AI
* Chat" toggle and "Playbooks" behind the Labs "Playbooks" toggle (both default
* off); the flags gate the UI only.
* Chat" toggle (default off); the flag gates the UI only.
*/
export const CORE_SETTINGS_CATEGORIES: readonly CoreSettingsCategory[] = [
{ id: 'database', label: 'Database' },
{ id: 'display', label: 'Display' },
{ id: 'ai-models', label: 'AI Models', visible: () => labsFlags.aiChatEnabled },
{ id: 'import', label: 'Import Sources' },
{ id: 'integrations', label: 'Integrations' },
{ id: 'playbooks', label: 'Playbooks', visible: () => labsFlags.playbooksEnabled },
{ id: 'labs', label: 'Labs' },
{ id: 'about', label: 'About' }
];
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,6 @@
import DiagnosticsSettings from './sections/diagnostics-settings.svelte';
import ModelManager from './model-manager.svelte';
import IntegrationsSettings from './sections/integrations-settings.svelte';
import PlaybooksSettings from './sections/playbooks-settings.svelte';
import LabsSettings from './sections/labs-settings.svelte';

const initial = settingsStore.initialCategory;
Expand Down Expand Up @@ -53,8 +52,6 @@
<ModelManager />
{:else if activeCategory === 'integrations'}
<IntegrationsSettings />
{:else if activeCategory === 'playbooks'}
<PlaybooksSettings />
{:else if activeCategory === 'labs'}
<LabsSettings />
{:else if activeCategory === 'about'}
Expand Down
Loading