@michengai/dsh-automation 0.1.39 → 0.1.40

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -2,7 +2,13 @@
2
2
 
3
3
  [简体中文](CHANGELOG.zh-CN.md)
4
4
 
5
- The five most recent published versions are listed below. Git tags and GitHub Releases now mirror these entries; historical sections retain links to their original release commits.
5
+ Upcoming changes and recent published versions are listed below. Git tags and GitHub Releases now mirror these entries; historical sections retain links to their original release commits.
6
+
7
+ ## 0.1.40 - 2026-09-12
8
+
9
+ - Configure concurrency per automation to allow overlapping executions of the same task. The default is `1`, preserving existing task behavior. When full, scheduled triggers are skipped and manual starts are rejected; lowering the limit does not cancel existing runs.
10
+ - Reduce the minimum interval from 5 minutes to 1 minute.
11
+ - Agent create/update tools accept `max_concurrent_runs`, and Web RPC accepts per-task `maxConcurrentRuns`; these are separate from the retired plugin-level global concurrency setting.
6
12
 
7
13
  ## 0.1.39 - 2026-09-12
8
14
 
@@ -2,7 +2,13 @@
2
2
 
3
3
  [English](CHANGELOG.md)
4
4
 
5
- 以下记录最近发布的五个版本。Git 标签与 GitHub Release 现已和这些条目同步;历史条目继续保留原始发布提交链接。
5
+ 以下记录待发布变更及近期已发布版本。Git 标签与 GitHub Release 现已和这些条目同步;历史条目继续保留原始发布提交链接。
6
+
7
+ ## 0.1.40 - 2026-09-12
8
+
9
+ - 支持为单个自动化设置并发数量,让同一任务的多次执行可以同时进行。默认 `1`,旧任务保持原有行为;满额时跳过定时触发并拒绝手动启动,调低上限不取消已有运行。
10
+ - 间隔计划的最短间隔由 5 分钟缩短为 1 分钟。
11
+ - Agent 创建、更新工具支持 `max_concurrent_runs`,Web RPC 支持任务级 `maxConcurrentRuns`;它们与已停用的插件级全局并发配置无关。
6
12
 
7
13
  ## 0.1.39 - 2026-09-12
8
14
 
package/README.md CHANGED
@@ -130,8 +130,9 @@ Each dispatched run uses the saved prompt, workspace, model, and permission boun
130
130
  | Approval | Chat create follows the session policy. Full access (`never`) proceeds; Workspace Write / Read Only (`ask`) shows the official card. Unattended runs stay fail-closed `never`. |
131
131
  | Retry | No automatic retry after a started run. |
132
132
  | Host restart | Leftover `queued` / `running` records become `failed(host_interrupted)`. |
133
- | Concurrency | Different automations have no plugin-level global concurrency cap, including those in the same directory. The legacy `maxConcurrentRuns` setting no longer takes effect. |
134
- | Overlap | One active run per rule. A colliding occurrence is recorded as `skipped(overlap)`. |
133
+ | Concurrency | Different automations have no plugin-level global concurrency cap, including those in the same directory. The legacy plugin-level global concurrency setting (`maxConcurrentRuns` in plugin config) no longer takes effect; it is separate from the per-task field below. |
134
+ | Minimum interval | Since `0.1.40`, interval schedules accept whole minutes starting at `1`. Missed triggers are not replayed individually; only the latest due occurrence is considered within the recovery window. |
135
+ | Per-task concurrency | Available since `0.1.40`. Set Concurrency in the create/edit dialog (positive integer, default `1`). Queued and running executions share this limit. When full, scheduled triggers become `skipped(overlap)` and manual runs return a limit error. Lowering the limit does not cancel existing runs. Agent create/update tools accept `max_concurrent_runs`; Web RPC accepts `maxConcurrentRuns`. Existing tasks default to `1`. |
135
136
 
136
137
  A schedule stores future intent. It is not a cached permission grant.
137
138
 
package/README.zh-CN.md CHANGED
@@ -130,8 +130,9 @@ dsh --profile web --dump-config
130
130
  | 审批 | 对话创建跟随当前会话策略。Full access(`never`)直接创建;Workspace Write / Read Only(`ask`)走官方授权卡。无人值守运行仍是 fail-closed 的 `never`。 |
131
131
  | 重试 | 已经开始的运行不会自动重试。 |
132
132
  | Host 重启 | 遗留的 `queued` / `running` 会变成 `failed(host_interrupted)`。 |
133
- | 并发 | 不同自动化任务不设插件级全局并发上限,同一目录下也可同时运行。旧配置 `maxConcurrentRuns` 不再生效。 |
134
- | 重叠 | 同一规则同时最多一个 active run。冲突 occurrence 记为 `skipped(overlap)`。 |
133
+ | 并发 | 不同自动化任务不设插件级全局并发上限,同一目录下也可同时运行。旧插件级全局并发配置(插件 config 中的 `maxConcurrentRuns`)不再生效,与下方任务级字段不同。 |
134
+ | 最短间隔 | 自 `0.1.40` 起,间隔计划支持最短 `1` 分钟,仅接受整数分钟。恢复时仅处理补跑窗口内最近的到期点,不逐次补齐漏掉的触发。 |
135
+ | 单任务并发 | 自 `0.1.40` 起支持。在任务的新建/编辑页设置“并发数量”(正整数,默认 `1`)。同一任务的排队及运行记录共用此上限;满额时定时触发记为 `skipped(overlap)`,手动运行返回上限提示。调低上限不取消已有运行。Agent 创建/更新工具使用 `max_concurrent_runs`,Web RPC 使用 `maxConcurrentRuns`;旧任务默认 `1`。 |
135
136
 
136
137
  计划只表达未来意图,不是缓存下来的授权。
137
138
 
package/lib/client.js CHANGED
@@ -74,6 +74,7 @@ function defaultFormState(now = /* @__PURE__ */ new Date(), workspaces = [], def
74
74
  scheduleKind: "daily",
75
75
  onceAt: localDateTimeValue(now),
76
76
  everyMinutes: "60",
77
+ maxConcurrentRuns: "1",
77
78
  intervalAnchor: "",
78
79
  time: "09:00",
79
80
  weekdays: [1, 2, 3, 4, 5],
@@ -89,6 +90,8 @@ function defaultFormState(now = /* @__PURE__ */ new Date(), workspaces = [], def
89
90
  };
90
91
  }
91
92
  function buildCreateInput(form, workspaces, models, now = /* @__PURE__ */ new Date(), options = {}) {
93
+ const maxConcurrentRuns = Number(form.maxConcurrentRuns);
94
+ if (!Number.isSafeInteger(maxConcurrentRuns) || maxConcurrentRuns < 1) throw new AutomationFormError("form.error.maxConcurrentRuns");
92
95
  const name2 = form.name.trim();
93
96
  const prompt = form.prompt.trim();
94
97
  if (name2 === "") throw new AutomationFormError("form.error.name");
@@ -107,7 +110,7 @@ function buildCreateInput(form, workspaces, models, now = /* @__PURE__ */ new Da
107
110
  }
108
111
  case "interval": {
109
112
  const everyMinutes = Number(form.everyMinutes);
110
- if (!Number.isInteger(everyMinutes) || everyMinutes < 5 || everyMinutes > 43200) {
113
+ if (!Number.isInteger(everyMinutes) || everyMinutes < 1 || everyMinutes > 43200) {
111
114
  throw new AutomationFormError("form.error.interval");
112
115
  }
113
116
  schedule = {
@@ -151,6 +154,7 @@ function buildCreateInput(form, workspaces, models, now = /* @__PURE__ */ new Da
151
154
  schedule,
152
155
  timeZone: form.timeZone,
153
156
  permission: form.permission,
157
+ maxConcurrentRuns,
154
158
  workspaceId: workspace.id,
155
159
  cwd: workspace.path,
156
160
  ...selected === void 0 ? { provider: null, model: null } : { provider: selected.provider, model: selected.model },
@@ -306,6 +310,7 @@ function formFromAutomation(item, workspaces = [], defaultModel, defaultPermissi
306
310
  name: item.name,
307
311
  prompt: item.prompt,
308
312
  permission: item.permission,
313
+ maxConcurrentRuns: String(item.maxConcurrentRuns ?? 1),
309
314
  workspaceId: item.workspaceId ?? base.workspaceId,
310
315
  modelKey,
311
316
  reasoningEffort: item.reasoningEffort ?? "none",
@@ -1141,49 +1146,55 @@ function CreateModal({
1141
1146
  t("form.name"),
1142
1147
  /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("input", { value: form.name, placeholder: t("form.namePlaceholder"), onChange: (event) => update({ name: event.target.value }) })
1143
1148
  ] }),
1144
- /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { className: "dsh-st-field", children: [
1145
- t("form.planTime"),
1146
- /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { className: "dsh-st-inline", children: [
1147
- /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
1148
- MenuSelect,
1149
- {
1150
- value: form.scheduleKind,
1151
- options: KINDS.map((kind) => ({ value: kind, label: t(`form.${kind}`) })),
1152
- onChange: (value) => update({ scheduleKind: value })
1153
- }
1154
- ),
1155
- form.scheduleKind === "once" && /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(import_jsx_runtime3.Fragment, { children: [
1156
- /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("input", { type: "date", min: today, value: datePart, onChange: (event) => update({ onceAt: clampOnceAt(`${event.target.value}T${timePart}`) }) }),
1157
- /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(TimeSelect, { value: timePart, ...minOnceTime === void 0 ? {} : { minTime: minOnceTime }, onChange: (value) => update({ onceAt: clampOnceAt(`${datePart}T${value}`) }) })
1158
- ] }),
1159
- form.scheduleKind === "interval" && /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(import_jsx_runtime3.Fragment, { children: [
1160
- /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("input", { className: "is-narrow", type: "number", min: 5, value: form.everyMinutes, onChange: (event) => update({ everyMinutes: event.target.value }) }),
1161
- /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("span", { className: "dsh-st-suffix", children: t("form.minutesShort") })
1162
- ] }),
1163
- form.scheduleKind === "hourly" && /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(import_jsx_runtime3.Fragment, { children: [
1164
- /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(MenuSelect, { value: form.hourlyMinute, options: MINUTES.map((item) => ({ value: item, label: item })), onChange: (value) => update({ hourlyMinute: value }) }),
1165
- /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("span", { className: "dsh-st-suffix", children: t("form.minutesShort") })
1166
- ] }),
1167
- (form.scheduleKind === "daily" || form.scheduleKind === "weekly") && /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(TimeSelect, { value: form.time, onChange: (value) => update({ time: value }) }),
1168
- form.scheduleKind === "monthly" && /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(import_jsx_runtime3.Fragment, { children: [
1149
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { className: "dsh-st-plan-row", children: [
1150
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { className: "dsh-st-field", children: [
1151
+ t("form.planTime"),
1152
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { className: "dsh-st-inline", children: [
1169
1153
  /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
1170
1154
  MenuSelect,
1171
1155
  {
1172
- value: form.monthDay,
1173
- options: Array.from({ length: 31 }, (_, index) => {
1174
- const day = String(index + 1);
1175
- return { value: day, label: t("form.monthDay", { day }) };
1176
- }),
1177
- onChange: (value) => update({ monthDay: value })
1156
+ value: form.scheduleKind,
1157
+ options: KINDS.map((kind) => ({ value: kind, label: t(`form.${kind}`) })),
1158
+ onChange: (value) => update({ scheduleKind: value })
1178
1159
  }
1179
1160
  ),
1180
- /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(TimeSelect, { value: form.time, onChange: (value) => update({ time: value }) })
1181
- ] }),
1182
- form.scheduleKind === "custom" && /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(import_jsx_runtime3.Fragment, { children: [
1183
- /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("input", { className: "is-narrow", type: "number", min: 1, value: form.customDays, onChange: (event) => update({ customDays: event.target.value }) }),
1184
- /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("span", { className: "dsh-st-suffix", children: t("form.daysShort") }),
1185
- /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(TimeSelect, { value: form.time, onChange: (value) => update({ time: value }) })
1161
+ form.scheduleKind === "once" && /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(import_jsx_runtime3.Fragment, { children: [
1162
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("input", { type: "date", min: today, value: datePart, onChange: (event) => update({ onceAt: clampOnceAt(`${event.target.value}T${timePart}`) }) }),
1163
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(TimeSelect, { value: timePart, ...minOnceTime === void 0 ? {} : { minTime: minOnceTime }, onChange: (value) => update({ onceAt: clampOnceAt(`${datePart}T${value}`) }) })
1164
+ ] }),
1165
+ form.scheduleKind === "interval" && /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(import_jsx_runtime3.Fragment, { children: [
1166
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("input", { className: "is-narrow", type: "number", min: 1, value: form.everyMinutes, onChange: (event) => update({ everyMinutes: event.target.value }) }),
1167
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("span", { className: "dsh-st-suffix", children: t("form.minutesShort") })
1168
+ ] }),
1169
+ form.scheduleKind === "hourly" && /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(import_jsx_runtime3.Fragment, { children: [
1170
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(MenuSelect, { value: form.hourlyMinute, options: MINUTES.map((item) => ({ value: item, label: item })), onChange: (value) => update({ hourlyMinute: value }) }),
1171
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("span", { className: "dsh-st-suffix", children: t("form.minutesShort") })
1172
+ ] }),
1173
+ (form.scheduleKind === "daily" || form.scheduleKind === "weekly") && /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(TimeSelect, { value: form.time, onChange: (value) => update({ time: value }) }),
1174
+ form.scheduleKind === "monthly" && /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(import_jsx_runtime3.Fragment, { children: [
1175
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
1176
+ MenuSelect,
1177
+ {
1178
+ value: form.monthDay,
1179
+ options: Array.from({ length: 31 }, (_, index) => {
1180
+ const day = String(index + 1);
1181
+ return { value: day, label: t("form.monthDay", { day }) };
1182
+ }),
1183
+ onChange: (value) => update({ monthDay: value })
1184
+ }
1185
+ ),
1186
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(TimeSelect, { value: form.time, onChange: (value) => update({ time: value }) })
1187
+ ] }),
1188
+ form.scheduleKind === "custom" && /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(import_jsx_runtime3.Fragment, { children: [
1189
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("input", { className: "is-narrow", type: "number", min: 1, value: form.customDays, onChange: (event) => update({ customDays: event.target.value }) }),
1190
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("span", { className: "dsh-st-suffix", children: t("form.daysShort") }),
1191
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(TimeSelect, { value: form.time, onChange: (value) => update({ time: value }) })
1192
+ ] })
1186
1193
  ] })
1194
+ ] }),
1195
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("label", { className: "dsh-st-field dsh-st-concurrency", title: t("form.maxConcurrentRunsHint"), children: [
1196
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("span", { children: t("form.maxConcurrentRuns") }),
1197
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("input", { type: "number", min: 1, step: 1, required: true, value: form.maxConcurrentRuns, onChange: (event) => update({ maxConcurrentRuns: event.target.value }) })
1187
1198
  ] })
1188
1199
  ] }),
1189
1200
  form.scheduleKind === "weekly" && /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("div", { className: "dsh-st-weekdays", children: WEEKDAYS.map((day) => /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
@@ -2633,6 +2644,9 @@ var en = {
2633
2644
  "form.time": "Time",
2634
2645
  "form.days": "Weekdays",
2635
2646
  "form.timeZone": "Time zone",
2647
+ "form.maxConcurrentRuns": "Concurrency",
2648
+ "form.maxConcurrentRunsHint": "Queued and running executions share this limit. Defaults to 1; when full, scheduled triggers are skipped and manual runs are rejected.",
2649
+ "form.error.maxConcurrentRuns": "Enter a positive whole number for concurrent runs.",
2636
2650
  "form.permission": "Permission boundary",
2637
2651
  "permission.readOnly": "Read Only",
2638
2652
  "permission.workspaceWrite": "Workspace Write",
@@ -2643,7 +2657,7 @@ var en = {
2643
2657
  "form.error.name": "Enter a name.",
2644
2658
  "form.error.prompt": "Enter a complete, standalone task prompt.",
2645
2659
  "form.error.once": "Choose a valid future date and time.",
2646
- "form.error.interval": "The interval must be between 5 and 43,200 minutes.",
2660
+ "form.error.interval": "The interval must be between 1 and 43,200 minutes.",
2647
2661
  "form.error.weekdays": "Select at least one weekday.",
2648
2662
  "day.1": "Mon",
2649
2663
  "day.2": "Tue",
@@ -2848,6 +2862,9 @@ var zh = {
2848
2862
  "form.time": "\u65F6\u95F4",
2849
2863
  "form.days": "\u661F\u671F",
2850
2864
  "form.timeZone": "\u65F6\u533A",
2865
+ "form.maxConcurrentRuns": "\u5E76\u53D1\u6570\u91CF",
2866
+ "form.maxConcurrentRunsHint": "\u540C\u4E00\u4EFB\u52A1\u7684\u6392\u961F\u53CA\u8FD0\u884C\u6B21\u6570\u5171\u7528\u6B64\u4E0A\u9650\uFF0C\u9ED8\u8BA4 1\uFF1B\u6EE1\u989D\u65F6\u8DF3\u8FC7\u5F53\u6B21\u5B9A\u65F6\u89E6\u53D1\uFF0C\u624B\u52A8\u8FD0\u884C\u5C06\u88AB\u62D2\u7EDD\u3002",
2867
+ "form.error.maxConcurrentRuns": "\u5E76\u53D1\u6570\u91CF\u5FC5\u987B\u4E3A\u5927\u4E8E\u7B49\u4E8E 1 \u7684\u6574\u6570\u3002",
2851
2868
  "form.permission": "\u6743\u9650\u8FB9\u754C",
2852
2869
  "permission.readOnly": "\u53EA\u8BFB",
2853
2870
  "permission.workspaceWrite": "\u5DE5\u4F5C\u533A\u5199\u5165",
@@ -2858,7 +2875,7 @@ var zh = {
2858
2875
  "form.error.name": "\u8BF7\u8F93\u5165\u540D\u79F0\u3002",
2859
2876
  "form.error.prompt": "\u8BF7\u8F93\u5165\u5B8C\u6574\u3001\u72EC\u7ACB\u7684\u4EFB\u52A1\u6307\u4EE4\u3002",
2860
2877
  "form.error.once": "\u8BF7\u9009\u62E9\u6709\u6548\u7684\u672A\u6765\u65E5\u671F\u548C\u65F6\u95F4\u3002",
2861
- "form.error.interval": "\u8FD0\u884C\u95F4\u9694\u5FC5\u987B\u5728 5 \u5230 43,200 \u5206\u949F\u4E4B\u95F4\u3002",
2878
+ "form.error.interval": "\u8FD0\u884C\u95F4\u9694\u5FC5\u987B\u5728 1 \u5230 43,200 \u5206\u949F\u4E4B\u95F4\u3002",
2862
2879
  "form.error.weekdays": "\u8BF7\u81F3\u5C11\u9009\u62E9\u4E00\u5929\u3002",
2863
2880
  "day.1": "\u5468\u4E00",
2864
2881
  "day.2": "\u5468\u4E8C",
@@ -4051,6 +4068,7 @@ var CSS_TEXT = `
4051
4068
  .dsh-st-model-select{position:relative;z-index:1;min-width:0;flex:none}.dsh-st-model-select.is-open{z-index:30}.dsh-st-model-select-trigger{display:flex;align-items:center;gap:4px;min-width:0;max-width:260px;height:28px;padding:0 4px 0 8px;border:0;border-radius:24px;background:transparent;color:var(--dsw-alias-label-secondary);font-size:13px;font-weight:500;line-height:20px;cursor:pointer}.dsh-st-model-select-trigger:hover{background:var(--dsw-alias-interactive-bg-hover);color:var(--dsw-alias-label-primary)}.dsh-st-model-select-trigger>span:first-child{min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.dsh-st-model-trigger-effort{flex:none;color:var(--dsw-alias-label-tertiary);white-space:nowrap}.dsh-st-model-trigger-chevron{flex:none;transition:transform .16s ease}.dsh-st-model-trigger-chevron.is-open{transform:rotate(180deg)}.dsh-st-model-select-menu{z-index:30;width:max-content;min-width:min(240px,calc(100vw - 32px));max-width:calc(100vw - 32px);max-height:min(360px,calc(100vh - 96px));overflow-y:auto;padding:4px;border:1px solid var(--dsw-alias-border-inverted,var(--dsw-alias-border-l2));border-radius:12px;background:var(--dsw-specific-menu,var(--dsw-alias-bg-base));box-shadow:var(--dsw-shadow-lv3)}.dsh-st-model-select-menu.is-float{position:absolute;z-index:1200;box-sizing:border-box}.dsh-st-model-select-menu .dsh-st-menu-row{min-height:40px;padding:0 10px;border-radius:10px;font-size:14px}.dsh-st-model-select-menu .dsh-st-menu-row-side>span{min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.dsh-st-model-select-menu .dsh-st-menu-row.is-kv .dsh-st-menu-row-side{font-size:13px;color:var(--dsw-alias-label-tertiary)}.dsh-st-model-group+.dsh-st-model-group{margin-top:4px}.dsh-st-model-group-title{position:sticky;top:0;z-index:1;padding:5px 8px 3px;background:var(--dsw-specific-menu,var(--dsw-alias-bg-base));color:var(--dsw-alias-label-tertiary);font-size:12px;font-weight:500;line-height:18px}.dsh-st-model-option{display:flex;width:100%;min-height:38px;align-items:center;gap:8px;padding:6px 8px;border:0;border-radius:10px;background:transparent;color:var(--dsw-alias-label-primary);text-align:left;cursor:pointer}.dsh-st-model-option:hover,.dsh-st-model-option:focus-visible{background:var(--dsw-alias-interactive-bg-hover);outline:none}.dsh-st-model-option-copy{display:flex;min-width:0;flex:1;flex-direction:column}.dsh-st-model-name{overflow:hidden;font-size:14px;font-weight:500;line-height:20px;text-overflow:ellipsis;white-space:nowrap}.dsh-st-model-description{overflow:hidden;color:var(--dsw-alias-label-tertiary);font-size:12px;line-height:18px;text-overflow:ellipsis;white-space:nowrap}.dsh-st-model-check{display:grid;flex:0 0 18px;place-items:center;color:var(--dsw-alias-label-primary)}.dsh-st-model-warning{margin:4px;padding:8px;border-radius:8px;background:var(--dsw-alias-interactive-bg-hover-danger,rgba(248,81,73,.1));color:var(--dsw-alias-state-error-primary,#f85149);font-size:12px;line-height:18px}.dsh-st-model-empty{padding:14px 12px;color:var(--dsw-alias-label-tertiary);font-size:12px;text-align:center}
4052
4069
  .dsh-st-suffix{color:var(--dsw-alias-label-secondary);font-size:13px}
4053
4070
  .dsh-st-inline input.is-narrow{width:72px;flex:none}
4071
+ .dsh-st-plan-row{display:flex;align-items:flex-start;gap:16px}.dsh-st-plan-row>.dsh-st-field:first-child{flex:1}.dsh-st-plan-row>.dsh-st-concurrency{flex:0 0 120px}
4054
4072
  .dsh-st-time{display:inline-flex;align-items:center;gap:2px;min-height:36px;padding:0 8px;border:1px solid var(--dsw-alias-border-l2);border-radius:12px;background:rgba(255,255,255,.04)}
4055
4073
  .dsh-st-time .dsh-st-select{min-width:48px}
4056
4074
  .dsh-st-time .dsh-st-select-btn{width:auto;min-height:32px;padding:0 6px;border:0;background:transparent}