diff --git a/web/src/components/graph-console/GraphRunPane.vue b/web/src/components/graph-console/GraphRunPane.vue
index 975c0c0..ef4b046 100644
--- a/web/src/components/graph-console/GraphRunPane.vue
+++ b/web/src/components/graph-console/GraphRunPane.vue
@@ -28,6 +28,7 @@
+
下一步将执行 {{ nextNode.name || nextNode.defNodeId }}
自动模式下一批最多并发 {{ nextNodes.length }} 个:
{{ n.name || n.defNodeId }}
@@ -67,6 +68,35 @@
+
+
+
@@ -116,6 +146,8 @@ const err = ref('');
const notice = ref('');
const mode = ref('manual');
const confirmAuto = ref(false);
+const showBudgetModal = ref(false);
+const budgetForm = ref({ hours: 8, calls: '', tokens: '', resume: true });
const sseStatus = ref('idle');
let es = null;
@@ -138,6 +170,14 @@ const nextNodes = computed(() => {
const nextNode = computed(() => nextNodes.value[0] || null); // 手动模式「下一步」提示仍用单数
const selectedNode = computed(() => nodes.value.find((n) => String(n.defNodeId) === selectedDefId.value) || null);
const canRetry = computed(() => selectedNode.value && ['fail', 'blocked'].includes(selectedNode.value.status));
+// 与后端 ensureStatus 同一张表(§四.C):/budget 只在 run_status='blocked' 时可用;
+// budget 为 null 说明这一层不是挂预算的 root run(扣减与熔断只在 root run 上生效),
+// 追加了也没有意义,按钮直接不亮
+const canExtendBudget = computed(() => !!budget.value && run.value?.run_status === 'blocked');
+const budgetHasAnyDelta = computed(() => {
+ const f = budgetForm.value;
+ return [f.hours, f.calls, f.tokens].some((v) => Number(v) > 0);
+});
const remain = computed(() => {
const b = budget.value || {};
@@ -277,6 +317,21 @@ function onStart() {
}
function onRetry() { if (canRetry.value) act('retry'); }
+function openBudgetModal() {
+ budgetForm.value = { hours: 8, calls: '', tokens: '', resume: true };
+ showBudgetModal.value = true;
+}
+function submitBudget() {
+ const f = budgetForm.value;
+ const patch = {};
+ if (Number(f.hours) > 0) patch.wallclock_ms = Number(f.hours) * 3600 * 1000;
+ if (Number(f.calls) > 0) patch.total_agent_calls = Number(f.calls);
+ if (Number(f.tokens) > 0) patch.total_tokens = Number(f.tokens);
+ patch.resume = !!f.resume;
+ showBudgetModal.value = false;
+ act('budget', patch);
+}
+
function onSelect(node) {
selectedDefId.value = String(node.defNodeId);
emit('select', node);
@@ -374,6 +429,7 @@ function fmtNum(n) { return (n || 0).toLocaleString(); }
.btn:disabled { opacity:.4; cursor:not-allowed; }
.btn.go { background:#1a7f37; border-color:#1a7f37; color:#fff; }
.btn.retry { border-color:#c96a00; color:#c96a00; }
+.btn.budget { border-color:#8a5cd6; color:#8a5cd6; }
.next-hint { font-size:11px; color:#666; }
.next-hint code { background:#eef4ff; padding:1px 5px; border-radius:3px; }
.notice { color: var(--text-secondary); background: var(--bg-overlay); border: 1px dashed var(--border-primary);
@@ -389,4 +445,7 @@ function fmtNum(n) { return (n || 0).toLocaleString(); }
.est { font-size:12.5px; color:#444; padding-left:18px; margin:0 0 16px; }
.est li { margin-bottom:4px; }
.modal-btns { display:flex; justify-content:flex-end; gap:8px; }
+.budget-field { display:flex; align-items:center; justify-content:space-between; gap:10px; font-size:12.5px; color:#444; margin-bottom:8px; }
+.budget-field input { width:140px; font-size:12.5px; padding:3px 6px; border:1px solid #ddd; border-radius:4px; }
+.budget-resume { display:flex; align-items:center; gap:6px; font-size:12.5px; color:#444; margin:4px 0 14px; }