Skip to content

feat(workflows): start a run from the selected version's own form - #70

Merged
EricWvi merged 8 commits into
ora-space:mainfrom
zhangshilong123:feat/148-workflow-start-form
Oct 21, 2026
Merged

EricWvi merged 8 commits into
ora-space:mainfrom
zhangshilong123:feat/148-workflow-start-form

Conversation

@zhangshilong123

@zhangshilong123 zhangshilong123 commented Oct 16, 2026 •

Copy link
Copy Markdown
Contributor

Refs ora-space/circus#148

What this changes

The editor's run button created a run immediately with an empty body, so a run executed whatever
the published version happened to default to and the member never saw what that version asks for.
The button now opens a dialog: choose the Project, choose the version, answer what it asks for,
then start.

A run executes one frozen version, so the form is projected from that version's own graph — the
snapshot list already returns graph, so no new server route is involved. Switching versions
remounts the form, which is what discards a previous version's answers rather than carrying them
onto fields that may no longer exist.

The Project is one of the two choices the form is framed by, and it is stated rather than inferred:
a workflow belongs to a space, so nothing else could name the Project its Workspace provisions in.
No Project stands selected until the member picks one, and Start stays disabled until then; a space
with no live Project says so instead of offering an empty picker.

Eight commits, split by purpose:

  • 1f5f05a — runtime/run-input.ts, the pure projection (which fields the version asks for, which
    are still unanswered, what the request's input is). No React, no Issues types, so the
    Workflows feature does not grow a dependency on Issues.
  • a00ecc2 — the dialog, the toolbar wiring, the fault messages and the tests.
  • 08ac5c0 — the bounds the server puts on that projection, mirrored on this side, with the cases
    that hold each one.
  • af31568 — the select choices the server keeps, offered and defaulted the same way.
  • 7a9923d — the kickoff prompt's default held to the same value bound a declared default is, so a
    prompt past it opens the field empty rather than clipped.
  • 38772d0 — the platform's own field labelled in the reader's language rather than by its key.
  • 086b46c — the Project picker: the space's live Projects, the chosen one on the request body,
    Start disabled until one is chosen, and the guidance a Project-less space shows.
  • 2f310eb — the integration read-back of the frozen project_id from the run row.

How the projection lines up with the server

run-input.ts mirrors the server's own projection so the form and the admission gate cannot
disagree about what a variable is called, what type it takes, whether it is required, or how long
an answer may be:

Server Client
workflowStartVariables → startInputField declared variables, control per field type
appendGlobalVariableFields dotted global variables, after declared ones
injectPlatformFields (prompt entry) the prompt field, defaulted from the Start node's kickoff prompt
validFieldValue acceptsValue
workflowStartVariables (unnamed skipped) unnamed variables skipped
maxFormFields the one descriptor bound the declared variables, the globals and the prompt share
validOpaqueToken, len(label) <= 200 isFieldKey, and the label check in startVariableField
maxFieldValueLength the value bound acceptsValue applies to a text default

The collision rules match too: an author-declared variable named prompt keeps the key and takes
the Start node's initial prompt as its default, and a declared variable wins over a same-named
global.

Three of those bounds are shared rather than per-field, and the sharing is the point. The server
appends the globals and its own platform fields to the author's declared list, so one descriptor
bound covers all three: the declared variables fill it first, the globals take what is left, and
the kickoff prompt — the platform's own field and the last one added — is what gives way, which is
exactly where the server stops adding its own fields rather than emit a descriptor it would refuse.
A variable the server drops is dropped here rather than rewritten or clipped: a name outside the
grammar is dropped because the confirmed values reach the workflow keyed by name, so a sanitized key
would feed the run the wrong variable, and an over-long label is dropped because that is what
startInputField does with it.

All three count UTF-8 bytes, which is what Go's len counts, so they are byte checks rather than
character checks: 67 Chinese characters are over the label bound where 200 ASCII ones are not, and
1334 Chinese characters are over the value bound where 4000 ASCII ones are not.

Deliberately not in this PR

  • repository, branch, context_refs. platformFormFields defaults all three from the issue
    context, and a manual run has no issue. This entry names a project instead — the repository and
    branch follow from it, and context_refs has nothing to offer without an issue — so none of the
    three is a field here. prompt is asked for, because it is well defined without an issue: the
    Start node's kickoff prompt is its default.
  • workflow_version_fields_changed. The gate raises it on the @ confirm path
    (issue_interactions.go:146); a direct run never takes that path, so no message mapping was added
    for a code this surface cannot receive.

Evidence

npm run check is green on the rebased branch: format, lint, typecheck, 1206 tests in 142 files
with coverage above the thresholds, check:modules --base upstream/main, exports-documented, knip,
jscpd (0 clones), build. The branch is rebased onto the current main (ff72afa, which carries the
projectId admission gate), so its CI result is fresh rather than carried over from 945aa3f.

The dialog's own cases cover the newest version and its prefilled defaults, the version switch
replacing the fields (and keeping the Project), the required gate refusing to submit, a server
refusal reported on the form with nothing created, a retry replaying one idempotency key, the
platform's own field labelled in the reader's language rather than by its key, Start staying
disabled until a Project is chosen, and a space with no Project showing its guidance with no
submit. The toolbar cases go through the dialog and assert the body the server actually receives —
projectId, snapshotId plus the answers — and the app-routes case still proves the started run
lands on the real run page under the workspace slug rather than the tenant UUID. The Go integration
test reads workflow_runs.project_id back from the row (the run summary does not project it) and
compares it with the Project the request named.

The bound cases are eight, one per rule: the hundred-and-first variable is not projected and the
hundredth is, the prompt gives way when the author's fields fill the descriptor, a global gets no
room in a full descriptor, the name grammar (including a padded name, which the server does not trim
into a field), the label bound on both sides of the byte boundary, the value bound on both sides of
it, and the kickoff prompt's default on both sides of that same value bound. Each rule was falsified
by removing it: counting characters instead of bytes fails exactly the two byte-sensitive cases,
dropping the grammar fails exactly the two name cases, removing the descriptor bound fails exactly
the three bound cases, and dropping the default's bound fails exactly the prompt case.

This is fixture-level evidence only. It shows the interaction and the request; it does not show a
real execution, and no browser/HTTP run was performed.

Status

This is ora-space/circus#148, the run of #72. The dependency this run was waiting on has landed:
project_id is on workflow_runs and the direct-run admission gate requires it, so the three items
the review asked for (Project picker, projectId in the request body, read-back of it) are in this
branch and ready for re-review.

Specs: this implements part of
decisions/cloud/workflow/0-cloud-side-scheduler-owns-workflow-execution.md, whose status stays
approved — the ADR becomes implemented only when the executor and its real-boundary acceptance
land.

@Zpc001 Zpc001 left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Approve

表单从被选中的那一版快照投影、答案按那一版的键提交、必填只当提前提示(服务端仍按同一份冻结文档复检),这几点都对;snapshotId 用的是新键。我按服务端的 workflowInputFields(internal/core/workflow_collaboration.go:186)和 startInputField / appendGlobalVariableFields 逐条对照了这份 TypeScript 投影,降级规则是逐项一致的:

  • file → 单行文本、file-list / json → 多行文本、没有选项的 select → 文本(startInputField 的 case "file-list","json" 与 len(options)==0 两个分支);
  • 全局变量必须含点、且不与已声明字段重名;
  • 默认值只在控件渲染得出来时才采用:validFieldValue 对 number / boolean / select 的要求与 acceptsValue 一致;
  • 声明了 prompt 键时平台不再提供自己的 prompt 字段,且把 Start 节点的初始 prompt 当作它的默认值。

下面三条是同一份投影对不上的地方,都不阻塞合并,但都是"表单问了一个服务端会丢掉的问题",建议一并补:

  1. 字段数上限没镜像。 internal/core/form_descriptor.go:59 是 maxFormFields = 100,workflowInputFields 和 appendGlobalVariableFields 都在这里截断;表单侧没有上限。图里声明超过 100 个变量时,表单会多问,而服务端的 requireWorkflowRunInput 只按它自己那 100 个字段检查、未知键直接忽略(internal/core/workflow_admission.go:61),多出来的答案会静默丢弃。
  2. 变量名的合法性没镜像。 服务端用 validOpaqueToken(form_descriptor.go,只接受 [A-Za-z0-9._:@-] 且长度 ≤200)过滤声明的 Start 变量,名字不合规的字段直接不投影;displayName 超过 200 字符也丢。发布时不校验这些名字(internal/core/workflow_graph.go 里没有 inputVariables 相关校验),所以真能构造出这类图:表单会把它渲染成(甚至可能是必填的)问题,而服务端从来不认这个键,答案哪儿都没去。
  3. 文本长度上限没镜像。 maxFieldValueLength = 4000(form_descriptor.go:53)是服务端对单行/多行文本的长度门禁,前端没有对应提示。

另外一个是设计上的问题:仓库里已经有生成好的客户端方法 GET /api/v1/tenants/{tid}/workflows/{wfid}/snapshots/{snapshotId}/form(frontend/src/api/workflows/workflows.ts:1068,服务端实现在 snapshotFormDescriptor)。对话框选的正是某一版快照,这个接口刚好是同一份投影的服务端版本。现在把 workflowInputFields 手抄了一份到 TypeScript,两边必须永远保持一致(这份 PR 自己的注释也这么写),上面三条就是它的第一次分叉,而且没有任何测试把两边钉在一起。要不要改成取服务端那份投影、前端只负责渲染,值得在这个 PR 或后续里定一下;如果决定继续各自实现,建议至少加一条对比测试,用固定的图断言两边的字段集合一致。

(背景:这份分支基于 7704089,早于 #65;snapshotId 的读写和上面那个 form 接口都已在 df507f0 上,投影口径我按 df507f0 核的。)

Refs ora-space/circus#148

🤖 Generated with Claude Code

@Zpc001 Zpc001 left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Request changes(更正我自己的上一条 Approve)

作者把 repository / branch 的落位问题交给我定之后,我重读了根决策和执行身份用例,我上一条 Approve 下早了:投影和快照选择部分我仍然认可(下面那三条分叉也都成立、不阻塞),但这个入口少了一样已批准决策要求的东西——项目的选择,缺它这条入口跑不起来。

这是已批准决策的原文要求,不是待决契约

specs/decisions/cloud/workflow/0-cloud-side-scheduler-owns-workflow-execution.md 的 D3 冻结表原文:

| Project | 新增 workflow_runs.project_id —— 运行 Workspace 的归属;直接运行必须在请求体给出并通过调用者权限校验 |

对照用例 specs/test-cases/cloud/workflow/execution-identity-and-invocation.md#the-execution-identity-is-frozen-in-the-transaction-that-creates-the-run 的义务也是"发起者、Project、Workspace 归属、逐槽位绑定与插件版本一并冻结,缺一即回滚"。所以直接运行的创建请求必须带项目、并在创建事务里冻结,这一条不需要新决策,也不需要 owner 再拍一次。

载体是 project,不是 input.repository / input.branch。 首期这条入口的机制决策只定了一个:请求体给出 Project。repository / branch 是 issue 侧的平台启动字段(repository = issue 的 project_ref → projects.repository_url,branch 取同一项目的 default_branch),直接运行没有 issue,把这两个键塞进 input 会与 IssueRun 的解析方式分叉:IssueRun 的项目来自 issue、检出 ref 来自项目的 default_branch(createRunWorkspace / createWorkflowRunWorkspace 都是 p := projects(...) 后取 p.defaultBranch,default_branch_required 是它自己的拒绝码)。直接运行应与之一致:选项目,仓库地址与分支由项目决定;首期 Workspace 就按项目默认分支检出,所以表单不必给 branch。要让 branch 可改是新增语义,那才需要 owner。

context_refs 不收是对的,与 §38.37c 规则 5 一致(候选项只来自 issue 的 project / parent,没有 issue 就没有可选项)。

因此请追加这三样

  1. 对话框在快照选择之外再选一个项目(列表来自调用者可见的项目;不是让用户填仓库地址)。
  2. 创建运行请求体带上该项目,落在 #68 已经加好的 workflow_runs.project_id 上——那条冻结列现在只有 @ 入口会写,createWorkflowRun 的注释里写着"project_id stays unset on this path"。不补这一步,从这个对话框建出来的运行 project_id 为空,createWorkflowRunWorkspace 会以 409 workflow_run_project_required 诚实地拒绝它,也就是永远执行不了(#68 的注释把 editor path 记成"今天未绑定",本 issue 正是来补它的)。
  3. 验收第 4 条那条"API/PG 回读的 snapshotId/input 与用户选择一致"里,把 projectId 一并回读断言。

#72 的交付边界原文是"在现有运行入口提供快照选择、所选版本 Start 输入和批准的 prompt/项目或仓库/分支/global 参数",验收第 2 条是"必填/类型/默认值及批准的启动参数可填写并正确发送",这两条本来就覆盖它——所以这不是本运行能自己决定的范围问题,是本运行按范围该做的工作。

前三条分叉(字段数上限 100、validOpaqueToken 的变量名规则与 200 字符 label、maxFieldValueLength = 4000)仍然只是建议,和这次要改的东西一起做或另开都行。

Refs ora-space/circus#148

🤖 Generated with Claude Code

@Zpc001 Zpc001 left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

审的是 4aa80362ffd8d00b823868b95022ccefcae0ee1c。投影部分我逐条对照服务端核过,正确;结论是 Request changes,但只有一处真要我改的东西,其余是等 #68 的延迟项。

必须改(1 处,很小)

promptFields(runtime/run-input.ts)对 kickoff prompt 的默认值没有 4000 字节上限:if (initial !== '') field.defaultValue = initial 会预填完整 prompt。而你 09:40 那条评论说"超 4000 字节就不预填",只有 startVariableField 的作者自声明 prompt 路径(经 acceptsValue)是这么做的,平台自有 prompt 字段这条路径不是。所以现在两条 prompt 路径不一致,代码和你写的结论也对不上。

改成和 startVariableField 一致:默认值只在 utf8Length(initial) <= MAX_FIELD_VALUE 时预填,超了不预填(让运行回落到图自己的完整 prompt)。我认可"不预填"这个取向,就按你说的这个做,不用改成 clip。

已核对正确

  • 投影与 workflowInputFields / startInputField / appendGlobalVariableFields / globalVariableField / inputOptions / validFieldValue 逐条一致;四个边界(100 字段、100 选项、200 字节 key/label、4000 字节值)都按 UTF-8 字节算,validOpaqueToken 语法一致。
  • select 选项过滤(非字符串 / 空串 / >200 / 重复 / >100)与 inputOptions 顺序一致,dup 不占额度。
  • 快照选择 + 切版本重挂表单(key={selected.id})→ 验收 1。
  • 幂等:请求对象 memo 化 + idempotencyKeyFor 按同一对象复用 → 验收 3。
  • 三个 fault 码映射正确,workflow_version_fields_changed 正确地没加。
  • workspace slug 路由正确 → 验收 4。
  • snapshotId 走请求体(workflowId 作 path param 正确消费,没进 body)。

仍卡 #68 的三条(不怪你,等 #68)

项目选择器、请求体 projectId、验收 4 回读断言加 projectId,都等 workflow_runs.project_id 落到 main。你 09:30 在 #141 提的"把 createWorkflowRun 接受可选 projectId 并进 #68"我看到了,等 Kyber5323 定。#68 落地后你 rebase 补上这三条,我再复审。

非阻塞建议

prompt 字段的 label 是字面 "prompt"(PROMPT_FIELD_KEY),@ 表单里是"提示词"。建议给个本地化 label("提示词" / "Prompt"),不是 gate 问题。

@Zpc001 Zpc001 left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Approve

复审 945aa3ffeb3e3406136869857e55618a7a78558b(基于 df507f0,MERGEABLE/CLEAN)。上一轮两处都已改对,没有新的问题。

一、kickoff prompt 默认值(必须改的那处)✅

promptFields 现在是 if (initial !== '' && acceptsValue(field.control, [], initial)) field.defaultValue = initial。acceptsValue 对 textarea 走 default 分支 typeof value === 'string' && utf8Length(value) <= MAX_FIELD_VALUE(4000 字节),与 startVariableField 的上限一致。行为正是上轮要的:≤4000 字节预填、超了不预填(回落到图自己的完整 prompt)。

保留 initial !== '' 是对的,不是多余:acceptsValue('textarea', [], '') 为 true,去掉它会把「没有 prompt」播种成 defaultValue: '',与函数「缺席 prompt = 无默认值」的约定相反。字节边界两侧都测了(4000 ASCII 预填 / 4001 不预填 / 1334 汉字=4002 字节不预填),反向证伪也挂了。

二、prompt label 本地化(非阻塞那条)✅

  • 投影层给平台自己的字段加 labelKey: 'workflows.launchFields.prompt';作者声明的变量不带这个键、label 原样显示——两种来源在投影层就分开了,界面层不用猜。测试断言 prompt?.labelKey 命中、declared?.labelKey 为 undefined。
  • fieldLabel 统一解析,runFormDescriptor 的字段 label 和「还需要填写」那一行都走它,两处不会对不上。
  • useWorkflowTranslator 是 useCallback 稳定引用,作 useMemo 依赖成立;键 workflows.launchFields.prompt 在 launch-field-translations.ts 里定义为「提示词」,由 translations.ts spread 进 i18n bundle,t() 能翻到。测试 findByLabelText(/^提示词/) 命中、/^prompt/ 不再命中。

仍等 #68 的三条(上轮已确认,非本 PR)

项目选择器、请求体 projectId、验收第 4 条回读断言——cloud#68 仍 OPEN,project_id 未落 main,这三条不怪本 PR。等 #68 合入后 rebase 补上再复审。

Refs ora-space/circus#148

creegon2 added a commit to creegon2/cloud that referenced this pull request Oct 17, 2026
Reuse the real .orax, HTTPS marketplace and PostgreSQL harness to prove imported workflows enter the Issue target directory and their selected snapshot forms remain stable across administrators and import-trigger deactivation. Compare pending run bindings across those identities.

Verify Confirm links its IssueRun to a WorkflowRun bound to the imported snapshot, direct starts select the same snapshot, and editing the draft cannot replace the frozen graph. After provider uninstall, preserve the old snapshot, publish a new version and create a run from it.

Evidence stops at WorkflowRun creation and binding; real node execution remains with ora-space#70. No import implementation or specs changes.

Validation: go build ./..., the core package, three new import invocation cases and 23 existing invocation/confirm/assets regressions with 30-minute timeouts. The merge has no overlap with this branch's frontend edits, so the conditional frontend gate does not apply.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
zhangshilong123 and others added 8 commits October 20, 2026 21:58
…ual run

The editor's run button sends an empty body, so a run executes whatever the
published version happened to default to and the member never sees what that
version asks for. The version's frozen graph is already client-visible — the
snapshot list returns it — so the form can be projected here instead of from a
new server route (ora-space/circus#148).

run-input.ts is that projection and nothing else: given an envelope it answers
which fields the version asks for, which are still unanswered, and what the
request's `input` is. It mirrors the server's own projection
(workflowInputFields, appendGlobalVariableFields, injectPlatformFields) so the
form and the admission gate cannot disagree about what a variable is called,
what type it takes or whether it is required. It holds no React and no Issues
types, so the projection stays testable on its own and the Workflows feature
does not grow a dependency on Issues.

Evidence: 19 cases over envelopes built by the real parseWorkflowGraphValue —
the prompt leads and is prefilled from the Start node's kickoff prompt, a
declared variable wins over a same-named global, an author-declared `prompt`
keeps its key and takes that same initial prompt, a select with no options
degrades to text rather than failing closed, unnamed variables are skipped,
`false` and `0` are sent while blank is not, and required gating reads the
author's declaration over the platform fallback.

Co-Authored-By: Claude Code <noreply@anthropic.com>
The toolbar's run button created a run immediately with an empty body. A run
executes one frozen version, so the member has to see which version and answer
what it asks for before anything is created; the button now opens a dialog that
does both (ora-space/circus#148).

The dialog reads the versions from the snapshot list, opens on the newest, and
remounts the form per version — which is what discards a previous version's
answers instead of carrying them onto fields that may no longer exist. The
request object is built once per set of answers and reused for every attempt, so
a retry replays one idempotency key and reaches the run the first attempt
created rather than starting a second one. A refusal stays on the form: the
dialog closes only once the server has returned a run, and the member navigates
to that run's page.

The three fault codes a direct run can raise through the admission gate
(required_field_missing, invalid_field_value, workflow_executor_unconfigured)
now have messages. workflow_version_fields_changed is deliberately absent: the
gate raises it on the @ confirm path, which a manual run never takes.

`repository`, `branch` and `context_refs` are not asked for. The platform
defaults all three from the issue context, which a manual run has none of, so
where they belong in a direct run's input is an open contract question for
ora-space/circus#69 rather than something to settle here. `prompt` is asked for
because it is well defined without an issue: the Start node's kickoff prompt is
its default.

Evidence: the dialog's own cases cover the newest version and its prefilled
defaults, the version switch replacing the fields, the required gate refusing to
submit, a server refusal reported on the form with nothing created, and a retry
replaying one idempotency key. The toolbar cases now go through the dialog and
assert the body the server actually receives — `snapshotId` plus the answers —
and the app-routes case still proves the started run lands on the real run page
under the workspace slug rather than the tenant UUID.

Co-Authored-By: Claude Code <noreply@anthropic.com>
The manual run form projects the fields from the version's frozen graph itself,
and it applied none of the bounds the server applies to the same projection
(ora-space/circus#148). It asked about variables the server drops, opened on
defaults the gate refuses, and had no field bound at all — so the two sides could
disagree about which fields exist, which is either a question whose answer never
reaches the run or an answer the run refuses.

The bounds are the server's own: `maxFormFields` (100), the 200-byte bound it
puts on a field key and on a field label alike, and `maxFieldValueLength`
(4000). A variable whose name is outside `validOpaqueToken` is now dropped rather
than trimmed into acceptance, because the server drops it — the confirmed values
reach the workflow keyed by name, so a rewritten key would feed the run the wrong
variable. A variable whose label is over the bound is dropped rather than
clipped, which is also what the server does, and the check reads the declared
display name untrimmed because that is the value the server decides from.

All three count UTF-8 bytes, which is what Go's `len` counts, so the label and
value checks are byte checks: sixty-seven Chinese characters are over the label
bound where two hundred ASCII ones are not, and 1334 Chinese characters are over
the value bound where 4000 ASCII ones are not.

The descriptor bound is shared: the declared variables fill it first and the
dotted globals take whatever room is left, because the server appends them to one
list. The kickoff prompt is the platform's own field and the last one added, so
it is what gives way when the author's fields already fill the descriptor — the
same point at which the server stops adding its own fields rather than emit an
invalid descriptor.

Evidence: seven cases in run-input.test.ts cover the hundred-and-first variable
being projected and the hundredth not, the prompt giving way, a global left out
of a full descriptor, the name grammar (including a padded name, which the server
does not trim into a field), the label bound on both sides of the byte boundary,
and the value bound on both sides of it. Each rule was falsified by removing it:
counting characters instead of bytes fails exactly the two byte-sensitive cases,
dropping the grammar fails the two name cases, and removing the descriptor bound
fails the three bound cases.

Co-Authored-By: Claude Code <noreply@anthropic.com>
…eeps

The manual run form reads a select's choices from the version's frozen graph and
filtered them itself, where the server filters them too (ora-space/circus#148):
`inputOptions` drops a choice that is not a string, is empty, is over the same
200-byte bound a field key and a field label carry, repeats one already offered,
or falls past `maxFormOptions` (100). The form kept every non-empty one.

A choice the server dropped is not a value its gate accepts — a select's answer
is matched against the choices the field kept — so the form was offering answers
whose only outcome is `400 invalid_field_value`, and a declared default among
them opened the form on an answer it refuses.

The filter mirrors `inputOptions` in the same order, so the two lists agree
choice for choice, and the default check follows from it: a value that is no
longer one of the choices is no longer a default either. The byte bound is the
same one the key and the label are held to, so two hundred ASCII characters are
a legal choice where sixty-seven Chinese ones are not.
…ue bound

The platform's own prompt field prefilled whatever the Start node carried,
while every declared default was held to the 4000-byte bound a field value
may carry. A prompt past that bound now opens the field empty instead: the
server would have clipped it, and half an instruction is not a default a
member should be handed to edit. An empty field leaves the run to fall back
to the version's own prompt, which is all of it.
…anguage

The kickoff prompt is the platform's own field, so its wording is the
platform's too — the run form showed the literal key `prompt` where the @
form and the editor's launch-field table both say 提示词. The projection now
marks such a field with the translation key it is named by, and the dialog
resolves it, so the label above the control and the "still missing" line
below it agree.
A workflow belongs to a space, so nothing but the member can name the
Project the run's Workspace provisions in. The start dialog now lists the
space's live Projects and states the chosen one on the create request;
Start stays disabled until a Project is chosen, and a space with no
Project says so instead of offering an empty picker. CreateWorkflowRunInput
requires projectId, as the contract does.
The acceptance reads back what the run froze. The run summary does not
project project_id, so the check reads the stored row directly.
@zhangshilong123
zhangshilong123 force-pushed the feat/148-workflow-start-form branch from 945aa3f to 2f310eb Compare October 20, 2026 14:09

@Zpc001 Zpc001 left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

复审新 head 2f310eb(rebase 到 main ff72afa,含 #68 的 projectId 门禁):

  • 项目选择器:只列当前空间 lifecycle 非 deleting/deleted 的项目;无默认选中、未选时「开始运行」不可用;空间无项目时给出创建提示而不是空选择器;关闭清空两项、切换版本保留项目(都有测试)。
  • 请求体 projectId:CreateWorkflowRunInput.projectId 改为必填,hook 对 '' 保留 workflow_run_project_required 兜底;dialog 测试断言请求体带 projectId。
  • 验收 4 回读:integration/workflow_run_test.go 直接从 PG 读 workflow_runs.project_id 与请求比对。
  • 后端侧无需本 PR 改动:main(cloud#71 已合)的 route Fields 与 OpenAPI 已把 projectId 列为必填。
  • CI check 12m26s / frontend 2m53s 通过;14 个改动文件全是代码、测试与 README,无过程记录。批准。

@EricWvi
EricWvi merged commit b7e1801 into ora-space:main Oct 21, 2026
2 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants