reasonix 0.30.4 → 0.31.0

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.
@@ -19264,8 +19264,36 @@ var en = {
19264
19264
  effortHigh: "high (cheaper / faster)",
19265
19265
  webSearch: "web search",
19266
19266
  webSearchNote: "web_fetch + web_search tools",
19267
+ sectionCompute: "Compute",
19268
+ proNext: "/pro one-shot",
19269
+ proArm: "Arm for next turn",
19270
+ proArmed: "Armed \u2014 disarms after next turn",
19271
+ proNextNote: "next turn runs on deepseek-v4-pro, then auto-disarms",
19272
+ sectionBudget: "Budget",
19273
+ budgetOf: "of",
19274
+ budgetSetCap: "set a cap",
19275
+ budgetCustom: "custom",
19276
+ budgetBumpHint: "bump the cap to keep going",
19277
+ budgetClear: "Clear cap",
19278
+ budgetIdleLine: "warns at 80% \xB7 refuses past 100%",
19279
+ budgetWarnLine: "approaching cap \u2014 loop will refuse past 100%",
19280
+ budgetRefusing: "cap exhausted \u2014 next turn refused until bumped or cleared",
19281
+ sectionLoop: "Loop",
19282
+ loopIdleHint: "Auto-resubmit a prompt on a fixed interval.",
19283
+ loopCostHint: "Each iteration costs ~{cost} (last turn).",
19284
+ loopInterval: "interval",
19285
+ loopCustom: "custom",
19286
+ loopRangeError: "interval must fall in 5s..6h",
19287
+ loopPrompt: "prompt",
19288
+ loopPromptPlaceholder: "e.g. check the deploy status and report any errors",
19289
+ loopStart: "Start loop",
19290
+ loopStop: "Stop",
19291
+ loopRunning: "running",
19292
+ loopIter: "iter {iter}",
19293
+ loopFiresIn: "fires in {remaining}",
19267
19294
  sectionRuntime: "Runtime",
19268
19295
  activeModel: "active model",
19296
+ modelPricingLine: "${hit} hit \xB7 ${miss} miss \xB7 ${out} out per 1M tok",
19269
19297
  editMode: "edit mode",
19270
19298
  editModeNote: "switch from the Chat tab header",
19271
19299
  sectionLanguage: "Language",
@@ -19348,6 +19376,7 @@ var en = {
19348
19376
  tokens7d: "tokens \xB7 7d",
19349
19377
  cacheHit: "cache hit",
19350
19378
  toolCalls24h: "tool calls \xB7 24h",
19379
+ budget: "budget",
19351
19380
  currentSession: "current session",
19352
19381
  noSession: "No live session \u2014 /dashboard from inside reasonix code to attach.",
19353
19382
  promptTok: "prompt tok",
@@ -19578,6 +19607,7 @@ var en = {
19578
19607
  filterPlaceholder: "filter plans",
19579
19608
  active: "active",
19580
19609
  done: "done",
19610
+ idle: "idle",
19581
19611
  steps: "steps",
19582
19612
  pickHint: "Pick a plan on the left.",
19583
19613
  noTitle: "(no title)",
@@ -19709,7 +19739,17 @@ var en = {
19709
19739
  accept: "Accept",
19710
19740
  reject: "Reject",
19711
19741
  arguments: "arguments",
19712
- revisePlaceholder: "What needs to change before the next step? Leave blank to just continue."
19742
+ revisePlaceholder: "What needs to change before the next step? Leave blank to just continue.",
19743
+ pickerFilter: "Filter\u2026",
19744
+ pickerEmpty: "Nothing to show.",
19745
+ pickerLoadMore: "Load more",
19746
+ pickerPick: "Open",
19747
+ pickerInstall: "Install",
19748
+ pickerUninstall: "Uninstall",
19749
+ pickerRename: "Rename\u2026",
19750
+ pickerNew: "New\u2026",
19751
+ pickerNewPlaceholder: "Name (leave blank for default)",
19752
+ viewerClose: "Close"
19713
19753
  }
19714
19754
  };
19715
19755
 
@@ -19777,8 +19817,36 @@ var zhCN = {
19777
19817
  effortHigh: "high\uFF08\u66F4\u4FBF\u5B9C / \u66F4\u5FEB\uFF09",
19778
19818
  webSearch: "\u7F51\u9875\u641C\u7D22",
19779
19819
  webSearchNote: "web_fetch + web_search \u5DE5\u5177",
19820
+ sectionCompute: "\u8BA1\u7B97",
19821
+ proNext: "/pro \u5355\u8F6E",
19822
+ proArm: "\u4E3A\u4E0B\u4E00\u8F6E\u88C5\u5907",
19823
+ proArmed: "\u5DF2\u88C5\u5907 \u2014 \u4E0B\u4E00\u8F6E\u540E\u81EA\u52A8\u89E3\u9664",
19824
+ proNextNote: "\u4E0B\u4E00\u8F6E\u4F7F\u7528 deepseek-v4-pro\uFF0C\u4E4B\u540E\u81EA\u52A8\u89E3\u9664",
19825
+ sectionBudget: "\u9884\u7B97",
19826
+ budgetOf: "/",
19827
+ budgetSetCap: "\u8BBE\u7F6E\u4E0A\u9650",
19828
+ budgetCustom: "\u81EA\u5B9A\u4E49",
19829
+ budgetBumpHint: "\u63D0\u9AD8\u4E0A\u9650\u4EE5\u7EE7\u7EED",
19830
+ budgetClear: "\u6E05\u9664\u4E0A\u9650",
19831
+ budgetIdleLine: "80% \u65F6\u63D0\u9192 \xB7 100% \u540E\u62D2\u7EDD\u6267\u884C",
19832
+ budgetWarnLine: "\u63A5\u8FD1\u4E0A\u9650 \u2014 \u8D85\u8FC7 100% \u5C06\u62D2\u7EDD\u6267\u884C",
19833
+ budgetRefusing: "\u5DF2\u8D85\u51FA\u4E0A\u9650 \u2014 \u63D0\u9AD8\u6216\u6E05\u9664\u540E\u624D\u4F1A\u7EE7\u7EED",
19834
+ sectionLoop: "\u5FAA\u73AF",
19835
+ loopIdleHint: "\u6309\u56FA\u5B9A\u95F4\u9694\u81EA\u52A8\u91CD\u65B0\u63D0\u4EA4\u4E00\u6BB5\u63D0\u793A\u8BCD\u3002",
19836
+ loopCostHint: "\u6BCF\u6B21\u8FED\u4EE3\u7EA6 {cost}\uFF08\u4E0A\u4E00\u8F6E\u6210\u672C\uFF09\u3002",
19837
+ loopInterval: "\u95F4\u9694",
19838
+ loopCustom: "\u81EA\u5B9A\u4E49",
19839
+ loopRangeError: "\u95F4\u9694\u9700\u5728 5s..6h \u4E4B\u95F4",
19840
+ loopPrompt: "\u63D0\u793A\u8BCD",
19841
+ loopPromptPlaceholder: "\u4F8B\u5982\uFF1A\u68C0\u67E5\u90E8\u7F72\u72B6\u6001\u5E76\u6C47\u62A5\u4EFB\u4F55\u9519\u8BEF",
19842
+ loopStart: "\u542F\u52A8\u5FAA\u73AF",
19843
+ loopStop: "\u505C\u6B62",
19844
+ loopRunning: "\u8FD0\u884C\u4E2D",
19845
+ loopIter: "\u7B2C {iter} \u6B21",
19846
+ loopFiresIn: "{remaining} \u540E\u89E6\u53D1",
19780
19847
  sectionRuntime: "\u8FD0\u884C\u65F6",
19781
19848
  activeModel: "\u5F53\u524D\u6A21\u578B",
19849
+ modelPricingLine: "${hit} \u547D\u4E2D \xB7 ${miss} \u672A\u547D\u4E2D \xB7 ${out} \u8F93\u51FA / 100 \u4E07 tok",
19782
19850
  editMode: "\u7F16\u8F91\u6A21\u5F0F",
19783
19851
  editModeNote: "\u5728\u5BF9\u8BDD\u6807\u7B7E\u9875\u5934\u90E8\u5207\u6362",
19784
19852
  sectionLanguage: "\u8BED\u8A00",
@@ -19861,6 +19929,7 @@ var zhCN = {
19861
19929
  tokens7d: "tokens \xB7 7 \u5929",
19862
19930
  cacheHit: "\u7F13\u5B58\u547D\u4E2D",
19863
19931
  toolCalls24h: "\u5DE5\u5177\u8C03\u7528 \xB7 24 \u5C0F\u65F6",
19932
+ budget: "\u9884\u7B97",
19864
19933
  currentSession: "\u5F53\u524D\u4F1A\u8BDD",
19865
19934
  noSession: "\u65E0\u6D3B\u8DC3\u4F1A\u8BDD \u2014 \u5728 reasonix code \u5185\u6267\u884C /dashboard \u8FDB\u884C\u8FDE\u63A5\u3002",
19866
19935
  promptTok: "\u63D0\u793A tokens",
@@ -20091,6 +20160,7 @@ var zhCN = {
20091
20160
  filterPlaceholder: "\u7B5B\u9009\u8BA1\u5212",
20092
20161
  active: "\u8FDB\u884C\u4E2D",
20093
20162
  done: "\u5DF2\u5B8C\u6210",
20163
+ idle: "\u672A\u5F00\u59CB",
20094
20164
  steps: "\u6B65\u9AA4",
20095
20165
  pickHint: "\u9009\u62E9\u5DE6\u4FA7\u7684\u8BA1\u5212\u3002",
20096
20166
  noTitle: "\uFF08\u65E0\u6807\u9898\uFF09",
@@ -20222,7 +20292,17 @@ var zhCN = {
20222
20292
  accept: "\u63A5\u53D7",
20223
20293
  reject: "\u62D2\u7EDD",
20224
20294
  arguments: "\u53C2\u6570",
20225
- revisePlaceholder: "\u4E0B\u4E00\u6B65\u4E4B\u524D\u9700\u8981\u66F4\u6539\u4EC0\u4E48\uFF1F\u7559\u7A7A\u5219\u76F4\u63A5\u7EE7\u7EED\u3002"
20295
+ revisePlaceholder: "\u4E0B\u4E00\u6B65\u4E4B\u524D\u9700\u8981\u66F4\u6539\u4EC0\u4E48\uFF1F\u7559\u7A7A\u5219\u76F4\u63A5\u7EE7\u7EED\u3002",
20296
+ pickerFilter: "\u8FC7\u6EE4\u2026",
20297
+ pickerEmpty: "\u6682\u65E0\u5185\u5BB9\u3002",
20298
+ pickerLoadMore: "\u52A0\u8F7D\u66F4\u591A",
20299
+ pickerPick: "\u6253\u5F00",
20300
+ pickerInstall: "\u5B89\u88C5",
20301
+ pickerUninstall: "\u5378\u8F7D",
20302
+ pickerRename: "\u91CD\u547D\u540D\u2026",
20303
+ pickerNew: "\u65B0\u5EFA\u2026",
20304
+ pickerNewPlaceholder: "\u540D\u79F0\uFF08\u7559\u7A7A\u4F7F\u7528\u9ED8\u8BA4\uFF09",
20305
+ viewerClose: "\u5173\u95ED"
20226
20306
  }
20227
20307
  };
20228
20308
 
@@ -22930,6 +23010,156 @@ function CheckpointModal({ modal, onResolve }) {
22930
23010
  <//>
22931
23011
  `;
22932
23012
  }
23013
+ function PickerModal({
23014
+ modal,
23015
+ onResolve
23016
+ }) {
23017
+ useLang();
23018
+ const [selectedId, setSelectedId] = d2(modal.items[0]?.id ?? null);
23019
+ const [query2, setQuery] = d2(modal.query ?? "");
23020
+ const [renameTarget, setRenameTarget] = d2(null);
23021
+ const [renameText, setRenameText] = d2("");
23022
+ const [showNew, setShowNew] = d2(false);
23023
+ const [newText, setNewText] = d2("");
23024
+ const has = (a3) => modal.actions.includes(a3);
23025
+ const selected = modal.items.find((i3) => i3.id === selectedId) ?? null;
23026
+ const submitRefine = (next) => {
23027
+ setQuery(next);
23028
+ if (has("refine")) onResolve("picker", { action: "refine", query: next });
23029
+ };
23030
+ const startRename = (id) => {
23031
+ const item = modal.items.find((i3) => i3.id === id);
23032
+ if (!item) return;
23033
+ setRenameTarget(id);
23034
+ setRenameText(item.title);
23035
+ };
23036
+ const sendRename = () => {
23037
+ if (!renameTarget || !renameText.trim()) return;
23038
+ onResolve("picker", { action: "rename", id: renameTarget, text: renameText });
23039
+ setRenameTarget(null);
23040
+ setRenameText("");
23041
+ };
23042
+ const sendNew = () => {
23043
+ onResolve("picker", newText.trim() ? { action: "new", text: newText } : { action: "new" });
23044
+ setShowNew(false);
23045
+ setNewText("");
23046
+ };
23047
+ return html4`
23048
+ <${ModalCard}
23049
+ accent="#fcd34d"
23050
+ icon="≡"
23051
+ title=${modal.title}
23052
+ subtitle=${modal.hint}
23053
+ >
23054
+ ${has("refine") ? html4`<input
23055
+ class="modal-picker-search"
23056
+ type="search"
23057
+ placeholder=${t4("modal.pickerFilter")}
23058
+ value=${query2}
23059
+ onInput=${(e3) => submitRefine(e3.target.value)}
23060
+ />` : null}
23061
+ <div class="modal-picker-list">
23062
+ ${modal.items.length === 0 ? html4`<div class="modal-picker-empty">${t4("modal.pickerEmpty")}</div>` : modal.items.map(
23063
+ (it) => html4`
23064
+ <button
23065
+ key=${it.id}
23066
+ class=${`modal-picker-row${it.id === selectedId ? " selected" : ""}`}
23067
+ onClick=${() => setSelectedId(it.id)}
23068
+ onDblClick=${() => has("pick") && onResolve("picker", { action: "pick", id: it.id })}
23069
+ >
23070
+ <span class="modal-picker-title">${it.title}</span>
23071
+ ${it.badge ? html4`<span class="modal-picker-badge">${it.badge}</span>` : null}
23072
+ ${it.subtitle ? html4`<span class="modal-picker-subtitle">${it.subtitle}</span>` : null}
23073
+ ${it.meta ? html4`<span class="modal-picker-meta">${it.meta}</span>` : null}
23074
+ </button>
23075
+ `
23076
+ )}
23077
+ </div>
23078
+ ${modal.hasMore && has("load-more") ? html4`<button
23079
+ class="modal-picker-more"
23080
+ onClick=${() => onResolve("picker", { action: "load-more" })}
23081
+ >${t4("modal.pickerLoadMore")}</button>` : null}
23082
+ ${renameTarget ? html4`
23083
+ <div class="modal-picker-form">
23084
+ <input
23085
+ type="text"
23086
+ value=${renameText}
23087
+ onInput=${(e3) => setRenameText(e3.target.value)}
23088
+ />
23089
+ <div class="modal-actions">
23090
+ <button class="primary" onClick=${sendRename} disabled=${!renameText.trim()}>${t4("common.save")}</button>
23091
+ <button onClick=${() => setRenameTarget(null)}>${t4("common.back")}</button>
23092
+ </div>
23093
+ </div>
23094
+ ` : showNew ? html4`
23095
+ <div class="modal-picker-form">
23096
+ <input
23097
+ type="text"
23098
+ placeholder=${t4("modal.pickerNewPlaceholder")}
23099
+ value=${newText}
23100
+ onInput=${(e3) => setNewText(e3.target.value)}
23101
+ />
23102
+ <div class="modal-actions">
23103
+ <button class="primary" onClick=${sendNew}>${t4("common.add")}</button>
23104
+ <button onClick=${() => setShowNew(false)}>${t4("common.back")}</button>
23105
+ </div>
23106
+ </div>
23107
+ ` : html4`
23108
+ <div class="modal-actions">
23109
+ ${has("pick") && selected ? html4`<button
23110
+ class="primary"
23111
+ onClick=${() => onResolve("picker", { action: "pick", id: selected.id })}
23112
+ >${t4("modal.pickerPick")}</button>` : null}
23113
+ ${has("install") && selected ? html4`<button
23114
+ class="primary"
23115
+ onClick=${() => onResolve("picker", { action: "install", id: selected.id })}
23116
+ >${t4("modal.pickerInstall")}</button>` : null}
23117
+ ${has("uninstall") && selected ? html4`<button
23118
+ onClick=${() => onResolve("picker", { action: "uninstall", id: selected.id })}
23119
+ >${t4("modal.pickerUninstall")}</button>` : null}
23120
+ ${has("rename") && selected ? html4`<button onClick=${() => startRename(selected.id)}>${t4("modal.pickerRename")}</button>` : null}
23121
+ ${has("delete") && selected ? html4`<button
23122
+ class="danger"
23123
+ onClick=${() => onResolve("picker", { action: "delete", id: selected.id })}
23124
+ >${t4("common.delete")}</button>` : null}
23125
+ ${has("new") ? html4`<button onClick=${() => setShowNew(true)}>${t4("modal.pickerNew")}</button>` : null}
23126
+ <button onClick=${() => onResolve("picker", { action: "cancel" })}>${t4("modal.cancel")}</button>
23127
+ </div>
23128
+ `}
23129
+ <//>
23130
+ `;
23131
+ }
23132
+ function ViewerModal({
23133
+ modal,
23134
+ onResolve
23135
+ }) {
23136
+ useLang();
23137
+ return html4`
23138
+ <${ModalCard}
23139
+ accent="#67e8f9"
23140
+ icon="◇"
23141
+ title=${modal.title}
23142
+ subtitle=${modal.meta}
23143
+ >
23144
+ ${modal.steps && modal.steps.length > 0 ? html4`
23145
+ <ol class="modal-viewer-steps">
23146
+ ${modal.steps.map(
23147
+ (s3) => html4`
23148
+ <li key=${s3.id} class=${`modal-viewer-step modal-viewer-step-${s3.status}`}>
23149
+ <span class="modal-viewer-step-mark">${s3.status === "done" ? "\u2713" : "\xB7"}</span>
23150
+ <span class="modal-viewer-step-title">${s3.title}</span>
23151
+ </li>
23152
+ `
23153
+ )}
23154
+ </ol>
23155
+ ` : null}
23156
+ ${modal.body ? html4`<div class="md modal-viewer-body" dangerouslySetInnerHTML=${{ __html: marked.parse(modal.body) }}></div>` : null}
23157
+ <div class="modal-actions">
23158
+ <button onClick=${() => onResolve("viewer", { action: "close" })}>${t4("modal.viewerClose")}</button>
23159
+ </div>
23160
+ <//>
23161
+ `;
23162
+ }
22933
23163
  function RevisionModal({ modal, onResolve }) {
22934
23164
  useLang();
22935
23165
  const riskColor = (r3) => r3 === "high" ? "#f87171" : r3 === "med" ? "#fbbf24" : r3 === "low" ? "#86efac" : "#9ca3af";
@@ -23514,7 +23744,7 @@ function ChatPanel() {
23514
23744
  </div>` : null}
23515
23745
  ${error ? html4`<div class="notice err">${error}</div>` : null}
23516
23746
 
23517
- ${modal ? modal.kind === "shell" ? html4`<${ShellModal} modal=${modal} onResolve=${resolveModal} />` : modal.kind === "choice" ? html4`<${ChoiceModal} modal=${modal} onResolve=${resolveModal} />` : modal.kind === "plan" ? html4`<${PlanModal} modal=${modal} onResolve=${resolveModal} />` : modal.kind === "edit-review" ? html4`<${EditReviewModal} modal=${modal} onResolve=${resolveModal} />` : modal.kind === "workspace" ? html4`<${WorkspaceModal} modal=${modal} onResolve=${resolveModal} />` : modal.kind === "checkpoint" ? html4`<${CheckpointModal} modal=${modal} onResolve=${resolveModal} />` : modal.kind === "revision" ? html4`<${RevisionModal} modal=${modal} onResolve=${resolveModal} />` : null : null}
23747
+ ${modal ? modal.kind === "shell" ? html4`<${ShellModal} modal=${modal} onResolve=${resolveModal} />` : modal.kind === "choice" ? html4`<${ChoiceModal} modal=${modal} onResolve=${resolveModal} />` : modal.kind === "plan" ? html4`<${PlanModal} modal=${modal} onResolve=${resolveModal} />` : modal.kind === "edit-review" ? html4`<${EditReviewModal} modal=${modal} onResolve=${resolveModal} />` : modal.kind === "workspace" ? html4`<${WorkspaceModal} modal=${modal} onResolve=${resolveModal} />` : modal.kind === "checkpoint" ? html4`<${CheckpointModal} modal=${modal} onResolve=${resolveModal} />` : modal.kind === "revision" ? html4`<${RevisionModal} modal=${modal} onResolve=${resolveModal} />` : modal.kind === "picker" ? html4`<${PickerModal} modal=${modal} onResolve=${resolveModal} />` : modal.kind === "viewer" ? html4`<${ViewerModal} modal=${modal} onResolve=${resolveModal} />` : null : null}
23518
23748
 
23519
23749
  <div class="chat-body">
23520
23750
  <div class="chat-main">
@@ -23599,7 +23829,7 @@ function SideRail({ stats, budgetUsd, activePlan }) {
23599
23829
  const cacheTone = cachePct >= 80 ? "ok" : cachePct >= 50 ? "" : "warn";
23600
23830
  const showBudget = stats != null && typeof budgetUsd === "number" && budgetUsd > 0;
23601
23831
  const budgetPct = showBudget ? Math.min(120, stats.totalCostUsd / budgetUsd * 100) : 0;
23602
- const budgetTone = budgetPct >= 100 ? "err" : budgetPct >= 80 ? "warn" : "";
23832
+ const budgetTone2 = budgetPct >= 100 ? "err" : budgetPct >= 80 ? "warn" : "";
23603
23833
  const walletCurrency = stats?.balance?.[0]?.currency;
23604
23834
  return html4`
23605
23835
  <aside class="chat-rail">
@@ -23622,8 +23852,8 @@ function SideRail({ stats, budgetUsd, activePlan }) {
23622
23852
  <div class="rh">${t4("chat.railToolBudget")}</div>
23623
23853
  <div class="progress-row">
23624
23854
  <span class="lbl">${t4("chat.railSpend")}</span>
23625
- <div class=${`progress ${budgetTone}`}><div class="progress-fill" style=${`width:${Math.min(100, budgetPct)}%`}></div></div>
23626
- <span class="v" style=${budgetTone === "err" ? "color:var(--c-err)" : budgetTone === "warn" ? "color:var(--c-warn)" : ""}>${fmtCost(stats.totalCostUsd, walletCurrency)} / ${fmtCost(budgetUsd, walletCurrency)}</span>
23855
+ <div class=${`progress ${budgetTone2}`}><div class="progress-fill" style=${`width:${Math.min(100, budgetPct)}%`}></div></div>
23856
+ <span class="v" style=${budgetTone2 === "err" ? "color:var(--c-err)" : budgetTone2 === "warn" ? "color:var(--c-warn)" : ""}>${fmtCost(stats.totalCostUsd, walletCurrency)} / ${fmtCost(budgetUsd, walletCurrency)}</span>
23627
23857
  </div>
23628
23858
  </div>
23629
23859
  ` : null}
@@ -24564,6 +24794,35 @@ function MemoryPanel() {
24564
24794
  `;
24565
24795
  }
24566
24796
 
24797
+ // dashboard/src/lib/budget.ts
24798
+ function deriveBudgetState(cap, spent) {
24799
+ const safeSpent = typeof spent === "number" && spent >= 0 ? spent : 0;
24800
+ if (typeof cap !== "number" || cap <= 0) {
24801
+ return { kind: "off", spent: safeSpent };
24802
+ }
24803
+ const pct = safeSpent / cap * 100;
24804
+ if (pct >= 100) return { kind: "exhausted", cap, spent: safeSpent, pct };
24805
+ if (pct >= 80) return { kind: "warn", cap, spent: safeSpent, pct };
24806
+ return { kind: "running", cap, spent: safeSpent, pct };
24807
+ }
24808
+ var QUICK_CAPS_USD = [1, 5, 10, 25, 50];
24809
+ function bumpSuggestions(currentCap) {
24810
+ if (currentCap <= 0) return [];
24811
+ return [niceUp(currentCap * 1.5), niceUp(currentCap * 2), niceUp(currentCap * 4)];
24812
+ }
24813
+ function niceUp(n3) {
24814
+ const eps = 1e-9;
24815
+ if (n3 < 1) return Math.ceil((n3 - eps) * 10) / 10;
24816
+ if (n3 < 10) return Math.ceil((n3 - eps) * 2) / 2;
24817
+ if (n3 < 100) return Math.ceil(n3 - eps);
24818
+ return Math.ceil((n3 - eps) / 5) * 5;
24819
+ }
24820
+ function budgetTone(state) {
24821
+ if (state.kind === "exhausted") return "err";
24822
+ if (state.kind === "warn") return "warn";
24823
+ return "";
24824
+ }
24825
+
24567
24826
  // dashboard/src/lib/use-poll.ts
24568
24827
  function usePoll(path, intervalMs = 2e3) {
24569
24828
  const [data, setData] = d2(null);
@@ -24634,6 +24893,19 @@ function balanceKpi(c3) {
24634
24893
  const symbol = c3.balance.currency === "CNY" ? "\xA5" : c3.balance.currency === "USD" ? "$" : "";
24635
24894
  return kpi(t4("overview.balance"), `${symbol}${c3.balance.total}`, c3.balance.currency, "flat");
24636
24895
  }
24896
+ function budgetKpi(o3) {
24897
+ const state = deriveBudgetState(o3.budgetUsd, o3.cockpit?.currentSession?.totalCostUsd ?? null);
24898
+ if (state.kind === "off") return null;
24899
+ const tone = budgetTone(state);
24900
+ const valueColor = tone === "err" ? "color:var(--c-err)" : tone === "warn" ? "color:var(--c-warn)" : "";
24901
+ return html4`
24902
+ <div class="kpi cock-w-1">
24903
+ <div class="label">${t4("overview.budget")}</div>
24904
+ <div class="value" style=${valueColor}>${fmtUsd(state.spent)} / ${fmtUsd(state.cap)}</div>
24905
+ <div class=${`progress ${tone}`} style="margin-top:4px"><div class="progress-fill" style=${`width:${Math.min(100, state.pct)}%`}></div></div>
24906
+ </div>
24907
+ `;
24908
+ }
24637
24909
  function tokens7dKpi(c3) {
24638
24910
  if (!c3.tokens7d) return kpi(t4("overview.tokens7d"), "\u2014", t4("overview.noUsageYet"), "flat");
24639
24911
  const d3 = deltaPctText(c3.tokens7d.deltaPct);
@@ -24789,6 +25061,7 @@ function OverviewPanel() {
24789
25061
  ${tokens7dKpi(c3)}
24790
25062
  ${cacheHitKpi(c3)}
24791
25063
  ${toolCallsKpi(c3)}
25064
+ ${budgetKpi(o3)}
24792
25065
 
24793
25066
  ${currentSessionBlock(c3)}
24794
25067
  ${costTrendSpark(c3)}
@@ -25001,7 +25274,7 @@ function PermissionsPanel() {
25001
25274
  function statusPill(p3) {
25002
25275
  if (p3.completionRatio >= 1) return html4`<span class="pill ok">${t4("plans.done")}</span>`;
25003
25276
  if (p3.completionRatio > 0) return html4`<span class="pill info">${t4("plans.active")}</span>`;
25004
- return html4`<span class="pill">idle</span>`;
25277
+ return html4`<span class="pill">${t4("plans.idle")}</span>`;
25005
25278
  }
25006
25279
  function PlansPanel() {
25007
25280
  useLang();
@@ -25828,7 +26101,314 @@ function SessionsPanel() {
25828
26101
  `;
25829
26102
  }
25830
26103
 
26104
+ // dashboard/src/lib/loop-control.ts
26105
+ var INTERVAL_PRESETS_MS = [
26106
+ { ms: 3e4, label: "30s" },
26107
+ { ms: 6e4, label: "1m" },
26108
+ { ms: 5 * 6e4, label: "5m" },
26109
+ { ms: 15 * 6e4, label: "15m" },
26110
+ { ms: 60 * 6e4, label: "1h" },
26111
+ { ms: 6 * 60 * 6e4, label: "6h" }
26112
+ ];
26113
+ var UNIT_TO_MS = {
26114
+ s: 1e3,
26115
+ m: 6e4,
26116
+ h: 60 * 6e4
26117
+ };
26118
+ var MIN_INTERVAL_MS = 5e3;
26119
+ var MAX_INTERVAL_MS = 6 * 60 * 6e4;
26120
+ function parseCustomInterval(value, unit) {
26121
+ const n3 = Number.parseFloat(value);
26122
+ if (!Number.isFinite(n3) || n3 <= 0) return null;
26123
+ const ms = Math.round(n3 * UNIT_TO_MS[unit]);
26124
+ if (ms < MIN_INTERVAL_MS || ms > MAX_INTERVAL_MS) return null;
26125
+ return ms;
26126
+ }
26127
+ function formatRemaining(ms) {
26128
+ const safe = Math.max(0, Math.floor(ms / 1e3));
26129
+ const h3 = Math.floor(safe / 3600);
26130
+ const m3 = Math.floor(safe % 3600 / 60);
26131
+ const s3 = safe % 60;
26132
+ if (h3 > 0) return m3 > 0 ? `${h3}h ${m3}m` : `${h3}h`;
26133
+ if (m3 > 0) return s3 > 0 ? `${m3}m ${s3}s` : `${m3}m`;
26134
+ return `${s3}s`;
26135
+ }
26136
+
25831
26137
  // dashboard/src/panels/settings.ts
26138
+ function fmtUsd22(n3) {
26139
+ return `$${n3.toFixed(n3 < 1 ? 4 : 2)}`;
26140
+ }
26141
+ function formatPricing(p3) {
26142
+ if (!p3) return null;
26143
+ return t4("settings.modelPricingLine", {
26144
+ hit: p3.inputCacheHit.toFixed(3),
26145
+ miss: p3.inputCacheMiss.toFixed(3),
26146
+ out: p3.output.toFixed(3)
26147
+ });
26148
+ }
26149
+ function ModelRow({
26150
+ current,
26151
+ catalog,
26152
+ saving,
26153
+ onPick
26154
+ }) {
26155
+ const list2 = catalog?.models ?? null;
26156
+ const ready = list2 && list2.length > 0;
26157
+ if (!ready) {
26158
+ return html4`<code class="mono">${current ?? "\u2014"}</code>`;
26159
+ }
26160
+ const options2 = list2.includes(current) ? list2 : [current, ...list2];
26161
+ const price = catalog?.pricing[current];
26162
+ return html4`
26163
+ <span style="display:inline-flex;flex-direction:column;gap:4px">
26164
+ <select
26165
+ value=${current}
26166
+ onChange=${(e3) => {
26167
+ const next = e3.target.value;
26168
+ if (next && next !== current) onPick(next);
26169
+ }}
26170
+ disabled=${saving}
26171
+ style="font-family:var(--font-mono);min-width:200px"
26172
+ >
26173
+ ${options2.map((m3) => html4`<option key=${m3} value=${m3}>${m3}</option>`)}
26174
+ </select>
26175
+ ${price ? html4`<span style="color:var(--fg-3);font-size:11px;font-family:var(--font-mono)">${formatPricing(price)}</span>` : null}
26176
+ </span>
26177
+ `;
26178
+ }
26179
+ function BudgetGauge({ state }) {
26180
+ if (state.kind === "off") return null;
26181
+ const tone = budgetTone(state);
26182
+ const fill = Math.min(100, state.pct);
26183
+ const valueColor = tone === "err" ? "color:var(--c-err)" : tone === "warn" ? "color:var(--c-warn)" : "color:var(--fg-1)";
26184
+ return html4`
26185
+ <div style="display:flex;flex-direction:column;gap:6px">
26186
+ <div style="display:flex;justify-content:space-between;align-items:baseline;font-size:13px">
26187
+ <span style=${valueColor}>
26188
+ <strong style="font-family:var(--font-mono)">${fmtUsd22(state.spent)}</strong>
26189
+ <span style="color:var(--fg-3)"> ${t4("settings.budgetOf")} </span>
26190
+ <strong style="font-family:var(--font-mono)">${fmtUsd22(state.cap)}</strong>
26191
+ </span>
26192
+ <span style=${`font-family:var(--font-mono);font-size:11px;${valueColor}`}>${state.pct.toFixed(1)}%</span>
26193
+ </div>
26194
+ <div class=${`progress ${tone}`}><div class="progress-fill" style=${`width:${fill}%`}></div></div>
26195
+ <span style="color:var(--fg-3);font-size:11px">
26196
+ ${state.kind === "exhausted" ? t4("settings.budgetRefusing") : state.kind === "warn" ? t4("settings.budgetWarnLine") : t4("settings.budgetIdleLine")}
26197
+ </span>
26198
+ </div>
26199
+ `;
26200
+ }
26201
+ function BudgetSection({ state, saving, onSetCap, onClear }) {
26202
+ const [custom, setCustom] = d2("");
26203
+ const submitCustom = () => {
26204
+ const n3 = Number.parseFloat(custom);
26205
+ if (Number.isFinite(n3) && n3 > 0) {
26206
+ onSetCap(n3);
26207
+ setCustom("");
26208
+ }
26209
+ };
26210
+ const quickButtons = (caps) => caps.map(
26211
+ (c3) => html4`
26212
+ <button
26213
+ key=${c3}
26214
+ class="btn"
26215
+ style="font-family:var(--font-mono)"
26216
+ disabled=${saving}
26217
+ onClick=${() => onSetCap(c3)}
26218
+ >$${c3}</button>
26219
+ `
26220
+ );
26221
+ const customField = html4`
26222
+ <span style="display:inline-flex;align-items:center;gap:4px;margin-left:auto">
26223
+ <span style="color:var(--fg-3);font-size:11px">${t4("settings.budgetCustom")}</span>
26224
+ <input
26225
+ type="number"
26226
+ min="0.01"
26227
+ step="0.01"
26228
+ value=${custom}
26229
+ placeholder="0.00"
26230
+ onInput=${(e3) => setCustom(e3.target.value)}
26231
+ onKeyDown=${(e3) => {
26232
+ if (e3.key === "Enter") submitCustom();
26233
+ }}
26234
+ style="width:72px;font-family:var(--font-mono)"
26235
+ disabled=${saving}
26236
+ />
26237
+ <button
26238
+ class="btn primary"
26239
+ disabled=${saving || !(Number.parseFloat(custom) > 0)}
26240
+ onClick=${submitCustom}
26241
+ >→</button>
26242
+ </span>
26243
+ `;
26244
+ return html4`
26245
+ <div class="card" style="display:flex;flex-direction:column;gap:12px">
26246
+ <${BudgetGauge} state=${state} />
26247
+
26248
+ ${state.kind === "off" ? html4`
26249
+ <div>
26250
+ <div style="color:var(--fg-3);font-size:11px;margin-bottom:6px">${t4("settings.budgetSetCap")}</div>
26251
+ <div style="display:flex;gap:6px;align-items:center;flex-wrap:wrap">
26252
+ ${quickButtons(QUICK_CAPS_USD)}
26253
+ ${customField}
26254
+ </div>
26255
+ </div>
26256
+ ` : state.kind === "warn" || state.kind === "exhausted" ? html4`
26257
+ <div>
26258
+ <div style="color:var(--fg-3);font-size:11px;margin-bottom:6px">${t4("settings.budgetBumpHint")}</div>
26259
+ <div style="display:flex;gap:6px;align-items:center;flex-wrap:wrap">
26260
+ ${bumpSuggestions(state.cap).map(
26261
+ (next) => html4`
26262
+ <button
26263
+ key=${next}
26264
+ class="btn primary"
26265
+ style="font-family:var(--font-mono)"
26266
+ disabled=${saving}
26267
+ onClick=${() => onSetCap(next)}
26268
+ >→ $${next % 1 === 0 ? next : next.toFixed(2)}</button>
26269
+ `
26270
+ )}
26271
+ ${customField}
26272
+ </div>
26273
+ <div style="margin-top:8px">
26274
+ <button class="btn" disabled=${saving} onClick=${onClear}>${t4("settings.budgetClear")}</button>
26275
+ </div>
26276
+ </div>
26277
+ ` : html4`
26278
+ <div style="display:flex;gap:6px;align-items:center;flex-wrap:wrap">
26279
+ ${bumpSuggestions(state.cap).map(
26280
+ (next) => html4`
26281
+ <button
26282
+ key=${next}
26283
+ class="btn"
26284
+ style="font-family:var(--font-mono)"
26285
+ disabled=${saving}
26286
+ onClick=${() => onSetCap(next)}
26287
+ >→ $${next % 1 === 0 ? next : next.toFixed(2)}</button>
26288
+ `
26289
+ )}
26290
+ ${customField}
26291
+ <button
26292
+ class="btn"
26293
+ style="margin-left:8px"
26294
+ disabled=${saving}
26295
+ onClick=${onClear}
26296
+ >${t4("settings.budgetClear")}</button>
26297
+ </div>
26298
+ `}
26299
+ </div>
26300
+ `;
26301
+ }
26302
+ function LoopSection({
26303
+ status,
26304
+ remainingMs,
26305
+ avgIterCostUsd,
26306
+ busy,
26307
+ onStart,
26308
+ onStop
26309
+ }) {
26310
+ const [intervalMs, setIntervalMs] = d2(INTERVAL_PRESETS_MS[1].ms);
26311
+ const [prompt, setPrompt] = d2("");
26312
+ const [customValue, setCustomValue] = d2("");
26313
+ const [customUnit, setCustomUnit] = d2("m");
26314
+ if (status) {
26315
+ return html4`
26316
+ <div class="card" style="display:flex;flex-direction:column;gap:10px">
26317
+ <div style="display:flex;justify-content:space-between;align-items:baseline">
26318
+ <span style="color:var(--c-warn);font-family:var(--font-mono);font-size:11px">⟳ ${t4("settings.loopRunning")}</span>
26319
+ <span style="color:var(--fg-3);font-size:11px">
26320
+ ${t4("settings.loopIter", { iter: status.iter })} · ${t4("settings.loopFiresIn", { remaining: formatRemaining(remainingMs) })}
26321
+ </span>
26322
+ </div>
26323
+ <div style="background:var(--bg-elev-2);border:1px solid var(--bd);border-radius:var(--r);padding:8px 10px;font-family:var(--font-mono);font-size:12px;color:var(--fg-1);white-space:pre-wrap;max-height:120px;overflow-y:auto">${status.prompt}</div>
26324
+ <div>
26325
+ <button class="btn danger" disabled=${busy} onClick=${onStop}>${t4("settings.loopStop")}</button>
26326
+ </div>
26327
+ </div>
26328
+ `;
26329
+ }
26330
+ const customMs = parseCustomInterval(customValue, customUnit);
26331
+ const canStart = !busy && intervalMs > 0 && prompt.trim().length > 0;
26332
+ return html4`
26333
+ <div class="card" style="display:flex;flex-direction:column;gap:10px">
26334
+ <div style="color:var(--fg-3);font-size:11px">
26335
+ ${t4("settings.loopIdleHint")}
26336
+ ${typeof avgIterCostUsd === "number" && avgIterCostUsd > 0 ? html4` ${t4("settings.loopCostHint", { cost: `$${avgIterCostUsd.toFixed(4)}` })}` : null}
26337
+ </div>
26338
+ <div style="display:flex;flex-direction:column;gap:6px">
26339
+ <span style="color:var(--fg-3);font-size:11px">${t4("settings.loopInterval")}</span>
26340
+ <div style="display:flex;gap:6px;align-items:center;flex-wrap:wrap">
26341
+ ${INTERVAL_PRESETS_MS.map(
26342
+ (p3) => html4`
26343
+ <button
26344
+ key=${p3.ms}
26345
+ class=${`btn ${intervalMs === p3.ms && customValue === "" ? "primary" : ""}`}
26346
+ style="font-family:var(--font-mono)"
26347
+ disabled=${busy}
26348
+ onClick=${() => {
26349
+ setIntervalMs(p3.ms);
26350
+ setCustomValue("");
26351
+ }}
26352
+ >${p3.label}</button>
26353
+ `
26354
+ )}
26355
+ <span style="display:inline-flex;align-items:center;gap:4px;margin-left:auto">
26356
+ <span style="color:var(--fg-3);font-size:11px">${t4("settings.loopCustom")}</span>
26357
+ <input
26358
+ type="number"
26359
+ min="1"
26360
+ step="1"
26361
+ value=${customValue}
26362
+ onInput=${(e3) => {
26363
+ const raw = e3.target.value;
26364
+ setCustomValue(raw);
26365
+ const ms = parseCustomInterval(raw, customUnit);
26366
+ if (ms !== null) setIntervalMs(ms);
26367
+ }}
26368
+ style="width:64px;font-family:var(--font-mono)"
26369
+ disabled=${busy}
26370
+ />
26371
+ <select
26372
+ value=${customUnit}
26373
+ onChange=${(e3) => {
26374
+ const next = e3.target.value;
26375
+ setCustomUnit(next);
26376
+ if (customValue) {
26377
+ const ms = parseCustomInterval(customValue, next);
26378
+ if (ms !== null) setIntervalMs(ms);
26379
+ }
26380
+ }}
26381
+ disabled=${busy}
26382
+ >
26383
+ <option value="s">s</option>
26384
+ <option value="m">m</option>
26385
+ <option value="h">h</option>
26386
+ </select>
26387
+ </span>
26388
+ </div>
26389
+ ${customValue && customMs === null ? html4`<span style="color:var(--c-err);font-size:11px">${t4("settings.loopRangeError")}</span>` : null}
26390
+ </div>
26391
+ <div style="display:flex;flex-direction:column;gap:6px">
26392
+ <span style="color:var(--fg-3);font-size:11px">${t4("settings.loopPrompt")}</span>
26393
+ <textarea
26394
+ rows="3"
26395
+ placeholder=${t4("settings.loopPromptPlaceholder")}
26396
+ value=${prompt}
26397
+ onInput=${(e3) => setPrompt(e3.target.value)}
26398
+ style="width:100%;font-family:var(--font-mono);resize:vertical"
26399
+ disabled=${busy}
26400
+ ></textarea>
26401
+ </div>
26402
+ <div>
26403
+ <button
26404
+ class="btn primary"
26405
+ disabled=${!canStart}
26406
+ onClick=${() => onStart(intervalMs, prompt.trim())}
26407
+ >${t4("settings.loopStart")}</button>
26408
+ </div>
26409
+ </div>
26410
+ `;
26411
+ }
25832
26412
  function SettingsPanel() {
25833
26413
  useLang();
25834
26414
  const [data, setData] = d2(null);
@@ -25836,6 +26416,12 @@ function SettingsPanel() {
25836
26416
  const [saving, setSaving] = d2(false);
25837
26417
  const [saved, setSaved] = d2(null);
25838
26418
  const [draft, setDraft] = d2({});
26419
+ const [catalog, setCatalog] = d2(null);
26420
+ const [loopStatus, setLoopStatus] = d2(null);
26421
+ const [loopAvgCost, setLoopAvgCost] = d2(null);
26422
+ const [loopBusy, setLoopBusy] = d2(false);
26423
+ const lastStatusSyncRef = A2(0);
26424
+ const [now, setNow] = d2(() => Date.now());
25839
26425
  const load = q2(async () => {
25840
26426
  try {
25841
26427
  const r3 = await api("/settings");
@@ -25848,6 +26434,64 @@ function SettingsPanel() {
25848
26434
  y2(() => {
25849
26435
  load();
25850
26436
  }, [load]);
26437
+ y2(() => {
26438
+ api("/models").then(setCatalog).catch(() => void 0);
26439
+ }, []);
26440
+ const refreshLoop = q2(async () => {
26441
+ try {
26442
+ const r3 = await api("/loop/status");
26443
+ setLoopStatus(r3.status);
26444
+ lastStatusSyncRef.current = Date.now();
26445
+ } catch {
26446
+ }
26447
+ try {
26448
+ const r3 = await api("/overview");
26449
+ setLoopAvgCost(r3.stats?.lastTurnCostUsd ?? null);
26450
+ } catch {
26451
+ }
26452
+ }, []);
26453
+ y2(() => {
26454
+ let cancelled = false;
26455
+ refreshLoop();
26456
+ const id = setInterval(() => {
26457
+ if (!cancelled) refreshLoop();
26458
+ }, 5e3);
26459
+ return () => {
26460
+ cancelled = true;
26461
+ clearInterval(id);
26462
+ };
26463
+ }, [refreshLoop]);
26464
+ y2(() => {
26465
+ if (!loopStatus) return;
26466
+ const id = setInterval(() => setNow(Date.now()), 1e3);
26467
+ return () => clearInterval(id);
26468
+ }, [loopStatus]);
26469
+ const remainingMs = loopStatus ? Math.max(0, loopStatus.nextFireMs - (now - lastStatusSyncRef.current)) : 0;
26470
+ const startLoop = q2(
26471
+ async (intervalMs, prompt) => {
26472
+ setLoopBusy(true);
26473
+ try {
26474
+ await api("/loop/start", { method: "POST", body: { intervalMs, prompt } });
26475
+ await refreshLoop();
26476
+ } catch (err) {
26477
+ setError(err.message);
26478
+ } finally {
26479
+ setLoopBusy(false);
26480
+ }
26481
+ },
26482
+ [refreshLoop]
26483
+ );
26484
+ const stopLoop = q2(async () => {
26485
+ setLoopBusy(true);
26486
+ try {
26487
+ await api("/loop/stop", { method: "POST" });
26488
+ await refreshLoop();
26489
+ } catch (err) {
26490
+ setError(err.message);
26491
+ } finally {
26492
+ setLoopBusy(false);
26493
+ }
26494
+ }, [refreshLoop]);
25851
26495
  const save = q2(
25852
26496
  async (fields) => {
25853
26497
  setSaving(true);
@@ -25991,11 +26635,50 @@ function SettingsPanel() {
25991
26635
  )}
25992
26636
  </div>
25993
26637
 
26638
+ ${sectionH3(t4("settings.sectionCompute"))}
26639
+ <div class="card">
26640
+ ${fieldRow(
26641
+ t4("settings.proNext"),
26642
+ html4`
26643
+ <button
26644
+ class=${`btn ${v3.proNext ? "primary" : ""}`}
26645
+ onClick=${() => save({ proNext: !v3.proNext })}
26646
+ disabled=${saving}
26647
+ >${v3.proNext ? t4("settings.proArmed") : t4("settings.proArm")}</button>
26648
+ `,
26649
+ t4("settings.proNextNote")
26650
+ )}
26651
+ </div>
26652
+
26653
+ ${sectionH3(t4("settings.sectionBudget"))}
26654
+ <${BudgetSection}
26655
+ state=${deriveBudgetState(v3.budgetUsd, v3.sessionSpendUsd)}
26656
+ saving=${saving}
26657
+ onSetCap=${(usd) => save({ budgetUsd: usd })}
26658
+ onClear=${() => save({ budgetUsd: null })}
26659
+ />
26660
+
26661
+ ${sectionH3(t4("settings.sectionLoop"))}
26662
+ <${LoopSection}
26663
+ status=${loopStatus}
26664
+ remainingMs=${remainingMs}
26665
+ avgIterCostUsd=${loopAvgCost}
26666
+ busy=${loopBusy}
26667
+ onStart=${startLoop}
26668
+ onStop=${stopLoop}
26669
+ />
26670
+
25994
26671
  ${sectionH3(t4("settings.sectionRuntime"))}
25995
26672
  <div class="card">
25996
26673
  ${fieldRow(
25997
26674
  t4("settings.activeModel"),
25998
- html4`<code class="mono">${v3.model ?? "\u2014"}</code>`
26675
+ html4`<${ModelRow}
26676
+ current=${v3.model ?? "\u2014"}
26677
+ catalog=${catalog}
26678
+ saving=${saving}
26679
+ onPick=${(m3) => save({ model: m3 })}
26680
+ />`,
26681
+ t4("settings.appliesNextTurn")
25999
26682
  )}
26000
26683
  ${fieldRow(
26001
26684
  t4("settings.editMode"),