From 3d0819718221f885b61e73d02b43dcc853c7d02a Mon Sep 17 00:00:00 2001 From: Guilhem Date: Wed, 16 Sep 2026 14:46:49 +0200 Subject: [PATCH] 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 * fix: keep a malformed draft value from aborting the runnables list Co-Authored-By: Claude Fable 5.1 * fix: only a JSON boolean marks a draft flow as chat-enabled Co-Authored-By: Claude Fable 5.1 --------- Co-authored-by: Claude Fable 5.1 --- backend/windmill-api-flows/src/flows.rs | 5 ++++ backend/windmill-api/openapi.yaml | 13 ++++++++++ backend/windmill-api/src/runnables.rs | 25 +++++++++++++++---- backend/windmill-types/src/flows.rs | 5 ++++ .../components/common/table/FlowRow.svelte | 9 +++++++ .../lib/components/common/table/Row.svelte | 12 ++++++++- .../lib/components/flows/ChatFlowBadge.svelte | 20 +++++++++++++++ 7 files changed, 83 insertions(+), 6 deletions(-) create mode 100644 frontend/src/lib/components/flows/ChatFlowBadge.svelte diff --git a/backend/windmill-api-flows/src/flows.rs b/backend/windmill-api-flows/src/flows.rs index 525e3cca46..5ac0e0588c 100644 --- a/backend/windmill-api-flows/src/flows.rs +++ b/backend/windmill-api-flows/src/flows.rs @@ -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, diff --git a/backend/windmill-api/openapi.yaml b/backend/windmill-api/openapi.yaml index cf4f2cc41f..2cb8a0e831 100644 --- a/backend/windmill-api/openapi.yaml +++ b/backend/windmill-api/openapi.yaml @@ -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: diff --git a/backend/windmill-api/src/runnables.rs b/backend/windmill-api/src/runnables.rs index ea6d0c5bba..43ef23aff2 100644 --- a/backend/windmill-api/src/runnables.rs +++ b/backend/windmill-api/src/runnables.rs @@ -116,6 +116,10 @@ struct RunnableItem { use_codebase: Option, #[serde(skip_serializing_if = "Option::is_none")] has_deploy_errors: Option, + // 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, // app-only #[serde(skip_serializing_if = "Option::is_none")] raw_app: Option, @@ -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 ( \ diff --git a/backend/windmill-types/src/flows.rs b/backend/windmill-types/src/flows.rs index cb1c53a579..af183b462f 100644 --- a/backend/windmill-types/src/flows.rs +++ b/backend/windmill-types/src/flows.rs @@ -98,6 +98,11 @@ pub struct ListableFlow { pub deployment_msg: Option, #[serde(skip_serializing_if = "Option::is_none")] pub labels: Option>, + /// 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, /// 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)] diff --git a/frontend/src/lib/components/common/table/FlowRow.svelte b/frontend/src/lib/components/common/table/FlowRow.svelte index 8d88fe8f01..1eb0d35878 100644 --- a/frontend/src/lib/components/common/table/FlowRow.svelte +++ b/frontend/src/lib/components/common/table/FlowRow.svelte @@ -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 @@ {/if} +{#snippet chatBadge()} + +{/snippet} + {#snippet badges()} {#if flow.archived} diff --git a/frontend/src/lib/components/common/table/Row.svelte b/frontend/src/lib/components/common/table/Row.svelte index 77434d3c59..4fb90151c1 100644 --- a/frontend/src/lib/components/common/table/Row.svelte +++ b/frontend/src/lib/components/common/table/Row.svelte @@ -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 @@ {/if}
-
+
{#if customSummary} {@render customSummary?.()} {:else if marked} @@ -283,6 +292,7 @@ {:else} {!summary || summary.length == 0 ? displayPath : summary} {/if} + {@render titleBadge?.()}
{#if pathDisplay} diff --git a/frontend/src/lib/components/flows/ChatFlowBadge.svelte b/frontend/src/lib/components/flows/ChatFlowBadge.svelte new file mode 100644 index 0000000000..1787528269 --- /dev/null +++ b/frontend/src/lib/components/flows/ChatFlowBadge.svelte @@ -0,0 +1,20 @@ + + + + Chat +