feat: badge chat-input flows on the home list (#11164)

* feat: badge chat-input flows on the home list

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

* fix: keep a malformed draft value from aborting the runnables list

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

* fix: only a JSON boolean marks a draft flow as chat-enabled

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

---------

Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
Guilhem
2026-09-16 14:46:49 +02:00
committed by GitHub
co-authored by Claude Fable 5.1
parent c3e11bd223
commit 3d08197182
7 changed files with 83 additions and 6 deletions
+5
View File
@@ -154,6 +154,7 @@ async fn list_flows(
"favorite.path IS NOT NULL as starred",
"ws_error_handler_muted",
"o.labels",
"(o.value->>'chat_input_enabled')::bool as chat_input_enabled",
"draft.email IS NOT NULL as is_draft",
// Per-path draft owners as a JSON array; see scripts.rs for the rationale
// (non-member superadmin identity fallback via `password`, legacy NULL-email row).
@@ -301,6 +302,10 @@ async fn list_flows(
ws_error_handler_muted: None,
deployment_msg: None,
labels: None,
chat_input_enabled: v
.get("value")
.and_then(|fv| fv.get("chat_input_enabled"))
.and_then(|b| b.as_bool()),
// No deployed row to inherit folder labels from.
inherited_labels: None,
is_draft: true,
+13
View File
@@ -12010,6 +12010,13 @@ paths:
properties:
draft_only:
type: boolean
chat_input_enabled:
type: boolean
description: |
`chat_input_enabled` of the flow's value,
projected so the list can mark flows that open
as a chat. Omitted when the value has no such
field.
is_draft:
type: boolean
description: |
@@ -28951,6 +28958,12 @@ components:
type: boolean
has_deploy_errors:
type: boolean
chat_input_enabled:
type: boolean
description: >-
flow-only. `chat_input_enabled` of the flow's value, projected so
the list can mark flows that open as a chat. Omitted when the
value has no such field.
raw_app:
type: boolean
execution_mode:
+20 -5
View File
@@ -116,6 +116,10 @@ struct RunnableItem {
use_codebase: Option<bool>,
#[serde(skip_serializing_if = "Option::is_none")]
has_deploy_errors: Option<bool>,
// flow-only: projected from the value so the home list can badge flows that
// open as a chat without fetching every flow's value.
#[serde(skip_serializing_if = "Option::is_none")]
chat_input_enabled: Option<bool>,
// app-only
#[serde(skip_serializing_if = "Option::is_none")]
raw_app: Option<bool>,
@@ -274,7 +278,7 @@ fn branch_sqls() -> Branches {
o.ws_error_handler_muted, o.created_at as edited_at, \
o.hash, o.language::text as language, o.kind::text as script_kind, o.auto_kind, \
o.codebase IS NOT NULL as use_codebase, \
(o.lock_error_logs IS NOT NULL) as has_deploy_errors, \
(o.lock_error_logs IS NOT NULL) as has_deploy_errors, NULL::bool as chat_input_enabled, \
NULL::bool as raw_app, NULL::text as execution_mode, NULL::bigint as id, NULL::bigint as version, \
o.created_at as sort_time, lower(COALESCE(NULLIF(o.summary, ''), o.path)) as sort_name, o.hash as tiebreak \
FROM script o \
@@ -291,6 +295,7 @@ fn branch_sqls() -> Branches {
o.ws_error_handler_muted, o.edited_at, \
NULL::bigint as hash, NULL::text as language, NULL::text as script_kind, NULL::text as auto_kind, \
NULL::bool as use_codebase, NULL::bool as has_deploy_errors, \
(o.value->>'chat_input_enabled')::bool as chat_input_enabled, \
NULL::bool as raw_app, NULL::text as execution_mode, NULL::bigint as id, NULL::bigint as version, \
o.edited_at as sort_time, lower(COALESCE(NULLIF(o.summary, ''), o.path)) as sort_name, 0::bigint as tiebreak \
FROM flow o \
@@ -306,7 +311,7 @@ fn branch_sqls() -> Branches {
{draft_users}, o.labels, folder_labels(o.workspace_id, o.path) as inherited_labels, \
NULL::bool as ws_error_handler_muted, av.created_at as edited_at, \
NULL::bigint as hash, NULL::text as language, NULL::text as script_kind, NULL::text as auto_kind, \
NULL::bool as use_codebase, NULL::bool as has_deploy_errors, \
NULL::bool as use_codebase, NULL::bool as has_deploy_errors, NULL::bool as chat_input_enabled, \
av.raw_app, o.policy->>'execution_mode' as execution_mode, o.id, \
o.versions[array_upper(o.versions, 1)] as version, \
COALESCE(av.created_at, 'epoch'::timestamptz) as sort_time, lower(COALESCE(NULLIF(o.summary, ''), o.path)) as sort_name, 0::bigint as tiebreak \
@@ -344,11 +349,21 @@ fn draft_branch_sql(kind: &str) -> String {
let kind_cols = match kind {
"script" => {
"d.value->>'language' as language, d.value->>'kind' as script_kind, \
d.value->>'auto_kind' as auto_kind, false as raw_app"
d.value->>'auto_kind' as auto_kind, false as raw_app, \
NULL::bool as chat_input_enabled"
}
// Type-guarded rather than a bare `::bool` cast: draft JSON is stored
// unvalidated, so a malformed value must yield NULL, not abort the list,
// and the string "true" must not count as enabled.
"flow" => {
"NULL::text as language, NULL::text as script_kind, NULL::text as auto_kind, \
false as raw_app, \
CASE WHEN json_typeof(d.value->'value'->'chat_input_enabled') = 'boolean' \
THEN (d.value->'value'->>'chat_input_enabled')::bool END as chat_input_enabled"
}
_ => {
"NULL::text as language, NULL::text as script_kind, NULL::text as auto_kind, \
(d.typ = 'raw_app') as raw_app"
(d.typ = 'raw_app') as raw_app, NULL::bool as chat_input_enabled"
}
};
format!(
@@ -359,7 +374,7 @@ fn draft_branch_sql(kind: &str) -> String {
NULL::text[] as labels, NULL::text[] as inherited_labels, \
NULL::bool as ws_error_handler_muted, o.created_at as edited_at, \
NULL::bigint as hash, o.language, o.script_kind, o.auto_kind, \
NULL::bool as use_codebase, NULL::bool as has_deploy_errors, \
NULL::bool as use_codebase, NULL::bool as has_deploy_errors, o.chat_input_enabled, \
o.raw_app, NULL::text as execution_mode, NULL::bigint as id, NULL::bigint as version, \
o.created_at as sort_time, lower(COALESCE(NULLIF(o.summary, ''), o.draft_path, o.path)) as sort_name, 0::bigint as tiebreak \
FROM ( \
+5
View File
@@ -98,6 +98,11 @@ pub struct ListableFlow {
pub deployment_msg: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub labels: Option<Vec<String>>,
/// Projected from the flow value so a list can mark a flow that opens as a
/// chat without fetching every flow's value.
#[sqlx(default)]
#[serde(skip_serializing_if = "Option::is_none")]
pub chat_input_enabled: Option<bool>,
/// True when the authed user has a draft for this flow (draft-only or layered
/// over the deployed row). See ListableScript in scripts.rs.
#[serde(default)]
@@ -35,6 +35,7 @@
HistoryIcon
} from 'lucide-svelte'
import FlowHistory from '$lib/components/flows/FlowHistory.svelte'
import ChatFlowBadge from '$lib/components/flows/ChatFlowBadge.svelte'
import InheritedLabels from '$lib/components/InheritedLabels.svelte'
import { getDeployUiSettings } from '$lib/components/home/deploy_ui'
import { editInForkAllowed, editInForkLabel, onEditInForkClick } from '$lib/utils/editInFork'
@@ -48,6 +49,9 @@
draft_path?: string
draft_users?: { username?: string | null }[]
canWrite: boolean
/** Projected from the flow value by the listing; a chat-input flow opens
* as a conversation and is badged as such. */
chat_input_enabled?: boolean
}
marked: string | undefined
shareModal: ShareModal
@@ -124,6 +128,10 @@
<FlowHistory bind:this={flowHistory} path={flow.path} />
{/if}
{#snippet chatBadge()}
<ChatFlowBadge />
{/snippet}
<Row
aiId={`flow-row-${flow.path}`}
aiDescription={`Button to access the form to run the flow ${flow.summary ?? flow.path}`}
@@ -140,6 +148,7 @@
canFavorite={!flow.draft_only}
{depth}
{rowSelection}
titleBadge={flow.chat_input_enabled ? chatBadge : undefined}
>
{#snippet badges()}
{#if flow.archived}
@@ -67,6 +67,9 @@
badges?: import('svelte').Snippet
actions?: import('svelte').Snippet
customSummary?: import('svelte').Snippet
/** Rendered inline right after the title, unlike `badges`, which sit in
* their own column and are hidden below `lg`. */
titleBadge?: import('svelte').Snippet
/** Overrides the secondary path line (e.g. to strike a renamed path).
* Falls back to the plain `path` string when not provided. */
pathDisplay?: import('svelte').Snippet
@@ -101,6 +104,7 @@
badges,
actions,
customSummary,
titleBadge,
pathDisplay,
onSelect = () => {}
}: Props = $props()
@@ -275,7 +279,12 @@
</div>
{/if}
<div class="grow min-w-0">
<div class="text-emphasis flex-wrap text-left text-xs font-semibold">
<div
class={twMerge(
'text-emphasis flex-wrap text-left text-xs font-semibold',
titleBadge ? 'inline-flex items-center gap-2' : ''
)}
>
{#if customSummary}
{@render customSummary?.()}
{:else if marked}
@@ -283,6 +292,7 @@
{:else}
{!summary || summary.length == 0 ? displayPath : summary}
{/if}
{@render titleBadge?.()}
</div>
<div class="text-hint text-3xs truncate text-left font-normal" title={path}>
{#if pathDisplay}
@@ -0,0 +1,20 @@
<script lang="ts">
import { MessageSquare } from 'lucide-svelte'
import Badge from '$lib/components/common/badge/Badge.svelte'
interface Props {
title?: string
}
let { title = 'Chat input enabled: this flow opens as a conversation' }: Props = $props()
</script>
<Badge
small
color="gray"
class="px-1.5 shrink-0"
icon={{ icon: MessageSquare, position: 'left' }}
{title}
>
Chat
</Badge>