@ckanner/flywheel 0.32.1 → 0.33.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.
- package/CHANGELOG.md +10 -1
- package/README.md +7 -0
- package/README.zh-CN.md +5 -0
- package/THIRD_PARTY_NOTICES.md +42 -0
- package/bin/runtime.js +25 -31
- package/cordis.patch.yml +5 -0
- package/dist/bridge.mjs +421 -0
- package/dist/claude-code-pro-bridge.mjs +82 -0
- package/dist/claude-code-pro-worker.mjs +5 -0
- package/dist/client.js +4078 -0
- package/dist/codex-pro-bridge.mjs +75 -0
- package/dist/codex-pro-worker.mjs +5 -0
- package/dist/console-read.mjs +51 -0
- package/dist/control.mjs +218 -0
- package/dist/index.mjs +185 -0
- package/dist/installation.mjs +56 -0
- package/dist/machine-worker.mjs +241 -0
- package/dist/policy.mjs +91 -0
- package/dist/prewarm.mjs +136 -0
- package/dist/protocol.mjs +260 -0
- package/dist/task-association.mjs +127 -0
- package/dist/worker.mjs +52 -0
- package/dist/workspaces.mjs +42 -0
- package/docs/README.md +1 -1
- package/docs/README.zh-CN.md +1 -1
- package/docs/design.md +7 -4
- package/docs/en/configuration.md +1 -1
- package/docs/en/contributing.md +5 -6
- package/docs/en/flow-console.md +1 -1
- package/docs/en/getting-started.md +9 -2
- package/docs/en/releasing.md +132 -174
- package/docs/validation/2026-11-13-release-0.32.1.md +83 -0
- package/docs/validation/2026-11-14-single-package.md +60 -0
- package/docs/validation/README.md +2 -0
- package/docs/zh-CN/configuration.md +1 -1
- package/docs/zh-CN/contributing.md +4 -5
- package/docs/zh-CN/flow-console.md +1 -1
- package/docs/zh-CN/getting-started.md +7 -3
- package/docs/zh-CN/releasing.md +97 -81
- package/native/darwin-amd64/flywheel +0 -0
- package/native/darwin-arm64/flywheel +0 -0
- package/native/linux-amd64/flywheel +0 -0
- package/native/linux-arm64/flywheel +0 -0
- package/native/manifest.json +22 -0
- package/node_modules/dsh-subagent-codex-pro/CHANGELOG.md +403 -0
- package/node_modules/dsh-subagent-codex-pro/LICENSE +21 -0
- package/node_modules/dsh-subagent-codex-pro/README.md +557 -0
- package/node_modules/dsh-subagent-codex-pro/cordis.patch.yml +30 -0
- package/node_modules/dsh-subagent-codex-pro/docs/README.zh.md +355 -0
- package/node_modules/dsh-subagent-codex-pro/docs/machine-lifecycle-validation.md +65 -0
- package/node_modules/dsh-subagent-codex-pro/docs/machine-lifecycle.md +144 -0
- package/node_modules/dsh-subagent-codex-pro/docs/machine-lifecycle.zh.md +98 -0
- package/node_modules/dsh-subagent-codex-pro/docs/machine.md +194 -0
- package/node_modules/dsh-subagent-codex-pro/docs/orchestration.zh.md +86 -0
- package/node_modules/dsh-subagent-codex-pro/lib/codex-exec.js +68 -0
- package/node_modules/dsh-subagent-codex-pro/lib/codex-exec.js.map +1 -0
- package/node_modules/dsh-subagent-codex-pro/lib/dsh-types.js +15 -0
- package/node_modules/dsh-subagent-codex-pro/lib/dsh-types.js.map +1 -0
- package/node_modules/dsh-subagent-codex-pro/lib/index.js +830 -0
- package/node_modules/dsh-subagent-codex-pro/lib/index.js.map +1 -0
- package/node_modules/dsh-subagent-codex-pro/lib/jobs.js +35 -0
- package/node_modules/dsh-subagent-codex-pro/lib/jobs.js.map +1 -0
- package/node_modules/dsh-subagent-codex-pro/lib/jsonrpc.js +229 -0
- package/node_modules/dsh-subagent-codex-pro/lib/jsonrpc.js.map +1 -0
- package/node_modules/dsh-subagent-codex-pro/lib/machine-cli.js +101 -0
- package/node_modules/dsh-subagent-codex-pro/lib/machine-cli.js.map +1 -0
- package/node_modules/dsh-subagent-codex-pro/lib/machine-local.js +122 -0
- package/node_modules/dsh-subagent-codex-pro/lib/machine-local.js.map +1 -0
- package/node_modules/dsh-subagent-codex-pro/lib/machine-recovery.js +167 -0
- package/node_modules/dsh-subagent-codex-pro/lib/machine-recovery.js.map +1 -0
- package/node_modules/dsh-subagent-codex-pro/lib/machine-supervision.js +141 -0
- package/node_modules/dsh-subagent-codex-pro/lib/machine-supervision.js.map +1 -0
- package/node_modules/dsh-subagent-codex-pro/lib/machine-supervisor.js +128 -0
- package/node_modules/dsh-subagent-codex-pro/lib/machine-supervisor.js.map +1 -0
- package/node_modules/dsh-subagent-codex-pro/lib/machine.js +104 -0
- package/node_modules/dsh-subagent-codex-pro/lib/machine.js.map +1 -0
- package/node_modules/dsh-subagent-codex-pro/lib/process-guardian.js +156 -0
- package/node_modules/dsh-subagent-codex-pro/lib/process-guardian.js.map +1 -0
- package/node_modules/dsh-subagent-codex-pro/lib/run.js +842 -0
- package/node_modules/dsh-subagent-codex-pro/lib/run.js.map +1 -0
- package/node_modules/dsh-subagent-codex-pro/lib/structured.js +329 -0
- package/node_modules/dsh-subagent-codex-pro/lib/structured.js.map +1 -0
- package/node_modules/dsh-subagent-codex-pro/lib/tool.js +295 -0
- package/node_modules/dsh-subagent-codex-pro/lib/tool.js.map +1 -0
- package/node_modules/dsh-subagent-codex-pro/lib/types/codex-exec.d.ts +88 -0
- package/node_modules/dsh-subagent-codex-pro/lib/types/codex-exec.d.ts.map +1 -0
- package/node_modules/dsh-subagent-codex-pro/lib/types/dsh-types.d.ts +179 -0
- package/node_modules/dsh-subagent-codex-pro/lib/types/dsh-types.d.ts.map +1 -0
- package/node_modules/dsh-subagent-codex-pro/lib/types/index.d.ts +266 -0
- package/node_modules/dsh-subagent-codex-pro/lib/types/index.d.ts.map +1 -0
- package/node_modules/dsh-subagent-codex-pro/lib/types/jobs.d.ts +34 -0
- package/node_modules/dsh-subagent-codex-pro/lib/types/jobs.d.ts.map +1 -0
- package/node_modules/dsh-subagent-codex-pro/lib/types/jsonrpc.d.ts +113 -0
- package/node_modules/dsh-subagent-codex-pro/lib/types/jsonrpc.d.ts.map +1 -0
- package/node_modules/dsh-subagent-codex-pro/lib/types/machine-cli.d.ts +3 -0
- package/node_modules/dsh-subagent-codex-pro/lib/types/machine-cli.d.ts.map +1 -0
- package/node_modules/dsh-subagent-codex-pro/lib/types/machine-local.d.ts +4 -0
- package/node_modules/dsh-subagent-codex-pro/lib/types/machine-local.d.ts.map +1 -0
- package/node_modules/dsh-subagent-codex-pro/lib/types/machine-recovery.d.ts +53 -0
- package/node_modules/dsh-subagent-codex-pro/lib/types/machine-recovery.d.ts.map +1 -0
- package/node_modules/dsh-subagent-codex-pro/lib/types/machine-supervision.d.ts +8 -0
- package/node_modules/dsh-subagent-codex-pro/lib/types/machine-supervision.d.ts.map +1 -0
- package/node_modules/dsh-subagent-codex-pro/lib/types/machine-supervisor.d.ts +2 -0
- package/node_modules/dsh-subagent-codex-pro/lib/types/machine-supervisor.d.ts.map +1 -0
- package/node_modules/dsh-subagent-codex-pro/lib/types/machine.d.ts +70 -0
- package/node_modules/dsh-subagent-codex-pro/lib/types/machine.d.ts.map +1 -0
- package/node_modules/dsh-subagent-codex-pro/lib/types/process-guardian.d.ts +22 -0
- package/node_modules/dsh-subagent-codex-pro/lib/types/process-guardian.d.ts.map +1 -0
- package/node_modules/dsh-subagent-codex-pro/lib/types/run.d.ts +386 -0
- package/node_modules/dsh-subagent-codex-pro/lib/types/run.d.ts.map +1 -0
- package/node_modules/dsh-subagent-codex-pro/lib/types/structured.d.ts +35 -0
- package/node_modules/dsh-subagent-codex-pro/lib/types/structured.d.ts.map +1 -0
- package/node_modules/dsh-subagent-codex-pro/lib/types/tool.d.ts +107 -0
- package/node_modules/dsh-subagent-codex-pro/lib/types/tool.d.ts.map +1 -0
- package/node_modules/dsh-subagent-codex-pro/lib/types/usage.d.ts +131 -0
- package/node_modules/dsh-subagent-codex-pro/lib/types/usage.d.ts.map +1 -0
- package/node_modules/dsh-subagent-codex-pro/lib/types/wire.d.ts +321 -0
- package/node_modules/dsh-subagent-codex-pro/lib/types/wire.d.ts.map +1 -0
- package/node_modules/dsh-subagent-codex-pro/lib/usage.js +356 -0
- package/node_modules/dsh-subagent-codex-pro/lib/usage.js.map +1 -0
- package/node_modules/dsh-subagent-codex-pro/lib/wire.js +986 -0
- package/node_modules/dsh-subagent-codex-pro/lib/wire.js.map +1 -0
- package/node_modules/dsh-subagent-codex-pro/package.json +76 -0
- package/npm/install.js +59 -6
- package/package.json +74 -12
- package/plugins/dsh/LICENSE +21 -0
- package/plugins/dsh/PROTOCOL.md +607 -0
- package/plugins/dsh/README.en.md +4 -0
- package/plugins/dsh/README.md +342 -0
- package/plugins/dsh/README.zh-CN.md +279 -0
- package/plugins/dsh/THIRD_PARTY_NOTICES.md +42 -0
- package/plugins/dsh/vendor/README.md +17 -0
- package/plugins/dsh/vendor/codex-pro.json +16 -0
package/dist/client.js
ADDED
|
@@ -0,0 +1,4078 @@
|
|
|
1
|
+
window.__ModuleLoader__.load({id:"@ckanner/flywheel",factory:(require)=>{
|
|
2
|
+
const module={exports:{}};const exports=module.exports;
|
|
3
|
+
// The build wraps this file in DSH's lazy browser ModuleLoader factory.
|
|
4
|
+
// React is the only runtime module dependency. External plain-JS plugins must
|
|
5
|
+
// not import Harness Client packages (DSH practices, b830ec950faba0a46dda564be3e2d854afed94f1).
|
|
6
|
+
const React = require('react');
|
|
7
|
+
const h = React.createElement;
|
|
8
|
+
const NS = 'flywheel-ui';
|
|
9
|
+
// build.mjs supplies scoped CSS; React owns its style element inside this view.
|
|
10
|
+
const css = {"actions":"flywheel_actions_K2PPkW","advanced":"flywheel_advanced_K2PPkW","advancedBody":"flywheel_advancedBody_K2PPkW","badge":"flywheel_badge_K2PPkW","binding":"flywheel_binding_K2PPkW","bindingHeader":"flywheel_bindingHeader_K2PPkW","bindingTitle":"flywheel_bindingTitle_K2PPkW","bindings":"flywheel_bindings_K2PPkW","budget":"flywheel_budget_K2PPkW","button":"flywheel_button_K2PPkW","buttonOutline":"flywheel_buttonOutline_K2PPkW","buttonPrimary":"flywheel_buttonPrimary_K2PPkW","buttonSmall":"flywheel_buttonSmall_K2PPkW","canvasBreadcrumb":"flywheel_canvasBreadcrumb_K2PPkW","canvasEdges":"flywheel_canvasEdges_K2PPkW","canvasFooter":"flywheel_canvasFooter_K2PPkW","canvasHeader":"flywheel_canvasHeader_K2PPkW","canvasLegend":"flywheel_canvasLegend_K2PPkW","canvasList":"flywheel_canvasList_K2PPkW","canvasListNode":"flywheel_canvasListNode_K2PPkW","canvasMain":"flywheel_canvasMain_K2PPkW","canvasNode":"flywheel_canvasNode_K2PPkW","canvasPagination":"flywheel_canvasPagination_K2PPkW","canvasScaleBounds":"flywheel_canvasScaleBounds_K2PPkW","canvasViewport":"flywheel_canvasViewport_K2PPkW","canvasWorkbench":"flywheel_canvasWorkbench_K2PPkW","canvasWorld":"flywheel_canvasWorld_K2PPkW","check":"flywheel_check_K2PPkW","checkBody":"flywheel_checkBody_K2PPkW","checkState":"flywheel_checkState_K2PPkW","checks":"flywheel_checks_K2PPkW","configCallout":"flywheel_configCallout_K2PPkW","configContent":"flywheel_configContent_K2PPkW","configField":"flywheel_configField_K2PPkW","configFieldControl":"flywheel_configFieldControl_K2PPkW","configFieldLabel":"flywheel_configFieldLabel_K2PPkW","configFields":"flywheel_configFields_K2PPkW","configHeader":"flywheel_configHeader_K2PPkW","configHintLine":"flywheel_configHintLine_K2PPkW","configLoading":"flywheel_configLoading_K2PPkW","configMain":"flywheel_configMain_K2PPkW","configMore":"flywheel_configMore_K2PPkW","configNav":"flywheel_configNav_K2PPkW","configSaveBar":"flywheel_configSaveBar_K2PPkW","configSource":"flywheel_configSource_K2PPkW","configSwitch":"flywheel_configSwitch_K2PPkW","configSwitchState":"flywheel_configSwitchState_K2PPkW","configSwitchTrack":"flywheel_configSwitchTrack_K2PPkW","configTiming":"flywheel_configTiming_K2PPkW","configTitle":"flywheel_configTitle_K2PPkW","confirmation":"flywheel_confirmation_K2PPkW","consoleCanvas":"flywheel_consoleCanvas_K2PPkW","consoleProperties":"flywheel_consoleProperties_K2PPkW","consoleTabs":"flywheel_consoleTabs_K2PPkW","consoleToolbar":"flywheel_consoleToolbar_K2PPkW","details":"flywheel_details_K2PPkW","dialogActions":"flywheel_dialogActions_K2PPkW","dialogBody":"flywheel_dialogBody_K2PPkW","dialogHeader":"flywheel_dialogHeader_K2PPkW","dialogTitle":"flywheel_dialogTitle_K2PPkW","editor":"flywheel_editor_K2PPkW","editorBinding":"flywheel_editorBinding_K2PPkW","editorBindings":"flywheel_editorBindings_K2PPkW","editorField":"flywheel_editorField_K2PPkW","editorGrid":"flywheel_editorGrid_K2PPkW","editorInput":"flywheel_editorInput_K2PPkW","editorJSON":"flywheel_editorJSON_K2PPkW","empty":"flywheel_empty_K2PPkW","error":"flywheel_error_K2PPkW","errorText":"flywheel_errorText_K2PPkW","feedback":"flywheel_feedback_K2PPkW","flywheel-dash":"flywheel_flywheel-dash_K2PPkW","flywheel-spin":"flywheel_flywheel-spin_K2PPkW","focusCanvas":"flywheel_focusCanvas_K2PPkW","focusIdentity":"flywheel_focusIdentity_K2PPkW","header":"flywheel_header_K2PPkW","heading":"flywheel_heading_K2PPkW","historyPage":"flywheel_historyPage_K2PPkW","idleFeedback":"flywheel_idleFeedback_K2PPkW","inputUnit":"flywheel_inputUnit_K2PPkW","inspectorActions":"flywheel_inspectorActions_K2PPkW","inspectorBody":"flywheel_inspectorBody_K2PPkW","inspectorFooter":"flywheel_inspectorFooter_K2PPkW","inspectorHeader":"flywheel_inspectorHeader_K2PPkW","inspectorSection":"flywheel_inspectorSection_K2PPkW","inspectorTabs":"flywheel_inspectorTabs_K2PPkW","launchOptions":"flywheel_launchOptions_K2PPkW","launchOptionsBody":"flywheel_launchOptionsBody_K2PPkW","legacySummary":"flywheel_legacySummary_K2PPkW","loading":"flywheel_loading_K2PPkW","loadingCompact":"flywheel_loadingCompact_K2PPkW","meta":"flywheel_meta_K2PPkW","metricRow":"flywheel_metricRow_K2PPkW","metricScope":"flywheel_metricScope_K2PPkW","modeSelect":"flywheel_modeSelect_K2PPkW","modelRow":"flywheel_modelRow_K2PPkW","modelRows":"flywheel_modelRows_K2PPkW","nodeInspector":"flywheel_nodeInspector_K2PPkW","nodeIssue":"flywheel_nodeIssue_K2PPkW","nodeOutcome":"flywheel_nodeOutcome_K2PPkW","nodeProvider":"flywheel_nodeProvider_K2PPkW","nodeStages":"flywheel_nodeStages_K2PPkW","nodeState":"flywheel_nodeState_K2PPkW","notice":"flywheel_notice_K2PPkW","packets":"flywheel_packets_K2PPkW","page":"flywheel_page_K2PPkW","path":"flywheel_path_K2PPkW","properties":"flywheel_properties_K2PPkW","raw":"flywheel_raw_K2PPkW","reasonInput":"flywheel_reasonInput_K2PPkW","repositoryLinks":"flywheel_repositoryLinks_K2PPkW","runDetail":"flywheel_runDetail_K2PPkW","runExplorer":"flywheel_runExplorer_K2PPkW","runList":"flywheel_runList_K2PPkW","runMetrics":"flywheel_runMetrics_K2PPkW","runMetricsHint":"flywheel_runMetricsHint_K2PPkW","runRow":"flywheel_runRow_K2PPkW","runSplit":"flywheel_runSplit_K2PPkW","runTitle":"flywheel_runTitle_K2PPkW","runToolbar":"flywheel_runToolbar_K2PPkW","section":"flywheel_section_K2PPkW","sectionHeader":"flywheel_sectionHeader_K2PPkW","segmented":"flywheel_segmented_K2PPkW","selection":"flywheel_selection_K2PPkW","sessionRecord":"flywheel_sessionRecord_K2PPkW","sessionSummary":"flywheel_sessionSummary_K2PPkW","settingsHelp":"flywheel_settingsHelp_K2PPkW","settingsPage":"flywheel_settingsPage_K2PPkW","settingsSubheading":"flywheel_settingsSubheading_K2PPkW","spinner":"flywheel_spinner_K2PPkW","spinnerArc":"flywheel_spinnerArc_K2PPkW","spinnerMotion":"flywheel_spinnerMotion_K2PPkW","spinnerTrack":"flywheel_spinnerTrack_K2PPkW","stageScope":"flywheel_stageScope_K2PPkW","stageTable":"flywheel_stageTable_K2PPkW","stageTableScroll":"flywheel_stageTableScroll_K2PPkW","stageTimeline":"flywheel_stageTimeline_K2PPkW","taskChooser":"flywheel_taskChooser_K2PPkW","taskContext":"flywheel_taskContext_K2PPkW","taskLocations":"flywheel_taskLocations_K2PPkW","taskSelect":"flywheel_taskSelect_K2PPkW","title":"flywheel_title_K2PPkW","warningText":"flywheel_warningText_K2PPkW","workbenchDialog":"flywheel_workbenchDialog_K2PPkW","workbenchEmpty":"flywheel_workbenchEmpty_K2PPkW","workbenchHeader":"flywheel_workbenchHeader_K2PPkW","workbenchNav":"flywheel_workbenchNav_K2PPkW","workbenchStatus":"flywheel_workbenchStatus_K2PPkW","workflow":"flywheel_workflow_K2PPkW","workflowPage":"flywheel_workflowPage_K2PPkW"};
|
|
11
|
+
const stylesheet = ".flywheel_page_K2PPkW {\n box-sizing: border-box;\n width: 100%;\n min-width: 0;\n max-width: none;\n height: clamp(540px, 100dvh - 160px, 900px);\n color: var(--dsw-alias-label-primary);\n overflow-wrap: anywhere;\n flex-direction: column;\n margin: 0;\n padding: 0;\n font-family: inherit;\n font-size: 13px;\n line-height: 20px;\n display: flex;\n overflow: hidden;\n}\n\n.flywheel_page_K2PPkW p, .flywheel_page_K2PPkW h2, .flywheel_page_K2PPkW h3, .flywheel_page_K2PPkW h4 {\n margin: 0;\n}\n\n.flywheel_page_K2PPkW code, .flywheel_raw_K2PPkW {\n font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;\n font-size: inherit;\n}\n\n.flywheel_header_K2PPkW {\n gap: 4px;\n margin-bottom: 24px;\n display: grid;\n}\n\n.flywheel_title_K2PPkW {\n font-size: 18px;\n font-weight: 500;\n line-height: 28px;\n}\n\n.flywheel_heading_K2PPkW {\n font-size: 14px;\n font-weight: 500;\n line-height: 22px;\n}\n\n.flywheel_meta_K2PPkW {\n color: var(--dsw-alias-label-secondary);\n}\n\n.flywheel_selection_K2PPkW {\n gap: 10px;\n display: grid;\n}\n\n.flywheel_button_K2PPkW {\n box-sizing: border-box;\n min-height: 34px;\n color: var(--dsw-alias-label-primary);\n font: inherit;\n cursor: pointer;\n background: none;\n border: 0;\n border-radius: 8px;\n flex-shrink: 0;\n justify-content: center;\n align-items: center;\n gap: 6px;\n padding: 6px 12px;\n font-weight: 500;\n line-height: 20px;\n display: inline-flex;\n}\n\n.flywheel_button_K2PPkW:disabled {\n opacity: .4;\n cursor: not-allowed;\n}\n\n.flywheel_button_K2PPkW:focus-visible {\n outline: 2px solid var(--dsw-alias-state-business-primary);\n outline-offset: 2px;\n}\n\n.flywheel_buttonSmall_K2PPkW {\n border-radius: 8px;\n min-height: 32px;\n padding: 5px 10px;\n font-size: 12px;\n line-height: 20px;\n}\n\n.flywheel_buttonPrimary_K2PPkW {\n background: var(--dsw-alias-state-business-primary);\n color: var(--dsw-alias-label-primary-foreground);\n}\n\n.flywheel_buttonOutline_K2PPkW {\n border: .5px solid var(--dsw-alias-border-l3);\n}\n\n@media (hover: hover) {\n .flywheel_buttonPrimary_K2PPkW:hover:not(:disabled) {\n background: color-mix(in srgb, var(--dsw-alias-state-business-primary), var(--dsw-alias-bg-layer-1) 12%);\n }\n\n .flywheel_buttonOutline_K2PPkW:hover:not(:disabled) {\n background: var(--dsw-alias-interactive-bg-hover);\n }\n}\n\n.flywheel_actions_K2PPkW {\n flex-wrap: wrap;\n align-items: center;\n gap: 8px;\n display: flex;\n}\n\n.flywheel_actions_K2PPkW > button {\n overflow-wrap: anywhere;\n max-width: 100%;\n height: auto;\n}\n\n.flywheel_page_K2PPkW {\n container-type: inline-size;\n}\n\n.flywheel_page_K2PPkW *, .flywheel_page_K2PPkW :before, .flywheel_page_K2PPkW :after {\n box-sizing: border-box;\n}\n\n.flywheel_page_K2PPkW [hidden] {\n display: none !important;\n}\n\n.flywheel_workbenchHeader_K2PPkW {\n border-bottom: .5px solid var(--dsw-alias-border-l3);\n flex: none;\n align-items: center;\n gap: 20px;\n min-height: 76px;\n padding: 16px 24px;\n display: flex;\n}\n\n.flywheel_repositoryLinks_K2PPkW {\n flex-wrap: wrap;\n align-items: center;\n gap: 12px;\n min-width: 0;\n margin-left: auto;\n font-size: 12px;\n display: flex;\n}\n\n.flywheel_repositoryLinks_K2PPkW a {\n color: var(--dsw-alias-link);\n overflow-wrap: anywhere;\n font-weight: 500;\n text-decoration: none;\n}\n\n.flywheel_repositoryLinks_K2PPkW a:hover {\n text-underline-offset: 3px;\n text-decoration: underline dotted;\n}\n\n.flywheel_consoleToolbar_K2PPkW {\n flex-wrap: wrap;\n align-items: center;\n gap: 12px;\n display: flex;\n}\n\n.flywheel_consoleTabs_K2PPkW {\n border: 0;\n flex: none;\n align-self: stretch;\n gap: 22px;\n padding: 0;\n display: flex;\n}\n\n.flywheel_consoleTabs_K2PPkW .flywheel_button_K2PPkW {\n height: 48px;\n color: var(--dsw-alias-label-secondary);\n border: 0;\n border-bottom: 2px solid #0000;\n border-radius: 0;\n align-self: flex-start;\n padding: 0;\n font-size: 13px;\n}\n\n.flywheel_consoleTabs_K2PPkW .flywheel_button_K2PPkW[aria-pressed=\"true\"] {\n color: var(--dsw-alias-label-primary);\n border-bottom-color: var(--dsw-alias-label-primary);\n font-weight: 500;\n}\n\n.flywheel_workbenchStatus_K2PPkW {\n flex-wrap: wrap;\n justify-content: flex-end;\n align-items: center;\n gap: 8px;\n margin-left: auto;\n padding: 6px 0;\n font-size: 12px;\n display: flex;\n}\n\n.flywheel_workbenchStatus_K2PPkW > span {\n margin-right: 8px;\n}\n\n.flywheel_workbenchEmpty_K2PPkW {\n text-align: center;\n flex: 1;\n align-content: center;\n justify-items: center;\n gap: 12px;\n min-height: 200px;\n padding: 32px 20px;\n display: grid;\n}\n\n.flywheel_workbenchEmpty_K2PPkW p {\n max-width: 480px;\n}\n\n.flywheel_taskChooser_K2PPkW {\n gap: 5px;\n min-width: 0;\n display: grid;\n}\n\n.flywheel_taskChooser_K2PPkW h3 {\n font-size: 17px;\n font-weight: 500;\n line-height: 24px;\n}\n\n.flywheel_taskChooser_K2PPkW .flywheel_consoleToolbar_K2PPkW {\n gap: 12px;\n}\n\n.flywheel_taskSelect_K2PPkW {\n border: .5px solid var(--dsw-alias-border-l3);\n background: var(--dsw-alias-bg-layer-1);\n width: 100%;\n min-width: 0;\n min-height: 36px;\n color: var(--dsw-alias-label-primary);\n font: inherit;\n border-radius: 12px;\n padding: 6px 10px;\n}\n\n.flywheel_taskSelect_K2PPkW:focus-visible {\n outline: 2px solid var(--dsw-alias-state-business-primary);\n outline-offset: 2px;\n}\n\n.flywheel_taskLocations_K2PPkW {\n border-top: .5px solid var(--dsw-alias-border-l3);\n color: var(--dsw-alias-label-secondary);\n flex-wrap: wrap;\n flex: none;\n gap: 6px 20px;\n padding: 8px 24px;\n font-size: 11px;\n line-height: 16px;\n display: flex;\n}\n\n.flywheel_taskLocations_K2PPkW dt {\n color: var(--dsw-alias-label-secondary);\n}\n\n.flywheel_taskLocations_K2PPkW dd {\n overflow-wrap: anywhere;\n margin: 0;\n}\n\n.flywheel_feedback_K2PPkW[data-state=\"idle\"]:empty {\n display: none;\n}\n\n.flywheel_launchOptions_K2PPkW, .flywheel_legacySummary_K2PPkW {\n min-width: 0;\n color: var(--dsw-alias-label-secondary);\n}\n\n.flywheel_launchOptionsBody_K2PPkW {\n flex-wrap: wrap;\n align-items: flex-end;\n gap: 10px;\n padding: 16px 24px;\n display: flex;\n}\n\n.flywheel_legacySummary_K2PPkW {\n margin-top: 16px;\n}\n\n.flywheel_consoleCanvas_K2PPkW {\n flex-direction: column;\n flex: 1;\n min-height: 0;\n display: flex;\n}\n\n.flywheel_stageScope_K2PPkW {\n flex-wrap: wrap;\n justify-content: space-between;\n align-items: center;\n gap: 10px;\n padding: 10px 0;\n display: flex;\n}\n\n.flywheel_stageScope_K2PPkW strong {\n font-weight: 500;\n}\n\n.flywheel_stageScope_K2PPkW p {\n font-size: 12px;\n line-height: 18px;\n}\n\n.flywheel_canvasWorkbench_K2PPkW {\n background: var(--dsw-alias-bg-module-platform);\n border: 0;\n border-radius: 0;\n flex: 1;\n min-width: 0;\n min-height: 0;\n display: flex;\n position: relative;\n overflow: hidden;\n}\n\n.flywheel_canvasMain_K2PPkW {\n flex-direction: column;\n flex: 1;\n min-width: 0;\n min-height: 0;\n display: flex;\n}\n\n.flywheel_focusCanvas_K2PPkW {\n width: calc(100vw - 32px);\n max-width: none;\n max-height: calc(100dvh - var(--dsh-frame-top-clearance, 0px) - 32px);\n box-shadow: var(--dsw-elevation-prominent);\n color: var(--dsw-alias-label-primary);\n font: inherit;\n border: 0;\n border-radius: 28px;\n margin-block: 16px;\n padding: 0;\n inset-block-start: var(--dsh-frame-top-clearance, 0px);\n}\n\n.flywheel_focusCanvas_K2PPkW::backdrop {\n background: #00000059;\n}\n\n.flywheel_focusCanvas_K2PPkW .flywheel_canvasViewport_K2PPkW {\n cursor: grab;\n background-image: radial-gradient(var(--dsw-alias-border-l3) .65px,transparent .65px);\n background-size: 22px 22px;\n flex: 1;\n min-height: 240px;\n padding: 20px;\n display: flex;\n overflow: auto;\n}\n\n.flywheel_focusCanvas_K2PPkW .flywheel_canvasList_K2PPkW {\n flex: 1;\n align-content: start;\n gap: 8px;\n min-height: 0;\n padding: 16px;\n display: grid;\n overflow: auto;\n}\n\n.flywheel_canvasHeader_K2PPkW {\n flex: none;\n justify-content: space-between;\n align-items: center;\n gap: 10px;\n padding: 12px 20px;\n display: flex;\n}\n\n.flywheel_canvasBreadcrumb_K2PPkW {\n flex-wrap: wrap;\n align-items: center;\n gap: 8px;\n min-width: 0;\n font-size: 12px;\n display: flex;\n}\n\n.flywheel_focusIdentity_K2PPkW {\n font-weight: 500;\n}\n\n.flywheel_segmented_K2PPkW {\n background: var(--dsw-alias-bg-layer-2);\n border-radius: 12px;\n flex-wrap: wrap;\n gap: 4px;\n width: fit-content;\n padding: 3px;\n display: flex;\n}\n\n.flywheel_segmented_K2PPkW .flywheel_button_K2PPkW {\n background: none;\n border: 0;\n border-radius: 8px;\n}\n\n.flywheel_segmented_K2PPkW .flywheel_button_K2PPkW[aria-pressed=\"true\"] {\n background: var(--dsw-alias-bg-layer-1);\n font-weight: 500;\n}\n\n.flywheel_canvasViewport_K2PPkW {\n cursor: grab;\n background-image: radial-gradient(var(--dsw-alias-border-l3) .65px, transparent .65px);\n background-size: 20px 20px;\n height: clamp(468px, 62vh, 720px);\n padding: 4px;\n display: flex;\n overflow: auto;\n}\n\n.flywheel_canvasScaleBounds_K2PPkW {\n width: var(--scaled-width);\n height: var(--scaled-height);\n flex: none;\n margin: auto;\n position: relative;\n}\n\n.flywheel_canvasWorld_K2PPkW {\n width: var(--canvas-width);\n height: var(--canvas-height);\n transform: scale(var(--canvas-scale));\n transform-origin: 0 0;\n position: relative;\n}\n\n.flywheel_canvasEdges_K2PPkW {\n pointer-events: none;\n position: absolute;\n inset: 0;\n}\n\n.flywheel_canvasEdges_K2PPkW path {\n fill: none;\n stroke: var(--dsw-alias-border-l2);\n stroke-width: 1.5px;\n}\n\n.flywheel_canvasNode_K2PPkW {\n left: var(--node-x);\n top: var(--node-y);\n text-align: left;\n border: .5px solid var(--dsw-alias-border-l3);\n background: var(--dsw-alias-bg-layer-1);\n width: 164px;\n min-height: 116px;\n color: var(--dsw-alias-label-primary);\n font: inherit;\n cursor: pointer;\n border-radius: 11px;\n flex-direction: column;\n gap: 6px;\n padding: 13px;\n display: flex;\n position: absolute;\n}\n\n.flywheel_canvasNode_K2PPkW strong {\n overflow-wrap: anywhere;\n font-size: 13px;\n font-weight: 500;\n line-height: 20px;\n}\n\n.flywheel_canvasNode_K2PPkW:hover {\n background: var(--dsw-alias-interactive-bg-hover);\n}\n\n.flywheel_canvasNode_K2PPkW[aria-pressed=\"true\"] {\n outline: 1.5px solid var(--dsw-alias-state-business-primary);\n outline-offset: 1px;\n}\n\n.flywheel_canvasNode_K2PPkW:focus-visible, .flywheel_canvasListNode_K2PPkW:focus-visible {\n outline: 2px solid var(--dsw-alias-state-business-primary);\n outline-offset: 3px;\n}\n\n.flywheel_canvasNode_K2PPkW[data-tone=\"error\"] {\n background: color-mix(in srgb, var(--dsw-alias-state-error-primary) 6%, var(--dsw-alias-bg-layer-1));\n}\n\n.flywheel_canvasNode_K2PPkW[data-tone=\"active\"] {\n background: color-mix(in srgb, var(--dsw-alias-state-business-primary) 6%, var(--dsw-alias-bg-layer-1));\n}\n\n.flywheel_nodeState_K2PPkW, .flywheel_nodeProvider_K2PPkW {\n color: var(--dsw-alias-label-secondary);\n font-size: 12px;\n line-height: 18px;\n}\n\n.flywheel_nodeProvider_K2PPkW {\n text-overflow: ellipsis;\n white-space: nowrap;\n overflow: hidden;\n}\n\n.flywheel_nodeState_K2PPkW {\n align-items: center;\n gap: 6px;\n display: inline-flex;\n}\n\n.flywheel_nodeState_K2PPkW:before, .flywheel_canvasLegend_K2PPkW span:before {\n content: \"\";\n corner-shape: round;\n background: var(--dsw-alias-label-tertiary);\n border-radius: 50%;\n flex-shrink: 0;\n width: 6px;\n height: 6px;\n}\n\n[data-tone=\"success\"] .flywheel_nodeState_K2PPkW:before, .flywheel_canvasLegend_K2PPkW [data-tone=\"success\"]:before {\n background: var(--dsw-alias-state-success-primary);\n}\n\n[data-tone=\"active\"] .flywheel_nodeState_K2PPkW:before, .flywheel_canvasLegend_K2PPkW [data-tone=\"active\"]:before {\n background: var(--dsw-alias-state-business-primary);\n}\n\n[data-tone=\"warning\"] .flywheel_nodeState_K2PPkW:before, .flywheel_canvasLegend_K2PPkW [data-tone=\"warning\"]:before {\n background: var(--dsw-alias-state-warn-primary);\n}\n\n[data-tone=\"error\"] .flywheel_nodeState_K2PPkW:before, .flywheel_canvasLegend_K2PPkW [data-tone=\"error\"]:before {\n background: var(--dsw-alias-state-error-primary);\n}\n\n.flywheel_nodeStages_K2PPkW {\n gap: 4px;\n display: flex;\n}\n\n.flywheel_nodeStages_K2PPkW span {\n border-top: 3px solid var(--dsw-alias-border-l3);\n white-space: nowrap;\n width: 25%;\n color: var(--dsw-alias-label-secondary);\n padding-top: 2px;\n font-size: 11px;\n line-height: 16px;\n overflow: hidden;\n}\n\n.flywheel_nodeStages_K2PPkW [data-tone=\"success\"] {\n border-color: var(--dsw-alias-state-success-primary);\n}\n\n.flywheel_nodeStages_K2PPkW [data-tone=\"error\"] {\n border-color: var(--dsw-alias-state-error-primary);\n}\n\n.flywheel_nodeStages_K2PPkW [data-tone=\"active\"] {\n border-color: var(--dsw-alias-state-business-primary);\n}\n\n.flywheel_canvasFooter_K2PPkW {\n background: var(--dsw-alias-bg-layer-1);\n border-top: .5px solid var(--dsw-alias-border-l3);\n flex-wrap: wrap;\n flex: none;\n justify-content: space-between;\n align-items: center;\n gap: 10px;\n padding: 8px 16px;\n display: flex;\n}\n\n.flywheel_canvasFooter_K2PPkW .flywheel_button_K2PPkW {\n border-color: #0000;\n}\n\n.flywheel_canvasLegend_K2PPkW {\n color: var(--dsw-alias-label-secondary);\n flex-wrap: wrap;\n gap: 12px;\n font-size: 12px;\n line-height: 18px;\n display: flex;\n}\n\n.flywheel_canvasLegend_K2PPkW span {\n align-items: center;\n gap: 5px;\n display: inline-flex;\n}\n\n.flywheel_canvasPagination_K2PPkW {\n align-items: center;\n gap: 12px;\n padding: 8px 12px;\n display: flex;\n}\n\n.flywheel_canvasList_K2PPkW {\n align-content: start;\n gap: 8px;\n min-height: 420px;\n padding: 12px;\n display: grid;\n}\n\n.flywheel_canvasListNode_K2PPkW {\n text-align: left;\n border: .5px solid var(--dsw-alias-border-l3);\n background: var(--dsw-alias-bg-layer-1);\n color: var(--dsw-alias-label-primary);\n font: inherit;\n cursor: pointer;\n border-radius: 12px;\n justify-content: space-between;\n align-items: center;\n gap: 12px;\n padding: 12px;\n display: flex;\n}\n\n.flywheel_canvasListNode_K2PPkW strong {\n font-weight: 500;\n}\n\n.flywheel_nodeInspector_K2PPkW {\n background: var(--dsw-alias-bg-layer-1);\n border-left: .5px solid var(--dsw-alias-border-l3);\n overflow-wrap: anywhere;\n flex-direction: column;\n flex: none;\n width: 326px;\n min-height: 0;\n display: flex;\n position: static;\n}\n\n.flywheel_inspectorHeader_K2PPkW {\n flex: none;\n justify-content: space-between;\n align-items: center;\n gap: 10px;\n padding: 18px 20px;\n display: flex;\n}\n\n.flywheel_inspectorHeader_K2PPkW > div {\n min-width: 0;\n}\n\n.flywheel_inspectorHeader_K2PPkW .flywheel_button_K2PPkW {\n border: 0;\n}\n\n.flywheel_inspectorTabs_K2PPkW {\n border-bottom: .5px solid var(--dsw-alias-border-l3);\n gap: 16px;\n padding: 0 16px;\n display: flex;\n}\n\n.flywheel_inspectorTabs_K2PPkW .flywheel_button_K2PPkW {\n border: 0;\n border-bottom: 2px solid #0000;\n border-radius: 0;\n padding: 6px 0;\n}\n\n.flywheel_inspectorTabs_K2PPkW .flywheel_button_K2PPkW[aria-pressed=\"true\"] {\n border-bottom-color: var(--dsw-alias-label-primary);\n font-weight: 500;\n}\n\n.flywheel_inspectorBody_K2PPkW {\n flex-direction: column;\n flex: 1;\n gap: 12px;\n min-height: 0;\n padding: 0 20px 20px;\n display: flex;\n overflow: auto;\n}\n\n.flywheel_inspectorFooter_K2PPkW {\n border-top: .5px solid var(--dsw-alias-border-l3);\n gap: 8px;\n padding: 12px 16px;\n display: grid;\n}\n\n.flywheel_inspectorFooter_K2PPkW .flywheel_meta_K2PPkW {\n font-size: 12px;\n line-height: 18px;\n}\n\n.flywheel_nodeOutcome_K2PPkW {\n font-weight: 500;\n}\n\n.flywheel_nodeOutcome_K2PPkW[data-tone=\"error\"] {\n color: var(--dsw-alias-state-error-primary);\n}\n\n.flywheel_nodeOutcome_K2PPkW[data-tone=\"active\"] {\n color: var(--dsw-alias-state-business-primary);\n}\n\n.flywheel_stageTimeline_K2PPkW {\n gap: 8px;\n margin: 0;\n padding: 8px 0;\n font-size: 12px;\n list-style: none;\n display: grid;\n}\n\n.flywheel_stageTimeline_K2PPkW li {\n border-left: 2px solid var(--dsw-alias-border-l3);\n grid-template-columns: minmax(0, 1fr) auto;\n gap: 10px;\n padding-left: 10px;\n display: grid;\n}\n\n.flywheel_stageTimeline_K2PPkW li[data-tone=\"success\"] {\n border-color: var(--dsw-alias-state-success-primary);\n}\n\n.flywheel_stageTimeline_K2PPkW li[data-tone=\"error\"] {\n border-color: var(--dsw-alias-state-error-primary);\n}\n\n.flywheel_stageTimeline_K2PPkW li[data-tone=\"active\"] {\n border-color: var(--dsw-alias-state-business-primary);\n}\n\n.flywheel_consoleProperties_K2PPkW {\n grid-template-columns: 88px minmax(0, 1fr);\n gap: 8px 12px;\n margin: 0;\n display: grid;\n}\n\n.flywheel_consoleProperties_K2PPkW dt {\n color: var(--dsw-alias-label-secondary);\n}\n\n.flywheel_consoleProperties_K2PPkW dd {\n overflow-wrap: anywhere;\n margin: 0;\n}\n\n.flywheel_sessionRecord_K2PPkW {\n border-top: .5px solid var(--dsw-alias-border-l3);\n gap: 10px;\n padding: 12px 0;\n display: grid;\n}\n\n.flywheel_sessionRecord_K2PPkW strong {\n font-weight: 500;\n}\n\n.flywheel_editorField_K2PPkW {\n min-width: 0;\n color: var(--dsw-alias-label-secondary);\n gap: 6px;\n display: grid;\n}\n\n.flywheel_editorInput_K2PPkW, .flywheel_editorJSON_K2PPkW, .flywheel_reasonInput_K2PPkW {\n box-sizing: border-box;\n border: .5px solid var(--dsw-alias-border-l3);\n width: 100%;\n min-width: 0;\n color: var(--dsw-alias-label-primary);\n background: var(--dsw-alias-bg-layer-1);\n font: inherit;\n border-radius: 12px;\n padding: 6px 10px;\n}\n\n.flywheel_editorInput_K2PPkW {\n min-height: 36px;\n}\n\n.flywheel_editorJSON_K2PPkW {\n resize: vertical;\n min-height: 220px;\n font-family: ui-monospace, monospace;\n}\n\n.flywheel_reasonInput_K2PPkW {\n resize: vertical;\n min-height: 90px;\n}\n\n.flywheel_editorInput_K2PPkW:focus-visible, .flywheel_editorJSON_K2PPkW:focus-visible, .flywheel_reasonInput_K2PPkW:focus-visible {\n outline: 2px solid var(--dsw-alias-state-business-primary);\n outline-offset: 2px;\n}\n\n.flywheel_editorGrid_K2PPkW, .flywheel_editorBindings_K2PPkW {\n grid-template-columns: repeat(2, minmax(0, 1fr));\n gap: 16px;\n display: grid;\n}\n\n.flywheel_editorBinding_K2PPkW {\n border: .5px solid var(--dsw-alias-border-l3);\n background: var(--dsw-alias-bg-layer-1);\n overflow-wrap: anywhere;\n border-radius: 20px;\n min-width: 0;\n padding: 16px;\n}\n\n.flywheel_editorField_K2PPkW select {\n width: 100%;\n}\n\n.flywheel_confirmation_K2PPkW {\n align-items: flex-start;\n gap: 8px;\n display: flex;\n}\n\n.flywheel_confirmation_K2PPkW input {\n width: 16px;\n height: 16px;\n accent-color: var(--dsw-alias-state-business-primary);\n margin-top: 3px;\n}\n\n.flywheel_workbenchDialog_K2PPkW {\n color: var(--dsw-alias-label-primary);\n background: var(--dsw-alias-bg-layer-1);\n box-shadow: var(--dsw-elevation-prominent);\n width: min(560px, 100% - 32px);\n max-height: calc(100dvh - var(--dsh-frame-top-clearance, 48px) - 48px);\n font: inherit;\n border: 0;\n border-radius: 28px;\n padding: 24px;\n overflow: auto;\n}\n\n.flywheel_workbenchDialog_K2PPkW::backdrop {\n background: #00000059;\n}\n\n.flywheel_workbenchDialog_K2PPkW[open] {\n flex-direction: column;\n display: flex;\n overflow: hidden;\n}\n\n.flywheel_dialogHeader_K2PPkW {\n justify-content: space-between;\n align-items: flex-start;\n gap: 12px;\n display: flex;\n}\n\n.flywheel_dialogHeader_K2PPkW, .flywheel_dialogActions_K2PPkW {\n flex: none;\n}\n\n.flywheel_dialogHeader_K2PPkW .flywheel_button_K2PPkW {\n border: 0;\n}\n\n.flywheel_dialogTitle_K2PPkW {\n margin: 0;\n font-size: 16px;\n font-weight: 500;\n line-height: 24px;\n}\n\n.flywheel_dialogBody_K2PPkW {\n flex: auto;\n gap: 14px;\n min-height: 0;\n padding: 16px 0;\n display: grid;\n overflow: auto;\n}\n\n.flywheel_dialogActions_K2PPkW {\n flex-wrap: wrap;\n justify-content: flex-end;\n gap: 8px;\n display: flex;\n}\n\n.flywheel_editor_K2PPkW {\n flex: 1;\n grid-template-rows: 60px minmax(0, 1fr) 58px;\n grid-template-columns: 180px minmax(0, 1fr);\n height: 100%;\n min-height: 0;\n display: grid;\n}\n\n.flywheel_configNav_K2PPkW {\n box-sizing: border-box;\n background: var(--dsw-alias-bg-module-platform);\n border-right: .5px solid var(--dsw-alias-border-l3);\n flex-direction: column;\n grid-area: 2 / 1;\n align-content: flex-start;\n align-items: stretch;\n gap: 4px;\n min-height: 0;\n padding: 20px 12px;\n display: flex;\n overflow: auto;\n}\n\n.flywheel_configNav_K2PPkW .flywheel_button_K2PPkW {\n white-space: nowrap;\n border: 0;\n border-radius: 7px;\n flex: 0 0 36px;\n justify-content: flex-start;\n height: 36px;\n min-height: 36px;\n max-height: 36px;\n padding: 0 12px;\n font-size: 13px;\n}\n\n.flywheel_configNav_K2PPkW .flywheel_button_K2PPkW[aria-pressed=\"true\"] {\n background: var(--dsw-alias-bg-layer-1);\n font-weight: 500;\n}\n\n.flywheel_configMain_K2PPkW {\n grid-area: 2 / 2;\n min-width: 0;\n min-height: 0;\n overflow: auto;\n}\n\n.flywheel_configHeader_K2PPkW {\n border-bottom: .5px solid var(--dsw-alias-border-l3);\n grid-area: 1 / 1 / auto / -1;\n align-items: center;\n gap: 16px;\n min-height: 0;\n padding: 0 24px;\n display: flex;\n}\n\n.flywheel_configMore_K2PPkW > summary {\n color: var(--dsw-alias-label-secondary);\n cursor: pointer;\n font-size: 12px;\n}\n\n.flywheel_configMore_K2PPkW > .flywheel_actions_K2PPkW {\n padding-top: 8px;\n}\n\n.flywheel_configContent_K2PPkW {\n max-width: 800px;\n margin: 0 auto;\n padding: 28px 32px;\n}\n\n.flywheel_configFields_K2PPkW {\n border: 0;\n min-width: 0;\n margin: 0;\n padding: 0;\n}\n\n.flywheel_configTitle_K2PPkW {\n margin: 0 0 18px;\n font-size: 17px;\n font-weight: 500;\n line-height: 24px;\n}\n\n.flywheel_configField_K2PPkW {\n border-bottom: .5px solid var(--dsw-alias-border-l3);\n grid-template-columns: minmax(140px, 1fr) minmax(160px, 260px);\n align-items: center;\n gap: 20px;\n min-width: 0;\n min-height: 66px;\n padding: 14px 0;\n display: grid;\n}\n\n.flywheel_configFieldLabel_K2PPkW {\n gap: 4px;\n display: grid;\n}\n\n.flywheel_configFieldLabel_K2PPkW label {\n font-weight: 500;\n}\n\n.flywheel_configFieldLabel_K2PPkW .flywheel_meta_K2PPkW {\n font-size: 11px;\n line-height: 17px;\n}\n\n.flywheel_configFieldControl_K2PPkW {\n min-width: 0;\n}\n\n.flywheel_configFieldControl_K2PPkW select {\n border-radius: 7px;\n width: 100%;\n height: 34px;\n font-size: 12px;\n}\n\n.flywheel_configSwitch_K2PPkW {\n cursor: pointer;\n align-items: center;\n gap: 8px;\n min-width: 44px;\n min-height: 44px;\n display: inline-flex;\n position: relative;\n}\n\n.flywheel_configSwitch_K2PPkW input {\n opacity: 0;\n width: 100%;\n height: 100%;\n cursor: inherit;\n margin: 0;\n position: absolute;\n inset: 0;\n}\n\n.flywheel_configSwitchTrack_K2PPkW {\n box-sizing: border-box;\n border: 1px solid var(--dsw-alias-border-l3);\n background: var(--dsw-alias-bg-layer-2);\n pointer-events: none;\n border-radius: 12px;\n flex: none;\n width: 36px;\n height: 20px;\n position: relative;\n}\n\n.flywheel_configSwitchTrack_K2PPkW:after {\n content: \"\";\n background: var(--dsw-alias-label-secondary);\n border-radius: 12px;\n width: 14px;\n height: 14px;\n position: absolute;\n top: 2px;\n left: 2px;\n}\n\n.flywheel_configSwitch_K2PPkW input:checked + .flywheel_configSwitchTrack_K2PPkW {\n border-color: var(--dsw-alias-state-business-primary);\n background: var(--dsw-alias-state-business-primary);\n}\n\n.flywheel_configSwitch_K2PPkW input:checked + .flywheel_configSwitchTrack_K2PPkW:after {\n background: var(--dsw-alias-label-primary-foreground);\n transform: translateX(16px);\n}\n\n.flywheel_configSwitch_K2PPkW input:focus-visible + .flywheel_configSwitchTrack_K2PPkW {\n outline: 2px solid var(--dsw-alias-state-business-primary);\n outline-offset: 3px;\n}\n\n.flywheel_configSwitch_K2PPkW input:disabled {\n cursor: not-allowed;\n}\n\n.flywheel_configSwitch_K2PPkW input:disabled ~ span {\n opacity: .4;\n}\n\n.flywheel_configSwitchState_K2PPkW {\n color: var(--dsw-alias-label-primary);\n pointer-events: none;\n}\n\n.flywheel_configHintLine_K2PPkW {\n margin-top: 6px;\n display: block;\n}\n\n.flywheel_configContent_K2PPkW .flywheel_configTiming_K2PPkW {\n color: var(--dsw-alias-label-secondary);\n margin-bottom: 12px;\n font-size: 12px;\n line-height: 18px;\n}\n\n.flywheel_inputUnit_K2PPkW {\n border: .5px solid var(--dsw-alias-border-l3);\n background: var(--dsw-alias-bg-layer-1);\n border-radius: 12px;\n align-items: center;\n gap: 4px;\n display: flex;\n}\n\n.flywheel_inputUnit_K2PPkW input {\n width: 100%;\n min-width: 0;\n min-height: 36px;\n color: var(--dsw-alias-label-primary);\n font: inherit;\n border-radius: inherit;\n background: none;\n border: 0;\n padding: 7px 10px;\n}\n\n.flywheel_inputUnit_K2PPkW input:focus-visible {\n outline: 2px solid var(--dsw-alias-state-business-primary);\n}\n\n.flywheel_inputUnit_K2PPkW span {\n white-space: nowrap;\n color: var(--dsw-alias-label-secondary);\n padding-right: 10px;\n font-size: 12px;\n}\n\n.flywheel_configSaveBar_K2PPkW {\n border-top: .5px solid var(--dsw-alias-border-l3);\n background: var(--dsw-alias-bg-layer-1);\n grid-area: 3 / 1 / auto / -1;\n justify-content: space-between;\n align-items: center;\n gap: 12px;\n padding: 0 24px;\n display: flex;\n}\n\n.flywheel_configSaveBar_K2PPkW .flywheel_meta_K2PPkW {\n font-size: 12px;\n line-height: 18px;\n}\n\n.flywheel_configSource_K2PPkW {\n margin-top: 20px;\n font-size: 12px;\n line-height: 18px;\n}\n\n.flywheel_configSource_K2PPkW p, .flywheel_configSource_K2PPkW code {\n margin-top: 8px;\n display: block;\n}\n\n.flywheel_configLoading_K2PPkW {\n place-items: center;\n min-height: 260px;\n display: grid;\n}\n\n.flywheel_configCallout_K2PPkW {\n background: var(--dsw-alias-bg-layer-2);\n border-radius: 12px;\n margin: 12px 0;\n padding: 12px;\n}\n\n.flywheel_modelRows_K2PPkW {\n gap: 0;\n display: grid;\n}\n\n.flywheel_modelRow_K2PPkW {\n border-bottom: .5px solid var(--dsw-alias-border-l3);\n grid-template-columns: 110px minmax(0, 1fr) auto;\n align-items: center;\n gap: 12px;\n padding: 14px 0;\n display: grid;\n}\n\n.flywheel_modelRow_K2PPkW > div {\n overflow-wrap: anywhere;\n min-width: 0;\n}\n\n.flywheel_modelRow_K2PPkW .flywheel_meta_K2PPkW {\n font-size: 12px;\n line-height: 18px;\n}\n\n.flywheel_runExplorer_K2PPkW {\n min-width: 0;\n padding: 16px 0;\n}\n\n.flywheel_runToolbar_K2PPkW {\n align-items: center;\n gap: 12px;\n margin-bottom: 16px;\n display: flex;\n}\n\n.flywheel_runToolbar_K2PPkW p {\n font-size: 12px;\n line-height: 18px;\n}\n\n.flywheel_runSplit_K2PPkW {\n grid-template-columns: 240px minmax(0, 1fr);\n min-height: 460px;\n display: grid;\n}\n\n.flywheel_runList_K2PPkW {\n flex-direction: column;\n gap: 4px;\n max-height: 620px;\n padding: 0 12px 0 0;\n display: flex;\n overflow: auto;\n}\n\n.flywheel_runRow_K2PPkW {\n text-align: left;\n color: var(--dsw-alias-label-primary);\n font: inherit;\n cursor: pointer;\n background: none;\n border: 0;\n border-radius: 12px;\n gap: 5px;\n padding: 12px;\n display: grid;\n}\n\n.flywheel_runRow_K2PPkW strong {\n overflow-wrap: anywhere;\n font-weight: 500;\n}\n\n.flywheel_runRow_K2PPkW span {\n font-size: 12px;\n line-height: 18px;\n}\n\n.flywheel_runRow_K2PPkW[aria-pressed=\"true\"] {\n background: var(--dsw-alias-interactive-bg-hover);\n}\n\n.flywheel_runRow_K2PPkW:focus-visible {\n outline: 2px solid var(--dsw-alias-state-business-primary);\n outline-offset: -2px;\n}\n\n.flywheel_runDetail_K2PPkW {\n border-left: .5px solid var(--dsw-alias-border-l3);\n flex-direction: column;\n gap: 18px;\n min-width: 0;\n padding: 0 24px;\n display: flex;\n}\n\n.flywheel_runTitle_K2PPkW {\n gap: 4px;\n display: grid;\n}\n\n.flywheel_metricScope_K2PPkW {\n color: var(--dsw-alias-label-secondary);\n flex-wrap: wrap;\n align-items: center;\n gap: 6px;\n font-size: 12px;\n display: flex;\n}\n\n.flywheel_metricScope_K2PPkW select {\n max-width: 100%;\n}\n\n.flywheel_runTitle_K2PPkW .flywheel_configTitle_K2PPkW {\n margin: 0;\n}\n\n.flywheel_runMetrics_K2PPkW {\n border-top: .5px solid var(--dsw-alias-border-l3);\n gap: 12px;\n margin-top: 20px;\n padding-top: 16px;\n display: grid;\n}\n\n.flywheel_metricRow_K2PPkW {\n gap: 8px;\n padding: 12px 0;\n display: grid;\n}\n\n.flywheel_runMetricsHint_K2PPkW {\n color: var(--dsw-alias-label-secondary);\n padding: 16px 0;\n font-size: 12px;\n line-height: 18px;\n}\n\n@container (width <= 800px) {\n .flywheel_editor_K2PPkW {\n grid-template-rows: 60px 54px minmax(0, 1fr) 64px;\n grid-template-columns: minmax(0, 1fr);\n }\n\n .flywheel_configNav_K2PPkW {\n border-right: 0;\n border-bottom: .5px solid var(--dsw-alias-border-l3);\n flex-flow: row;\n grid-area: 2 / 1;\n align-items: center;\n height: 54px;\n padding: 9px 12px;\n }\n\n .flywheel_configNav_K2PPkW .flywheel_button_K2PPkW {\n flex: none;\n height: 36px;\n min-height: 36px;\n max-height: 36px;\n }\n\n .flywheel_configNav_K2PPkW .flywheel_button_K2PPkW:last-child {\n margin: 0;\n }\n\n .flywheel_configMain_K2PPkW {\n grid-area: 3 / 1;\n }\n\n .flywheel_configSaveBar_K2PPkW {\n grid-row: 4;\n padding: 0 12px;\n }\n\n .flywheel_configHeader_K2PPkW {\n gap: 8px;\n padding: 0 12px;\n }\n\n .flywheel_configContent_K2PPkW {\n padding: 24px 20px;\n }\n\n .flywheel_workbenchHeader_K2PPkW {\n padding: 12px 16px;\n }\n\n .flywheel_repositoryLinks_K2PPkW a:first-child:not(:last-of-type) {\n display: none;\n }\n\n .flywheel_workbenchNav_K2PPkW {\n flex-wrap: wrap;\n gap: 12px;\n padding: 0 16px;\n }\n\n .flywheel_workbenchStatus_K2PPkW {\n gap: 6px;\n }\n\n .flywheel_taskLocations_K2PPkW {\n padding: 8px 16px;\n }\n\n .flywheel_runSplit_K2PPkW {\n grid-template-columns: minmax(0, 1fr);\n gap: 16px;\n }\n\n .flywheel_runList_K2PPkW {\n flex-direction: row;\n padding: 0 0 12px;\n }\n\n .flywheel_runRow_K2PPkW {\n min-width: 180px;\n }\n\n .flywheel_runDetail_K2PPkW {\n border-left: 0;\n padding: 0;\n }\n}\n\n@container (width <= 720px) {\n .flywheel_canvasWorkbench_K2PPkW[data-inspecting=\"true\"] .flywheel_canvasMain_K2PPkW {\n display: none;\n }\n\n .flywheel_nodeInspector_K2PPkW {\n border-left: 0;\n width: 100%;\n }\n\n .flywheel_canvasFooter_K2PPkW {\n gap: 6px;\n }\n\n .flywheel_canvasLegend_K2PPkW {\n display: none;\n }\n\n .flywheel_workbenchHeader_K2PPkW {\n flex-wrap: wrap;\n gap: 8px;\n }\n}\n\n@container (width <= 440px) {\n .flywheel_configField_K2PPkW {\n grid-template-columns: minmax(0, 1fr);\n gap: 8px;\n }\n\n .flywheel_configNav_K2PPkW .flywheel_button_K2PPkW {\n padding: 0 10px;\n font-size: 12px;\n }\n\n .flywheel_configSaveBar_K2PPkW > .flywheel_meta_K2PPkW {\n max-width: 80px;\n font-size: 11px;\n }\n\n .flywheel_configSaveBar_K2PPkW .flywheel_actions_K2PPkW {\n gap: 4px;\n }\n\n .flywheel_configSaveBar_K2PPkW .flywheel_button_K2PPkW {\n padding: 4px 6px;\n font-size: 11px;\n }\n\n .flywheel_consoleTabs_K2PPkW {\n gap: 18px;\n }\n\n .flywheel_repositoryLinks_K2PPkW {\n flex-wrap: nowrap;\n }\n\n .flywheel_taskLocations_K2PPkW {\n gap: 4px;\n }\n}\n\n@media (width <= 560px) {\n .flywheel_workbenchDialog_K2PPkW .flywheel_configField_K2PPkW {\n grid-template-columns: minmax(0, 1fr);\n gap: 8px;\n }\n}\n\n@media (pointer: coarse) {\n .flywheel_page_K2PPkW .flywheel_button_K2PPkW, .flywheel_taskSelect_K2PPkW, .flywheel_configFieldControl_K2PPkW select {\n min-height: 44px;\n }\n\n .flywheel_configNav_K2PPkW .flywheel_button_K2PPkW {\n flex-basis: 44px;\n height: 44px;\n min-height: 44px;\n max-height: 44px;\n }\n\n @container (width <= 800px) {\n .flywheel_configNav_K2PPkW {\n padding-top: 5px;\n padding-bottom: 5px;\n }\n\n .flywheel_configNav_K2PPkW .flywheel_button_K2PPkW {\n flex-basis: auto;\n }\n }\n\n .flywheel_page_K2PPkW input, .flywheel_page_K2PPkW textarea, .flywheel_page_K2PPkW select {\n font-size: 16px;\n }\n\n .flywheel_page_K2PPkW summary {\n min-height: 36px;\n }\n}\n\n@media (forced-colors: active) {\n .flywheel_configSwitch_K2PPkW input {\n opacity: 1;\n width: 20px;\n height: 20px;\n position: static;\n }\n\n .flywheel_configSwitchTrack_K2PPkW {\n display: none;\n }\n}\n\n.flywheel_modeSelect_K2PPkW {\n border: .5px solid var(--dsw-alias-border-l3);\n background: var(--dsw-alias-bg-layer-1);\n min-height: 30px;\n color: var(--dsw-alias-label-primary);\n font: inherit;\n border-radius: 8px;\n padding: 4px 8px;\n}\n\n.flywheel_modeSelect_K2PPkW:focus-visible {\n outline: 2px solid var(--dsw-alias-state-business-primary);\n outline-offset: 2px;\n}\n\n.flywheel_workflow_K2PPkW {\n border: .5px solid var(--dsw-alias-border-l4);\n border-radius: 8px;\n gap: 8px;\n padding: 12px;\n display: grid;\n}\n\n.flywheel_workflow_K2PPkW ul {\n margin: 4px 0 0;\n padding-left: 20px;\n}\n\n.flywheel_feedback_K2PPkW {\n box-sizing: border-box;\n border: 1px solid #0000;\n border-radius: 8px;\n min-width: 0;\n height: auto;\n max-height: 224px;\n margin-top: 12px;\n padding: 12px;\n scroll-padding: 12px;\n overflow: auto;\n}\n\n.flywheel_feedback_K2PPkW[data-state=\"uncertain\"] {\n border-color: var(--dsw-alias-state-warn-primary);\n background: var(--dsw-alias-state-warn-tertiary);\n}\n\n.flywheel_feedback_K2PPkW[data-state=\"error\"] {\n border-color: var(--dsw-alias-state-error-primary);\n background: var(--dsw-alias-bg-layer-1);\n}\n\n.flywheel_feedback_K2PPkW:focus-visible {\n outline: 2px solid var(--dsw-alias-state-business-primary);\n outline-offset: 2px;\n}\n\n.flywheel_idleFeedback_K2PPkW {\n height: 100%;\n color: var(--dsw-alias-label-secondary);\n align-content: center;\n gap: 8px;\n display: grid;\n}\n\n.flywheel_loading_K2PPkW {\n text-align: center;\n flex-direction: column;\n justify-content: center;\n align-items: center;\n gap: 8px;\n min-height: 100%;\n display: flex;\n}\n\n.flywheel_loadingCompact_K2PPkW {\n justify-content: center;\n align-items: center;\n padding-bottom: 8px;\n display: flex;\n}\n\n.flywheel_spinner_K2PPkW {\n color: var(--dsw-alias-label-tertiary);\n flex: none;\n}\n\n.flywheel_spinnerMotion_K2PPkW {\n transform-origin: center;\n animation: 1.5s linear infinite flywheel_flywheel-spin_K2PPkW;\n}\n\n.flywheel_spinnerTrack_K2PPkW, .flywheel_spinnerArc_K2PPkW {\n fill: none;\n stroke: currentColor;\n stroke-width: 2px;\n stroke-linecap: round;\n}\n\n.flywheel_spinnerTrack_K2PPkW {\n opacity: .25;\n}\n\n.flywheel_spinnerArc_K2PPkW {\n stroke-dasharray: 12 150;\n animation: 1.5s ease-in-out infinite flywheel_flywheel-dash_K2PPkW;\n}\n\n@keyframes flywheel_flywheel-spin_K2PPkW {\n to {\n transform: rotate(360deg);\n }\n}\n\n@keyframes flywheel_flywheel-dash_K2PPkW {\n 0%, 100% {\n stroke-dasharray: 12 150;\n stroke-dashoffset: 0;\n }\n\n 50% {\n stroke-dasharray: 24 150;\n stroke-dashoffset: -6px;\n }\n}\n\n@media (prefers-reduced-motion: reduce) {\n .flywheel_spinnerMotion_K2PPkW, .flywheel_spinnerArc_K2PPkW {\n animation: none;\n }\n\n .flywheel_spinnerArc_K2PPkW {\n stroke-dasharray: 18 150;\n stroke-dashoffset: -3px;\n }\n}\n\n.flywheel_notice_K2PPkW, .flywheel_error_K2PPkW {\n justify-items: start;\n gap: 8px;\n min-width: 0;\n display: grid;\n}\n\n.flywheel_notice_K2PPkW + .flywheel_error_K2PPkW {\n margin-top: 16px;\n}\n\n.flywheel_error_K2PPkW > h3, .flywheel_errorText_K2PPkW {\n color: var(--dsw-alias-state-error-primary);\n}\n\n.flywheel_warningText_K2PPkW {\n border-left: 1px solid var(--dsw-alias-state-warn-primary);\n color: var(--dsw-alias-label-secondary);\n padding-left: 8px;\n}\n\n.flywheel_path_K2PPkW {\n white-space: pre-wrap;\n min-width: 0;\n max-width: 100%;\n}\n\n.flywheel_section_K2PPkW, .flywheel_empty_K2PPkW {\n gap: 8px;\n min-width: 0;\n margin-top: 24px;\n display: grid;\n}\n\n.flywheel_empty_K2PPkW {\n padding: 20px 0;\n}\n\n.flywheel_sectionHeader_K2PPkW, .flywheel_bindingHeader_K2PPkW {\n flex-wrap: wrap;\n justify-content: space-between;\n align-items: baseline;\n gap: 6px 16px;\n min-width: 0;\n display: flex;\n}\n\n.flywheel_sectionHeader_K2PPkW > *, .flywheel_bindingHeader_K2PPkW > * {\n min-width: 0;\n}\n\n.flywheel_badge_K2PPkW {\n background: var(--dsw-alias-bg-layer-2);\n color: var(--dsw-alias-label-secondary);\n border-radius: 8px;\n padding: 2px 8px;\n}\n\n.flywheel_packets_K2PPkW {\n table-layout: fixed;\n border-collapse: collapse;\n text-align: left;\n width: 100%;\n margin-top: 8px;\n}\n\n.flywheel_packets_K2PPkW caption {\n text-align: left;\n color: var(--dsw-alias-label-secondary);\n padding-bottom: 8px;\n}\n\n.flywheel_packets_K2PPkW th {\n color: var(--dsw-alias-label-secondary);\n font-weight: 500;\n}\n\n.flywheel_packets_K2PPkW th, .flywheel_packets_K2PPkW td {\n border-bottom: .5px solid var(--dsw-alias-border-l4);\n vertical-align: top;\n padding: 10px 12px 10px 0;\n}\n\n.flywheel_packets_K2PPkW th:first-child {\n width: 42%;\n}\n\n.flywheel_packets_K2PPkW th:last-child, .flywheel_packets_K2PPkW td:last-child {\n padding-right: 0;\n}\n\n.flywheel_budget_K2PPkW {\n gap: 4px;\n margin-top: 8px;\n display: grid;\n}\n\n.flywheel_advanced_K2PPkW {\n border-top: .5px solid var(--dsw-alias-border-l3);\n margin: 24px 0 0;\n padding-top: 20px;\n}\n\n.flywheel_advancedBody_K2PPkW {\n gap: 12px;\n min-width: 0;\n padding-top: 12px;\n display: grid;\n}\n\n.flywheel_bindings_K2PPkW {\n grid-template-columns: repeat(auto-fit, minmax(min(100%, 290px), 1fr));\n gap: 12px;\n margin-top: 8px;\n display: grid;\n}\n\n.flywheel_binding_K2PPkW {\n border: .5px solid var(--dsw-alias-settings-card-stroke);\n background: var(--dsw-alias-settings-card-fill);\n border-radius: 12px;\n min-width: 0;\n padding: 14px;\n}\n\n.flywheel_bindingTitle_K2PPkW {\n font-size: 13px;\n font-weight: 500;\n line-height: 20px;\n}\n\n.flywheel_properties_K2PPkW {\n grid-template-columns: minmax(76px, .7fr) minmax(0, 1.3fr);\n gap: 6px 12px;\n margin: 12px 0 0;\n display: grid;\n}\n\n.flywheel_properties_K2PPkW dt {\n color: var(--dsw-alias-label-secondary);\n}\n\n.flywheel_properties_K2PPkW dd {\n min-width: 0;\n margin: 0;\n}\n\n.flywheel_checks_K2PPkW {\n gap: 12px;\n margin: 8px 0 0;\n padding: 0;\n list-style: none;\n display: grid;\n}\n\n.flywheel_check_K2PPkW {\n grid-template-columns: 4em minmax(0, 1fr);\n align-items: start;\n gap: 12px;\n display: grid;\n}\n\n.flywheel_checkBody_K2PPkW {\n gap: 3px;\n min-width: 0;\n display: grid;\n}\n\n.flywheel_checkState_K2PPkW {\n border-left: .5px solid var(--dsw-alias-border-l3);\n color: var(--dsw-alias-label-secondary);\n padding-left: 6px;\n}\n\n.flywheel_checkState_K2PPkW[data-state=\"pass\"] {\n border-left-width: 1px;\n border-color: var(--dsw-alias-state-success-primary);\n}\n\n.flywheel_checkState_K2PPkW[data-state=\"fail\"] {\n border-left-width: 1px;\n border-color: var(--dsw-alias-state-error-primary);\n}\n\n.flywheel_checkState_K2PPkW[data-state=\"warn\"] {\n border-left-width: 1px;\n border-color: var(--dsw-alias-state-warn-primary);\n}\n\n.flywheel_details_K2PPkW {\n width: 100%;\n min-width: 0;\n}\n\n.flywheel_details_K2PPkW > h4 {\n color: var(--dsw-alias-label-secondary);\n}\n\n.flywheel_raw_K2PPkW {\n box-sizing: border-box;\n background: var(--dsw-alias-bg-layer-2);\n white-space: pre-wrap;\n overflow-wrap: anywhere;\n border-radius: 8px;\n max-width: 100%;\n max-height: 320px;\n margin: 8px 0 0;\n padding: 12px;\n overflow: auto;\n}\n\n.flywheel_taskContext_K2PPkW {\n color: var(--dsw-alias-label-secondary);\n flex-wrap: wrap;\n gap: 8px;\n font-size: 11px;\n line-height: 16px;\n display: flex;\n}\n\n.flywheel_taskLocations_K2PPkW > span {\n overflow-wrap: anywhere;\n min-width: 0;\n}\n\n.flywheel_workbenchNav_K2PPkW {\n border-bottom: .5px solid var(--dsw-alias-border-l3);\n flex: none;\n align-items: center;\n gap: 16px;\n min-height: 48px;\n padding: 0 24px;\n display: flex;\n}\n\n.flywheel_workflowPage_K2PPkW {\n flex-direction: column;\n flex: 1;\n min-height: 0;\n display: flex;\n overflow: auto;\n}\n\n.flywheel_settingsPage_K2PPkW {\n flex-direction: column;\n flex: 1;\n min-height: 0;\n display: flex;\n}\n\n.flywheel_historyPage_K2PPkW {\n flex: 1;\n min-height: 0;\n padding: 16px 24px;\n overflow: auto;\n}\n\n.flywheel_inspectorActions_K2PPkW {\n flex-wrap: wrap;\n align-items: center;\n gap: 8px;\n display: flex;\n}\n\n.flywheel_nodeIssue_K2PPkW {\n font-size: 14px;\n line-height: 23px;\n}\n\n.flywheel_inspectorSection_K2PPkW {\n border-top: .5px solid var(--dsw-alias-border-l3);\n margin-top: 4px;\n padding-top: 16px;\n}\n\n.flywheel_inspectorSection_K2PPkW h4 {\n margin: 0 0 10px;\n font-size: 13px;\n font-weight: 500;\n}\n\n.flywheel_sessionSummary_K2PPkW {\n margin-top: 12px;\n}\n\n.flywheel_configHeader_K2PPkW .flywheel_segmented_K2PPkW {\n margin-left: auto;\n}\n\n.flywheel_configNav_K2PPkW .flywheel_button_K2PPkW:last-child {\n margin-top: auto;\n}\n\n.flywheel_configContent_K2PPkW[data-section=\"models\"] {\n max-width: 1100px;\n}\n\n.flywheel_configHeader_K2PPkW .flywheel_configTitle_K2PPkW {\n margin: 0;\n}\n\n.flywheel_configSaveBar_K2PPkW[data-dirty=\"true\"] {\n background: var(--dsw-alias-bg-layer-1);\n}\n\n.flywheel_stageTableScroll_K2PPkW {\n margin: 18px 0;\n overflow-x: auto;\n}\n\n.flywheel_stageTable_K2PPkW {\n table-layout: fixed;\n border-collapse: collapse;\n text-align: left;\n width: 100%;\n min-width: 680px;\n font-size: 12px;\n}\n\n.flywheel_stageTable_K2PPkW th, .flywheel_stageTable_K2PPkW td {\n border-bottom: .5px solid var(--dsw-alias-border-l3);\n vertical-align: middle;\n padding: 12px 8px;\n font-weight: 400;\n}\n\n.flywheel_stageTable_K2PPkW thead th {\n color: var(--dsw-alias-label-secondary);\n font-size: 11px;\n}\n\n.flywheel_stageTable_K2PPkW th:first-child {\n width: 110px;\n padding-left: 0;\n}\n\n.flywheel_stageTable_K2PPkW th:nth-child(2) {\n width: 140px;\n}\n\n.flywheel_stageTable_K2PPkW th:nth-child(4) {\n width: 90px;\n}\n\n.flywheel_stageTable_K2PPkW th:nth-child(5) {\n width: 150px;\n}\n\n.flywheel_stageTable_K2PPkW select, .flywheel_stageTable_K2PPkW input {\n border-radius: 7px;\n width: 100%;\n min-width: 0;\n height: 34px;\n font-size: 12px;\n}\n\n.flywheel_stageTable_K2PPkW .flywheel_button_K2PPkW {\n white-space: normal;\n justify-content: flex-start;\n width: 100%;\n}\n\n.flywheel_settingsHelp_K2PPkW {\n color: var(--dsw-alias-label-secondary);\n font-size: 12px;\n line-height: 18px;\n}\n\n.flywheel_settingsSubheading_K2PPkW {\n margin: 28px 0 4px;\n font-size: 14px;\n font-weight: 500;\n}\n";
|
|
12
|
+
// Host controlTimeoutMs is bounded at 120 s, with up to 20 s diagnostic cleanup.
|
|
13
|
+
// A browser timeout reports uncertainty; it cannot cancel a detached Go task.
|
|
14
|
+
const CONTROL_WAIT_MS = 150000;
|
|
15
|
+
|
|
16
|
+
// Own the button control we use. Markup and native-prop/ref forwarding follow
|
|
17
|
+
// DSH b830ec950faba0a46dda564be3e2d854afed94f1:
|
|
18
|
+
// packages/client/ui-primitives/src/Button.tsx. Scoped styles below copy
|
|
19
|
+
// their compact geometry; only public alias colors are shared with the host.
|
|
20
|
+
// The copied portions retain their MIT notice in THIRD_PARTY_NOTICES.md.
|
|
21
|
+
const Button = React.forwardRef(function FlywheelButton({
|
|
22
|
+
variant = 'outline', size = 'sm', className, children, ...rest
|
|
23
|
+
}, ref) {
|
|
24
|
+
return h('button', { type: 'button', ...rest, ref,
|
|
25
|
+
className: [css.button, variant === 'primary' ? css.buttonPrimary : css.buttonOutline,
|
|
26
|
+
size === 'sm' ? css.buttonSmall : '', className].filter(Boolean).join(' '),
|
|
27
|
+
}, children);
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
// The ongoing glyph and reduced-motion behavior are copied from the same
|
|
31
|
+
// pinned host's ui-primitives StateDot; this plugin owns the SVG and CSS.
|
|
32
|
+
function Spinner() {
|
|
33
|
+
const synchronize = element => {
|
|
34
|
+
if (!element) return;
|
|
35
|
+
for (const animation of element.getAnimations?.({ subtree: true }) ?? []) animation.startTime = 0;
|
|
36
|
+
};
|
|
37
|
+
return h('svg', { className: css.spinner, width: 20, height: 20,
|
|
38
|
+
viewBox: '0 0 24 24', 'aria-hidden': true, ref: synchronize,
|
|
39
|
+
onAnimationStart: event => synchronize(event.currentTarget) },
|
|
40
|
+
h('g', { className: css.spinnerMotion },
|
|
41
|
+
h('circle', { className: css.spinnerTrack, cx: 12, cy: 12, r: 9.5 }),
|
|
42
|
+
h('circle', { className: css.spinnerArc, cx: 12, cy: 12, r: 9.5 })));
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// A small immutable snapshot source for this plugin's flat write sets. This is
|
|
46
|
+
// not a generic deep-draft engine: all updates replace root fields, and nested
|
|
47
|
+
// engine reports are retained as read-only values. Only the host subscribes.
|
|
48
|
+
function createSnapshot(initial) {
|
|
49
|
+
let snapshot = Object.freeze({ ...initial });
|
|
50
|
+
const listeners = new Set();
|
|
51
|
+
return {
|
|
52
|
+
getSnapshot: () => snapshot,
|
|
53
|
+
subscribe: listener => {
|
|
54
|
+
listeners.add(listener);
|
|
55
|
+
return () => { listeners.delete(listener); };
|
|
56
|
+
},
|
|
57
|
+
update: mutate => {
|
|
58
|
+
const next = { ...snapshot };
|
|
59
|
+
mutate(next);
|
|
60
|
+
if (Object.keys(next).every(key => Object.is(next[key], snapshot[key]))) return;
|
|
61
|
+
snapshot = Object.freeze(next);
|
|
62
|
+
for (const listener of [...listeners]) listener();
|
|
63
|
+
},
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// Public structural StoreHandle / StoreInstance contract, verified at pinned
|
|
68
|
+
// packages/client/store/src/contract.ts. The renderer creates and caches each
|
|
69
|
+
// instance by session binding, then provides useStore/actions to the component.
|
|
70
|
+
// We do not import its engine, subscribe from React, or persist live state.
|
|
71
|
+
function createViewStore() {
|
|
72
|
+
const spec = {
|
|
73
|
+
init: () => ({ taskDir: '', cwd: '', mode: 'auto' }),
|
|
74
|
+
actions: {
|
|
75
|
+
setSelection: (draft, next) => { draft.taskDir = next.taskDir; draft.cwd = next.cwd; },
|
|
76
|
+
setMode: (draft, mode) => { draft.mode = mode; },
|
|
77
|
+
},
|
|
78
|
+
};
|
|
79
|
+
return {
|
|
80
|
+
spec,
|
|
81
|
+
create: () => {
|
|
82
|
+
const source = createSnapshot(spec.init());
|
|
83
|
+
const actions = Object.fromEntries(Object.entries(spec.actions).map(([name, action]) =>
|
|
84
|
+
[name, (...args) => source.update(draft => action(draft, ...args))]));
|
|
85
|
+
return { actions, getSnapshot: source.getSnapshot, subscribe: source.subscribe,
|
|
86
|
+
clearPersisted: () => {},
|
|
87
|
+
};
|
|
88
|
+
},
|
|
89
|
+
};
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
const zh = {
|
|
93
|
+
view: 'Flywheel',
|
|
94
|
+
title: 'Flywheel 任务',
|
|
95
|
+
description: '准备任务后,在这里查看进度、启动、继续或停止',
|
|
96
|
+
lifecycle: '任务在后台独立运行。切换视图或会话不会停止任务;需要停止时请使用这里的“停止”',
|
|
97
|
+
selection: '打开已有任务',
|
|
98
|
+
taskDir: '任务资料目录',
|
|
99
|
+
taskHint: '填写助手准备任务时给出的完整路径,里面保存任务说明和验收清单;只查看配置或做检查时可留空',
|
|
100
|
+
cwd: '项目目录(可选)',
|
|
101
|
+
cwdHint: '留空时使用当前 DSH 会话的工作目录。若它不是本任务的项目根目录,填写助手给出的项目完整路径',
|
|
102
|
+
pathInvalid: '请填写以 / 开头的完整路径',
|
|
103
|
+
pathControl: '路径不能包含换行或空字符',
|
|
104
|
+
taskRequired: '填写任务资料目录后,可刷新状态、启动或停止任务',
|
|
105
|
+
status: '刷新状态',
|
|
106
|
+
start: '启动',
|
|
107
|
+
launch: '启动准备好的任务',
|
|
108
|
+
launchMode: '新任务流程',
|
|
109
|
+
launchHelp: 'Auto 由引擎判断 Quick 或 Full;继续已有任务使用“启动”,沿用已保存的流程',
|
|
110
|
+
workflow: '执行流程',
|
|
111
|
+
requestedMode: '请求:{mode}',
|
|
112
|
+
effectiveMode: '生效:{mode}',
|
|
113
|
+
workflowReason: '选择原因:{reason}',
|
|
114
|
+
legacyFull: '这个任务未保存自适应流程信息,按原有 Full 流程运行',
|
|
115
|
+
applicable: '适用',
|
|
116
|
+
notApplicable: '不适用',
|
|
117
|
+
upgrades: '流程升级记录',
|
|
118
|
+
upgrade: '{from} → {to}:{reason}',
|
|
119
|
+
finalReview: 'Quick 仍须通过验收、最终检查与独立的任务级 Review;Plan / Judge 不适用不代表通过',
|
|
120
|
+
associationUnconfirmed: '会话记录中存在任务请求;引擎结果尚未确认',
|
|
121
|
+
stop: '停止',
|
|
122
|
+
config: '查看模型配置',
|
|
123
|
+
doctor: '检查安装与配置',
|
|
124
|
+
probe: '检查 DSH 接入',
|
|
125
|
+
doctorHelp: '检查安装与配置:核对本机工具、模型配置和所选任务文件,不会启动任务',
|
|
126
|
+
probeHelp: '检查 DSH 接入:临时启动独立的 DSH 运行环境,检查配置、项目目录和权限后退出;可能联网读取模型目录',
|
|
127
|
+
busy: '正在等待“{action}”确认…',
|
|
128
|
+
busyOther: '正在等待上一个目录的“{action}”结束;其结果不会覆盖当前选择',
|
|
129
|
+
mutationLock: '操作结果确认前,保持当前任务关联并暂停后续操作',
|
|
130
|
+
unknownOutcome: '“{action}”结果未确认',
|
|
131
|
+
unknownHelp: '请求可能已经送达引擎。先刷新这个任务的状态,再决定下一步操作',
|
|
132
|
+
reconcile: '刷新待确认任务',
|
|
133
|
+
errorTitle: '“{action}”未完成',
|
|
134
|
+
errorHelp: '检查连接和目录后,重新执行这项只读检查',
|
|
135
|
+
notDispatchedTitle: '“{action}”未发送到引擎',
|
|
136
|
+
notDispatchedHelp: '检查错误详情,修正目录或引擎配置后重试',
|
|
137
|
+
errorDetails: '错误详情',
|
|
138
|
+
feedback: '操作反馈',
|
|
139
|
+
statusHeading: '任务状态',
|
|
140
|
+
emptyHeading: '还没有任务状态',
|
|
141
|
+
emptyBody: '在“对话”输入 /flywheel-task 和需求。助手准备并启动任务后,会自动关联到本会话,无需复制路径。/flywheel-new 仍可只准备任务;也可填写已有任务目录后操作',
|
|
142
|
+
emptySelected: '点击“刷新状态”查看这个任务;打开本页不会自动启动任务',
|
|
143
|
+
lastChecked: '上次确认:{time}',
|
|
144
|
+
stale: '本次状态未确认,下方保留上次结果',
|
|
145
|
+
stateUnconfirmed: '状态待确认',
|
|
146
|
+
previousState: '上次确认状态:{status}',
|
|
147
|
+
task: '任务',
|
|
148
|
+
running: '运行中',
|
|
149
|
+
uninitialized: '尚未初始化',
|
|
150
|
+
engineMissing: '引擎未运行,状态待核对',
|
|
151
|
+
unknownState: '未知状态({status})',
|
|
152
|
+
unknown: '未知',
|
|
153
|
+
unreported: '未报告',
|
|
154
|
+
pending: '待开始',
|
|
155
|
+
paused: '已暂停',
|
|
156
|
+
stopped: '已停止',
|
|
157
|
+
completed: '已完成',
|
|
158
|
+
escalated: '需要人工处理',
|
|
159
|
+
budgetStopped: '预算已停止',
|
|
160
|
+
milestone: '当前里程碑:{name}',
|
|
161
|
+
packets: '工作单元',
|
|
162
|
+
packetsCount: '工作单元(共 {count} 个)',
|
|
163
|
+
packet: '工作单元',
|
|
164
|
+
state: '状态',
|
|
165
|
+
route: '路由',
|
|
166
|
+
packetPending: '待执行',
|
|
167
|
+
awaitingCredentials: '等待凭证',
|
|
168
|
+
executing: '执行中',
|
|
169
|
+
executed: '待验收',
|
|
170
|
+
l1Passed: '验收已通过',
|
|
171
|
+
l1Failed: '验收未通过',
|
|
172
|
+
reviewFailed: '评审退回',
|
|
173
|
+
interrupted: '已中断',
|
|
174
|
+
inbox: '等待处理',
|
|
175
|
+
inboxCount: '待人工处理记录:{count} 条',
|
|
176
|
+
budget: '用量与成本',
|
|
177
|
+
cost: '已报告成本合计:$' + '{amount}',
|
|
178
|
+
costUnknown: '成本:未知',
|
|
179
|
+
costPartial: '已知成本合计:$' + '{amount};另有 {count} 次调用成本未知,合计不完整',
|
|
180
|
+
costMissing: '成本:未知({count} 次调用未报告结算成本)',
|
|
181
|
+
tokens: '已报告 Token:{tokens}',
|
|
182
|
+
tokensMissing: ';{count} 次调用用量未知,合计不完整',
|
|
183
|
+
tokenLimit: '每日 Token 限额:{limit} · 预算策略:{policy}',
|
|
184
|
+
budgetWarning: '预算提示:{message}',
|
|
185
|
+
advanced: '配置与排查(按需)',
|
|
186
|
+
advancedHelp: '首次安装、升级或运行失败时可用,平时不必每次点击;两项检查都不向模型发送消息,也不能验证账号、额度或实际回答能力',
|
|
187
|
+
configuration: '实际模型配置',
|
|
188
|
+
configurationHelp: '这里显示 Flywheel 从下方来源读取的实际生效配置;DSH 对话输入框当前选择的模型不会替代它',
|
|
189
|
+
source: '配置来源:{source}',
|
|
190
|
+
embedded: '内嵌模板',
|
|
191
|
+
scope: '配置层级:{scope}',
|
|
192
|
+
outOfScope: '未用于本次检查',
|
|
193
|
+
model: '模型',
|
|
194
|
+
profile: 'Profile',
|
|
195
|
+
harness: 'Harness',
|
|
196
|
+
provider: 'Provider',
|
|
197
|
+
channel: '渠道',
|
|
198
|
+
thinking: '推理',
|
|
199
|
+
permission: '阶段权限要求',
|
|
200
|
+
permissionFull: '完全权限(无需逐步确认)',
|
|
201
|
+
permissionWorkspace: '项目内写入',
|
|
202
|
+
permissionReadOnly: '只读',
|
|
203
|
+
session: '会话策略',
|
|
204
|
+
step: '运行此阶段', 'config-edit': '读取配置', 'config-save': '保存配置',
|
|
205
|
+
enabled: '启用',
|
|
206
|
+
disabled: '关闭',
|
|
207
|
+
notConfigured: '未配置',
|
|
208
|
+
nativeProvider: '由原生运行时管理',
|
|
209
|
+
default: '默认',
|
|
210
|
+
undeclared: '未声明',
|
|
211
|
+
planRole: '规划',
|
|
212
|
+
executeRole: '执行',
|
|
213
|
+
reviewRole: '评审',
|
|
214
|
+
judgeRole: '终验',
|
|
215
|
+
intakeRole: '需求收集',
|
|
216
|
+
gardenRole: '知识维护',
|
|
217
|
+
panelRole: '第二意见',
|
|
218
|
+
diagnostic: '环境检查结果',
|
|
219
|
+
doctorPassed: '检查结束:未发现阻断项',
|
|
220
|
+
doctorFailed: '检查结束:存在阻断项',
|
|
221
|
+
localOnly: '本次检查本机工具、配置和所选任务文件,未启动 DSH 接入检查',
|
|
222
|
+
probeRequested: '已请求 DSH 接入检查(不调用模型)',
|
|
223
|
+
inferenceBoundary: '没有向模型发送消息;账号授权、可用额度和实际回答能力仍未验证',
|
|
224
|
+
pass: '通过',
|
|
225
|
+
fail: '失败',
|
|
226
|
+
warn: '注意',
|
|
227
|
+
unverified: '未验证',
|
|
228
|
+
skipped: '跳过',
|
|
229
|
+
errorCode: '错误码:{code}',
|
|
230
|
+
remedy: '处理建议:{remedy}',
|
|
231
|
+
raw: '引擎原始结果(JSON)',
|
|
232
|
+
};
|
|
233
|
+
const en = {
|
|
234
|
+
view: 'Flywheel',
|
|
235
|
+
title: 'Flywheel tasks',
|
|
236
|
+
description: 'After preparing a task, inspect its progress, start, resume or stop it here',
|
|
237
|
+
lifecycle: 'Tasks run independently in the background. Switching views or sessions does not stop a task; use Stop here when needed.',
|
|
238
|
+
selection: 'Open an existing task',
|
|
239
|
+
taskDir: 'Task files directory',
|
|
240
|
+
taskHint: 'Use the full path supplied by the assistant when it prepared the task instructions and acceptance checks; leave blank to inspect configuration or run diagnostics',
|
|
241
|
+
cwd: 'Project directory (optional)',
|
|
242
|
+
cwdHint: 'Leave blank to use this DSH session’s current working directory. If that is not this task’s project root, enter the full project path supplied by the assistant.',
|
|
243
|
+
pathInvalid: 'Enter a full path beginning with /',
|
|
244
|
+
pathControl: 'Paths cannot contain line breaks or null characters',
|
|
245
|
+
taskRequired: 'Enter the task files directory to refresh status, start or stop the task',
|
|
246
|
+
status: 'Refresh status',
|
|
247
|
+
start: 'Start',
|
|
248
|
+
launch: 'Launch prepared task',
|
|
249
|
+
launchMode: 'New task workflow',
|
|
250
|
+
launchHelp: 'Auto lets the engine choose Quick or Full. Use Start to continue an existing task with its saved workflow.',
|
|
251
|
+
workflow: 'Workflow',
|
|
252
|
+
requestedMode: 'Requested: {mode}',
|
|
253
|
+
effectiveMode: 'Effective: {mode}',
|
|
254
|
+
workflowReason: 'Selection reason: {reason}',
|
|
255
|
+
legacyFull: 'This task has no saved adaptive workflow; the original Full workflow applies.',
|
|
256
|
+
applicable: 'Applicable',
|
|
257
|
+
notApplicable: 'Not applicable',
|
|
258
|
+
upgrades: 'Workflow upgrades',
|
|
259
|
+
upgrade: '{from} → {to}: {reason}',
|
|
260
|
+
finalReview: 'Quick still requires verification, final checks and an independent task-level Review. Not-applicable Plan / Judge stages do not mean passed.',
|
|
261
|
+
associationUnconfirmed: 'The conversation records a task request; its engine outcome is still unconfirmed.',
|
|
262
|
+
stop: 'Stop',
|
|
263
|
+
config: 'Inspect model configuration',
|
|
264
|
+
doctor: 'Check installation and configuration',
|
|
265
|
+
probe: 'Check DSH integration',
|
|
266
|
+
doctorHelp: 'Installation and configuration: check local tools, model settings and selected task files without starting the task.',
|
|
267
|
+
probeHelp: 'DSH integration: briefly start a separate DSH runtime, verify its configuration, project directory and permissions, then close it. It may fetch a model catalog online.',
|
|
268
|
+
busy: 'Waiting for “{action}” confirmation…',
|
|
269
|
+
busyOther: 'Waiting for “{action}” for the previous directory; its result will not replace the current selection',
|
|
270
|
+
mutationLock: 'Task and project directories are locked until start or stop is confirmed',
|
|
271
|
+
unknownOutcome: '“{action}” outcome is unconfirmed',
|
|
272
|
+
unknownHelp: 'The request may have reached the engine. Refresh this task before choosing the next action.',
|
|
273
|
+
reconcile: 'Refresh unconfirmed task',
|
|
274
|
+
errorTitle: '“{action}” did not complete',
|
|
275
|
+
errorHelp: 'Check the connection and directories, then retry this read-only check.',
|
|
276
|
+
notDispatchedTitle: '“{action}” was not dispatched to the engine',
|
|
277
|
+
notDispatchedHelp: 'Check the error details, check the session association or engine configuration, then retry.',
|
|
278
|
+
errorDetails: 'Error details',
|
|
279
|
+
feedback: 'Operation feedback',
|
|
280
|
+
statusHeading: 'Task status',
|
|
281
|
+
emptyHeading: 'No task status yet',
|
|
282
|
+
emptyBody: 'In Chat, enter /flywheel-task and your requirement. After the assistant prepares and launches the task, this conversation links to its exact directory automatically. /flywheel-new still prepares only. You can also enter an existing task directory here.',
|
|
283
|
+
emptySelected: 'Refresh status to inspect this task. Opening this view does not start it.',
|
|
284
|
+
lastChecked: 'Last confirmed: {time}',
|
|
285
|
+
stale: 'Current status is unconfirmed; the previous result is retained below',
|
|
286
|
+
stateUnconfirmed: 'Status unconfirmed',
|
|
287
|
+
previousState: 'Previously confirmed state: {status}',
|
|
288
|
+
task: 'Task',
|
|
289
|
+
running: 'Running',
|
|
290
|
+
uninitialized: 'Not initialized',
|
|
291
|
+
engineMissing: 'Engine is not running; state needs verification',
|
|
292
|
+
unknownState: 'Unknown state ({status})',
|
|
293
|
+
unknown: 'Unknown',
|
|
294
|
+
unreported: 'Not reported',
|
|
295
|
+
pending: 'Pending',
|
|
296
|
+
paused: 'Paused',
|
|
297
|
+
stopped: 'Stopped',
|
|
298
|
+
completed: 'Completed',
|
|
299
|
+
escalated: 'Needs human attention',
|
|
300
|
+
budgetStopped: 'Stopped by budget',
|
|
301
|
+
milestone: 'Current milestone: {name}',
|
|
302
|
+
packets: 'Work packets',
|
|
303
|
+
packetsCount: 'Work packets ({count})',
|
|
304
|
+
packet: 'Work packet',
|
|
305
|
+
state: 'State',
|
|
306
|
+
route: 'Route',
|
|
307
|
+
packetPending: 'Pending',
|
|
308
|
+
awaitingCredentials: 'Waiting for credentials',
|
|
309
|
+
executing: 'Executing',
|
|
310
|
+
executed: 'Awaiting verification',
|
|
311
|
+
l1Passed: 'Verification passed',
|
|
312
|
+
l1Failed: 'Verification failed',
|
|
313
|
+
reviewFailed: 'Changes requested',
|
|
314
|
+
interrupted: 'Interrupted',
|
|
315
|
+
inbox: 'Waiting for attention',
|
|
316
|
+
inboxCount: 'Human attention records: {count}',
|
|
317
|
+
budget: 'Usage and cost',
|
|
318
|
+
cost: 'Reported cost total: $' + '{amount}',
|
|
319
|
+
costUnknown: 'Cost: unknown',
|
|
320
|
+
costPartial: 'Known cost: $' + '{amount}; {count} other calls have unknown cost, so this total is incomplete',
|
|
321
|
+
costMissing: 'Cost: unknown ({count} calls did not report billed cost)',
|
|
322
|
+
tokens: 'Reported tokens: {tokens}',
|
|
323
|
+
tokensMissing: '; usage is unknown for {count} calls, so this total is incomplete',
|
|
324
|
+
tokenLimit: 'Daily token limit: {limit} · Budget policy: {policy}',
|
|
325
|
+
budgetWarning: 'Budget notice: {message}',
|
|
326
|
+
advanced: 'Configuration and troubleshooting (optional)',
|
|
327
|
+
advancedHelp: 'Useful after installation, upgrades or a failed run; neither check is required before every task. Neither sends messages to a model or verifies account access, quota or real model responses.',
|
|
328
|
+
configuration: 'Effective model configuration',
|
|
329
|
+
configurationHelp: 'These are the effective Flywheel settings loaded from the source below. The model selected in the DSH chat composer does not replace them.',
|
|
330
|
+
source: 'Configuration source: {source}',
|
|
331
|
+
embedded: 'Embedded template',
|
|
332
|
+
scope: 'Configuration level: {scope}',
|
|
333
|
+
outOfScope: 'Not used for this check',
|
|
334
|
+
model: 'Model',
|
|
335
|
+
profile: 'Profile',
|
|
336
|
+
harness: 'Harness',
|
|
337
|
+
provider: 'Provider',
|
|
338
|
+
channel: 'Channel',
|
|
339
|
+
thinking: 'Reasoning',
|
|
340
|
+
permission: 'Required permission',
|
|
341
|
+
permissionFull: 'Full access (no step-by-step approval)',
|
|
342
|
+
permissionWorkspace: 'Write within the project',
|
|
343
|
+
permissionReadOnly: 'Read only',
|
|
344
|
+
session: 'Session policy',
|
|
345
|
+
step: 'Run stage', 'config-edit': 'Read configuration', 'config-save': 'Save configuration',
|
|
346
|
+
enabled: 'Enabled',
|
|
347
|
+
disabled: 'Disabled',
|
|
348
|
+
notConfigured: 'Not configured',
|
|
349
|
+
nativeProvider: 'Managed by the native runtime',
|
|
350
|
+
default: 'Default',
|
|
351
|
+
undeclared: 'Not declared',
|
|
352
|
+
planRole: 'Plan',
|
|
353
|
+
executeRole: 'Execute',
|
|
354
|
+
reviewRole: 'Review',
|
|
355
|
+
judgeRole: 'Judge',
|
|
356
|
+
intakeRole: 'Intake',
|
|
357
|
+
gardenRole: 'Knowledge maintenance',
|
|
358
|
+
panelRole: 'Second opinion',
|
|
359
|
+
diagnostic: 'Environment check results',
|
|
360
|
+
doctorPassed: 'Check complete: no blockers found',
|
|
361
|
+
doctorFailed: 'Check complete: blockers found',
|
|
362
|
+
localOnly: 'Local tools, configuration and selected task files were checked; no DSH integration check ran.',
|
|
363
|
+
probeRequested: 'A DSH integration check was requested (no model call)',
|
|
364
|
+
inferenceBoundary: 'No message was sent to a model. Account access, available quota and real model responses remain unverified.',
|
|
365
|
+
pass: 'Passed',
|
|
366
|
+
fail: 'Failed',
|
|
367
|
+
warn: 'Notice',
|
|
368
|
+
unverified: 'Unverified',
|
|
369
|
+
skipped: 'Skipped',
|
|
370
|
+
errorCode: 'Error code: {code}',
|
|
371
|
+
remedy: 'Next step: {remedy}',
|
|
372
|
+
raw: 'Raw engine response (JSON)',
|
|
373
|
+
};
|
|
374
|
+
|
|
375
|
+
const taskLabels = {
|
|
376
|
+
pending: 'pending', paused: 'paused', stopped: 'stopped', completed: 'completed',
|
|
377
|
+
escalated: 'escalated', 'budget-stopped': 'budgetStopped',
|
|
378
|
+
};
|
|
379
|
+
const packetLabels = {
|
|
380
|
+
pending: 'packetPending', 'awaiting-credentials': 'awaitingCredentials',
|
|
381
|
+
executing: 'executing', executed: 'executed', 'l1-passed': 'l1Passed',
|
|
382
|
+
'l1-failed': 'l1Failed', 'review-failed': 'reviewFailed', completed: 'completed',
|
|
383
|
+
interrupted: 'interrupted', escalated: 'escalated', inbox: 'inbox',
|
|
384
|
+
};
|
|
385
|
+
const roleLabels = { plan: 'planRole', execute: 'executeRole', review: 'reviewRole', judge: 'judgeRole',
|
|
386
|
+
intake: 'intakeRole', garden: 'gardenRole', 'judge-panel': 'panelRole' };
|
|
387
|
+
const checkLabels = { pass: 'pass', fail: 'fail', warn: 'warn', unknown: 'unverified', skipped: 'skipped' };
|
|
388
|
+
const permissionLabels = {
|
|
389
|
+
'danger-full-access': 'permissionFull',
|
|
390
|
+
'workspace-write': 'permissionWorkspace',
|
|
391
|
+
'read-only': 'permissionReadOnly',
|
|
392
|
+
};
|
|
393
|
+
const record = value => value !== null && typeof value === 'object' && !Array.isArray(value);
|
|
394
|
+
const textValue = (value, fallback = '—') => typeof value === 'string' || typeof value === 'number' ? String(value) : fallback;
|
|
395
|
+
const selectionOf = value => ({ taskDir: value.taskDir.trim(), cwd: value.cwd.trim() });
|
|
396
|
+
const selectionKey = value => JSON.stringify([value.taskDir, value.cwd]);
|
|
397
|
+
const mutates = action => ['investigate', 'launch', 'start', 'stop', 'step', 'session', 'deliver', 'config-save'].includes(action);
|
|
398
|
+
const actionKey = value => value.probe ? 'probe' : value.action;
|
|
399
|
+
class NotDispatchedControlReply extends Error {}
|
|
400
|
+
|
|
401
|
+
function pathProblem(value) {
|
|
402
|
+
if (/[\0\r\n]/.test(value)) return 'pathControl';
|
|
403
|
+
if (value.trim() && !value.trim().startsWith('/')) return 'pathInvalid';
|
|
404
|
+
return null;
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
function taskLabel(value, t) {
|
|
408
|
+
if (value.running) return t('running');
|
|
409
|
+
if (value.preparation) return t('investigationState_' + value.preparation.status);
|
|
410
|
+
if (!value.state) return t('uninitialized');
|
|
411
|
+
if (value.state.status === 'running') return t('engineMissing');
|
|
412
|
+
const key = taskLabels[value.state.status];
|
|
413
|
+
return key ? t(key) : t('unknownState', { status: textValue(value.state.status, t('unreported')) });
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
function readReply(reply, request) {
|
|
417
|
+
if (!reply?.ok) throw new Error(reply?.error?.message ?? 'DSH command transport failed');
|
|
418
|
+
const result = reply.value?.result;
|
|
419
|
+
if (result?.kind !== 'success') {
|
|
420
|
+
let failure;
|
|
421
|
+
// This is a versioned command envelope, not a heuristic over error prose.
|
|
422
|
+
// Older hosts and malformed/ambiguous replies keep the conservative outcome.
|
|
423
|
+
if (result?.kind === 'error' && typeof result.text === 'string') {
|
|
424
|
+
try { failure = JSON.parse(result.text); } catch { /* legacy unstructured error */ }
|
|
425
|
+
}
|
|
426
|
+
const typed = record(failure) && failure.protocol === 1 &&
|
|
427
|
+
failure.type === 'flywheel-control-error' && failure.action === request.action &&
|
|
428
|
+
['not-dispatched', 'unknown'].includes(failure.dispatch) && typeof failure.message === 'string';
|
|
429
|
+
const message = typed ? failure.message : result?.text ?? 'Flywheel command unavailable';
|
|
430
|
+
if (typed && failure.dispatch === 'not-dispatched') throw new NotDispatchedControlReply(message);
|
|
431
|
+
throw new Error(message);
|
|
432
|
+
}
|
|
433
|
+
const value = JSON.parse(result.text);
|
|
434
|
+
// runControl has already matched the engine's task identity against the
|
|
435
|
+
// host's realpath-normalized request. A browser cannot compare that identity
|
|
436
|
+
// lexically with a user path (/var, symlinks, trailing slash or "..").
|
|
437
|
+
if (!record(value) || value.protocol !== 1 || typeof value.ok !== 'boolean' ||
|
|
438
|
+
typeof value.taskDir !== 'string' ||
|
|
439
|
+
(request.taskDir ? !value.taskDir.startsWith('/') : value.taskDir !== '')) {
|
|
440
|
+
throw new Error('Flywheel control protocol or task identity mismatch');
|
|
441
|
+
}
|
|
442
|
+
if (!value.ok && !(request.action === 'doctor' && value.diagnostic?.ok === false)) {
|
|
443
|
+
throw new Error(typeof value.error === 'string' ? value.error : 'Flywheel control failed');
|
|
444
|
+
}
|
|
445
|
+
const state = value.state;
|
|
446
|
+
const configurationValid = report => record(report) && Array.isArray(report.bindings) &&
|
|
447
|
+
report.bindings.every(record) && (report.jev === undefined || record(report.jev) &&
|
|
448
|
+
typeof report.jev.enabled === 'boolean' && typeof report.jev.model === 'string' &&
|
|
449
|
+
Number.isSafeInteger(report.jev['max-calls']) && report.jev['max-calls'] > 0);
|
|
450
|
+
if (['status', 'launch', 'start', 'stop', 'step', 'session', 'deliver', 'node', 'investigate'].includes(request.action) &&
|
|
451
|
+
(typeof value.running !== 'boolean' || (state !== null && !record(state)) ||
|
|
452
|
+
(state?.packets != null && (!record(state.packets) || !Object.values(state.packets).every(validExecutionPolicyHistory))) ||
|
|
453
|
+
(state?.inbox != null && !Array.isArray(state.inbox)) ||
|
|
454
|
+
!validRuntimeDecisions(state?.['runtime-decisions']) ||
|
|
455
|
+
(state?.budget != null && (!record(state.budget) ||
|
|
456
|
+
(state.budget.warnings != null && !Array.isArray(state.budget.warnings)))))) {
|
|
457
|
+
throw new Error('Flywheel control returned malformed task status');
|
|
458
|
+
}
|
|
459
|
+
const investigationStatusAfterHandoff = request.investigation?.action === 'status' && value.preparation === undefined &&
|
|
460
|
+
record(state) && Number.isSafeInteger(state.revision) && state.revision > 0 && typeof state.status === 'string' &&
|
|
461
|
+
state.status !== '' && record(state.packets);
|
|
462
|
+
if ((value.preparation !== undefined && !validPreparationSnapshot(value.preparation)) ||
|
|
463
|
+
(request.action === 'investigate' && !value.preparation && !investigationStatusAfterHandoff)) throw new Error('Flywheel control returned malformed investigation status');
|
|
464
|
+
if (['config-edit', 'config-save'].includes(request.action) && (!record(value.editor) || !record(value.editor.values) || !record(value.editor.defaults) || value.editor.scope !== request.editor.scope || typeof value.editor.revision !== 'string')) {
|
|
465
|
+
throw new Error('Flywheel configuration editor returned a malformed report');
|
|
466
|
+
}
|
|
467
|
+
if (['delivery','deliver'].includes(request.action) && (!record(value.delivery) || !Array.isArray(value.delivery.pulls))) throw new Error('Malformed delivery snapshot');
|
|
468
|
+
if (request.action === 'node' && (!record(value.node) || value.node.id !== request.packet || !validExecutionPolicyHistory(value.node.packet))) throw new Error('Malformed node detail');
|
|
469
|
+
if (request.action === 'history' && (!record(value.history) || value.history.key !== request.sessionKey)) throw new Error('Malformed session history');
|
|
470
|
+
if (request.action === 'runs' && (!Array.isArray(value.runs) || !value.runs.every(record))) throw new Error('Malformed run index');
|
|
471
|
+
if (request.action === 'metrics' && (!record(value.metrics) || !Array.isArray(value.metrics.groups))) throw new Error('Malformed run metrics');
|
|
472
|
+
if (request.action === 'config' && !configurationValid(value.configuration)) {
|
|
473
|
+
throw new Error('Flywheel control returned malformed model configuration');
|
|
474
|
+
}
|
|
475
|
+
if (request.action === 'doctor' && (!record(value.diagnostic) ||
|
|
476
|
+
value.diagnostic.ok !== value.ok || !Array.isArray(value.diagnostic.checks) ||
|
|
477
|
+
!value.diagnostic.checks.every(record) ||
|
|
478
|
+
(value.diagnostic.configuration !== undefined && !configurationValid(value.diagnostic.configuration)))) {
|
|
479
|
+
throw new Error('Flywheel control returned malformed environment report');
|
|
480
|
+
}
|
|
481
|
+
return value;
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
function initialControl() {
|
|
485
|
+
return { pending: null, status: null, checkedAt: null, stale: false, configuration: null,
|
|
486
|
+
node: null, delivery: null, deliveryStale: false, runs: null, runsLoading: false, runsError: null, runsCwd: null, metrics: null, history: null, diagnostic: null, editor: null, configUncertainty: false, raw: null, error: null, uncertainty: null };
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
// Transport state belongs to this apply-owned controller, not a component or
|
|
490
|
+
// the draft store. Its observable reaches React only through slot injection.
|
|
491
|
+
function createControl(execute, pendingOperations, isDisposed) {
|
|
492
|
+
const source = createSnapshot(initialControl());
|
|
493
|
+
let selected = null;
|
|
494
|
+
let epoch = 0;
|
|
495
|
+
let active = null;
|
|
496
|
+
let lastAssociation = null;
|
|
497
|
+
let associatedKey = null;
|
|
498
|
+
let deferredAssociation = null;
|
|
499
|
+
let discovering = false;
|
|
500
|
+
// Discovery is a separate read lane: it cannot select a task, clear a pending
|
|
501
|
+
// mutation, or invalidate an in-flight status/configuration response.
|
|
502
|
+
const discover = async () => {
|
|
503
|
+
if (isDisposed() || discovering) return;
|
|
504
|
+
discovering = true;
|
|
505
|
+
source.update(draft => { draft.runsLoading = true; draft.runsError = null; });
|
|
506
|
+
let timer, disposeWait;
|
|
507
|
+
try {
|
|
508
|
+
const deadline = new Promise((_, reject) => {
|
|
509
|
+
timer = setTimeout(() => reject(new Error('Task discovery timed out')), CONTROL_WAIT_MS);
|
|
510
|
+
disposeWait = () => reject(new Error('Task discovery closed'));
|
|
511
|
+
pendingOperations.add(disposeWait);
|
|
512
|
+
});
|
|
513
|
+
const value = await Promise.race([execute({ action: 'runs' }), deadline]);
|
|
514
|
+
if (!isDisposed()) source.update(draft => { draft.runs = value.runs; draft.runsCwd = value.cwd; });
|
|
515
|
+
return value;
|
|
516
|
+
} catch (error) {
|
|
517
|
+
if (!isDisposed()) source.update(draft => { draft.runsError = error instanceof Error ? error.message : String(error); });
|
|
518
|
+
} finally {
|
|
519
|
+
clearTimeout(timer);
|
|
520
|
+
pendingOperations.delete(disposeWait);
|
|
521
|
+
discovering = false;
|
|
522
|
+
if (!isDisposed()) source.update(draft => { draft.runsLoading = false; });
|
|
523
|
+
}
|
|
524
|
+
};
|
|
525
|
+
const select = selection => {
|
|
526
|
+
const normalized = selectionOf(selection);
|
|
527
|
+
const key = selectionKey(normalized);
|
|
528
|
+
if (selected === key) return;
|
|
529
|
+
selected = key;
|
|
530
|
+
epoch += 1;
|
|
531
|
+
source.update(draft => {
|
|
532
|
+
Object.assign(draft, initialControl(), { pending: draft.pending, uncertainty: draft.uncertainty, runs: draft.runs, runsLoading: draft.runsLoading, runsError: draft.runsError, runsCwd: draft.runsCwd });
|
|
533
|
+
});
|
|
534
|
+
};
|
|
535
|
+
const run = async (action, selection, probe = false, extra = {}) => {
|
|
536
|
+
if (isDisposed() || active || (mutates(action) && source.getSnapshot().uncertainty)) return;
|
|
537
|
+
if (!['status', 'launch', 'start', 'stop', 'step', 'config', 'doctor', 'config-edit', 'config-save', 'runs', 'metrics', 'session', 'history', 'delivery', 'deliver', 'node', 'investigate'].includes(action)) return;
|
|
538
|
+
if (action === 'config-save' && source.getSnapshot().configUncertainty) return;
|
|
539
|
+
if (pathProblem(selection.taskDir) || pathProblem(selection.cwd)) return;
|
|
540
|
+
const target = selectionOf(selection);
|
|
541
|
+
if (['status', 'launch', 'start', 'stop', 'step', 'session', 'history', 'metrics', 'delivery', 'deliver', 'node', 'investigate'].includes(action) && !target.taskDir) return;
|
|
542
|
+
select(selection);
|
|
543
|
+
if (['launch', 'start'].includes(action) && source.getSnapshot().stale) return;
|
|
544
|
+
const requestEpoch = epoch;
|
|
545
|
+
const requestKey = selected;
|
|
546
|
+
const request = { action, ...(target.taskDir ? { taskDir: target.taskDir } : {}),
|
|
547
|
+
...(target.cwd ? { cwd: target.cwd } : {}), ...(probe ? { probe: true } : {}),
|
|
548
|
+
...(action === 'launch' ? { mode: selection.mode ?? 'auto' } : {}),
|
|
549
|
+
...(['config-edit', 'config-save'].includes(action) ? { editor: extra.editor } : {}),
|
|
550
|
+
...(['delivery','deliver'].includes(action) ? { delivery: extra.delivery } : {}),
|
|
551
|
+
...(action === 'node' ? { packet: extra.packet } : {}),
|
|
552
|
+
...(action === 'metrics' && extra.runId ? { runId: extra.runId } : {}),
|
|
553
|
+
...(action === 'history' ? { sessionKey: extra.sessionKey } : {}),
|
|
554
|
+
...(action === 'session' ? { session: extra.session } : {}),
|
|
555
|
+
...(action === 'investigate' ? { investigation: extra.investigation } : {}),
|
|
556
|
+
...(action === 'step' ? { only: extra.only, packet: extra.packet, revision: extra.revision, ...(extra.retryReason ? { retryReason: extra.retryReason } : {}) } : {}) };
|
|
557
|
+
const operation = { action, probe, ...target, key: requestKey };
|
|
558
|
+
active = operation;
|
|
559
|
+
source.update(draft => { draft.pending = operation; draft.error = null; });
|
|
560
|
+
let timer;
|
|
561
|
+
let disposeWait;
|
|
562
|
+
try {
|
|
563
|
+
const deadline = new Promise((_, reject) => {
|
|
564
|
+
timer = setTimeout(() => reject(new Error('Flywheel control confirmation timed out')), CONTROL_WAIT_MS);
|
|
565
|
+
disposeWait = () => reject(new Error('Flywheel UI was unloaded before confirmation'));
|
|
566
|
+
pendingOperations.add(disposeWait);
|
|
567
|
+
});
|
|
568
|
+
const value = await Promise.race([execute(request), deadline]);
|
|
569
|
+
if (isDisposed() || requestEpoch !== epoch) return;
|
|
570
|
+
source.update(draft => {
|
|
571
|
+
if (['status', 'launch', 'start', 'stop', 'step', 'session', 'deliver', 'node', 'investigate'].includes(action)) {
|
|
572
|
+
// A remote snapshot is bound to the previously observed candidate.
|
|
573
|
+
// A new engine revision requires another explicit remote inspection.
|
|
574
|
+
if (draft.delivery && value.state?.revision !== draft.status?.state?.revision) draft.deliveryStale = true;
|
|
575
|
+
draft.status = value;
|
|
576
|
+
draft.checkedAt = Date.now();
|
|
577
|
+
draft.stale = false;
|
|
578
|
+
if (action !== 'node' && draft.uncertainty?.key === requestKey) draft.uncertainty = null;
|
|
579
|
+
}
|
|
580
|
+
if (['config', 'doctor'].includes(action)) {
|
|
581
|
+
draft.configuration = value.configuration ?? value.diagnostic?.configuration ?? null;
|
|
582
|
+
draft.diagnostic = value.diagnostic ?? null;
|
|
583
|
+
}
|
|
584
|
+
if (['config-edit', 'config-save'].includes(action)) { draft.editor = value.editor; draft.configUncertainty = false; }
|
|
585
|
+
if (['delivery','deliver'].includes(action)) { draft.delivery = value.delivery; draft.deliveryStale = false; }
|
|
586
|
+
if (action === 'node') draft.node = value.node;
|
|
587
|
+
if (action === 'history') draft.history = value.history;
|
|
588
|
+
if (action === 'runs') draft.runs = value.runs;
|
|
589
|
+
if (action === 'metrics') draft.metrics = value.metrics;
|
|
590
|
+
draft.raw = value;
|
|
591
|
+
});
|
|
592
|
+
return value;
|
|
593
|
+
} catch (error) {
|
|
594
|
+
if (isDisposed() || requestEpoch !== epoch) return;
|
|
595
|
+
source.update(draft => {
|
|
596
|
+
const detail = error instanceof Error ? error.message : String(error);
|
|
597
|
+
const dispatch = error instanceof NotDispatchedControlReply ? 'not-dispatched' : 'unknown';
|
|
598
|
+
draft.error = { action, probe, detail, dispatch };
|
|
599
|
+
if (['delivery','deliver'].includes(action)) draft.deliveryStale = true;
|
|
600
|
+
if (action === 'status' || (mutates(action) && dispatch === 'unknown')) draft.stale = true;
|
|
601
|
+
if (action === 'config-save' && dispatch === 'unknown') draft.configUncertainty = true;
|
|
602
|
+
else if (mutates(action) && dispatch === 'unknown') draft.uncertainty = { ...operation, detail };
|
|
603
|
+
});
|
|
604
|
+
} finally {
|
|
605
|
+
clearTimeout(timer);
|
|
606
|
+
pendingOperations.delete(disposeWait);
|
|
607
|
+
active = null;
|
|
608
|
+
if (!isDisposed()) source.update(draft => { draft.pending = null; });
|
|
609
|
+
if (deferredAssociation) {
|
|
610
|
+
const next = deferredAssociation;
|
|
611
|
+
deferredAssociation = null;
|
|
612
|
+
associate(...next);
|
|
613
|
+
}
|
|
614
|
+
}
|
|
615
|
+
};
|
|
616
|
+
const associate = (value, setSelection) => {
|
|
617
|
+
if (isDisposed() || !record(value) || !Number.isSafeInteger(value.sequence) ||
|
|
618
|
+
typeof value.operationId !== 'string' || !['pending', 'confirmed', 'unknown'].includes(value.outcome) ||
|
|
619
|
+
typeof value.taskDir !== 'string' || !value.taskDir.startsWith('/') || pathProblem(value.taskDir) ||
|
|
620
|
+
typeof value.cwd !== 'string' || !value.cwd.startsWith('/') || pathProblem(value.cwd)) return;
|
|
621
|
+
if (lastAssociation && (value.sequence < lastAssociation.sequence ||
|
|
622
|
+
(value.operationId === lastAssociation.operationId && value.outcome === lastAssociation.outcome &&
|
|
623
|
+
value.taskDir === lastAssociation.taskDir && value.cwd === lastAssociation.cwd))) return;
|
|
624
|
+
if (active) { deferredAssociation = [value, setSelection]; return; }
|
|
625
|
+
const next = { taskDir: value.taskDir, cwd: value.cwd };
|
|
626
|
+
const sameOperation = lastAssociation?.operationId === value.operationId;
|
|
627
|
+
const shouldSelect = !lastAssociation || (!sameOperation && (mutates(value.action) || value.action === 'status' && value.operationId.startsWith('tool:'))) ||
|
|
628
|
+
(sameOperation && selected === associatedKey);
|
|
629
|
+
lastAssociation = value;
|
|
630
|
+
if (shouldSelect) {
|
|
631
|
+
setSelection(next);
|
|
632
|
+
select(next);
|
|
633
|
+
associatedKey = selectionKey(next);
|
|
634
|
+
}
|
|
635
|
+
source.update(draft => {
|
|
636
|
+
if (value.outcome !== 'confirmed' && mutates(value.action)) {
|
|
637
|
+
draft.uncertainty = { ...next, action: value.action, operationId: value.operationId,
|
|
638
|
+
key: selectionKey(next), detail: value.message || '' };
|
|
639
|
+
draft.stale = true;
|
|
640
|
+
} else if (draft.uncertainty && (draft.uncertainty.operationId === value.operationId ||
|
|
641
|
+
draft.uncertainty.key === selectionKey(next))) {
|
|
642
|
+
draft.uncertainty = null;
|
|
643
|
+
}
|
|
644
|
+
});
|
|
645
|
+
// Logs recover identity, never a live running state. A confirmed association
|
|
646
|
+
// gets one fresh read; pending/unknown mutations require explicit reconcile.
|
|
647
|
+
if (shouldSelect && value.outcome === 'confirmed' &&
|
|
648
|
+
(mutates(value.action) || !source.getSnapshot().status)) void run('status', next);
|
|
649
|
+
};
|
|
650
|
+
return { source, select, run, discover, associate, isMutating: () => active !== null && mutates(active.action) };
|
|
651
|
+
}
|
|
652
|
+
|
|
653
|
+
function Configuration({ value, t }) {
|
|
654
|
+
if (!value) return null;
|
|
655
|
+
const field = (name, value, code = false) => h(React.Fragment, { key: name },
|
|
656
|
+
h('dt', null, t(name)), h('dd', null, code ? h('code', null, value) : value));
|
|
657
|
+
return h('section', { className: css.section, 'aria-label': t('configuration') },
|
|
658
|
+
h('h3', { className: css.heading }, t('configuration')),
|
|
659
|
+
h('p', { className: css.meta }, t('configurationHelp')),
|
|
660
|
+
h('p', { className: css.meta }, t('source', { source: textValue(value.source, '') || t('embedded') })),
|
|
661
|
+
h('p', { className: css.meta }, t('scope', { scope: textValue(value.scope) })),
|
|
662
|
+
record(value.jev) && h('article', { className: css.binding },
|
|
663
|
+
h('h4', { className: css.bindingTitle }, t('cfgJev')),
|
|
664
|
+
h('p', null, t(value.jev.enabled === true ? 'consoleJEVConfiguredOn' : 'consoleJEVConfiguredOff')),
|
|
665
|
+
value.jev.enabled === true && h('dl', { className: css.properties },
|
|
666
|
+
field('consoleJEVVersion', textValue(value.jev.model), true),
|
|
667
|
+
field('cfgJevMaxCalls', value.jev['max-calls'] ?? t('unknown')))),
|
|
668
|
+
h('div', { className: css.bindings }, ...(value.bindings ?? []).map((binding, index) =>
|
|
669
|
+
h('article', { className: css.binding, key: textValue(binding?.id, String(index)) },
|
|
670
|
+
h('div', { className: css.bindingHeader },
|
|
671
|
+
h('h4', { className: css.bindingTitle }, roleLabels[binding?.role] ? t(roleLabels[binding.role]) : textValue(binding?.role)),
|
|
672
|
+
h('code', { className: css.meta }, textValue(binding?.route || binding?.id))),
|
|
673
|
+
binding?.inScope === false && h('p', { className: css.meta }, t('outOfScope')),
|
|
674
|
+
h('dl', { className: css.properties },
|
|
675
|
+
field('model', textValue(binding?.model, '') || t('notConfigured'), true),
|
|
676
|
+
binding?.profile && field('profile', textValue(binding.profile)),
|
|
677
|
+
field('harness', textValue(binding?.harness), true),
|
|
678
|
+
field('provider', textValue(binding?.provider, '') || (binding?.harness === 'dsh' ? t('notConfigured') : t('nativeProvider'))),
|
|
679
|
+
field('channel', textValue(binding?.channel, '') || t('notConfigured')),
|
|
680
|
+
field('thinking', textValue(binding?.thinking, '') || t('default')),
|
|
681
|
+
field('permission', Object.hasOwn(permissionLabels, binding?.permissionMode) ?
|
|
682
|
+
t(permissionLabels[binding.permissionMode]) : textValue(binding?.permissionMode, '') || t('undeclared')),
|
|
683
|
+
field('session', sessionModeLabel(binding, t)))))));
|
|
684
|
+
}
|
|
685
|
+
|
|
686
|
+
function Diagnostic({ value, t }) {
|
|
687
|
+
if (!value) return null;
|
|
688
|
+
return h('section', { className: css.section, 'aria-label': t('diagnostic') },
|
|
689
|
+
h('h3', { className: css.heading }, t(value.ok ? 'doctorPassed' : 'doctorFailed')),
|
|
690
|
+
h('p', { className: css.meta }, t(value.probeRequested ? 'probeRequested' : 'localOnly')),
|
|
691
|
+
h('p', { className: css.meta }, t('inferenceBoundary')),
|
|
692
|
+
h('ul', { className: css.checks }, ...(value.checks ?? []).map((check, index) =>
|
|
693
|
+
h('li', { className: css.check, key: textValue(check?.id, '') + ':' + index },
|
|
694
|
+
h('span', { className: css.checkState, 'data-state': textValue(check?.status, 'unknown') },
|
|
695
|
+
t(checkLabels[check?.status] ?? 'unverified')),
|
|
696
|
+
h('div', { className: css.checkBody },
|
|
697
|
+
h('h4', { className: css.bindingTitle }, textValue(check?.id)),
|
|
698
|
+
h('p', null, textValue(check?.summary)),
|
|
699
|
+
check?.code && h('p', { className: css.meta }, t('errorCode', { code: textValue(check.code) })),
|
|
700
|
+
check?.remedy && h('p', null, t('remedy', { remedy: textValue(check.remedy) })))))));
|
|
701
|
+
}
|
|
702
|
+
|
|
703
|
+
function Budget({ value, t }) {
|
|
704
|
+
if (!value) return null;
|
|
705
|
+
const cost = value.totals?.['cost-usd'];
|
|
706
|
+
const unknownCost = value['unknown-cost-calls'] ?? 0;
|
|
707
|
+
const unknownUsage = value['unknown-usage-calls'] ?? 0;
|
|
708
|
+
let costText = Number.isFinite(cost) ? t('cost', { amount: cost.toFixed(4) }) : t('costUnknown');
|
|
709
|
+
if (unknownCost > 0) {
|
|
710
|
+
costText = Number.isFinite(cost) && cost > 0 ?
|
|
711
|
+
t('costPartial', { amount: cost.toFixed(4), count: unknownCost }) : t('costMissing', { count: unknownCost });
|
|
712
|
+
}
|
|
713
|
+
return h('section', { className: css.budget, 'aria-label': t('budget') },
|
|
714
|
+
h('p', null, costText),
|
|
715
|
+
h('p', null, t('tokens', { tokens: value.totals?.tokens ?? t('unknown') }) +
|
|
716
|
+
(unknownUsage > 0 ? t('tokensMissing', { count: unknownUsage }) : '')),
|
|
717
|
+
value['daily-token-limit'] != null && h('p', { className: css.meta },
|
|
718
|
+
t('tokenLimit', { limit: value['daily-token-limit'], policy: textValue(value.enforce) })),
|
|
719
|
+
...(value.warnings ?? []).map((warning, index) => h('p', { key: index },
|
|
720
|
+
t('budgetWarning', { message: textValue(warning) }))));
|
|
721
|
+
}
|
|
722
|
+
|
|
723
|
+
function Workflow({ state, t }) {
|
|
724
|
+
if (!state) return null;
|
|
725
|
+
const workflow = state.workflow;
|
|
726
|
+
const modes = { auto: 'Auto', quick: 'Quick', full: 'Full' };
|
|
727
|
+
const mode = value => Object.hasOwn(modes, value) ? modes[value] : t('unknown');
|
|
728
|
+
const legacy = workflow == null;
|
|
729
|
+
return h('section', { className: css.workflow, 'aria-label': t('workflow') },
|
|
730
|
+
h('h4', { className: css.bindingTitle }, t('workflow')),
|
|
731
|
+
h('div', { className: css.actions },
|
|
732
|
+
h('span', { className: css.badge }, t('requestedMode', { mode: mode(legacy ? 'full' : workflow.requested) })),
|
|
733
|
+
h('span', { className: css.badge }, t('effectiveMode', { mode: mode(legacy ? 'full' : workflow.effective) }))),
|
|
734
|
+
h('p', null, legacy ? t('legacyFull') : t('workflowReason', { reason: textValue(workflow.reason, t('unreported')) })),
|
|
735
|
+
...['plan', 'judge'].map(stage => {
|
|
736
|
+
const value = workflow?.stages?.[stage];
|
|
737
|
+
return h('p', { key: stage, className: css.meta }, t(roleLabels[stage]) + ' · ' +
|
|
738
|
+
(legacy || value?.applicable === true ? t('applicable') : value?.applicable === false ? t('notApplicable') : t('unreported')) +
|
|
739
|
+
(typeof value?.reason === 'string' && value.reason ? ' · ' + value.reason : ''));
|
|
740
|
+
}),
|
|
741
|
+
workflow?.effective === 'quick' && h('p', { className: css.meta }, t('finalReview')),
|
|
742
|
+
Array.isArray(workflow?.upgrades) && workflow.upgrades.length > 0 && h('div', null,
|
|
743
|
+
h('h4', { className: css.bindingTitle }, t('upgrades')),
|
|
744
|
+
h('ul', null, ...workflow.upgrades.map((upgrade, index) => h('li', { key: index },
|
|
745
|
+
t('upgrade', { from: mode(upgrade?.from), to: mode(upgrade?.to), reason: textValue(upgrade?.reason) }),
|
|
746
|
+
typeof upgrade?.at === 'string' && h('span', { className: css.meta }, ' · ' + upgrade.at))))));
|
|
747
|
+
}
|
|
748
|
+
|
|
749
|
+
function TaskStatus({ control, selection, t }) {
|
|
750
|
+
const { status, checkedAt, stale } = control;
|
|
751
|
+
if (!status) return h('section', { className: css.empty, 'aria-label': t('statusHeading') },
|
|
752
|
+
h('h3', { className: css.heading }, t('emptyHeading')),
|
|
753
|
+
h('p', { className: css.meta }, t(selection.taskDir.trim() ? 'emptySelected' : 'emptyBody')));
|
|
754
|
+
const packets = Object.entries(status.state?.packets ?? {});
|
|
755
|
+
return h('section', { className: css.section, 'aria-label': t('statusHeading') },
|
|
756
|
+
h('div', { className: css.sectionHeader },
|
|
757
|
+
h('h3', { className: css.heading }, textValue(status.state?.slug, t('task'))),
|
|
758
|
+
h('span', { className: css.badge }, stale ? t('stateUnconfirmed') : taskLabel(status, t))),
|
|
759
|
+
h('p', { className: css.meta },
|
|
760
|
+
t('lastChecked', { time: new Date(checkedAt).toLocaleString() }) +
|
|
761
|
+
(status.running && status.pid > 0 ? ' · PID ' + status.pid : '')),
|
|
762
|
+
stale && h('p', { className: css.warningText },
|
|
763
|
+
t('stale') + ' · ' + t('previousState', { status: taskLabel(status, t) })),
|
|
764
|
+
h(Workflow, { state: status.state, t }),
|
|
765
|
+
status.state?.['current-milestone'] && h('p', null,
|
|
766
|
+
t('milestone', { name: textValue(status.state['current-milestone']) })),
|
|
767
|
+
packets.length > 0 && h('section', { className: css.details }, h('h4', null, t('packetsCount', { count: packets.length })), h('table', { className: css.packets, 'aria-label': t('packets') },
|
|
768
|
+
h('caption', null, t('packetsCount', { count: packets.length })),
|
|
769
|
+
h('thead', null, h('tr', null, ...['packet', 'state', 'route'].map(key => h('th', { key, scope: 'col' }, t(key))))),
|
|
770
|
+
h('tbody', null, ...packets.map(([id, packet]) => h('tr', { key: id },
|
|
771
|
+
h('td', null, id),
|
|
772
|
+
h('td', null, packetLabels[packet?.status] ? t(packetLabels[packet.status]) : textValue(packet?.status, t('unknown'))),
|
|
773
|
+
h('td', null, textValue(packet?.route))))))),
|
|
774
|
+
h(Budget, { value: status.state?.budget, t }),
|
|
775
|
+
status.state?.inbox?.length > 0 && h('section', { className: css.details },
|
|
776
|
+
h('h4', null, t('inboxCount', { count: status.state.inbox.length })),
|
|
777
|
+
h('pre', { className: css.raw }, JSON.stringify(status.state.inbox, null, 2))));
|
|
778
|
+
}
|
|
779
|
+
|
|
780
|
+
function Feedback({ control, selection, reconcile, t }) {
|
|
781
|
+
const busy = control.pending !== null;
|
|
782
|
+
const error = control.error &&
|
|
783
|
+
(!mutates(control.error.action) || control.error.dispatch === 'not-dispatched') ? control.error : null;
|
|
784
|
+
const mode = control.uncertainty ? 'uncertain' : error ? 'error' : busy ? 'loading' : 'idle';
|
|
785
|
+
const otherSelection = busy && control.pending.key !== selectionKey(selectionOf(selection));
|
|
786
|
+
const locked = busy && mutates(control.pending.action);
|
|
787
|
+
const pendingLabel = busy ? t(otherSelection ? 'busyOther' : 'busy', {
|
|
788
|
+
action: t(actionKey(control.pending)),
|
|
789
|
+
}) : '';
|
|
790
|
+
return h('div', { className: css.feedback, 'data-state': mode,
|
|
791
|
+
role: 'region', 'aria-label': t('feedback'), tabIndex: mode === 'uncertain' || mode === 'error' ? 0 : undefined },
|
|
792
|
+
control.status?.workspaceWarnings?.map((warning, index) => h('p', { key: index, role: 'alert', className: css.notice }, warning)),
|
|
793
|
+
busy && h('div', { className: control.uncertainty ? css.loadingCompact : css.loading,
|
|
794
|
+
role: 'status', 'aria-label': pendingLabel },
|
|
795
|
+
h(Spinner),
|
|
796
|
+
!control.uncertainty && (locked || otherSelection) && h('p', null, pendingLabel),
|
|
797
|
+
locked && h('p', { className: css.meta }, t('mutationLock'))),
|
|
798
|
+
control.uncertainty && h('section', { className: css.notice, role: 'alert' },
|
|
799
|
+
h('h3', { className: css.heading }, t('unknownOutcome', { action: t(control.uncertainty.action) })),
|
|
800
|
+
h('p', null, t('unknownHelp')),
|
|
801
|
+
h('code', { className: css.path }, control.uncertainty.taskDir),
|
|
802
|
+
h(Button, { variant: 'outline', size: 'sm', disabled: busy, onClick: reconcile }, t('reconcile')),
|
|
803
|
+
h('section', { className: css.details },
|
|
804
|
+
h('h4', null, t('errorDetails')),
|
|
805
|
+
h('pre', { className: css.raw }, control.uncertainty.detail || t('associationUnconfirmed')))),
|
|
806
|
+
error && h('section', { className: css.error, role: 'alert' },
|
|
807
|
+
h('h3', { className: css.heading }, t(error.dispatch === 'not-dispatched' ? 'notDispatchedTitle' : 'errorTitle',
|
|
808
|
+
{ action: t(actionKey(error)) })),
|
|
809
|
+
h('p', null, t(error.dispatch === 'not-dispatched' ? 'notDispatchedHelp' : 'errorHelp')),
|
|
810
|
+
h('section', { className: css.details },
|
|
811
|
+
h('h4', null, t('errorDetails')),
|
|
812
|
+
h('pre', { className: css.raw }, error.detail))),
|
|
813
|
+
mode === 'idle' && !control.status?.state && !control.status?.preparation && h('div', { className: css.idleFeedback },
|
|
814
|
+
h('p', null, t('lifecycle')),
|
|
815
|
+
!selection.taskDir.trim() && h('p', null, t('taskRequired'))));
|
|
816
|
+
}
|
|
817
|
+
|
|
818
|
+
exports.inject = ['slots', 'remote', 'remote.commands', 'locale'];
|
|
819
|
+
exports.apply = function apply(ctx) {
|
|
820
|
+
ctx.effect(() => ctx.locale.register(NS, { zh, en }), 'flywheel: locales');
|
|
821
|
+
const t = ctx.locale.bind(NS);
|
|
822
|
+
// Drafts are slot-owned view state. No busy/status value is persisted or
|
|
823
|
+
// restored as a live fact after a new Session binding or a plugin reload.
|
|
824
|
+
const viewStore = createViewStore();
|
|
825
|
+
const controls = new WeakMap();
|
|
826
|
+
const pendingOperations = new Set();
|
|
827
|
+
let disposed = false;
|
|
828
|
+
ctx.on('dispose', () => {
|
|
829
|
+
disposed = true;
|
|
830
|
+
for (const release of pendingOperations) release();
|
|
831
|
+
pendingOperations.clear();
|
|
832
|
+
});
|
|
833
|
+
const readsReady = ctx.remote.$mount({ package: '@ckanner/flywheel', descriptors: [{
|
|
834
|
+
id: '@ckanner/flywheel#flywheelConsole/read', service: 'flywheelConsole', namespace: 'flywheelConsole', method: 'read',
|
|
835
|
+
invocation: { kind: 'direct' }, parameters: [
|
|
836
|
+
{ name: 'agent', wire: 'sessionId', source: 'lookup', lookup: 'agent', codec: { mode: 'src-json' } },
|
|
837
|
+
{ name: 'input', wire: 'input', source: 'json', codec: { mode: 'src-json' } },
|
|
838
|
+
], cancellation: { parameter: 'signal' }, result: { mode: 'src-json' },
|
|
839
|
+
}] });
|
|
840
|
+
// A missing read capability is a visible error, never a noisy command fallback.
|
|
841
|
+
readsReady.catch(() => {});
|
|
842
|
+
ctx.slots.inject('conversation.view', () => ctx.slots.register({
|
|
843
|
+
// External views follow the built-in Chat, Trajectory and Statistics views.
|
|
844
|
+
name: 'conversation.view', id: 'flywheel', order: 1000, label: () => t('view'),
|
|
845
|
+
locale: NS, store: viewStore,
|
|
846
|
+
inject: (sessionId, actions) => {
|
|
847
|
+
// Bound actions identify one retained Session binding. A recycled
|
|
848
|
+
// session id with a fresh binding must not adopt an old async result.
|
|
849
|
+
let control = controls.get(actions);
|
|
850
|
+
if (!control) {
|
|
851
|
+
control = createControl(async request => {
|
|
852
|
+
const value = readReply(await (mutates(request.action) || request.action === 'status' && control?.source.getSnapshot().uncertainty
|
|
853
|
+
? ctx.remote.commands.execute(sessionId, '/flywheel ' + JSON.stringify(request), [])
|
|
854
|
+
: readsReady.then(() => ctx.remote.flywheelConsole.read(sessionId, request))), request);
|
|
855
|
+
if (Array.isArray(value.runs)) value.runs = value.runs.filter(item => item.hostSessionId === sessionId);
|
|
856
|
+
return value;
|
|
857
|
+
}, pendingOperations, () => disposed);
|
|
858
|
+
controls.set(actions, control);
|
|
859
|
+
}
|
|
860
|
+
return {
|
|
861
|
+
hostSessionID: sessionId,
|
|
862
|
+
hooks: { control: control.source },
|
|
863
|
+
changeSelection: next => {
|
|
864
|
+
if (disposed || control.isMutating()) return;
|
|
865
|
+
actions.setSelection(next);
|
|
866
|
+
control.select(next);
|
|
867
|
+
},
|
|
868
|
+
run: control.run,
|
|
869
|
+
discover: control.discover,
|
|
870
|
+
associate: value => control.associate(value, actions.setSelection),
|
|
871
|
+
reconcile: () => {
|
|
872
|
+
const outcome = control.source.getSnapshot().uncertainty;
|
|
873
|
+
if (!outcome || control.source.getSnapshot().pending) return;
|
|
874
|
+
const next = { taskDir: outcome.taskDir, cwd: outcome.cwd };
|
|
875
|
+
actions.setSelection(next);
|
|
876
|
+
return control.run('status', next);
|
|
877
|
+
},
|
|
878
|
+
};
|
|
879
|
+
},
|
|
880
|
+
}, TaskView));
|
|
881
|
+
};
|
|
882
|
+
|
|
883
|
+
Object.assign(zh, { projectHome: '项目工作区', executionHome: '实际执行目录', externalWorktree: '自行管理的 Worktree(保留目录)' });
|
|
884
|
+
Object.assign(en, { projectHome: 'Project workspace', executionHome: 'Execution directory', externalWorktree: 'Caller-managed Worktree (retained)' });
|
|
885
|
+
// Compiled into the same lazy factory as client.js. All writes use the Go control boundary.
|
|
886
|
+
Object.assign(zh, {
|
|
887
|
+
consoleLastOperation: '最近节点操作', consoleOperation_running: '等待结束回执', consoleOperation_passed: '已完成', consoleOperation_failed: '未通过', consoleOperation_cancelled: '已取消', consoleSnapshotStale: '快照已过期', consoleLaunchOptions: '准备任务与启动选项', consoleTaskSummary: '任务总览与累计用量', consoleMockModel: '离线演练 · 不调用模型', consoleFullNode: '读取完整节点证据', consoleExcerpt: '画布使用有界摘要;完整节点证据按需读取', consoleAssociated: '本会话关联的任务', consoleResource: '资源 / Provider', consoleCustom: '自定义绑定',
|
|
888
|
+
consoleReopenHelp: '重新执行将使此节点及后续节点的验收与交付证据失效;Git 历史、累计用量和失败记录保留。填写原因后追加一次执行,再独立复验。', consoleManual: '人工继续一次', consoleManualReason: '继续原因', consoleManualHelp: '追加一次 Execute / L1 尝试(含有界传输重试),累计失败次数和预算继续计入',
|
|
889
|
+
consoleDelivery: 'Git 交付',
|
|
890
|
+
consoleInherit: '沿用默认策略', consoleBudget: '每日 Token 上限', consoleBudgetPolicy: '预算策略', consoleTimeout: '单次运行超时(毫秒)', consoleRetries: '传输重试上限', consoleRouteSession: '执行角色共用此历史策略,每个任务的 Session 仍独立', consolePermissionEffective: '阶段覆盖优先于全局策略;后端不支持的权限会明确拒绝',
|
|
891
|
+
consoleTask: '任务目录', consoleCwd: '执行目录', consoleBranch: '分支', consoleRevision: '状态版本', consoleAttempts: '尝试次数', consoleFailures: '失败次数', consoleAction: '本次动作', consoleReason: '原因', consoleOutcome: '执行结果', consoleCleanup: '清理确认',
|
|
892
|
+
consoleCanvas: '运行画布', consoleConfig: '全局 / 项目配置', consoleRuns: '运行与对比',
|
|
893
|
+
consoleTitle: 'Flywheel 流程控制台', consoleHelp: '查看流程与任务分支,选择节点运行阶段或核对证据',
|
|
894
|
+
consoleParallel: '并行执行 · 串行集成', consoleConcurrency: '最大并发任务', consolePlanSplit: 'Plan · 规划与拆分', consoleSchedule: '调度', consoleAggregate: '集成与聚合', consoleSerial: '串行调度', consoleScheduling: '调度策略', consoleRolePermissions: '按角色分配(执行可写/评审只读)', consoleScope: '配置作用域', consoleGlobal: '全局', consoleProject: '项目',
|
|
895
|
+
consoleRead: '读取配置草稿', consoleSave: '保存配置', consoleDefaults: '载入本版本默认值',
|
|
896
|
+
consoleNext: '保存后的配置用于下次启动;任务级配置优先于项目和全局配置',
|
|
897
|
+
consoleSession: '会话策略', consoleFresh: '每次新会话 · fresh', consoleScoped: '同任务同角色续接 · scoped',
|
|
898
|
+
consoleLegacy: 'Kimi Plan 历史续接 · 已启用', consoleUnknown: '未报告',
|
|
899
|
+
consoleDetails: '节点详情', consoleEvidence: '证据与失败原因',
|
|
900
|
+
consoleStageRun: '运行此阶段', consoleStageHelp: '阶段结束后等待继续;Execute 包含有界执行与 L1 验证',
|
|
901
|
+
consoleBlocked: '先停止运行并刷新状态;阶段需满足前置验收和引擎接纳条件',
|
|
902
|
+
consoleExternal: '外部交付状态未关联', consoleWhole: '继续整个流程', consoleSaved: '配置已保存,请重新查看生效配置',
|
|
903
|
+
consoleReload: '保存结果未确认,请重新读取配置后核对', consoleDraft: '修改草稿',
|
|
904
|
+
consoleValues: '配置字段(JSON)', consolePreview: '保存差异', consoleInvalid: '请输入合法的配置 JSON',
|
|
905
|
+
consolePolicies: '历史与权限', consoleFull: '完整流程 Full', consoleQuick: 'ultraWork / Quick',
|
|
906
|
+
consoleHistory: '调用与返修记录', consoleNoHistory: '暂无记录', consoleIdentity: '项目与运行位置',
|
|
907
|
+
consoleUnconfirmed: '结果未确认', consoleCurrent: '定位当前节点', consoleLeft: '向左移动', consoleRight: '向右移动',
|
|
908
|
+
consolePage: '任务 {from}–{to} / {total}', consolePrevious: '上一组', consoleMore: '下一组',
|
|
909
|
+
consoleAdapter: '执行环境 Harness', consoleModel: '模型', consoleThinking: '推理等级',
|
|
910
|
+
consoleProvider: 'Provider', consoleChannel: '账号渠道', consoleScopeReason: '阶段与任务隔离;Intake / Garden 使用独立调用',
|
|
911
|
+
consoleCompareHelp: '按任务、契约与配置摘要核对比较条件;用量、成本和时延是已知量,部分与缺失调用单列。费用可能按配置单价估算。调用失败率不等于代码质量。',
|
|
912
|
+
consoleConfigHelp: '先读取作用域。新文件以本版本默认配置创建,现有文件保留未编辑字段',
|
|
913
|
+
consoleResetHelp: '完整替换为本版本默认模板;保存前查看差异',
|
|
914
|
+
});
|
|
915
|
+
Object.assign(en, {
|
|
916
|
+
consoleLastOperation: 'Latest node operation', consoleOperation_running: 'Awaiting terminal receipt', consoleOperation_passed: 'Complete', consoleOperation_failed: 'Failed', consoleOperation_cancelled: 'Cancelled', consoleSnapshotStale: 'Stale snapshot', consoleLaunchOptions: 'Task preparation and launch options', consoleTaskSummary: 'Task summary and lifetime usage', consoleMockModel: 'Offline fixture · no inference', consoleFullNode: 'Read full node evidence', consoleExcerpt: 'The canvas uses bounded excerpts; read full evidence on demand', consoleAssociated: 'Tasks associated with this session', consoleResource: 'Resource / Provider', consoleCustom: 'Custom binding',
|
|
917
|
+
consoleReopenHelp: 'Reopen this and downstream units. Evidence is archived; Git history, billing and failures are retained. One execution attempt requires a reason and independent revalidation.', consoleManual: 'Continue once', consoleManualReason: 'Reason for continuing', consoleManualHelp: 'One Execute/L1 attempt including bounded transport retries; accumulated failures and budgets remain accounted',
|
|
918
|
+
consoleDelivery: 'Git delivery',
|
|
919
|
+
consoleInherit: 'Inherit default policy', consoleBudget: 'Daily token limit', consoleBudgetPolicy: 'Budget policy', consoleTimeout: 'Run timeout (ms)', consoleRetries: 'Transport retry limit', consoleRouteSession: 'Execution routes share this history policy; each packet still has its own session', consolePermissionEffective: 'Stage overrides take precedence; unsupported permissions are rejected',
|
|
920
|
+
consoleTask: 'Task directory', consoleCwd: 'Working directory', consoleBranch: 'Branch', consoleRevision: 'State revision', consoleAttempts: 'Attempts', consoleFailures: 'Failed attempts', consoleAction: 'Action', consoleReason: 'Reason', consoleOutcome: 'Outcome', consoleCleanup: 'Cleanup',
|
|
921
|
+
consoleCanvas: 'Workflow canvas', consoleConfig: 'Global / project configuration', consoleRuns: 'Runs and comparison',
|
|
922
|
+
consoleTitle: 'Flywheel workflow console', consoleHelp: 'Inspect task branches, run a stage and review its evidence',
|
|
923
|
+
consoleParallel: 'Parallel execution · serial integration', consoleConcurrency: 'Maximum parallel packets', consolePlanSplit: 'Plan and decomposition', consoleSchedule: 'Scheduling', consoleAggregate: 'Integration', consoleSerial: 'Serial scheduling', consoleScheduling: 'Scheduling policy', consoleRolePermissions: 'Role based: execution writes, review reads', consoleScope: 'Configuration scope', consoleGlobal: 'Global', consoleProject: 'Project',
|
|
924
|
+
consoleRead: 'Read configuration draft', consoleSave: 'Save configuration', consoleDefaults: 'Load version defaults',
|
|
925
|
+
consoleNext: 'Saved configuration applies on the next start; task configuration takes precedence',
|
|
926
|
+
consoleSession: 'Session policy', consoleFresh: 'Fresh history per call', consoleScoped: 'Reuse history within each role scope',
|
|
927
|
+
consoleLegacy: 'Kimi Plan history enabled', consoleUnknown: 'Not reported',
|
|
928
|
+
consoleDetails: 'Node details', consoleEvidence: 'Evidence and failures', consoleStageRun: 'Run stage',
|
|
929
|
+
consoleStageHelp: 'Wait after this stage; Execute includes its bounded execution and L1 loop',
|
|
930
|
+
consoleBlocked: 'Stop and refresh first; stage admission and prior verification are enforced by the engine',
|
|
931
|
+
consoleExternal: 'External delivery is not associated', consoleWhole: 'Continue workflow',
|
|
932
|
+
consoleSaved: 'Configuration saved; inspect effective configuration again', consoleReload: 'Save unconfirmed; reread and reconcile',
|
|
933
|
+
consoleDraft: 'Edit draft', consoleValues: 'Configuration fields (JSON)', consolePreview: 'Changes to save',
|
|
934
|
+
consoleInvalid: 'Enter valid configuration JSON', consolePolicies: 'History and permissions',
|
|
935
|
+
consoleFull: 'Full workflow', consoleQuick: 'ultraWork / Quick', consoleHistory: 'Calls and repairs',
|
|
936
|
+
consoleNoHistory: 'No records', consoleIdentity: 'Project and run location', consoleUnconfirmed: 'Unconfirmed result',
|
|
937
|
+
consoleCurrent: 'Locate selected node', consoleLeft: 'Pan left', consoleRight: 'Pan right',
|
|
938
|
+
consolePage: 'Tasks {from}–{to} / {total}', consolePrevious: 'Previous', consoleMore: 'Next',
|
|
939
|
+
consoleAdapter: 'Harness', consoleModel: 'Model', consoleThinking: 'Reasoning effort', consoleProvider: 'Provider',
|
|
940
|
+
consoleChannel: 'Account channel', consoleScopeReason: 'Histories are isolated by task and role; Intake / Garden run independently',
|
|
941
|
+
consoleCompareHelp: 'Cost totals include known values only; paired cross-run quality and latency data are not connected yet',
|
|
942
|
+
consoleConfigHelp: 'Read a scope first. New files start from version defaults; unedited fields in existing files are retained',
|
|
943
|
+
consoleResetHelp: 'Replace with the complete version template; inspect changes before saving',
|
|
944
|
+
});
|
|
945
|
+
|
|
946
|
+
function sessionModeLabel(binding, t) {
|
|
947
|
+
const mode = binding?.session?.mode;
|
|
948
|
+
if (mode === 'scoped') return t('consoleScoped');
|
|
949
|
+
if (mode === 'fresh') return t('consoleFresh');
|
|
950
|
+
if (mode === 'legacy-plan' || binding?.residentSession) return t('consoleLegacy');
|
|
951
|
+
return t('consoleUnknown');
|
|
952
|
+
}
|
|
953
|
+
|
|
954
|
+
function consoleTone(status) {
|
|
955
|
+
if (['completed', 'passed', 'pass', 'l1-passed', 'executed'].includes(status)) return 'success';
|
|
956
|
+
if (['executing', 'running', 'intent'].includes(status)) return 'active';
|
|
957
|
+
if (['return', 'review-failed', 'paused', 'stopped', 'unknown', 'cancelled'].includes(status)) return 'warning';
|
|
958
|
+
if (['blocked', 'interrupted', 'escalated', 'budget-stopped', 'l1-failed', 'failed'].includes(status)) return 'error';
|
|
959
|
+
return 'idle';
|
|
960
|
+
}
|
|
961
|
+
|
|
962
|
+
Object.assign(zh, { runsObserved: '本轮新增交付 {completed} · 执行尝试 {attempts} · 失败尝试 {failed} · 总耗时 {seconds} s', runsPartial: '部分数据 {count} 次', runsBounded: '每个已显示分组均汇总其全部调用;明细最多 200 条、分组最多 500 个。按 run 筛选可查看其完整汇总。', runsAll: '当前任务全部调用(含早期记录)', runsRefresh: '刷新最近运行', runsOpen: '在画布中打开', runsMetrics: '读取所选运行指标', runsCompare: '比较条件', runsEqual: '相同契约与配置', runsDifferent: '条件不同,仅并列展示', runsLegacy: '早期调用(未登记 run ID)', runsSamples: '调用 / 失败', runsLatency: '已知调用耗时累计', runsUsage: '已知 Token / 缺失调用', runsCost: '已知费用 / 缺失调用', runsQuality: '验收质量见各节点证据;模型质量需要独立 oracle', runsEmpty: '暂无已登记运行。CLI 启动或桌面启动后会自动登记。', runsReference: '对照运行', runsLive: '尚无退出回执,打开画布核对', runsReadOnly: '登记记录用于发现;当前运行状态以刷新后的引擎回执为准。' });
|
|
963
|
+
Object.assign(en, { runsObserved: 'New deliveries {completed} · attempts {attempts} · failed attempts {failed} · elapsed {seconds} s', runsPartial: 'partial {count}', runsBounded: 'Each displayed group aggregates all its calls. Details are limited to 200 calls and 500 groups; filter by run for its full summary.', runsAll: 'All calls for this task, including legacy records', runsRefresh: 'Refresh recent runs', runsOpen: 'Open canvas', runsMetrics: 'Read selected run metrics', runsCompare: 'Comparison conditions', runsEqual: 'Same contracts and configuration', runsDifferent: 'Different conditions; descriptive comparison only', runsLegacy: 'Legacy call without run ID', runsSamples: 'Calls / failed', runsLatency: 'Sum of known call durations', runsUsage: 'Known tokens / missing calls', runsCost: 'Known cost / missing calls', runsQuality: 'Validation quality is recorded in node evidence; model quality needs an independent oracle', runsEmpty: 'No registered runs. CLI and desktop launches register automatically.', runsReference: 'Reference run', runsLive: 'No exit receipt; open canvas to reconcile', runsReadOnly: 'The index is for discovery; refresh engine status to establish liveness.' });
|
|
964
|
+
|
|
965
|
+
Object.assign(zh, { sessionAction_create: '新建', sessionAction_resume: '续接', sessionAction_rebuild: '重建', sessionAction_wait: '等待核对', sessionAction_reconcile: '核对回执', sessionRelease: '退出接管 · 保持暂停', sessionReconcile: '核对原会话回执', sessionLatest: '查看最近回复', sessionLatestHelp: '这里读取经过摘要校验的最近一次回复;Provider 的完整会话历史仍保存在原后端。', sessionClaim: '暂停并接管会话', sessionReason: '接管原因', sessionSend: '发送到原 Session', sessionReturn: '验证并交回流程', sessionPrompt: '人工 Prompt', sessionHeld: '人工接管中 · 自动执行已锁定', sessionCommand: '终端交互命令', sessionNoClaim: '仅已确认收尾、可恢复的任务会话支持接管;里程碑会话可查看回复。', sessionKeep: '离开页面不会释放接管;测试和独立 Review 通过后才交回。' });
|
|
966
|
+
Object.assign(en, { sessionAction_create: 'Create', sessionAction_resume: 'Resume', sessionAction_rebuild: 'Rebuild', sessionAction_wait: 'Await reconciliation', sessionAction_reconcile: 'Reconcile', sessionRelease: 'Release control and remain paused', sessionReconcile: 'Reconcile original invocation', sessionLatest: 'Read latest response', sessionLatestHelp: 'This is the latest digest-verified response, not the provider’s full history.', sessionClaim: 'Take over session', sessionReason: 'Takeover reason', sessionSend: 'Send to original session', sessionReturn: 'Validate and return', sessionPrompt: 'Human prompt', sessionHeld: 'Human control · automatic execution locked', sessionCommand: 'Terminal command', sessionNoClaim: 'Only settled, resumable packet sessions support interactive takeover. Milestone history remains readable.', sessionKeep: 'Leaving the page retains human control until validation and independent review pass.' });
|
|
967
|
+
function sessionActionLabel(action, t) { return ['create', 'resume', 'rebuild', 'wait', 'reconcile'].includes(action) ? t('sessionAction_' + action) : t('consoleUnknown'); }
|
|
968
|
+
const shellQuote = value => "'" + String(value).replaceAll("'", "'\\''") + "'";
|
|
969
|
+
function SessionInspector({ sessionKey, value, control, selection, run, t }) {
|
|
970
|
+
const [reason, setReason] = React.useState('');
|
|
971
|
+
const [prompt, setPrompt] = React.useState('');
|
|
972
|
+
const state = control.status.state;
|
|
973
|
+
const held = state.handoff?.key === sessionKey;
|
|
974
|
+
const busy = control.pending !== null || control.status.running || control.stale || control.uncertainty !== null;
|
|
975
|
+
const canClaim = !state.handoff && value['resume-candidate'] && value.last?.status === 'consumed' && value.last?.['cleanup-confirmed'] && /^(final:)?M\d+-T\d+$/.test(value.scope);
|
|
976
|
+
const send = action => run('session', selection, false, { session: { action, key: sessionKey, revision: state.revision, ...(action === 'claim' ? { reason } : action === 'send' ? { prompt } : {}) } });
|
|
977
|
+
const history = control.history?.key === sessionKey ? control.history : null;
|
|
978
|
+
return h('article', { className: css.sessionRecord },
|
|
979
|
+
h('h4', { className: css.heading }, (roleLabels[value.role] ? t(roleLabels[value.role]) : value.role) + ' / ' + value.scope),
|
|
980
|
+
h('p', { className: css.path }, value['thread-id'] || t('consoleUnknown')),
|
|
981
|
+
value['workspace-cwd'] && h('p', { className: css.path }, t('projectHome') + ': ' + value['workspace-cwd']),
|
|
982
|
+
value.cwd && h('p', { className: css.path }, t('executionHome') + ': ' + value.cwd),
|
|
983
|
+
h('p', { className: css.meta }, [sessionActionLabel(value.action, t), value.reason, value.last?.outcome].filter(Boolean).join(' · ')),
|
|
984
|
+
h('div', { className: css.actions }, h(Button, { disabled: control.pending !== null, onClick: () => run('history', selection, false, { sessionKey }) }, t('sessionLatest'))),
|
|
985
|
+
history && h('section', { className: css.details, 'data-section': 'history' }, h('h4', null, t('sessionLatest')), h('p', { className: css.meta }, t('sessionLatestHelp')), h('pre', { className: css.raw }, history.text)),
|
|
986
|
+
held ? h('div', null, h('p', { role: 'status', className: css.notice }, t('sessionHeld')), h('p', { className: css.meta }, t('sessionKeep')),
|
|
987
|
+
h('label', { className: css.editorField }, t('sessionPrompt'), h('textarea', { className: css.editorJSON, value: prompt, disabled: busy, maxLength: 16000, onChange: event => setPrompt(event.target.value) })),
|
|
988
|
+
h('div', { className: css.actions }, h(Button, { disabled: busy || state.handoff?.pending || !prompt.trim(), onClick: () => send('send') }, t('sessionSend')), h(Button, { disabled: busy, onClick: () => send('reconcile') }, t('sessionReconcile')), h(Button, { disabled: busy || state.handoff?.pending, onClick: () => send('return') }, t('sessionReturn')), h(Button, { disabled: busy || state.handoff?.pending, onClick: () => send('release') }, t('sessionRelease')))) :
|
|
989
|
+
h('section', { className: css.details }, h('h4', null, t('sessionClaim')), h('p', { className: css.meta }, t('sessionNoClaim')),
|
|
990
|
+
h('label', { className: css.editorField }, t('sessionReason'), h('input', { className: css.editorInput, disabled: busy || !canClaim, value: reason, maxLength: 300, onChange: event => setReason(event.target.value) })),
|
|
991
|
+
h(Button, { disabled: busy || !canClaim || !reason.trim(), onClick: () => send('claim') }, t('sessionClaim'))),
|
|
992
|
+
/^(final:)?M\d+-T\d+$/.test(value.scope) && h('section', { className: css.details }, h('h4', null, t('sessionCommand')),
|
|
993
|
+
h('code', { className: css.path }, 'cd -- ' + shellQuote(value.cwd || control.status.cwd || selection.cwd) + ' && flywheel session ' + shellQuote(control.status.taskDir) + ' --key ' + shellQuote(sessionKey))));
|
|
994
|
+
}
|
|
995
|
+
|
|
996
|
+
Object.assign(zh, { deliveryRefresh: '查询项目与 PR', deliveryRepo: '目标仓库 / 分支 / HEAD', deliveryConfirm: '授权按当前 HEAD 推送并创建 PR,或合并选定 PR', deliveryCreate: '推送并创建 PR', deliveryMerge: '合并此 PR', deliveryHead: '远端任务分支', deliveryBase: '目标分支', deliveryTitle: 'PR 标题', deliveryBody: 'PR 说明', deliveryHelp: '远端写入要求本地任务完成、当前候选仍通过验收,并在执行前再次核对 revision 和 HEAD。未确认的结果先查询;不会强推或绕过 GitHub 合并规则。', deliveryStale: '远端快照已过期,请重新查询', deliveryPending: '合并请求已接收,等待远端最终结果', deliveryChecks: '远端检查', deliveryNoChecks: '未配置或未报告检查', deliveryUnknown: '尚未查询远端;本地完成与远端合并分别记录' });
|
|
997
|
+
Object.assign(en, { deliveryRefresh: 'Inspect project and PRs', deliveryRepo: 'Repository / branch / HEAD', deliveryConfirm: 'Authorize push/create or merge at this exact HEAD', deliveryCreate: 'Push and create PR', deliveryMerge: 'Merge PR', deliveryHead: 'Remote task branch', deliveryBase: 'Base branch', deliveryTitle: 'PR title', deliveryBody: 'PR description', deliveryHelp: 'Remote writes require completed local acceptance and a matching revision and HEAD. Reconcile uncertain results first. No force push or GitHub rule bypass.', deliveryStale: 'Remote snapshot is stale; inspect again', deliveryPending: 'Merge requested; awaiting the final remote result', deliveryChecks: 'Remote checks', deliveryNoChecks: 'No configured or reported checks', deliveryUnknown: 'Remote state has not been queried; local completion and remote merge are separate' });
|
|
998
|
+
function DeliveryPanel({ control, selection, run, t }) {
|
|
999
|
+
const [confirmed, setConfirmed] = React.useState(false);
|
|
1000
|
+
const [base, setBase] = React.useState('');
|
|
1001
|
+
const [headBranch, setHeadBranch] = React.useState('');
|
|
1002
|
+
const [title, setTitle] = React.useState('');
|
|
1003
|
+
const [body, setBody] = React.useState('');
|
|
1004
|
+
const remote = control.delivery;
|
|
1005
|
+
React.useEffect(() => { setBase(remote?.defaultBase ?? ''); setHeadBranch(remote ? remote.branch === remote.defaultBase ? 'flywheel/' + (control.status?.state?.slug || 'task') : remote.branch : ''); }, [remote?.repo, remote?.head]);
|
|
1006
|
+
React.useEffect(() => { setConfirmed(false); }, [remote?.repo, remote?.head, base, headBranch]);
|
|
1007
|
+
const blocked = control.pending !== null || control.status?.running || control.uncertainty !== null;
|
|
1008
|
+
const writeBlocked = blocked || control.stale || control.deliveryStale || !confirmed || !remote || control.status?.state?.status !== 'completed';
|
|
1009
|
+
const deliver = (action, number) => run('deliver', selection, false, { delivery: { action, confirmed: true, revision: control.status.state.revision, expectedHead: remote.head, expectedRepo: remote.repo, headBranch, ...(action === 'create' ? { base, title, body } : { number, base: remote.pulls.find(pull => pull.number === number)?.baseRefName }) } });
|
|
1010
|
+
return h('section', { className: css.section, 'aria-label': 'PR / Merge' },
|
|
1011
|
+
h('div', { className: css.actions }, h(Button, { disabled: blocked, onClick: () => run('delivery', selection, false, { delivery: { action: 'inspect', ...(headBranch ? { headBranch } : {}) } }) }, t('deliveryRefresh'))),
|
|
1012
|
+
h('p', { className: css.meta }, t('deliveryHelp')),
|
|
1013
|
+
control.status?.state?.delivery?.action === 'merge' && control.status.state.delivery.status === 'pending' && h('p', { role: 'status' }, t('deliveryPending')),
|
|
1014
|
+
control.deliveryStale && h('p', { role: 'alert' }, t('deliveryStale')),
|
|
1015
|
+
remote ? h(React.Fragment, null,
|
|
1016
|
+
h('a', { href: remote.url, target: '_blank', rel: 'noopener noreferrer' }, remote.repo),
|
|
1017
|
+
h('p', { className: css.path }, (remote.localBranch || remote.branch) + ' → ' + remote.branch + ' · ' + remote.head),
|
|
1018
|
+
h('label', { className: css.confirmation }, h('input', { type: 'checkbox', checked: confirmed, disabled: blocked, onChange: event => setConfirmed(event.target.checked) }), t('deliveryConfirm')),
|
|
1019
|
+
h('div', { className: css.editorGrid },
|
|
1020
|
+
h('label', { className: css.editorField }, t('deliveryHead'), h('input', { className: css.editorInput, value: headBranch, disabled: blocked, onChange: event => setHeadBranch(event.target.value) })),
|
|
1021
|
+
h('label', { className: css.editorField }, t('deliveryBase'), h('input', { className: css.editorInput, value: base, disabled: blocked, onChange: event => setBase(event.target.value) })),
|
|
1022
|
+
h('label', { className: css.editorField }, t('deliveryTitle'), h('input', { className: css.editorInput, value: title, maxLength: 256, disabled: blocked, onChange: event => setTitle(event.target.value) }))),
|
|
1023
|
+
h('label', { className: css.editorField }, t('deliveryBody'), h('textarea', { className: css.editorJSON, value: body, disabled: blocked, maxLength: 16000, onChange: event => setBody(event.target.value) })),
|
|
1024
|
+
h(Button, { disabled: writeBlocked || !title.trim() || !base.trim() || !headBranch.trim() || headBranch === base, onClick: () => deliver('create') }, t('deliveryCreate')),
|
|
1025
|
+
...remote.pulls.map(pull => h('article', { className: css.sessionRecord, key: pull.number },
|
|
1026
|
+
h('a', { href: pull.url, target: '_blank', rel: 'noopener noreferrer' }, '#' + pull.number + ' · ' + pull.state),
|
|
1027
|
+
h('p', { className: css.meta }, [pull.baseRefName, pull.mergeStateStatus, pull.reviewDecision].filter(Boolean).join(' · ')),
|
|
1028
|
+
h('section', { className: css.details }, h('h4', null, t('deliveryChecks')),
|
|
1029
|
+
pull.statusCheckRollup?.length ? pull.statusCheckRollup.map((check,index) => h('p', { key: index, className: css.meta }, [check.name || check.context, check.conclusion || check.state || check.status].filter(Boolean).join(' · '))) : h('p', { className: css.meta }, t('deliveryNoChecks'))),
|
|
1030
|
+
h(Button, { disabled: writeBlocked || pull.headRefOid !== remote.head || pull.state !== 'OPEN' || pull.isDraft || pull.mergeStateStatus !== 'CLEAN', onClick: () => deliver('merge', pull.number) }, t('deliveryMerge'))))) : h('p', { className: css.meta }, t('deliveryUnknown')));
|
|
1031
|
+
}
|
|
1032
|
+
|
|
1033
|
+
Object.assign(zh, { consoleJEV: '启用 JEV', consoleJEVConfiguredOn: '配置已开启 · 在符合条件的决策点调用', consoleJEVConfiguredOff: '配置已关闭 · 沿用原路径', consoleJEVOff: '关闭 · 沿用原流程', consoleJEVOn: '开启 · 参与决策', consoleJEVVersion: '固定模型版本', consoleJEVEnv: '密钥环境变量名(不要填密钥)' });
|
|
1034
|
+
Object.assign(en, { consoleJEV: 'Enable JEV', consoleJEVConfiguredOn: 'Configured on · calls at eligible decision points', consoleJEVConfiguredOff: 'Configured off · original paths', consoleJEVOff: 'Off · existing workflow', consoleJEVOn: 'On · participate in decisions', consoleJEVVersion: 'Pinned model version', consoleJEVEnv: 'API key environment variable name (not the key)' });
|
|
1035
|
+
|
|
1036
|
+
// Task discovery consumes Go's registered runs; no directory scanning or shell parsing.
|
|
1037
|
+
Object.assign(zh, {
|
|
1038
|
+
taskSelected: '当前任务', taskNoSelection: '还没有关联任务',
|
|
1039
|
+
taskEmptyHelp: '此视图只显示当前会话关联的 Flywheel 任务。使用 /flywheel-task 准备并运行后,画布会自动出现;加载 Skill 本身不会启动引擎',
|
|
1040
|
+
taskDiscoveryError: '当前会话任务读取失败', taskRequired: '当前会话尚未关联任务', retry: '重试',
|
|
1041
|
+
taskWorkspace: '工作区', taskLocation: '任务位置(自动取得)', taskFolderLocation: '任务记录',
|
|
1042
|
+
emptyBody: '在当前会话中使用 /flywheel-task 准备并运行任务',
|
|
1043
|
+
stageFocus: '本次运行', stageFull: '完整流程', stageOnly: '仅运行{stage}阶段',
|
|
1044
|
+
stagePassed: '本次{stage}已通过', stageFailed: '本次{stage}未通过', stageCancelled: '本次{stage}已取消',
|
|
1045
|
+
stageRunning: '本次{stage}进行中', stageUnknown: '本次{stage}结果待核对',
|
|
1046
|
+
stageScopeHelp: '这里展示这次单阶段运行的回执。通过该阶段不代表完整任务已交付',
|
|
1047
|
+
stageContinue: '需要继续完整流程时', stageFullState: '整体交付:{status}', stageUndelivered: '尚未完成', stageDelivered: '已完成', stageRecordedScore: '记录评分', stageRecordedVerdict: '记录结论', stageMetadata: '运行记录与文件位置',
|
|
1048
|
+
});
|
|
1049
|
+
Object.assign(en, {
|
|
1050
|
+
taskSelected: 'Current task', taskNoSelection: 'No task associated yet',
|
|
1051
|
+
taskEmptyHelp: 'This view follows the current conversation only. Use /flywheel-task to prepare and run a task; its canvas appears automatically. Loading a skill alone does not start the engine',
|
|
1052
|
+
taskDiscoveryError: 'Could not read this conversation’s task', taskRequired: 'No task is associated with this conversation', retry: 'Retry',
|
|
1053
|
+
taskWorkspace: 'Workspace', taskLocation: 'Task location (automatic)', taskFolderLocation: 'Task records',
|
|
1054
|
+
emptyBody: 'Use /flywheel-task in this conversation to prepare and run a task',
|
|
1055
|
+
stageFocus: 'This run', stageFull: 'Full workflow', stageOnly: '{stage} stage only',
|
|
1056
|
+
stagePassed: 'This {stage} passed', stageFailed: 'This {stage} failed', stageCancelled: 'This {stage} was cancelled',
|
|
1057
|
+
stageRunning: 'This {stage} is running', stageUnknown: 'This {stage} is unconfirmed',
|
|
1058
|
+
stageScopeHelp: 'This view shows the recorded outcome of one stage. Passing it does not mean the entire task was delivered',
|
|
1059
|
+
stageContinue: 'To continue the full workflow', stageFullState: 'Overall delivery: {status}', stageUndelivered: 'Not completed', stageDelivered: 'Completed', stageRecordedScore: 'Recorded score', stageRecordedVerdict: 'Recorded verdict', stageMetadata: 'Run details and file locations',
|
|
1060
|
+
});
|
|
1061
|
+
|
|
1062
|
+
function singleRunKind(control) {
|
|
1063
|
+
const state = control.status?.state;
|
|
1064
|
+
const kind = state?.run?.kind;
|
|
1065
|
+
return ['plan', 'execute', 'verify', 'review', 'integrate', 'final'].includes(kind) && state.control?.kind === kind ? kind : null;
|
|
1066
|
+
}
|
|
1067
|
+
function stageName(kind, t) {
|
|
1068
|
+
return ({ plan: t('consolePlanSplit'), execute: t('executeRole'), verify: 'L1', review: t('reviewRole'), integrate: t('consoleAggregate'), final: 'L2 / L3 · Judge' })[kind];
|
|
1069
|
+
}
|
|
1070
|
+
function singleRunLabel(control, t) {
|
|
1071
|
+
const kind = singleRunKind(control), op = control.status?.state?.control;
|
|
1072
|
+
const key = control.stale || op?.status === 'running' && !control.status.running ? 'stageUnknown' :
|
|
1073
|
+
({ passed: 'stagePassed', failed: 'stageFailed', cancelled: 'stageCancelled', running: 'stageRunning' }[op?.status] ?? 'stageUnknown');
|
|
1074
|
+
return t(key, { stage: stageName(kind, t) });
|
|
1075
|
+
}
|
|
1076
|
+
function taskCatalog(runs, association, sessionID) {
|
|
1077
|
+
const tasks = new Map();
|
|
1078
|
+
for (const run of (runs ?? []).slice(0,100)) {
|
|
1079
|
+
if (run?.hostSessionId !== sessionID || !sessionID || !record(run) || typeof run.taskDir !== 'string' || !run.taskDir.startsWith('/') || pathProblem(run.taskDir) || typeof run.cwd !== 'string' || !run.cwd.startsWith('/') || pathProblem(run.cwd)) continue;
|
|
1080
|
+
const key = selectionKey(run), old = tasks.get(key);
|
|
1081
|
+
const belongs = run.hostSessionId === sessionID;
|
|
1082
|
+
if (!old || String(run.startedAt ?? '') > String(old.startedAt ?? '')) tasks.set(key, { ...run, belongs: !!(belongs || old?.belongs) });
|
|
1083
|
+
else if (belongs) old.belongs = true;
|
|
1084
|
+
}
|
|
1085
|
+
for (const task of Array.isArray(association?.tasks) ? association.tasks : []) {
|
|
1086
|
+
if (task?.outcome !== 'confirmed' || typeof task.taskDir !== 'string' || !task.taskDir.startsWith('/') || pathProblem(task.taskDir) || typeof task.cwd !== 'string' || !task.cwd.startsWith('/') || pathProblem(task.cwd)) continue;
|
|
1087
|
+
const key = selectionKey(task), old = tasks.get(key);
|
|
1088
|
+
if (old) old.belongs = true;
|
|
1089
|
+
else tasks.set(key, { taskDir: task.taskDir, cwd: task.cwd, belongs: true, startedAt: '', summary: { slug: task.taskDir.split('/').at(-1) } });
|
|
1090
|
+
}
|
|
1091
|
+
return [...tasks.values()].sort((a,b) => Number(b.belongs) - Number(a.belongs) || String(b.startedAt ?? '').localeCompare(String(a.startedAt ?? '')) || selectionKey(a).localeCompare(selectionKey(b)));
|
|
1092
|
+
}
|
|
1093
|
+
function TaskChooser({ control, selection, hostSessionID, association, changeSelection, run, discover, t }) {
|
|
1094
|
+
const refreshedCatalog = React.useRef(null);
|
|
1095
|
+
const catalogSelection = React.useRef(null);
|
|
1096
|
+
const tasks = taskCatalog(control.runs, association, hostSessionID).filter(task => task.belongs);
|
|
1097
|
+
const busy = control.pending !== null || control.uncertainty !== null || association && association.outcome !== 'confirmed';
|
|
1098
|
+
React.useEffect(() => {
|
|
1099
|
+
// An explicit session projection wins. CLI-only runs need an exact host ID;
|
|
1100
|
+
// choose the latest deterministically, never a global or directory guess.
|
|
1101
|
+
if (!association && !busy && tasks.length && (!selection.taskDir || catalogSelection.current === selectionKey(selection) && selectionKey(tasks[0]) !== catalogSelection.current)) {
|
|
1102
|
+
refreshedCatalog.current = control.runs;
|
|
1103
|
+
const next = { taskDir: tasks[0].taskDir, cwd: tasks[0].cwd };
|
|
1104
|
+
catalogSelection.current = selectionKey(next);
|
|
1105
|
+
changeSelection(next); void run('status', next);
|
|
1106
|
+
}
|
|
1107
|
+
const latest = tasks.find(task => task.taskDir === (control.status?.taskDir || selection.taskDir) && task.cwd === (control.status?.cwd || selection.cwd));
|
|
1108
|
+
if (latest?.id && !busy && refreshedCatalog.current !== control.runs && control.status?.state?.run?.id && latest.id !== control.status.state.run.id) {
|
|
1109
|
+
refreshedCatalog.current = control.runs;
|
|
1110
|
+
void run('status', selection);
|
|
1111
|
+
}
|
|
1112
|
+
}, [control.runs, busy, selection.taskDir, selection.cwd, association]);
|
|
1113
|
+
const current = control.status;
|
|
1114
|
+
const selected = tasks.find(task => task.taskDir === (current?.taskDir || selection.taskDir) && task.cwd === (current?.cwd || selection.cwd));
|
|
1115
|
+
const linked = association?.taskDir === selection.taskDir ? association : null;
|
|
1116
|
+
const taskPath = current?.taskDir || selected?.taskDir || linked?.taskDir || '';
|
|
1117
|
+
const project = current?.state?.project?.['workspace-cwd'] || selected?.workspaceCwd || current?.cwd || selected?.cwd || linked?.cwd || control.runsCwd || '';
|
|
1118
|
+
return h('section', { className: css.taskChooser, 'aria-label': t('taskSelected') },
|
|
1119
|
+
h('div', { className: css.taskContext },
|
|
1120
|
+
h('span', null, project.split('/').filter(Boolean).at(-1) || t('taskThisSession')),
|
|
1121
|
+
h('span', null, ' / ' + t('taskThisSession')),
|
|
1122
|
+
(current?.state?.run?.branch || selected?.branch) && h('code', null, current?.state?.run?.branch || selected.branch)),
|
|
1123
|
+
h('h3', { className: css.heading }, selection.taskDir ? current?.state?.slug || selected?.summary?.slug || selection.taskDir.split('/').filter(Boolean).at(-1) : 'Flywheel'),
|
|
1124
|
+
control.runsError && h('div', { className: css.actions, role: 'alert' },
|
|
1125
|
+
h('span', { className: css.errorText }, t('taskDiscoveryError')),
|
|
1126
|
+
h(Button, { disabled: control.runsLoading, onClick: discover }, t('retry'))));
|
|
1127
|
+
}
|
|
1128
|
+
|
|
1129
|
+
function TaskLocations({ control, selection, association, t }) {
|
|
1130
|
+
const current = control.status;
|
|
1131
|
+
const taskPath = current?.taskDir || association?.taskDir || selection.taskDir;
|
|
1132
|
+
const project = current?.state?.project?.['workspace-cwd'] || current?.cwd || association?.cwd || control.runsCwd;
|
|
1133
|
+
if (!project && !taskPath) return null;
|
|
1134
|
+
return h('footer', { className: css.taskLocations, 'aria-label': t('taskLocation') },
|
|
1135
|
+
project && h('span', null, t('taskWorkspace'), ' ', h('code', { 'data-workspace-path': project }, project)),
|
|
1136
|
+
current?.cwd && current.cwd !== project && h('span', null, t('executionHome'), ' ', h('code', null, current.cwd)),
|
|
1137
|
+
taskPath && h('span', null, t('taskFolderLocation'), ' ', h('code', { 'data-task-path': taskPath }, project && taskPath.startsWith(project + '/') ? taskPath.slice(project.length + 1) : taskPath)));
|
|
1138
|
+
}
|
|
1139
|
+
|
|
1140
|
+
Object.assign(zh, {
|
|
1141
|
+
investigate: '调查', investigationTitle: '有界调查', investigationHelp: '由 Go 运行已批准的取证动作与诊断实验。调查结论独立于代码验收和交付。',
|
|
1142
|
+
investigationRun: '继续调查', investigationReconcile: '核对原动作回执', investigationReport: '调查报告', investigationEvidence: '已收集证据', investigationBudget: '动作 {used}/{limit} · 累计 {ms} ms', investigationPartial: '有界片段 · 内容未完整展示', investigationDecision: '决策建议与实际采用',
|
|
1143
|
+
investigationState_ready: '等待调查', investigationState_running: '调查中', 'investigationState_awaiting-report': '等待证据报告', investigationState_blocked: '结果未知 · 需核对', investigationState_supported: '报告有证据支持', investigationState_inconclusive: '尚无定论', 'investigationState_access-blocked': '缺少访问', investigationState_cancelled: '调查已取消',
|
|
1144
|
+
investigationRepair: '已授权原范围内修复;形成合法 Spec 后由 Auto 选择 Quick / Full。', investigationReportOnly: '本目标以调查报告结束;没有启动代码实现。',
|
|
1145
|
+
investigationJEVObserved: '最近观察到的 JEV 状态', investigationJEV_enabled: 'JEV 已开启 · 在符合条件的决策点参与', investigationJEV_disabled: 'JEV 已关闭 · 后续动作沿用原路径', 'investigationJEV_config-unavailable': 'JEV 配置不可用 · 沿用原路径', 'investigationJEV_not-configured': '本次调查未配置 JEV · 沿用原路径', 'investigationJEV_policy-changed': 'JEV 配置已变更 · 本次调查沿用原路径',
|
|
1146
|
+
investigationAdopted: 'JEV 已采用', investigationBaseline: '原路径', investigationEffect_defer: '依据证据跳过后续动作', investigationEffect_finish: '提前结束取证 · 等待报告', investigationSkipped: '跳过动作', investigationCitations: '依据观察',
|
|
1147
|
+
investigationCommand: '隔离副本中的命令', investigationHTTP: '批准的 HTTP 查询', investigationExit: '退出码', investigationHTTPStatus: 'HTTP 状态', investigationSignal: '终止信号', investigationTimedOut: '动作超时', investigationCancelled: '动作已取消', investigationWorkspace: '实验副本', investigationCleanup_removed: '实验副本已清理', investigationCleanup_retained: '实验副本保留 · 需要核对收尾', 'investigationCleanup_not-created': '未创建实验副本',
|
|
1148
|
+
});
|
|
1149
|
+
Object.assign(en, {
|
|
1150
|
+
investigate: 'Investigate', investigationTitle: 'Bounded investigation', investigationHelp: 'Go runs approved evidence actions and diagnostic experiments. An investigation conclusion is separate from code acceptance and delivery.',
|
|
1151
|
+
investigationRun: 'Continue investigation', investigationReconcile: 'Reconcile original receipt', investigationReport: 'Investigation report', investigationEvidence: 'Collected evidence', investigationBudget: 'Actions {used}/{limit} · elapsed {ms} ms', investigationPartial: 'Bounded excerpt · incomplete content', investigationDecision: 'Advice and recorded adoption',
|
|
1152
|
+
investigationState_ready: 'Ready to investigate', investigationState_running: 'Investigating', 'investigationState_awaiting-report': 'Awaiting evidence report', investigationState_blocked: 'Unknown result · reconcile', investigationState_supported: 'Report supported by cited evidence', investigationState_inconclusive: 'Inconclusive', 'investigationState_access-blocked': 'Access blocked', investigationState_cancelled: 'Investigation cancelled',
|
|
1153
|
+
investigationRepair: 'Repair is authorized within the original scope. Prepare a valid Spec before Auto selects Quick or Full.', investigationReportOnly: 'The goal ends with an investigation report; implementation has not started.',
|
|
1154
|
+
investigationJEVObserved: 'Last observed JEV state', investigationJEV_enabled: 'JEV enabled · participates at eligible decision points', investigationJEV_disabled: 'JEV disabled · subsequent actions use the original path', 'investigationJEV_config-unavailable': 'JEV configuration unavailable · original path', 'investigationJEV_not-configured': 'JEV not configured for this investigation · original path', 'investigationJEV_policy-changed': 'JEV policy changed · this investigation uses the original path',
|
|
1155
|
+
investigationAdopted: 'JEV adopted', investigationBaseline: 'Original path', investigationEffect_defer: 'Evidence-backed actions skipped', investigationEffect_finish: 'Evidence collection ended early · awaiting report', investigationSkipped: 'Skipped actions', investigationCitations: 'Cited observations',
|
|
1156
|
+
investigationCommand: 'Command in an isolated copy', investigationHTTP: 'Approved HTTP query', investigationExit: 'Exit code', investigationHTTPStatus: 'HTTP status', investigationSignal: 'Termination signal', investigationTimedOut: 'Action timed out', investigationCancelled: 'Action cancelled', investigationWorkspace: 'Experiment copy', investigationCleanup_removed: 'Experiment copy cleaned up', investigationCleanup_retained: 'Experiment copy retained · reconcile cleanup', 'investigationCleanup_not-created': 'No experiment copy created',
|
|
1157
|
+
});
|
|
1158
|
+
|
|
1159
|
+
function validInvestigationExecution(value) {
|
|
1160
|
+
return value == null || record(value) && ['run_command', 'http_read'].includes(value.kind) &&
|
|
1161
|
+
['timed-out', 'cancelled'].every(key => typeof value[key] === 'boolean') &&
|
|
1162
|
+
['removed', 'retained', 'not-created'].includes(value.cleanup) &&
|
|
1163
|
+
(value['exit-code'] == null || Number.isSafeInteger(value['exit-code']) && value['exit-code'] >= 0 && value['exit-code'] <= 255) &&
|
|
1164
|
+
(value['http-status'] == null || Number.isSafeInteger(value['http-status']) && value['http-status'] >= 100 && value['http-status'] <= 599) &&
|
|
1165
|
+
['signal', 'workspace', 'input-hash'].every(key => value[key] == null || typeof value[key] === 'string');
|
|
1166
|
+
}
|
|
1167
|
+
|
|
1168
|
+
function validPreparationSnapshot(p) {
|
|
1169
|
+
const strings = (object, keys) => record(object) && keys.every(key => typeof object[key] === 'string');
|
|
1170
|
+
const stringArray = value => Array.isArray(value) && value.every(item => typeof item === 'string');
|
|
1171
|
+
return record(p) && p.version === 1 && Number.isSafeInteger(p.revision) && p.revision > 0 &&
|
|
1172
|
+
['ready', 'running', 'awaiting-report', 'blocked', 'supported', 'inconclusive', 'access-blocked', 'cancelled'].includes(p.status) &&
|
|
1173
|
+
typeof p.reason === 'string' &&
|
|
1174
|
+
strings(p.contract, ['id', 'goal', 'intent']) && ['investigate', 'investigate-and-fix'].includes(p.contract.intent) &&
|
|
1175
|
+
record(p.contract.limits) && Number.isSafeInteger(p.contract.limits['max-actions']) &&
|
|
1176
|
+
Array.isArray(p.observations) && p.observations.every(observation => record(observation) && strings(observation.invocation, ['id', 'action-id']) &&
|
|
1177
|
+
['output', 'error'].every(key => observation[key] == null || typeof observation[key] === 'string') && validInvestigationExecution(observation.execution)) &&
|
|
1178
|
+
(p.decisions == null || Array.isArray(p.decisions) && p.decisions.every(record)) &&
|
|
1179
|
+
(p['jev-status'] == null || typeof p['jev-status'] === 'string') &&
|
|
1180
|
+
(p['decision-effects'] == null || Array.isArray(p['decision-effects']) && p['decision-effects'].every(effect =>
|
|
1181
|
+
strings(effect, ['decision-id', 'action-id', 'kind']) && ['defer', 'finish'].includes(effect.kind) && stringArray(effect.actions) && stringArray(effect.evidence))) &&
|
|
1182
|
+
(p.report == null || strings(p.report, ['summary', 'counter-evidence', 'recommendation', 'regression', 'rollback']) && Array.isArray(p.report.findings) &&
|
|
1183
|
+
p.report.findings.every(finding => strings(finding, ['hypothesis', 'status', 'reason'])) &&
|
|
1184
|
+
(p.report.limitations == null || Array.isArray(p.report.limitations) && p.report.limitations.every(limit => typeof limit === 'string')));
|
|
1185
|
+
}
|
|
1186
|
+
|
|
1187
|
+
function InvestigationExecution({ value, t }) {
|
|
1188
|
+
if (!value) return null;
|
|
1189
|
+
const property = (label, content) => h(React.Fragment, { key: label }, h('dt', null, label), h('dd', null, String(content)));
|
|
1190
|
+
return h('div', { 'data-investigation-execution': value.kind },
|
|
1191
|
+
h('p', { className: css.meta }, t(value.kind === 'run_command' ? 'investigationCommand' : 'investigationHTTP')),
|
|
1192
|
+
h('dl', { className: css.consoleProperties },
|
|
1193
|
+
value['exit-code'] != null && property(t('investigationExit'), value['exit-code']),
|
|
1194
|
+
value['http-status'] != null && property(t('investigationHTTPStatus'), value['http-status']),
|
|
1195
|
+
value.signal && property(t('investigationSignal'), value.signal)),
|
|
1196
|
+
value['timed-out'] && h('p', { className: css.warningText }, t('investigationTimedOut')),
|
|
1197
|
+
value.cancelled && h('p', { className: css.warningText }, t('investigationCancelled')),
|
|
1198
|
+
h('p', { className: value.cleanup === 'retained' ? css.warningText : css.meta }, t('investigationCleanup_' + value.cleanup)),
|
|
1199
|
+
value.workspace && h('section', { className: css.details }, h('h4', null, t('investigationWorkspace')), h('p', { className: css.meta }, value.workspace)));
|
|
1200
|
+
}
|
|
1201
|
+
|
|
1202
|
+
function InvestigationPanel({ control, selection, run, t }) {
|
|
1203
|
+
const preparation = control.status?.preparation;
|
|
1204
|
+
if (!preparation) return null;
|
|
1205
|
+
const busy = control.pending !== null || control.uncertainty !== null || control.stale;
|
|
1206
|
+
const ended = !!preparation.report || preparation.status === 'cancelled';
|
|
1207
|
+
const mutate = action => run('investigate', selection, false, { investigation: { action, revision: preparation.revision } });
|
|
1208
|
+
return h('section', { className: css.section, 'aria-label': t('investigationTitle') },
|
|
1209
|
+
h('h3', { className: css.heading }, t('investigationTitle') + ' · ' + preparation.contract.id),
|
|
1210
|
+
h('p', { className: css.meta }, t('investigationHelp')),
|
|
1211
|
+
h('p', null, preparation.contract.goal),
|
|
1212
|
+
h('p', { role: 'status' }, t('investigationState_' + preparation.status)),
|
|
1213
|
+
h('p', { className: css.meta }, t('investigationBudget', { used: preparation['actions-used'], limit: preparation.contract.limits['max-actions'], ms: preparation['elapsed-ms'] })),
|
|
1214
|
+
h('p', { className: css.meta }, preparation.reason),
|
|
1215
|
+
preparation['jev-status'] && h('p', { className: css.meta }, t('investigationJEVObserved') + ': ' + t('investigationJEV_' + preparation['jev-status'])),
|
|
1216
|
+
h('div', { className: css.actions },
|
|
1217
|
+
h(Button, { disabled: busy || control.status.running || ended || preparation.status === 'awaiting-report', onClick: () => mutate('run') }, t(preparation.pending || preparation['decision-pending'] ? 'investigationReconcile' : 'investigationRun')),
|
|
1218
|
+
h(Button, { disabled: busy || ended || !control.status.running && !!preparation.pending, onClick: () => control.status.running ? run('stop', selection) : mutate('cancel') }, t('stop')),
|
|
1219
|
+
h(Button, { disabled: control.pending !== null, onClick: () => run('status', selection) }, t('status'))),
|
|
1220
|
+
h('p', { className: css.notice }, t(preparation.contract.intent === 'investigate-and-fix' ? 'investigationRepair' : 'investigationReportOnly')),
|
|
1221
|
+
preparation.report && h('article', null, h('h4', null, t('investigationReport')), h('p', null, preparation.report.summary),
|
|
1222
|
+
...preparation.report.findings.map(finding => h('p', { key: finding.hypothesis }, finding.hypothesis + ' · ' + finding.status + ': ' + finding.reason)),
|
|
1223
|
+
h('p', null, preparation.report['counter-evidence']),
|
|
1224
|
+
...(preparation.report.limitations ?? []).map((limit, i) => h('p', { key: i, className: css.warningText }, limit)),
|
|
1225
|
+
h('p', null, preparation.report.recommendation), h('p', null, preparation.report.regression), h('p', null, preparation.report.rollback)),
|
|
1226
|
+
h('section', { className: css.details }, h('h4', null, t('investigationEvidence')),
|
|
1227
|
+
...preparation.observations.map(observation => h('article', { key: 'observation:' + observation.invocation.id },
|
|
1228
|
+
h('h4', null, observation.invocation['action-id'] + ' · ' + observation.status),
|
|
1229
|
+
h('p', { className: css.meta }, observation.invocation.id + ' · ' + observation['duration-ms'] + ' ms'),
|
|
1230
|
+
h(InvestigationExecution, { value: observation.execution, t }),
|
|
1231
|
+
observation.truncated && h('p', { className: css.warningText }, t('investigationPartial')),
|
|
1232
|
+
h('pre', { className: css.raw }, observation.output || observation.error)))),
|
|
1233
|
+
(preparation.decisions?.length > 0 || preparation['decision-effects']?.length > 0) && h('section', { className: css.details }, h('h4', null, t('investigationDecision')),
|
|
1234
|
+
...(preparation.decisions ?? []).map(receipt => h('p', { key: 'receipt:' + receipt['decision-id'] }, receipt['decision-id'] + ' · ' + receipt.status + ' · ' + receipt.reason)),
|
|
1235
|
+
...preparation.observations.map(observation => h('p', { key: 'observation:' + observation.invocation.id }, observation.invocation['action-id'] + ' · ' + t(observation.invocation.adopted === true ? 'investigationAdopted' : 'investigationBaseline') + ' · ' + observation.invocation.reason)),
|
|
1236
|
+
...(preparation['decision-effects'] ?? []).map(effect => h('article', { key: 'effect:' + effect['decision-id'] },
|
|
1237
|
+
h('h4', null, t('investigationEffect_' + effect.kind)),
|
|
1238
|
+
h('p', { className: css.meta }, effect['decision-id']),
|
|
1239
|
+
effect.actions.length > 0 && h('p', null, t('investigationSkipped') + ': ' + effect.actions.join(', ')),
|
|
1240
|
+
h('p', { className: css.meta }, t('investigationCitations') + ': ' + effect.evidence.join(', '))))));
|
|
1241
|
+
}
|
|
1242
|
+
|
|
1243
|
+
Object.assign(zh, {
|
|
1244
|
+
runtimeDecisionTitle: 'JEV 执行决策', runtimeDecisionHelp: '展示引擎已记录的选择与执行结果;建议被接受不代表动作已经完成。', runtimeDecisionUsage: '任务请求 {used}/{limit} · 其中调查移交 {imported}', runtimeDecisionOmitted: '显示最近 {shown} 条,共 {total} 条;完整记录保留在任务账本',
|
|
1245
|
+
runtimeDecisionPending: '决策请求待核对', runtimeDecisionApplied: '控制决策已应用 · 后续执行见节点状态', runtimeDecisionCompleted: '控制动作已收尾 · 任务结果见节点状态', runtimeDecisionUnknown: '结果未知 · 核对原回执', runtimeDecisionStale: '输入已变化 · 未采用', runtimeDecisionBaseline: '沿用基线', runtimeDecisionAdopted: '采用 JEV 选择', runtimeDecisionRepair: '继续局部修复', runtimeDecisionReplan: '请求重新规划', runtimeDecisionModel: '切换批准的模型配置', runtimeDecisionFeedback: '补充短反馈检查', runtimeDecisionRecheck: '复查失败的验收项', runtimeDecisionFeedbackRunning: '反馈检查未收尾 · 恢复前核对原执行', runtimeDecisionFeedbackCompleted: '反馈检查已收尾', runtimeDecisionFeedbackUnknown: '反馈结果未知 · 核对原执行,禁止重放', runtimeDecisionFeedbackStale: '反馈改变了候选或验证输入 · 已阻断', runtimeDecisionFeedbackHelp: '这是诊断反馈;最终仍执行完整验收。', runtimeDecisionCall: '决策请求', runtimeDecisionReason: '原因', runtimeDecisionAttempt: '执行尝试', runtimeDecisionStatus: '最近观察状态', runtimeDecisionResultsOmitted: '省略了 {count} 项较早的反馈结果', runtimeDecisionIncomplete: '反馈结果不完整',
|
|
1246
|
+
runtimeDecisionReplanRunning: '重规划未收尾 · 恢复前核对原会话', runtimeDecisionUnapplied: 'JEV 选择未应用', runtimeModelHistory: '执行配置切换记录', runtimeModelHistoryHelp: '这里记录已应用的配置切换。实际启动的模型与会话以节点会话记录为准。', runtimeModelFrom: '切换前', runtimeModelTo: '切换后', runtimeModelJev: '采用 JEV 模型选择',
|
|
1247
|
+
});
|
|
1248
|
+
Object.assign(en, {
|
|
1249
|
+
runtimeDecisionTitle: 'JEV execution decisions', runtimeDecisionHelp: 'Recorded choices and effects from the engine. Accepting advice does not mean its action has completed.', runtimeDecisionUsage: 'Task requests {used}/{limit} · {imported} imported from investigation', runtimeDecisionOmitted: 'Showing {shown} of {total} records; the complete history remains in the task ledger',
|
|
1250
|
+
runtimeDecisionPending: 'Decision request needs reconciliation', runtimeDecisionApplied: 'Control decision applied · see node for subsequent execution', runtimeDecisionCompleted: 'Control action settled · see node for task outcome', runtimeDecisionUnknown: 'Unknown result · reconcile original receipt', runtimeDecisionStale: 'Inputs changed · not adopted', runtimeDecisionBaseline: 'Original baseline', runtimeDecisionAdopted: 'JEV choice accepted', runtimeDecisionRepair: 'Continue local repair', runtimeDecisionReplan: 'Request replanning', runtimeDecisionModel: 'Switch approved model configuration', runtimeDecisionFeedback: 'Collect a short feedback check', runtimeDecisionRecheck: 'Recheck failed acceptance', runtimeDecisionFeedbackRunning: 'Feedback action unsettled · reconcile before resuming', runtimeDecisionFeedbackCompleted: 'Feedback action settled', runtimeDecisionFeedbackUnknown: 'Unknown feedback result · reconcile original action, no replay', runtimeDecisionFeedbackStale: 'Feedback changed candidate or verification inputs · blocked', runtimeDecisionFeedbackHelp: 'This is diagnostic feedback. Final acceptance still runs in full.', runtimeDecisionCall: 'Decision request', runtimeDecisionReason: 'Reason', runtimeDecisionAttempt: 'Execution attempt', runtimeDecisionStatus: 'Last observed status', runtimeDecisionResultsOmitted: '{count} earlier feedback results omitted', runtimeDecisionIncomplete: 'Feedback observation is incomplete',
|
|
1251
|
+
runtimeDecisionReplanRunning: 'Replanning unsettled · reconcile original session before resuming', runtimeDecisionUnapplied: 'JEV choice not applied', runtimeModelHistory: 'Execution configuration changes', runtimeModelHistoryHelp: 'These are applied configuration changes. Actual model dispatch and sessions remain in the node session records.', runtimeModelFrom: 'Before', runtimeModelTo: 'After', runtimeModelJev: 'JEV model selection applied',
|
|
1252
|
+
});
|
|
1253
|
+
|
|
1254
|
+
function validRuntimeDecisions(value) {
|
|
1255
|
+
const strings = (object, keys) => record(object) && keys.every(key => typeof object[key] === 'string');
|
|
1256
|
+
const optionalStrings = (object, keys) => keys.every(key => object[key] == null || typeof object[key] === 'string');
|
|
1257
|
+
const count = number => Number.isSafeInteger(number) && number >= 0;
|
|
1258
|
+
const feedback = report => report == null || record(report) && typeof report.pass === 'boolean' && Array.isArray(report.results) && report.results.every(result =>
|
|
1259
|
+
record(result) && count(result['omitted-items']) && Object.keys(result).length === 1 ||
|
|
1260
|
+
strings(result, ['cmd']) && typeof result.pass === 'boolean' && Number.isSafeInteger(result.exitCode) &&
|
|
1261
|
+
['timedOut', 'cancelled', 'truncated', 'outputTruncated', 'observationComplete'].every(key => result[key] == null || typeof result[key] === 'boolean') &&
|
|
1262
|
+
optionalStrings(result, ['output', 'signal', 'cleanupError', 'spawnError', 'checkId']));
|
|
1263
|
+
return value == null || record(value) && value.version === 1 && count(value.used) && count(value['imported-calls']) && typeof value.status === 'string' &&
|
|
1264
|
+
(value['max-calls'] == null || count(value['max-calls'])) && (value['omitted-records'] == null || count(value['omitted-records'])) &&
|
|
1265
|
+
Array.isArray(value.records) && value.records.every(entry =>
|
|
1266
|
+
strings(entry, ['id', 'packet', 'action', 'status', 'started-at']) && count(entry.attempt) &&
|
|
1267
|
+
(entry['plan-revisions'] == null || count(entry['plan-revisions'])) && (entry.accounted == null || typeof entry.accounted === 'boolean') &&
|
|
1268
|
+
(entry.selection == null || strings(entry.selection, ['action-id', 'reason']) && typeof entry.selection.adopted === 'boolean') &&
|
|
1269
|
+
(entry.receipt == null || strings(entry.receipt, ['status', 'reason']) && optionalStrings(entry.receipt, ['model'])) && feedback(entry.feedback));
|
|
1270
|
+
}
|
|
1271
|
+
|
|
1272
|
+
function runtimeDecisionAction(action, t) {
|
|
1273
|
+
const kind = action.match(/^(model|feedback|recheck)-\d+$/)?.[1] ?? action;
|
|
1274
|
+
const label = { repair: 'runtimeDecisionRepair', replan: 'runtimeDecisionReplan', model: 'runtimeDecisionModel', feedback: 'runtimeDecisionFeedback', recheck: 'runtimeDecisionRecheck' }[kind];
|
|
1275
|
+
return label ? t(label) + (kind !== action ? ' · ' + action : '') : action;
|
|
1276
|
+
}
|
|
1277
|
+
|
|
1278
|
+
function RuntimeDecisionPanel({ ledger, packetID, t }) {
|
|
1279
|
+
if (!ledger) return null;
|
|
1280
|
+
const entries = ledger.records.filter(entry => !packetID || entry.packet === packetID);
|
|
1281
|
+
if (packetID && entries.length === 0) return null;
|
|
1282
|
+
const statuses = { pending: 'runtimeDecisionPending', baseline: 'runtimeDecisionBaseline', applied: 'runtimeDecisionApplied', completed: 'runtimeDecisionCompleted', stale: 'runtimeDecisionStale', 'replan-running': 'runtimeDecisionReplanRunning', 'feedback-running': 'runtimeDecisionFeedbackRunning', 'feedback-completed': 'runtimeDecisionFeedbackCompleted', 'feedback-unknown': 'runtimeDecisionFeedbackUnknown', 'feedback-stale': 'runtimeDecisionFeedbackStale' };
|
|
1283
|
+
const total = ledger.records.length + (ledger['omitted-records'] ?? 0);
|
|
1284
|
+
return h('section', { className: css.details, 'data-runtime-decisions': packetID ?? 'task' },
|
|
1285
|
+
h('h4', null, t('runtimeDecisionTitle')),
|
|
1286
|
+
h('p', { className: css.meta }, t('runtimeDecisionHelp')),
|
|
1287
|
+
h('p', { className: css.meta }, t('runtimeDecisionUsage', { used: ledger.used, limit: ledger['max-calls'] ?? '—', imported: ledger['imported-calls'] })),
|
|
1288
|
+
h('p', { className: css.meta }, t('runtimeDecisionStatus') + ': ' + ledger.status),
|
|
1289
|
+
!packetID && total > entries.length && h('p', { className: css.meta }, t('runtimeDecisionOmitted', { shown: entries.length, total })),
|
|
1290
|
+
...entries.map(entry => h('article', { key: entry.id, 'data-decision-id': entry.id },
|
|
1291
|
+
h('h4', null, entry.packet + ' · ' + runtimeDecisionAction(entry.action, t)),
|
|
1292
|
+
h('p', { className: css.meta }, entry.id + ' · ' + t('runtimeDecisionAttempt') + ' ' + entry.attempt + ' · ' + entry['started-at']),
|
|
1293
|
+
h('p', { className: ['pending', 'replan-running', 'feedback-running', 'feedback-unknown', 'feedback-stale'].includes(entry.status) ? css.warningText : css.meta }, statuses[entry.status] ? t(statuses[entry.status]) : entry.status),
|
|
1294
|
+
entry.selection && h('p', null, t(!entry.selection.adopted ? 'runtimeDecisionBaseline' : ['stale', 'baseline', 'pending'].includes(entry.status) ? 'runtimeDecisionUnapplied' : 'runtimeDecisionAdopted') + ' · ' + entry.selection.reason),
|
|
1295
|
+
entry.receipt && h('p', { className: css.meta }, t('runtimeDecisionCall') + ': ' + entry.receipt.status + ' · ' + entry.receipt.reason),
|
|
1296
|
+
entry.receipt?.status === 'unknown' && h('p', { className: css.warningText }, t('runtimeDecisionUnknown')),
|
|
1297
|
+
entry.feedback && h('div', { 'data-decision-feedback': entry.id },
|
|
1298
|
+
h('p', { className: css.meta }, t('runtimeDecisionFeedbackHelp')),
|
|
1299
|
+
...entry.feedback.results.map((result, index) => result['omitted-items'] != null
|
|
1300
|
+
? h('p', { key: index, className: css.meta }, t('runtimeDecisionResultsOmitted', { count: result['omitted-items'] }))
|
|
1301
|
+
: h('article', { key: index },
|
|
1302
|
+
h('p', null, result.cmd),
|
|
1303
|
+
h('p', { className: css.meta }, t('investigationExit') + ': ' + result.exitCode + ' · ' + (result.pass ? t('pass') : t('fail'))),
|
|
1304
|
+
result.timedOut && h('p', { className: css.warningText }, t('investigationTimedOut')),
|
|
1305
|
+
result.cancelled && h('p', { className: css.warningText }, t('investigationCancelled')),
|
|
1306
|
+
result.observationComplete === false && h('p', { className: css.warningText }, t('runtimeDecisionIncomplete')),
|
|
1307
|
+
(result.truncated || result.outputTruncated) && h('p', { className: css.warningText }, t('investigationPartial')),
|
|
1308
|
+
result.signal && h('p', { className: css.warningText }, t('investigationSignal') + ': ' + result.signal),
|
|
1309
|
+
(result.cleanupError || result.spawnError) && h('p', { className: css.warningText }, result.cleanupError || result.spawnError),
|
|
1310
|
+
result.output && h('pre', { className: css.raw }, result.output)))))));
|
|
1311
|
+
}
|
|
1312
|
+
|
|
1313
|
+
function validExecutionPolicyHistory(packet) {
|
|
1314
|
+
const policy = packet?.['execution-policy'];
|
|
1315
|
+
const binding = value => record(value) && ['harness', 'provider', 'channel', 'model', 'thinking', 'permission', 'sessionMode']
|
|
1316
|
+
.every(key => value[key] == null || typeof value[key] === 'string');
|
|
1317
|
+
return record(packet) && (policy == null || record(policy) &&
|
|
1318
|
+
(policy.history == null || Array.isArray(policy.history) && policy.history.every(change =>
|
|
1319
|
+
record(change) && ['kind', 'from', 'to', 'reason', 'at'].every(key => typeof change[key] === 'string') &&
|
|
1320
|
+
Number.isSafeInteger(change.attempt) && change.attempt >= 0 && binding(change.before) && binding(change.after))));
|
|
1321
|
+
}
|
|
1322
|
+
|
|
1323
|
+
function ExecutionPolicyChanges({ packets, packetID, t }) {
|
|
1324
|
+
const changes = Object.entries(packets ?? {}).flatMap(([id, packet]) => !packetID || id === packetID
|
|
1325
|
+
? (packet['execution-policy']?.history ?? []).map(change => ({ id, change })) : []);
|
|
1326
|
+
if (!changes.length) return null;
|
|
1327
|
+
const fields = ['harness', 'provider', 'channel', 'model', 'thinking', 'permission', 'sessionMode'];
|
|
1328
|
+
const bindingDetails = (binding, label) => h('div', null,
|
|
1329
|
+
h('p', null, t(label)),
|
|
1330
|
+
h('dl', { className: css.consoleProperties }, ...fields.filter(field => binding[field]).map(field =>
|
|
1331
|
+
h(React.Fragment, { key: field }, h('dt', null, field), h('dd', null, binding[field])))));
|
|
1332
|
+
return h('section', { className: css.details, 'data-execution-switches': packetID ?? 'task' },
|
|
1333
|
+
h('h4', null, t('runtimeModelHistory')),
|
|
1334
|
+
h('p', { className: css.meta }, t('runtimeModelHistoryHelp')),
|
|
1335
|
+
...changes.map(({ id, change }, index) => h('article', { key: index },
|
|
1336
|
+
h('h4', null, id + ' · ' + change.from + ' → ' + change.to),
|
|
1337
|
+
h('p', { className: css.meta }, (change.kind === 'jev-model-selection' ? t('runtimeModelJev') : change.kind) + ' · ' + t('runtimeDecisionAttempt') + ' ' + change.attempt + ' · ' + change.at),
|
|
1338
|
+
h('p', null, t('runtimeDecisionReason') + ': ' + change.reason),
|
|
1339
|
+
bindingDetails(change.before, 'runtimeModelFrom'),
|
|
1340
|
+
bindingDetails(change.after, 'runtimeModelTo'))));
|
|
1341
|
+
}
|
|
1342
|
+
|
|
1343
|
+
Object.assign(zh, { sharedBatch: '共享目录批次 · 统一验收与提交', sharedBatchError: '上次批次错误' });
|
|
1344
|
+
Object.assign(en, { sharedBatch: 'Shared checkout · collective verification and delivery', sharedBatchError: 'Previous batch error' });
|
|
1345
|
+
// Native Conversation View content; no host DOM replacement or second global sidebar.
|
|
1346
|
+
Object.assign(zh, {
|
|
1347
|
+
consoleCanvas: '流程',
|
|
1348
|
+
consoleConfig: '配置',
|
|
1349
|
+
consoleRuns: '运行记录',
|
|
1350
|
+
consoleTitle: 'Flywheel 流程工作台',
|
|
1351
|
+
consoleScoped: '同任务、同角色续接',
|
|
1352
|
+
consoleFresh: '每次创建新会话',
|
|
1353
|
+
wbOverview: '任务全貌',
|
|
1354
|
+
wbProgress: '进展',
|
|
1355
|
+
wbSession: '会话',
|
|
1356
|
+
wbConfiguration: '本次配置',
|
|
1357
|
+
wbClose: '关闭详情',
|
|
1358
|
+
wbCloseDialog: '关闭对话框',
|
|
1359
|
+
wbCanvas: '画布',
|
|
1360
|
+
wbCanvasRegion: '运行画布',
|
|
1361
|
+
wbList: '列表',
|
|
1362
|
+
wbBack: '返回任务全貌',
|
|
1363
|
+
wbExpand: '查看任务流程',
|
|
1364
|
+
wbExpandGroup: '查看阶段流程',
|
|
1365
|
+
wbFit: '适合宽度',
|
|
1366
|
+
wbFocus: '专注画布',
|
|
1367
|
+
wbExitFocus: '退出专注画布',
|
|
1368
|
+
wbZoomIn: '放大画布',
|
|
1369
|
+
wbZoomOut: '缩小画布',
|
|
1370
|
+
wbLocate: '定位当前步骤',
|
|
1371
|
+
wbBlocked: '处理卡点',
|
|
1372
|
+
wbAggregate: '汇总与终验',
|
|
1373
|
+
wbDelivery: 'PR 与 Merge',
|
|
1374
|
+
wbNoRun: '尚未启动',
|
|
1375
|
+
wbReadFirst: '读取任务状态后显示流程',
|
|
1376
|
+
wbStartHelp: '在当前对话使用 /flywheel-task 描述目标;引擎启动后自动关联画布',
|
|
1377
|
+
wbProposalHelp: '只准备方案可用 /flywheel-new;加载 Skill 本身不会启动引擎',
|
|
1378
|
+
wbSwitchMode: '启动选项',
|
|
1379
|
+
wbLoading: '正在读取',
|
|
1380
|
+
wbPending: '等待操作回执',
|
|
1381
|
+
wbUnknown: '先核对上次调用',
|
|
1382
|
+
wbRunning: '运行中,需先停止并确认收尾',
|
|
1383
|
+
wbStale: '状态尚未核对,请刷新',
|
|
1384
|
+
wbHeld: '人工接管中,需验证并交回',
|
|
1385
|
+
wbRevision: '缺少有效状态版本,请刷新',
|
|
1386
|
+
wbCompleted: '已完成的任务需说明原因后重新执行',
|
|
1387
|
+
wbL1Required: '需先通过本次确定性验证',
|
|
1388
|
+
wbReviewRequired: '需先通过独立 Review',
|
|
1389
|
+
wbPacketsRequired: '需先完成所有任务交付',
|
|
1390
|
+
wbDependencies: '等待依赖任务交付;依赖编号见运行记录与文件位置',
|
|
1391
|
+
wbRunTask: '运行此任务',
|
|
1392
|
+
wbRetry: '重试一次',
|
|
1393
|
+
wbReopen: '重新执行',
|
|
1394
|
+
wbConfirmStep: '确认节点操作',
|
|
1395
|
+
wbConfirm: '确认运行',
|
|
1396
|
+
wbCancel: '取消',
|
|
1397
|
+
wbRetryHelp: '追加一次 Execute / L1 尝试,保留累计失败与预算;不会自动重跑其他任务',
|
|
1398
|
+
wbStateChanged: '状态已变化,请关闭后重新选择',
|
|
1399
|
+
wbNoReason: '请说明继续的原因',
|
|
1400
|
+
wbTakeover: '人工接管',
|
|
1401
|
+
wbSnapshot: '本次运行的配置快照;修改默认配置只影响下次启动',
|
|
1402
|
+
wbExecutorHistory: '执行与 Review 使用独立角色会话',
|
|
1403
|
+
wbEvidence: '查看证据与失败原因',
|
|
1404
|
+
wbNoEvidence: '当前没有已记录的失败说明',
|
|
1405
|
+
wbFailureCode: '失败代码',
|
|
1406
|
+
wbCurrentResult: '本次结果',
|
|
1407
|
+
wbSerialReview: '执行按调度策略推进,Review 与集成串行',
|
|
1408
|
+
wbOperation: '最近操作',
|
|
1409
|
+
wbReadOnly: '运行位置由引擎取得,只读',
|
|
1410
|
+
wbPaths: '位置与来源',
|
|
1411
|
+
wbRepository: '仓库',
|
|
1412
|
+
wbNoSessions: '此节点还没有已登记的 Provider 会话',
|
|
1413
|
+
wbTechnical: '运行标识与详细证据',
|
|
1414
|
+
wbPRClosed: 'PR 已关闭',
|
|
1415
|
+
wbPRReady: 'PR 已创建',
|
|
1416
|
+
wbUnobserved: '尚未查询远端',
|
|
1417
|
+
wbScheduled: '已有执行记录',
|
|
1418
|
+
wbNotApplicable: '本流程不适用',
|
|
1419
|
+
wbGate: '确定性验证',
|
|
1420
|
+
});
|
|
1421
|
+
Object.assign(en, {
|
|
1422
|
+
consoleCanvas: 'Workflow',
|
|
1423
|
+
consoleConfig: 'Configuration',
|
|
1424
|
+
consoleRuns: 'Run history',
|
|
1425
|
+
consoleTitle: 'Flywheel workbench',
|
|
1426
|
+
wbOverview: 'Task overview',
|
|
1427
|
+
wbProgress: 'Progress',
|
|
1428
|
+
wbSession: 'Sessions',
|
|
1429
|
+
wbConfiguration: 'Run configuration',
|
|
1430
|
+
wbClose: 'Close details',
|
|
1431
|
+
wbCloseDialog: 'Close dialog',
|
|
1432
|
+
wbCanvas: 'Canvas',
|
|
1433
|
+
wbCanvasRegion: 'Workflow canvas',
|
|
1434
|
+
wbList: 'List',
|
|
1435
|
+
wbBack: 'Back to overview',
|
|
1436
|
+
wbExpand: 'Open task workflow',
|
|
1437
|
+
wbExpandGroup: 'Open stages',
|
|
1438
|
+
wbFit: 'Fit width',
|
|
1439
|
+
wbFocus: 'Focus canvas',
|
|
1440
|
+
wbExitFocus: 'Exit focus canvas',
|
|
1441
|
+
wbZoomIn: 'Zoom in',
|
|
1442
|
+
wbZoomOut: 'Zoom out',
|
|
1443
|
+
wbLocate: 'Locate current step',
|
|
1444
|
+
wbBlocked: 'Inspect blocker',
|
|
1445
|
+
wbAggregate: 'Integration and final checks',
|
|
1446
|
+
wbDelivery: 'PR and Merge',
|
|
1447
|
+
wbNoRun: 'Not started',
|
|
1448
|
+
wbReadFirst: 'Read task status to display its workflow',
|
|
1449
|
+
wbStartHelp:
|
|
1450
|
+
'Describe your goal with /flywheel-task in this conversation; the canvas is associated after engine launch',
|
|
1451
|
+
wbProposalHelp: 'Use /flywheel-new to prepare a proposal. Loading a skill does not launch the engine',
|
|
1452
|
+
wbSwitchMode: 'Launch options',
|
|
1453
|
+
wbLoading: 'Reading',
|
|
1454
|
+
wbPending: 'Awaiting operation receipt',
|
|
1455
|
+
wbUnknown: 'Reconcile the previous invocation first',
|
|
1456
|
+
wbRunning: 'Running; stop and confirm cleanup first',
|
|
1457
|
+
wbStale: 'Refresh to reconcile the snapshot',
|
|
1458
|
+
wbHeld: 'Human control is held; validate and return first',
|
|
1459
|
+
wbRevision: 'No valid state revision; refresh first',
|
|
1460
|
+
wbCompleted: 'Reopening a completed task requires a reason',
|
|
1461
|
+
wbL1Required: 'Current deterministic verification must pass first',
|
|
1462
|
+
wbReviewRequired: 'Independent review must pass first',
|
|
1463
|
+
wbPacketsRequired: 'All tasks must be delivered first',
|
|
1464
|
+
wbDependencies: 'Waiting for dependency delivery; dependency IDs are listed in run details',
|
|
1465
|
+
wbRunTask: 'Run this task',
|
|
1466
|
+
wbRetry: 'Retry once',
|
|
1467
|
+
wbReopen: 'Reopen task',
|
|
1468
|
+
wbConfirmStep: 'Confirm node operation',
|
|
1469
|
+
wbConfirm: 'Run',
|
|
1470
|
+
wbCancel: 'Cancel',
|
|
1471
|
+
wbRetryHelp:
|
|
1472
|
+
'Add one Execute/L1 attempt while retaining failures and budget. Other tasks are not automatically rerun',
|
|
1473
|
+
wbStateChanged: 'State changed. Close and select the operation again',
|
|
1474
|
+
wbNoReason: 'Explain why another attempt is needed',
|
|
1475
|
+
wbTakeover: 'Human takeover',
|
|
1476
|
+
wbSnapshot: 'Configuration frozen for this run; default edits apply to the next start',
|
|
1477
|
+
wbExecutorHistory: 'Execution and Review retain separate role histories',
|
|
1478
|
+
wbEvidence: 'Inspect evidence and failures',
|
|
1479
|
+
wbNoEvidence: 'No failure explanation has been recorded',
|
|
1480
|
+
wbFailureCode: 'Failure code',
|
|
1481
|
+
wbCurrentResult: 'Current result',
|
|
1482
|
+
wbSerialReview: 'Execution follows the scheduling policy; Review and integration remain serial',
|
|
1483
|
+
wbOperation: 'Latest operation',
|
|
1484
|
+
wbReadOnly: 'Read-only locations supplied by the engine',
|
|
1485
|
+
wbPaths: 'Locations and source',
|
|
1486
|
+
wbRepository: 'Repository',
|
|
1487
|
+
wbNoSessions: 'No Provider session has been registered for this node',
|
|
1488
|
+
wbTechnical: 'Run identity and detailed evidence',
|
|
1489
|
+
wbPRClosed: 'PR closed',
|
|
1490
|
+
wbPRReady: 'PR created',
|
|
1491
|
+
wbUnobserved: 'Remote not inspected',
|
|
1492
|
+
wbScheduled: 'Execution recorded',
|
|
1493
|
+
wbNotApplicable: 'Not applicable to this workflow',
|
|
1494
|
+
wbGate: 'Deterministic checks',
|
|
1495
|
+
});
|
|
1496
|
+
|
|
1497
|
+
function WorkbenchDialog({ title, children, onClose, actions, t }) {
|
|
1498
|
+
const ref = React.useRef(null);
|
|
1499
|
+
const titleID = React.useId();
|
|
1500
|
+
React.useEffect(() => {
|
|
1501
|
+
const dialog = ref.current,
|
|
1502
|
+
previous = document.activeElement;
|
|
1503
|
+
if (typeof dialog.showModal === 'function') dialog.showModal();
|
|
1504
|
+
else dialog.setAttribute('open', ''); // jsdom / legacy browser fallback; owned subtree only.
|
|
1505
|
+
return () => {
|
|
1506
|
+
if (dialog.open && typeof dialog.close === 'function') dialog.close();
|
|
1507
|
+
if (previous?.isConnected) previous.focus?.();
|
|
1508
|
+
};
|
|
1509
|
+
}, []);
|
|
1510
|
+
return h(
|
|
1511
|
+
'dialog',
|
|
1512
|
+
{
|
|
1513
|
+
ref,
|
|
1514
|
+
className: css.workbenchDialog,
|
|
1515
|
+
'aria-labelledby': titleID,
|
|
1516
|
+
onCancel: (event) => {
|
|
1517
|
+
event.preventDefault();
|
|
1518
|
+
onClose();
|
|
1519
|
+
},
|
|
1520
|
+
onClick: (event) => {
|
|
1521
|
+
if (event.target === ref.current) onClose();
|
|
1522
|
+
},
|
|
1523
|
+
},
|
|
1524
|
+
h(
|
|
1525
|
+
'div',
|
|
1526
|
+
{ className: css.dialogHeader },
|
|
1527
|
+
h('h3', { className: css.dialogTitle, id: titleID }, title),
|
|
1528
|
+
h(Button, { onClick: onClose, 'aria-label': t('wbCloseDialog') }, '×'),
|
|
1529
|
+
),
|
|
1530
|
+
h('div', { className: css.dialogBody }, children),
|
|
1531
|
+
actions && h('div', { className: css.dialogActions }, actions),
|
|
1532
|
+
);
|
|
1533
|
+
}
|
|
1534
|
+
|
|
1535
|
+
function canvasStatus(value, t) {
|
|
1536
|
+
const known = {
|
|
1537
|
+
pass: 'pass',
|
|
1538
|
+
passed: 'pass',
|
|
1539
|
+
failed: 'fail',
|
|
1540
|
+
return: 'reviewFailed',
|
|
1541
|
+
unknown: 'consoleUnconfirmed',
|
|
1542
|
+
closed: 'wbPRClosed',
|
|
1543
|
+
open: 'wbPRReady',
|
|
1544
|
+
na: 'wbNotApplicable',
|
|
1545
|
+
executed: 'executed',
|
|
1546
|
+
running: 'running',
|
|
1547
|
+
unobserved: 'wbUnobserved',
|
|
1548
|
+
scheduled: 'wbScheduled',
|
|
1549
|
+
cancelled: 'consoleOperation_cancelled',
|
|
1550
|
+
idle: 'pending',
|
|
1551
|
+
};
|
|
1552
|
+
return t(taskLabels[value] || packetLabels[value] || known[value] || 'unknown');
|
|
1553
|
+
}
|
|
1554
|
+
|
|
1555
|
+
// Projection only: these labels never grant admission or write state.
|
|
1556
|
+
function packetStages(packet, quick, t) {
|
|
1557
|
+
const evidence = packet?.evidence ?? {};
|
|
1558
|
+
const gate = evidence['final-gate'],
|
|
1559
|
+
injection = evidence['defect-injection'];
|
|
1560
|
+
const verification =
|
|
1561
|
+
evidence.l1?.pass === false ||
|
|
1562
|
+
(quick && (gate?.pass === false || (injection?.outcome && injection.outcome !== 'pass')))
|
|
1563
|
+
? 'failed'
|
|
1564
|
+
: evidence.l1?.pass === true && (!quick || gate?.pass === true)
|
|
1565
|
+
? 'passed'
|
|
1566
|
+
: 'pending';
|
|
1567
|
+
return [
|
|
1568
|
+
[
|
|
1569
|
+
'execute',
|
|
1570
|
+
t('executeRole'),
|
|
1571
|
+
packet?.status === 'executing' ? 'running' : evidence.executor ? 'executed' : 'pending',
|
|
1572
|
+
],
|
|
1573
|
+
['verify', quick ? 'L1 / L2 / L3' : 'L1', verification],
|
|
1574
|
+
['review', t('reviewRole'), evidence.review?.verdict ?? 'pending'],
|
|
1575
|
+
[
|
|
1576
|
+
'integrate',
|
|
1577
|
+
t('consoleDelivery'),
|
|
1578
|
+
packet?.status === 'completed'
|
|
1579
|
+
? 'completed'
|
|
1580
|
+
: packet?.commit?.error || packet?.commit?.['merge-error']
|
|
1581
|
+
? 'failed'
|
|
1582
|
+
: 'pending',
|
|
1583
|
+
],
|
|
1584
|
+
];
|
|
1585
|
+
}
|
|
1586
|
+
|
|
1587
|
+
function canvasModel(control, scope, page, whole, t) {
|
|
1588
|
+
const state = control.status?.state;
|
|
1589
|
+
if (!state) return null;
|
|
1590
|
+
const entries = Object.entries(state.packets ?? {})
|
|
1591
|
+
.filter(([, p]) => record(p))
|
|
1592
|
+
.sort(([a], [b]) => a.localeCompare(b));
|
|
1593
|
+
const pageIndex = Math.min(page, Math.max(0, Math.ceil(entries.length / 6) - 1));
|
|
1594
|
+
const visible = entries.slice(pageIndex * 6, pageIndex * 6 + 6);
|
|
1595
|
+
const quick = state.workflow?.effective === 'quick',
|
|
1596
|
+
single = singleRunKind(control),
|
|
1597
|
+
focused = !!single && !whole;
|
|
1598
|
+
const nodes = [],
|
|
1599
|
+
edges = [];
|
|
1600
|
+
const add = (id, title, status, x, y, packet, stage, group) =>
|
|
1601
|
+
nodes.push({ id, title, status, x, y, packet, stage, group });
|
|
1602
|
+
const milestones = Object.values(state.milestones ?? {}).filter(record);
|
|
1603
|
+
const plan =
|
|
1604
|
+
state.milestones?.[state['current-milestone']]?.plan ??
|
|
1605
|
+
milestones
|
|
1606
|
+
.map((m) => m.plan)
|
|
1607
|
+
.filter(Boolean)
|
|
1608
|
+
.sort((a, b) => (b['started-at'] || '').localeCompare(a['started-at'] || ''))[0];
|
|
1609
|
+
const delivered = entries.length > 0 && entries.every(([, p]) => p.status === 'completed');
|
|
1610
|
+
const closed = milestones.length > 0 && milestones.every((m) => m.status === 'closed');
|
|
1611
|
+
const failedGate = milestones.some(
|
|
1612
|
+
(m) => m.gate?.pass === false || (m.gate?.judge?.verdict && m.gate.judge.verdict !== 'pass'),
|
|
1613
|
+
);
|
|
1614
|
+
const finalStatus = (quick ? delivered : closed) ? 'completed' : failedGate ? 'failed' : 'pending';
|
|
1615
|
+
const remote = control.delivery;
|
|
1616
|
+
const pull =
|
|
1617
|
+
remote?.pulls?.find((p) => p.headRefOid === remote.head && p.state === 'OPEN') ??
|
|
1618
|
+
remote?.pulls?.find((p) => p.headRefOid === remote.head);
|
|
1619
|
+
const remoteStatus = control.deliveryStale
|
|
1620
|
+
? 'unknown'
|
|
1621
|
+
: pull?.state === 'MERGED'
|
|
1622
|
+
? 'completed'
|
|
1623
|
+
: pull?.state === 'OPEN'
|
|
1624
|
+
? 'open'
|
|
1625
|
+
: pull?.state === 'CLOSED'
|
|
1626
|
+
? 'closed'
|
|
1627
|
+
: state.delivery && ['pending', 'unknown'].includes(state.delivery.status)
|
|
1628
|
+
? 'unknown'
|
|
1629
|
+
: 'unobserved';
|
|
1630
|
+
const packetScope = scope?.packet && entries.find(([id]) => id === scope.packet);
|
|
1631
|
+
let height = 420,
|
|
1632
|
+
width = 984;
|
|
1633
|
+
if (focused) {
|
|
1634
|
+
const operationStatus =
|
|
1635
|
+
control.stale || (state.control.status === 'running' && !control.status.running)
|
|
1636
|
+
? 'unknown'
|
|
1637
|
+
: state.control.status;
|
|
1638
|
+
if (['plan', 'final', 'integrate'].includes(single))
|
|
1639
|
+
add(single, stageName(single, t), operationStatus, 24, 110, null, single);
|
|
1640
|
+
else {
|
|
1641
|
+
const units = state.control.packet ? entries.filter(([id]) => id === state.control.packet) : visible;
|
|
1642
|
+
units.forEach(([id, packet], row) => {
|
|
1643
|
+
const stages =
|
|
1644
|
+
single === 'review'
|
|
1645
|
+
? ['verify', 'review']
|
|
1646
|
+
: single === 'execute'
|
|
1647
|
+
? ['execute', 'verify']
|
|
1648
|
+
: ['verify'];
|
|
1649
|
+
stages.forEach((stage, col) => {
|
|
1650
|
+
let status = packetStages(packet, quick, t).find((s) => s[0] === stage)[2];
|
|
1651
|
+
if (stage === single && operationStatus !== 'passed') status = operationStatus;
|
|
1652
|
+
add(
|
|
1653
|
+
id + '/' + stage,
|
|
1654
|
+
id + ' · ' + stageName(stage, t),
|
|
1655
|
+
status,
|
|
1656
|
+
24 + col * 224,
|
|
1657
|
+
48 + row * 148,
|
|
1658
|
+
packet,
|
|
1659
|
+
stage,
|
|
1660
|
+
);
|
|
1661
|
+
if (col) edges.push([id + '/' + stages[col - 1], id + '/' + stage]);
|
|
1662
|
+
});
|
|
1663
|
+
});
|
|
1664
|
+
height = Math.max(height, units.length * 148 + 72);
|
|
1665
|
+
if (!nodes.length) add(single, stageName(single, t), 'unknown', 24, 110);
|
|
1666
|
+
}
|
|
1667
|
+
width = nodes.length > 1 ? 472 : 248;
|
|
1668
|
+
} else if (packetScope || (quick && entries.length === 1 && !scope)) {
|
|
1669
|
+
const [id, packet] = packetScope || entries[0];
|
|
1670
|
+
packetStages(packet, quick, t).forEach(([stage, title, status], col) => {
|
|
1671
|
+
add(id + '/' + stage, title, status, 24 + col * 224, 120, packet, stage);
|
|
1672
|
+
if (col) edges.push([nodes[col - 1].id, id + '/' + stage]);
|
|
1673
|
+
});
|
|
1674
|
+
width = 900;
|
|
1675
|
+
} else if (scope?.group === 'final') {
|
|
1676
|
+
add('aggregate', t('consoleAggregate'), delivered ? 'completed' : 'pending', 24, 120);
|
|
1677
|
+
add('judge', quick ? t('consoleDelivery') : 'L2 / L3 · Judge', finalStatus, 248, 120, null, 'final');
|
|
1678
|
+
edges.push(['aggregate', 'judge']);
|
|
1679
|
+
width = 472;
|
|
1680
|
+
} else if (scope?.group === 'remote') {
|
|
1681
|
+
add('delivery', 'PR', remoteStatus, 24, 120);
|
|
1682
|
+
add(
|
|
1683
|
+
'merge',
|
|
1684
|
+
'Merge',
|
|
1685
|
+
control.deliveryStale ? 'unknown' : pull?.state === 'MERGED' ? 'completed' : 'pending',
|
|
1686
|
+
248,
|
|
1687
|
+
120,
|
|
1688
|
+
);
|
|
1689
|
+
edges.push(['delivery', 'merge']);
|
|
1690
|
+
width = 472;
|
|
1691
|
+
} else {
|
|
1692
|
+
const center = 24 + Math.max(0, visible.length - 1) * 68;
|
|
1693
|
+
add(
|
|
1694
|
+
'plan',
|
|
1695
|
+
t('consolePlanSplit'),
|
|
1696
|
+
quick ? 'na' : (plan?.status ?? 'pending'),
|
|
1697
|
+
24,
|
|
1698
|
+
center,
|
|
1699
|
+
null,
|
|
1700
|
+
quick ? null : 'plan',
|
|
1701
|
+
);
|
|
1702
|
+
add(
|
|
1703
|
+
'schedule',
|
|
1704
|
+
t('consoleSchedule'),
|
|
1705
|
+
control.status.running
|
|
1706
|
+
? 'running'
|
|
1707
|
+
: entries.some(([, packet]) => packet.attempts > 0 || packet.evidence?.executor)
|
|
1708
|
+
? 'scheduled'
|
|
1709
|
+
: 'pending',
|
|
1710
|
+
220,
|
|
1711
|
+
center,
|
|
1712
|
+
);
|
|
1713
|
+
edges.push(['plan', 'schedule']);
|
|
1714
|
+
visible.forEach(([id, packet], row) => {
|
|
1715
|
+
add(id, id, packet.status, 416, 24 + row * 136, packet);
|
|
1716
|
+
edges.push(['schedule', id]);
|
|
1717
|
+
edges.push([id, 'aggregate']);
|
|
1718
|
+
});
|
|
1719
|
+
add('aggregate', t('wbAggregate'), finalStatus, 612, center, null, null, 'final');
|
|
1720
|
+
add('delivery', 'PR', remoteStatus, 808, center);
|
|
1721
|
+
add('merge', 'Merge', control.deliveryStale ? 'unknown' : pull?.state === 'MERGED' ? 'completed' : 'pending', 1004, center);
|
|
1722
|
+
edges.push(['aggregate', 'delivery'], ['delivery', 'merge']);
|
|
1723
|
+
width = 1192;
|
|
1724
|
+
height = Math.max(height, visible.length * 136 + 48);
|
|
1725
|
+
}
|
|
1726
|
+
return {
|
|
1727
|
+
state,
|
|
1728
|
+
entries,
|
|
1729
|
+
visible,
|
|
1730
|
+
pageIndex,
|
|
1731
|
+
nodes,
|
|
1732
|
+
edges,
|
|
1733
|
+
quick,
|
|
1734
|
+
single,
|
|
1735
|
+
focused,
|
|
1736
|
+
scope: packetScope,
|
|
1737
|
+
width,
|
|
1738
|
+
height,
|
|
1739
|
+
};
|
|
1740
|
+
}
|
|
1741
|
+
|
|
1742
|
+
function nodeBinding(node, state, quick, t) {
|
|
1743
|
+
const id = node.packet ? node.id.split('/')[0] : null;
|
|
1744
|
+
const role =
|
|
1745
|
+
node.stage === 'review'
|
|
1746
|
+
? 'review'
|
|
1747
|
+
: node.stage === 'plan'
|
|
1748
|
+
? 'plan'
|
|
1749
|
+
: node.stage === 'final' && !quick
|
|
1750
|
+
? 'judge'
|
|
1751
|
+
: node.packet && (!node.stage || node.stage === 'execute')
|
|
1752
|
+
? 'execute'
|
|
1753
|
+
: null;
|
|
1754
|
+
if (!role) return null;
|
|
1755
|
+
const scope = id || state['current-milestone'];
|
|
1756
|
+
const actual = Object.values(state.sessions?.records ?? {})
|
|
1757
|
+
.filter((s) => s.role === role && (!scope || s.scope === scope || s.scope === 'final:' + scope))
|
|
1758
|
+
.sort((a, b) => (b.last?.['started-at'] || '').localeCompare(a.last?.['started-at'] || ''))[0];
|
|
1759
|
+
if (state.run?.drivers?.[role] === 'mock')
|
|
1760
|
+
return { harness: 'mock', model: t('consoleMockModel'), sessionMode: 'fresh' };
|
|
1761
|
+
return actual
|
|
1762
|
+
? (actual.configuration ?? null)
|
|
1763
|
+
: state.run?.bindings?.[role === 'execute' ? 'routes.' + node.packet.route : 'loops.' + role];
|
|
1764
|
+
}
|
|
1765
|
+
|
|
1766
|
+
function bindingCaption(binding) {
|
|
1767
|
+
const provider = binding.provider || binding.harness;
|
|
1768
|
+
const label =
|
|
1769
|
+
{
|
|
1770
|
+
'deepseek-account': 'DeepSeek',
|
|
1771
|
+
chatgpt: 'ChatGPT',
|
|
1772
|
+
kimi: 'Kimi Code',
|
|
1773
|
+
'codex-pro': 'Codex Pro',
|
|
1774
|
+
'claude-code-pro': 'Claude Code Pro',
|
|
1775
|
+
}[provider] || provider;
|
|
1776
|
+
const model =
|
|
1777
|
+
{ 'deepseek-flash': 'Flash', 'kimi-code/k3.1-pro': 'K3.1 Pro' }[binding.model] || binding.model;
|
|
1778
|
+
return [label, model].filter(Boolean).join(' · ');
|
|
1779
|
+
}
|
|
1780
|
+
|
|
1781
|
+
function stageBlockReason(control, node, state) {
|
|
1782
|
+
if (control.pending) return 'wbPending';
|
|
1783
|
+
if (control.uncertainty) return 'wbUnknown';
|
|
1784
|
+
if (control.stale) return 'wbStale';
|
|
1785
|
+
if (control.status.running) return 'wbRunning';
|
|
1786
|
+
if (state.handoff) return 'wbHeld';
|
|
1787
|
+
if (!Number.isSafeInteger(state.revision) || state.revision < 1) return 'wbRevision';
|
|
1788
|
+
if (
|
|
1789
|
+
node?.packet &&
|
|
1790
|
+
(!node.stage || node.stage === 'execute') &&
|
|
1791
|
+
node.packet.schedule?.after?.some((id) => state.packets?.[id]?.status !== 'completed')
|
|
1792
|
+
)
|
|
1793
|
+
return 'wbDependencies';
|
|
1794
|
+
if (node?.stage === 'review' && node.packet?.evidence?.l1?.pass !== true) return 'wbL1Required';
|
|
1795
|
+
if (node?.stage === 'integrate' && node.packet && node.packet.evidence?.review?.verdict !== 'pass')
|
|
1796
|
+
return 'wbReviewRequired';
|
|
1797
|
+
if (node?.stage === 'final' && Object.values(state.packets ?? {}).some((p) => p.status !== 'completed'))
|
|
1798
|
+
return 'wbPacketsRequired';
|
|
1799
|
+
return null;
|
|
1800
|
+
}
|
|
1801
|
+
|
|
1802
|
+
function ConsoleCanvas({ control, selection, run, t, focusRequest = 0, onInspect }) {
|
|
1803
|
+
const [scope, setScope] = React.useState(null),
|
|
1804
|
+
[selected, setSelected] = React.useState(null);
|
|
1805
|
+
const [page, setPage] = React.useState(0),
|
|
1806
|
+
[whole, setWhole] = React.useState(false),
|
|
1807
|
+
[tab, setTab] = React.useState('progress');
|
|
1808
|
+
const [view, setView] = React.useState('canvas'),
|
|
1809
|
+
[zoom, setZoom] = React.useState(1);
|
|
1810
|
+
const [request, setRequest] = React.useState(null),
|
|
1811
|
+
[reason, setReason] = React.useState('');
|
|
1812
|
+
const [focusMode, setFocusMode] = React.useState(false);
|
|
1813
|
+
const canvasDialog = React.useRef(null);
|
|
1814
|
+
const focusToggle = React.useRef(null),
|
|
1815
|
+
wasFocused = React.useRef(false);
|
|
1816
|
+
const viewport = React.useRef(null),
|
|
1817
|
+
drag = React.useRef(null),
|
|
1818
|
+
lastFocus = React.useRef(0);
|
|
1819
|
+
const model = canvasModel(control, scope, page, whole, t);
|
|
1820
|
+
React.useEffect(() => { onInspect?.(!!selected); return () => onInspect?.(false); }, [selected, onInspect]);
|
|
1821
|
+
const manualView = React.useRef(false);
|
|
1822
|
+
React.useEffect(() => {
|
|
1823
|
+
const container = viewport.current?.parentElement;
|
|
1824
|
+
if (!container) return;
|
|
1825
|
+
const resize = () => {
|
|
1826
|
+
const width = container.clientWidth;
|
|
1827
|
+
if (width > 0 && !manualView.current) setView(width < 520 ? 'list' : 'canvas');
|
|
1828
|
+
if (width > 520 && viewport.current) setZoom(Math.min(1, Math.max(.9, Math.min((viewport.current.clientWidth - 40) / model.width, (viewport.current.clientHeight - 40) / model.height))));
|
|
1829
|
+
};
|
|
1830
|
+
resize();
|
|
1831
|
+
if (typeof ResizeObserver !== 'function') return;
|
|
1832
|
+
const observer = new ResizeObserver(resize);
|
|
1833
|
+
observer.observe(container);
|
|
1834
|
+
return () => observer.disconnect();
|
|
1835
|
+
}, [!!model, focusMode, scope]);
|
|
1836
|
+
React.useEffect(() => {
|
|
1837
|
+
if (!focusMode || !model) {
|
|
1838
|
+
if (wasFocused.current) focusToggle.current?.focus();
|
|
1839
|
+
wasFocused.current = false;
|
|
1840
|
+
return;
|
|
1841
|
+
}
|
|
1842
|
+
wasFocused.current = true;
|
|
1843
|
+
const dialog = canvasDialog.current;
|
|
1844
|
+
if (typeof dialog.showModal === 'function') dialog.showModal();
|
|
1845
|
+
else dialog.setAttribute('open', '');
|
|
1846
|
+
return () => {
|
|
1847
|
+
if (dialog.open && typeof dialog.close === 'function') dialog.close();
|
|
1848
|
+
};
|
|
1849
|
+
}, [focusMode, !!model]);
|
|
1850
|
+
React.useEffect(() => {
|
|
1851
|
+
if (control.uncertainty) setFocusMode(false);
|
|
1852
|
+
}, [control.uncertainty]);
|
|
1853
|
+
const locate = () => {
|
|
1854
|
+
if (!model) return;
|
|
1855
|
+
const { nodes } = model;
|
|
1856
|
+
if (!model.focused) {
|
|
1857
|
+
const index = model.entries.findIndex(([, packet]) => packet.status === 'executing');
|
|
1858
|
+
if (index >= 0 && !nodes.some((node) => node.packet === model.entries[index][1])) {
|
|
1859
|
+
setScope(null);
|
|
1860
|
+
setPage(Math.floor(index / 6));
|
|
1861
|
+
setSelected(model.entries[index][0]);
|
|
1862
|
+
setTab('progress');
|
|
1863
|
+
if (viewport.current) {
|
|
1864
|
+
viewport.current.scrollLeft = 392 * zoom;
|
|
1865
|
+
viewport.current.scrollTop = (index % 6) * 148 * zoom;
|
|
1866
|
+
}
|
|
1867
|
+
return;
|
|
1868
|
+
}
|
|
1869
|
+
}
|
|
1870
|
+
const target =
|
|
1871
|
+
nodes.find((n) => ['running', 'executing'].includes(n.status)) ||
|
|
1872
|
+
nodes.find(
|
|
1873
|
+
(n) =>
|
|
1874
|
+
['failed', 'l1-failed', 'review-failed', 'escalated', 'blocked', 'unknown'].includes(n.status) &&
|
|
1875
|
+
n.packet,
|
|
1876
|
+
) ||
|
|
1877
|
+
nodes.find((n) => n.packet?.status !== 'completed' && n.packet) ||
|
|
1878
|
+
nodes[0];
|
|
1879
|
+
setSelected(target.id);
|
|
1880
|
+
setTab('progress');
|
|
1881
|
+
if (viewport.current) {
|
|
1882
|
+
viewport.current.scrollLeft = Math.max(0, target.x * zoom - 24);
|
|
1883
|
+
viewport.current.scrollTop = Math.max(0, target.y * zoom - 48);
|
|
1884
|
+
}
|
|
1885
|
+
};
|
|
1886
|
+
React.useEffect(() => {
|
|
1887
|
+
if (focusRequest && focusRequest !== lastFocus.current && model) {
|
|
1888
|
+
lastFocus.current = focusRequest;
|
|
1889
|
+
locate();
|
|
1890
|
+
}
|
|
1891
|
+
}, [focusRequest, !!model]);
|
|
1892
|
+
if (!model) return h(InvestigationPanel, { control, selection, run, t });
|
|
1893
|
+
const { nodes, edges, state, quick, single, focused, entries, pageIndex } = model;
|
|
1894
|
+
const chosen = nodes.find((n) => n.id === selected);
|
|
1895
|
+
const selectNode = (node) => {
|
|
1896
|
+
setSelected(node.id);
|
|
1897
|
+
setTab('progress');
|
|
1898
|
+
};
|
|
1899
|
+
const expand = (node) => {
|
|
1900
|
+
setScope(node.packet ? { packet: node.id.split('/')[0] } : { group: node.group });
|
|
1901
|
+
setSelected(null);
|
|
1902
|
+
setZoom(1);
|
|
1903
|
+
};
|
|
1904
|
+
const back = () => {
|
|
1905
|
+
setScope(null);
|
|
1906
|
+
setSelected(null);
|
|
1907
|
+
setZoom(1);
|
|
1908
|
+
};
|
|
1909
|
+
const planRequest = (node, retry = false) => {
|
|
1910
|
+
if (stageBlockReason(control, node, state)) return;
|
|
1911
|
+
const next = { only: node.stage || 'execute', packet: node.packet ? node.id.split('/')[0] : undefined, revision: state.revision, retry, reopen: node.packet?.status === 'completed' };
|
|
1912
|
+
if (next.reopen) { setReason(''); setRequest(next); return; }
|
|
1913
|
+
// The explicit click authorizes exactly one revision-bound step. The audit
|
|
1914
|
+
// reason describes the action, not a fabricated explanation for the failure.
|
|
1915
|
+
void run('step', selection, false, { only: next.only, packet: next.packet, revision: next.revision,
|
|
1916
|
+
...(retry ? { retryReason: 'Manual retry requested from the Flywheel console' } : {}) });
|
|
1917
|
+
};
|
|
1918
|
+
const finishRequest = () => {
|
|
1919
|
+
const next = request;
|
|
1920
|
+
setRequest(null);
|
|
1921
|
+
void run('step', selection, false, {
|
|
1922
|
+
only: next.only,
|
|
1923
|
+
packet: next.packet,
|
|
1924
|
+
revision: next.revision,
|
|
1925
|
+
...(next.retry ? { retryReason: reason.trim() } : {}),
|
|
1926
|
+
});
|
|
1927
|
+
};
|
|
1928
|
+
const nodeButton = (node) => {
|
|
1929
|
+
const binding = nodeBinding(node, state, quick, t);
|
|
1930
|
+
return h(
|
|
1931
|
+
'button',
|
|
1932
|
+
{
|
|
1933
|
+
key: node.id,
|
|
1934
|
+
type: 'button',
|
|
1935
|
+
className: css.canvasNode,
|
|
1936
|
+
'data-node-id': node.id,
|
|
1937
|
+
'data-tone': consoleTone(node.status),
|
|
1938
|
+
'aria-pressed': selected === node.id,
|
|
1939
|
+
style: { '--node-x': node.x + 'px', '--node-y': node.y + 'px' },
|
|
1940
|
+
onClick: () => selectNode(node),
|
|
1941
|
+
onFocus: (event) => {
|
|
1942
|
+
if (event.currentTarget.matches?.(':focus-visible'))
|
|
1943
|
+
event.currentTarget.scrollIntoView?.({ block: 'nearest', inline: 'nearest' });
|
|
1944
|
+
},
|
|
1945
|
+
},
|
|
1946
|
+
h('span', { className: css.nodeState }, canvasStatus(node.status, t)),
|
|
1947
|
+
h('strong', null, node.title),
|
|
1948
|
+
h(
|
|
1949
|
+
'span',
|
|
1950
|
+
{ className: css.nodeProvider },
|
|
1951
|
+
binding
|
|
1952
|
+
? bindingCaption(binding)
|
|
1953
|
+
: node.packet?.route ||
|
|
1954
|
+
(node.stage && ['execute', 'review', 'plan', 'final'].includes(node.stage)
|
|
1955
|
+
? t('consoleUnknown')
|
|
1956
|
+
: 'Go / Git'),
|
|
1957
|
+
),
|
|
1958
|
+
node.packet &&
|
|
1959
|
+
!node.stage &&
|
|
1960
|
+
h(
|
|
1961
|
+
'span',
|
|
1962
|
+
{ className: css.nodeStages },
|
|
1963
|
+
...packetStages(node.packet, quick, t).map(([stage, title, status]) =>
|
|
1964
|
+
h(
|
|
1965
|
+
'span',
|
|
1966
|
+
{
|
|
1967
|
+
key: stage,
|
|
1968
|
+
'data-tone': consoleTone(status),
|
|
1969
|
+
title: title + ' · ' + canvasStatus(status, t),
|
|
1970
|
+
},
|
|
1971
|
+
title,
|
|
1972
|
+
),
|
|
1973
|
+
),
|
|
1974
|
+
),
|
|
1975
|
+
);
|
|
1976
|
+
};
|
|
1977
|
+
return h(
|
|
1978
|
+
'section',
|
|
1979
|
+
{ className: css.consoleCanvas, 'aria-label': t('wbCanvasRegion') },
|
|
1980
|
+
single &&
|
|
1981
|
+
h(
|
|
1982
|
+
'div',
|
|
1983
|
+
{ className: css.stageScope },
|
|
1984
|
+
h(
|
|
1985
|
+
'div',
|
|
1986
|
+
null,
|
|
1987
|
+
h('strong', null, singleRunLabel(control, t)),
|
|
1988
|
+
h(
|
|
1989
|
+
'p',
|
|
1990
|
+
{ className: css.meta },
|
|
1991
|
+
t('stageFullState', {
|
|
1992
|
+
status: t(state.status === 'completed' ? 'stageDelivered' : 'stageUndelivered'),
|
|
1993
|
+
}),
|
|
1994
|
+
),
|
|
1995
|
+
),
|
|
1996
|
+
h(
|
|
1997
|
+
'div',
|
|
1998
|
+
{ className: css.actions },
|
|
1999
|
+
h(
|
|
2000
|
+
Button,
|
|
2001
|
+
{
|
|
2002
|
+
'aria-pressed': !whole,
|
|
2003
|
+
onClick: () => {
|
|
2004
|
+
setWhole(false);
|
|
2005
|
+
setSelected(null);
|
|
2006
|
+
},
|
|
2007
|
+
},
|
|
2008
|
+
t('stageFocus'),
|
|
2009
|
+
),
|
|
2010
|
+
h(
|
|
2011
|
+
Button,
|
|
2012
|
+
{
|
|
2013
|
+
'aria-pressed': whole,
|
|
2014
|
+
onClick: () => {
|
|
2015
|
+
setWhole(true);
|
|
2016
|
+
back();
|
|
2017
|
+
},
|
|
2018
|
+
},
|
|
2019
|
+
t('stageFull'),
|
|
2020
|
+
),
|
|
2021
|
+
),
|
|
2022
|
+
),
|
|
2023
|
+
h(
|
|
2024
|
+
focusMode ? 'dialog' : 'div',
|
|
2025
|
+
{
|
|
2026
|
+
ref: canvasDialog,
|
|
2027
|
+
className: [css.canvasWorkbench, focusMode ? css.focusCanvas : ''].join(' '),
|
|
2028
|
+
'data-inspecting': !!chosen,
|
|
2029
|
+
'aria-label': focusMode ? t('wbFocus') : undefined,
|
|
2030
|
+
onCancel: (event) => {
|
|
2031
|
+
event.preventDefault();
|
|
2032
|
+
setFocusMode(false);
|
|
2033
|
+
},
|
|
2034
|
+
},
|
|
2035
|
+
h(
|
|
2036
|
+
'div',
|
|
2037
|
+
{ className: css.canvasMain },
|
|
2038
|
+
h(
|
|
2039
|
+
'div',
|
|
2040
|
+
{ className: css.canvasHeader },
|
|
2041
|
+
h(
|
|
2042
|
+
'div',
|
|
2043
|
+
{ className: css.canvasBreadcrumb },
|
|
2044
|
+
focusMode &&
|
|
2045
|
+
h(
|
|
2046
|
+
'span',
|
|
2047
|
+
{ className: css.focusIdentity },
|
|
2048
|
+
[state.slug, state.run?.branch].filter(Boolean).join(' · '),
|
|
2049
|
+
),
|
|
2050
|
+
scope && !focused
|
|
2051
|
+
? h(Button, { onClick: back }, t('wbBack'))
|
|
2052
|
+
: h('span', null, t(focused ? 'stageFocus' : 'wbOverview')),
|
|
2053
|
+
h(
|
|
2054
|
+
'span',
|
|
2055
|
+
{ className: css.meta },
|
|
2056
|
+
scope?.packet ||
|
|
2057
|
+
(focused
|
|
2058
|
+
? stageName(single, t)
|
|
2059
|
+
: t(quick ? 'consoleQuick' : 'consoleFull') +
|
|
2060
|
+
' · ' +
|
|
2061
|
+
t(state.scheduler?.mode === 'parallel' ? 'consoleParallel' : 'consoleSerial')),
|
|
2062
|
+
),
|
|
2063
|
+
),
|
|
2064
|
+
h(
|
|
2065
|
+
'div',
|
|
2066
|
+
{ className: css.segmented },
|
|
2067
|
+
...['canvas', 'list'].map((v) =>
|
|
2068
|
+
h(
|
|
2069
|
+
Button,
|
|
2070
|
+
{
|
|
2071
|
+
key: v,
|
|
2072
|
+
'aria-pressed': view === v,
|
|
2073
|
+
onClick: () => {
|
|
2074
|
+
manualView.current = true;
|
|
2075
|
+
setView(v);
|
|
2076
|
+
},
|
|
2077
|
+
},
|
|
2078
|
+
t(v === 'canvas' ? 'wbCanvas' : 'wbList'),
|
|
2079
|
+
),
|
|
2080
|
+
),
|
|
2081
|
+
),
|
|
2082
|
+
),
|
|
2083
|
+
h(
|
|
2084
|
+
'div',
|
|
2085
|
+
{
|
|
2086
|
+
className: css.canvasViewport,
|
|
2087
|
+
ref: viewport,
|
|
2088
|
+
hidden: view !== 'canvas',
|
|
2089
|
+
'aria-label': t('wbCanvas'),
|
|
2090
|
+
onPointerDown: (event) => {
|
|
2091
|
+
if (event.pointerType !== 'mouse' || event.button !== 0 || event.target.closest('button'))
|
|
2092
|
+
return;
|
|
2093
|
+
drag.current = {
|
|
2094
|
+
x: event.clientX,
|
|
2095
|
+
y: event.clientY,
|
|
2096
|
+
left: viewport.current.scrollLeft,
|
|
2097
|
+
top: viewport.current.scrollTop,
|
|
2098
|
+
};
|
|
2099
|
+
event.currentTarget.setPointerCapture?.(event.pointerId);
|
|
2100
|
+
},
|
|
2101
|
+
onPointerMove: (event) => {
|
|
2102
|
+
if (drag.current) {
|
|
2103
|
+
viewport.current.scrollLeft = drag.current.left - event.clientX + drag.current.x;
|
|
2104
|
+
viewport.current.scrollTop = drag.current.top - event.clientY + drag.current.y;
|
|
2105
|
+
}
|
|
2106
|
+
},
|
|
2107
|
+
onPointerUp: () => {
|
|
2108
|
+
drag.current = null;
|
|
2109
|
+
},
|
|
2110
|
+
onPointerCancel: () => {
|
|
2111
|
+
drag.current = null;
|
|
2112
|
+
},
|
|
2113
|
+
onLostPointerCapture: () => {
|
|
2114
|
+
drag.current = null;
|
|
2115
|
+
},
|
|
2116
|
+
},
|
|
2117
|
+
h(
|
|
2118
|
+
'div',
|
|
2119
|
+
{
|
|
2120
|
+
className: css.canvasScaleBounds,
|
|
2121
|
+
style: {
|
|
2122
|
+
'--scaled-width': model.width * zoom + 'px',
|
|
2123
|
+
'--scaled-height': model.height * zoom + 'px',
|
|
2124
|
+
},
|
|
2125
|
+
},
|
|
2126
|
+
h(
|
|
2127
|
+
'div',
|
|
2128
|
+
{
|
|
2129
|
+
className: css.canvasWorld,
|
|
2130
|
+
style: {
|
|
2131
|
+
'--canvas-width': model.width + 'px',
|
|
2132
|
+
'--canvas-height': model.height + 'px',
|
|
2133
|
+
'--canvas-scale': zoom,
|
|
2134
|
+
},
|
|
2135
|
+
},
|
|
2136
|
+
h(
|
|
2137
|
+
'svg',
|
|
2138
|
+
{ className: css.canvasEdges, width: model.width, height: model.height, 'aria-hidden': true },
|
|
2139
|
+
...edges.map(([from, to]) => {
|
|
2140
|
+
const a = nodes.find((n) => n.id === from),
|
|
2141
|
+
b = nodes.find((n) => n.id === to),
|
|
2142
|
+
mid = (a.x + 164 + b.x) / 2;
|
|
2143
|
+
return h('path', {
|
|
2144
|
+
key: from + to,
|
|
2145
|
+
d: `M${a.x + 164} ${a.y + 58} C${mid} ${a.y + 58} ${mid} ${b.y + 58} ${b.x} ${b.y + 58}`,
|
|
2146
|
+
});
|
|
2147
|
+
}),
|
|
2148
|
+
),
|
|
2149
|
+
...nodes.map(nodeButton),
|
|
2150
|
+
),
|
|
2151
|
+
),
|
|
2152
|
+
),
|
|
2153
|
+
view === 'list' &&
|
|
2154
|
+
h(
|
|
2155
|
+
'div',
|
|
2156
|
+
{ className: css.canvasList },
|
|
2157
|
+
...nodes.map((node) =>
|
|
2158
|
+
h(
|
|
2159
|
+
'button',
|
|
2160
|
+
{
|
|
2161
|
+
type: 'button',
|
|
2162
|
+
key: node.id,
|
|
2163
|
+
className: css.canvasListNode,
|
|
2164
|
+
'data-tone': consoleTone(node.status),
|
|
2165
|
+
onClick: () => selectNode(node),
|
|
2166
|
+
},
|
|
2167
|
+
h('strong', null, node.title),
|
|
2168
|
+
h('span', null, canvasStatus(node.status, t)),
|
|
2169
|
+
),
|
|
2170
|
+
),
|
|
2171
|
+
),
|
|
2172
|
+
h(
|
|
2173
|
+
'div',
|
|
2174
|
+
{ className: css.canvasFooter },
|
|
2175
|
+
h(
|
|
2176
|
+
'div',
|
|
2177
|
+
{ className: css.canvasLegend },
|
|
2178
|
+
...['completed', 'running', 'failed', 'pending'].map((status) =>
|
|
2179
|
+
h('span', { key: status, 'data-tone': consoleTone(status) }, canvasStatus(status, t)),
|
|
2180
|
+
),
|
|
2181
|
+
),
|
|
2182
|
+
h(
|
|
2183
|
+
'div',
|
|
2184
|
+
{ className: css.actions },
|
|
2185
|
+
h(Button, { onClick: locate }, t('wbLocate')),
|
|
2186
|
+
view === 'canvas' &&
|
|
2187
|
+
h(
|
|
2188
|
+
React.Fragment,
|
|
2189
|
+
null,
|
|
2190
|
+
h(
|
|
2191
|
+
Button,
|
|
2192
|
+
{
|
|
2193
|
+
'aria-label': t('wbZoomOut'),
|
|
2194
|
+
disabled: zoom <= 0.85,
|
|
2195
|
+
onClick: () => setZoom(Math.max(0.85, zoom - 0.1)),
|
|
2196
|
+
},
|
|
2197
|
+
'−',
|
|
2198
|
+
),
|
|
2199
|
+
h('span', { className: css.meta }, Math.round(zoom * 100) + '%'),
|
|
2200
|
+
h(
|
|
2201
|
+
Button,
|
|
2202
|
+
{
|
|
2203
|
+
'aria-label': t('wbZoomIn'),
|
|
2204
|
+
disabled: zoom >= 1.5,
|
|
2205
|
+
onClick: () => setZoom(Math.min(1.5, zoom + 0.1)),
|
|
2206
|
+
},
|
|
2207
|
+
'+',
|
|
2208
|
+
),
|
|
2209
|
+
h(
|
|
2210
|
+
Button,
|
|
2211
|
+
{
|
|
2212
|
+
onClick: () =>
|
|
2213
|
+
setZoom(
|
|
2214
|
+
Math.max(
|
|
2215
|
+
0.85,
|
|
2216
|
+
Math.min(1, ((viewport.current?.clientWidth || model.width) - 24) / model.width),
|
|
2217
|
+
),
|
|
2218
|
+
),
|
|
2219
|
+
},
|
|
2220
|
+
t('wbFit'),
|
|
2221
|
+
),
|
|
2222
|
+
),
|
|
2223
|
+
h(
|
|
2224
|
+
Button,
|
|
2225
|
+
{ ref: focusToggle, onClick: () => setFocusMode((value) => !value) },
|
|
2226
|
+
t(focusMode ? 'wbExitFocus' : 'wbFocus'),
|
|
2227
|
+
),
|
|
2228
|
+
),
|
|
2229
|
+
),
|
|
2230
|
+
!scope &&
|
|
2231
|
+
!focused &&
|
|
2232
|
+
entries.length > 6 &&
|
|
2233
|
+
h(
|
|
2234
|
+
'div',
|
|
2235
|
+
{ className: css.canvasPagination },
|
|
2236
|
+
h(
|
|
2237
|
+
Button,
|
|
2238
|
+
{
|
|
2239
|
+
disabled: pageIndex === 0,
|
|
2240
|
+
onClick: () => {
|
|
2241
|
+
setPage(pageIndex - 1);
|
|
2242
|
+
setSelected(null);
|
|
2243
|
+
},
|
|
2244
|
+
},
|
|
2245
|
+
t('consolePrevious'),
|
|
2246
|
+
),
|
|
2247
|
+
h(
|
|
2248
|
+
'span',
|
|
2249
|
+
{ className: css.meta },
|
|
2250
|
+
t('consolePage', {
|
|
2251
|
+
from: pageIndex * 6 + 1,
|
|
2252
|
+
to: Math.min((pageIndex + 1) * 6, entries.length),
|
|
2253
|
+
total: entries.length,
|
|
2254
|
+
}),
|
|
2255
|
+
),
|
|
2256
|
+
h(
|
|
2257
|
+
Button,
|
|
2258
|
+
{
|
|
2259
|
+
disabled: (pageIndex + 1) * 6 >= entries.length,
|
|
2260
|
+
onClick: () => {
|
|
2261
|
+
setPage(pageIndex + 1);
|
|
2262
|
+
setSelected(null);
|
|
2263
|
+
},
|
|
2264
|
+
},
|
|
2265
|
+
t('consoleMore'),
|
|
2266
|
+
),
|
|
2267
|
+
),
|
|
2268
|
+
),
|
|
2269
|
+
chosen &&
|
|
2270
|
+
h(WorkbenchNodeInspector, {
|
|
2271
|
+
chosen,
|
|
2272
|
+
model,
|
|
2273
|
+
control,
|
|
2274
|
+
selection,
|
|
2275
|
+
run,
|
|
2276
|
+
t,
|
|
2277
|
+
tab,
|
|
2278
|
+
setTab,
|
|
2279
|
+
onClose: () => setSelected(null),
|
|
2280
|
+
expand,
|
|
2281
|
+
planRequest,
|
|
2282
|
+
}),
|
|
2283
|
+
),
|
|
2284
|
+
request &&
|
|
2285
|
+
h(
|
|
2286
|
+
WorkbenchDialog,
|
|
2287
|
+
{
|
|
2288
|
+
t,
|
|
2289
|
+
title: t(request.retry ? 'wbRetry' : 'wbConfirmStep'),
|
|
2290
|
+
onClose: () => setRequest(null),
|
|
2291
|
+
actions: h(
|
|
2292
|
+
React.Fragment,
|
|
2293
|
+
null,
|
|
2294
|
+
h(Button, { onClick: () => setRequest(null) }, t('wbCancel')),
|
|
2295
|
+
h(
|
|
2296
|
+
Button,
|
|
2297
|
+
{
|
|
2298
|
+
variant: 'primary',
|
|
2299
|
+
disabled:
|
|
2300
|
+
!!stageBlockReason(control, null, state) ||
|
|
2301
|
+
state.revision !== request.revision ||
|
|
2302
|
+
(request.retry && !reason.trim()),
|
|
2303
|
+
onClick: finishRequest,
|
|
2304
|
+
},
|
|
2305
|
+
t('wbConfirm'),
|
|
2306
|
+
),
|
|
2307
|
+
),
|
|
2308
|
+
},
|
|
2309
|
+
h('p', null, (request.packet ? request.packet + ' · ' : '') + stageName(request.only, t)),
|
|
2310
|
+
h(
|
|
2311
|
+
'p',
|
|
2312
|
+
{ className: css.meta },
|
|
2313
|
+
t(request.reopen ? 'consoleReopenHelp' : request.retry ? 'wbRetryHelp' : 'consoleStageHelp'),
|
|
2314
|
+
),
|
|
2315
|
+
request.retry &&
|
|
2316
|
+
h(
|
|
2317
|
+
'label',
|
|
2318
|
+
{ className: css.editorField },
|
|
2319
|
+
t('consoleManualReason'),
|
|
2320
|
+
h('textarea', {
|
|
2321
|
+
className: css.reasonInput,
|
|
2322
|
+
value: reason,
|
|
2323
|
+
maxLength: 300,
|
|
2324
|
+
onChange: (e) => setReason(e.target.value),
|
|
2325
|
+
}),
|
|
2326
|
+
),
|
|
2327
|
+
state.revision !== request.revision && h('p', { role: 'alert' }, t('wbStateChanged')),
|
|
2328
|
+
),
|
|
2329
|
+
);
|
|
2330
|
+
}
|
|
2331
|
+
|
|
2332
|
+
function WorkbenchNodeInspector({
|
|
2333
|
+
chosen,
|
|
2334
|
+
model,
|
|
2335
|
+
control,
|
|
2336
|
+
selection,
|
|
2337
|
+
run,
|
|
2338
|
+
t,
|
|
2339
|
+
tab,
|
|
2340
|
+
setTab,
|
|
2341
|
+
onClose,
|
|
2342
|
+
expand,
|
|
2343
|
+
planRequest,
|
|
2344
|
+
}) {
|
|
2345
|
+
const { state, quick } = model,
|
|
2346
|
+
packetID = chosen.packet ? chosen.id.split('/')[0] : undefined;
|
|
2347
|
+
const packet =
|
|
2348
|
+
packetID && control.node?.id === packetID && control.node.revision === state.revision
|
|
2349
|
+
? control.node.packet
|
|
2350
|
+
: chosen.packet;
|
|
2351
|
+
const binding = nodeBinding(chosen, state, quick, t),
|
|
2352
|
+
blocked = stageBlockReason(control, chosen, state);
|
|
2353
|
+
const sessions = Object.entries(state.sessions?.records ?? {})
|
|
2354
|
+
.filter(([, s]) => record(s))
|
|
2355
|
+
.filter(([, s]) =>
|
|
2356
|
+
packetID
|
|
2357
|
+
? s.scope === packetID || s.scope === 'final:' + packetID
|
|
2358
|
+
: chosen.stage === 'plan'
|
|
2359
|
+
? s.role === 'plan'
|
|
2360
|
+
: chosen.stage === 'final'
|
|
2361
|
+
? s.role === 'judge'
|
|
2362
|
+
: false,
|
|
2363
|
+
);
|
|
2364
|
+
const primaryStage = chosen.stage || (chosen.packet ? 'execute' : null);
|
|
2365
|
+
const retry =
|
|
2366
|
+
primaryStage === 'execute' &&
|
|
2367
|
+
(packet?.status === 'completed' ||
|
|
2368
|
+
['l1-failed', 'review-failed', 'escalated', 'blocked', 'failed'].includes(packet?.status));
|
|
2369
|
+
const property = (label, value) =>
|
|
2370
|
+
h(React.Fragment, { key: label }, h('dt', null, label), h('dd', null, textValue(value)));
|
|
2371
|
+
const evidence = {
|
|
2372
|
+
milestones: ['judge', 'plan', 'final'].includes(chosen.id) ? state.milestones : undefined,
|
|
2373
|
+
evidence: packet?.evidence,
|
|
2374
|
+
feedback: packet?.['last-feedback'],
|
|
2375
|
+
retries: packet?.['retry-history'],
|
|
2376
|
+
history: packet?.['evidence-history'],
|
|
2377
|
+
commit: packet?.commit,
|
|
2378
|
+
};
|
|
2379
|
+
const action = primaryStage && h(Button, {
|
|
2380
|
+
variant: 'primary', disabled: !!blocked || (packet?.status === 'completed' && !retry),
|
|
2381
|
+
onClick: () => planRequest({ ...chosen, stage: primaryStage }, retry),
|
|
2382
|
+
}, t(retry ? packet.status === 'completed' ? 'wbReopen' : 'wbRetry' : chosen.stage ? 'consoleStageRun' : 'wbRunTask'));
|
|
2383
|
+
const feedback = typeof packet?.['last-feedback'] === 'string' ? packet['last-feedback'] : packet?.['last-feedback']?.reason || packet?.['last-feedback']?.['parallel-error'] || packet?.commit?.error;
|
|
2384
|
+
return h('section', { className: css.nodeInspector, 'aria-label': t('consoleDetails') },
|
|
2385
|
+
h('header', { className: css.inspectorHeader }, h('div', null, packetID && h('span', { className: css.meta }, packetID), h('h3', { className: css.heading }, chosen.title)), h(Button, { onClick: onClose, 'aria-label': t('wbClose') }, '×')),
|
|
2386
|
+
h('div', { className: css.inspectorBody },
|
|
2387
|
+
tab !== 'progress' && h(Button, { onClick: () => setTab('progress') }, t('wbBackToNode')),
|
|
2388
|
+
tab === 'progress' && h(React.Fragment, null,
|
|
2389
|
+
h('p', { className: css.nodeOutcome, 'data-tone': consoleTone(chosen.status) }, canvasStatus(chosen.status, t)),
|
|
2390
|
+
feedback && h('p', { className: css.nodeIssue }, textValue(feedback).slice(0, 800)),
|
|
2391
|
+
h('div', { className: css.inspectorActions }, action, sessions.length > 0 && h(Button, { onClick: () => setTab('session') }, t('wbTakeover'))),
|
|
2392
|
+
blocked && h('p', { className: css.settingsHelp }, t(blocked)),
|
|
2393
|
+
chosen.packet && !chosen.stage && h('ol', { className: css.stageTimeline }, ...packetStages(packet, quick, t).map(([stage, title, status]) => h('li', { key: stage, 'data-tone': consoleTone(status) }, h('span', null, title), h('span', { className: css.meta }, canvasStatus(status, t))))),
|
|
2394
|
+
((chosen.packet && !chosen.stage) || chosen.group) && h(Button, { onClick: () => expand(chosen) }, t(chosen.packet ? 'wbExpand' : 'wbExpandGroup')),
|
|
2395
|
+
['delivery', 'merge'].includes(chosen.id) && h(DeliveryPanel, { control, selection, run, t }),
|
|
2396
|
+
binding && h('section', { className: css.inspectorSection }, h('h4', null, t('wbConfiguration')),
|
|
2397
|
+
h('dl', { className: css.consoleProperties }, property(t('consoleProvider'), bindingCaption({ ...binding, model: '' })), property(t('consoleModel'), binding.model), property(t('consoleThinking'), ({off:'cfgEffortOff',minimal:'cfgEffortMinimal',low:'cfgEffortLow',medium:'cfgEffortMedium',high:'cfgEffortHigh',xhigh:'cfgEffortXhigh',max:'cfgEffortMax'})[binding.thinking] ? t(({off:'cfgEffortOff',minimal:'cfgEffortMinimal',low:'cfgEffortLow',medium:'cfgEffortMedium',high:'cfgEffortHigh',xhigh:'cfgEffortXhigh',max:'cfgEffortMax'})[binding.thinking]) : binding.thinking), property(t('permission'), permissionLabels[binding.permission] ? t(permissionLabels[binding.permission]) : binding.permission))),
|
|
2398
|
+
h('section', { className: css.inspectorSection }, h('h4', null, t('wbSession')),
|
|
2399
|
+
sessions.length ? sessions.map(([key, value]) => h('article', { key, className: css.sessionSummary },
|
|
2400
|
+
h('h4', null, (roleLabels[value.role] ? t(roleLabels[value.role]) : value.role) + ' · ' + value.scope),
|
|
2401
|
+
h('dl', { className: css.consoleProperties },
|
|
2402
|
+
property(t('consoleAction'), ['pending', 'intent', 'blocked'].includes(value.last?.status) || ['unknown', 'blocked'].includes(value.last?.outcome) ? t('consoleUnconfirmed') : sessionActionLabel(value.action, t)),
|
|
2403
|
+
property('Session', value['thread-id']), value.reason && property(t('consoleReason'), value.reason)))) : h('p', { className: css.meta }, t('wbNoSessions'))),
|
|
2404
|
+
chosen.stage === 'review' && packet?.evidence?.review && h('dl', { className: css.consoleProperties }, property(t('stageRecordedVerdict'), packet.evidence.review.verdict), property(t('stageRecordedScore'), packet.evidence.review.score)),
|
|
2405
|
+
packet && h('dl', { className: css.consoleProperties }, property(t('consoleAttempts'), packet.attempts), property(t('consoleFailures'), packet['failed-attempts'])),
|
|
2406
|
+
h('section', { className: css.inspectorSection }, h('h4', null, t('stageMetadata')),
|
|
2407
|
+
h('dl', { className: css.consoleProperties }, property('Run', state.run?.id), property(t('executionHome'), packet?.worktree?.path ?? control.status.cwd), property(t('consoleBranch'), packet?.worktree?.branch || state.run?.branch))),
|
|
2408
|
+
packetID && h(RuntimeDecisionPanel, { ledger: state['runtime-decisions'], packetID, t }),
|
|
2409
|
+
packetID && h(ExecutionPolicyChanges, { packets: { [packetID]: packet }, packetID, t }),
|
|
2410
|
+
h(Button, { onClick: () => setTab('evidence') }, t('wbEvidence'))),
|
|
2411
|
+
tab === 'session' && h(React.Fragment, null, ...sessions.map(([key, value]) => h(SessionInspector, { key, sessionKey: key, value, control, selection, run, t }))),
|
|
2412
|
+
tab === 'evidence' && h(React.Fragment, null,
|
|
2413
|
+
packetID && h(Button, { disabled: !!control.pending, onClick: () => run('node', selection, false, { packet: packetID }) }, t('consoleFullNode')),
|
|
2414
|
+
h('h4', { className: css.heading }, t('wbEvidence')), h('pre', { className: css.raw }, JSON.stringify(evidence, null, 2)),
|
|
2415
|
+
h('dl', { className: css.consoleProperties }, property('HEAD', state.run?.head), property(t('consoleTask'), control.status.taskDir), property(t('projectHome'), state.project?.['workspace-cwd']), property(t('consoleRevision'), state.revision), property('after', packet?.schedule?.after?.join(', ')), property('writes', packet?.schedule?.writes?.join(', '))))));
|
|
2416
|
+
}
|
|
2417
|
+
|
|
2418
|
+
function TaskView({
|
|
2419
|
+
hostSessionID,
|
|
2420
|
+
useStore,
|
|
2421
|
+
useProjection,
|
|
2422
|
+
actions,
|
|
2423
|
+
useControl,
|
|
2424
|
+
changeSelection,
|
|
2425
|
+
associate,
|
|
2426
|
+
run,
|
|
2427
|
+
discover,
|
|
2428
|
+
reconcile,
|
|
2429
|
+
t,
|
|
2430
|
+
}) {
|
|
2431
|
+
const selection = useStore((value) => value),
|
|
2432
|
+
control = useControl((value) => value),
|
|
2433
|
+
association = useProjection('flywheelTask');
|
|
2434
|
+
const [page, setPage] = React.useState('canvas'),
|
|
2435
|
+
[focusRequest, setFocusRequest] = React.useState(0);
|
|
2436
|
+
const [inspecting, setInspecting] = React.useState(false);
|
|
2437
|
+
const configDrafts = React.useRef(new Map());
|
|
2438
|
+
React.useEffect(() => {
|
|
2439
|
+
associate(association);
|
|
2440
|
+
}, [association, associate]);
|
|
2441
|
+
React.useEffect(() => {
|
|
2442
|
+
if (page !== 'canvas') return;
|
|
2443
|
+
let live = true,
|
|
2444
|
+
timer;
|
|
2445
|
+
void discover();
|
|
2446
|
+
const refresh = async () => {
|
|
2447
|
+
if (!document.hidden) await discover();
|
|
2448
|
+
if (live) timer = setTimeout(refresh, 5000);
|
|
2449
|
+
};
|
|
2450
|
+
timer = setTimeout(refresh, 5000);
|
|
2451
|
+
return () => {
|
|
2452
|
+
live = false;
|
|
2453
|
+
clearTimeout(timer);
|
|
2454
|
+
};
|
|
2455
|
+
}, [page, discover]);
|
|
2456
|
+
React.useEffect(() => {
|
|
2457
|
+
if (page !== 'canvas' || !control.status?.running || !selection.taskDir) return;
|
|
2458
|
+
let live = true,
|
|
2459
|
+
timer;
|
|
2460
|
+
const refresh = async () => {
|
|
2461
|
+
let value;
|
|
2462
|
+
if (!document.hidden) value = await run('status', selection);
|
|
2463
|
+
if (live) timer = setTimeout(refresh, value ? 2000 : 10000);
|
|
2464
|
+
};
|
|
2465
|
+
timer = setTimeout(refresh, 2000);
|
|
2466
|
+
return () => {
|
|
2467
|
+
live = false;
|
|
2468
|
+
clearTimeout(timer);
|
|
2469
|
+
};
|
|
2470
|
+
}, [page, control.status?.running, selection.taskDir, selection.cwd, run]);
|
|
2471
|
+
const busy = control.pending !== null,
|
|
2472
|
+
locked = busy && mutates(control.pending.action),
|
|
2473
|
+
state = control.status?.state,
|
|
2474
|
+
preparation = control.status?.preparation;
|
|
2475
|
+
const invalid = !!pathProblem(selection.taskDir) || !!pathProblem(selection.cwd),
|
|
2476
|
+
hasTask = !!selection.taskDir.trim();
|
|
2477
|
+
const button = (action, label, needsTask, probe = false, variant = 'outline') =>
|
|
2478
|
+
h(
|
|
2479
|
+
Button,
|
|
2480
|
+
{
|
|
2481
|
+
variant,
|
|
2482
|
+
disabled:
|
|
2483
|
+
busy ||
|
|
2484
|
+
invalid ||
|
|
2485
|
+
(needsTask && !hasTask) ||
|
|
2486
|
+
(mutates(action) && !!control.uncertainty) ||
|
|
2487
|
+
(['launch', 'start'].includes(action) && control.stale) ||
|
|
2488
|
+
(['launch', 'start'].includes(action) && !!state?.handoff),
|
|
2489
|
+
onClick: () => run(action, selection, probe),
|
|
2490
|
+
},
|
|
2491
|
+
t(label),
|
|
2492
|
+
);
|
|
2493
|
+
const blocked = Object.values(state?.packets ?? {}).some((p) =>
|
|
2494
|
+
['l1-failed', 'review-failed', 'escalated', 'blocked'].includes(p.status),
|
|
2495
|
+
);
|
|
2496
|
+
const diagnostics = h('section', { className: css.advanced, 'aria-label': t('consoleDiagnostics') },
|
|
2497
|
+
h('h3', { className: css.heading }, t('consoleDiagnostics')),
|
|
2498
|
+
h('div', { className: css.actions }, button('config', 'config', false), button('doctor', 'doctor', false), button('doctor', 'probe', false, true)),
|
|
2499
|
+
h('p', { className: css.settingsHelp }, t('inferenceBoundary')),
|
|
2500
|
+
h(Diagnostic, { value: control.diagnostic, t }), h(Configuration, { value: control.configuration, t }),
|
|
2501
|
+
control.raw && h('section', { className: css.details }, h('h4', null, t('raw')), h('pre', { className: css.raw }, JSON.stringify(control.raw, null, 2))));
|
|
2502
|
+
return h('section', { className: css.page, 'aria-label': t('consoleTitle'), 'data-page': page },
|
|
2503
|
+
h('style', { 'data-plugin': '@ckanner/flywheel', 'data-plugin-css': '@ckanner/flywheel/client.module.css' }, stylesheet),
|
|
2504
|
+
h('header', { className: css.workbenchHeader },
|
|
2505
|
+
h(TaskChooser, { control, selection, hostSessionID, association, changeSelection, run, discover, t }),
|
|
2506
|
+
h('div', { className: css.repositoryLinks },
|
|
2507
|
+
state?.project?.url && h('a', { href: state.project.url, target: '_blank', rel: 'noopener noreferrer' }, state.project.repo),
|
|
2508
|
+
h('a', { href: 'https://github.com/ckanner/flywheel', target: '_blank', rel: 'noopener noreferrer' }, 'Flywheel · GitHub ↗'),
|
|
2509
|
+
page !== 'config' && h(Button, { onClick: () => setPage('config') }, t('consoleConfig')))),
|
|
2510
|
+
h('div', { className: css.workbenchNav, hidden: page === 'config' },
|
|
2511
|
+
h('nav', { className: css.consoleTabs, 'aria-label': t('view') },
|
|
2512
|
+
...['canvas', 'runs'].map(key => h(Button, { key, 'aria-pressed': page === key, onClick: () => setPage(key) }, t(key === 'canvas' ? 'consoleCanvas' : 'consoleRuns')))),
|
|
2513
|
+
page === 'canvas' && hasTask && !preparation && h('div', { className: css.workbenchStatus },
|
|
2514
|
+
state && h('span', { 'data-tone': consoleTone(control.stale ? 'unknown' : control.status.running ? 'running' : state.status) }, singleRunKind(control) ? singleRunLabel(control, t) : taskLabel(control.status, t)),
|
|
2515
|
+
button('status', 'status', true),
|
|
2516
|
+
blocked && !inspecting && !control.status?.running && h(Button, { variant: 'primary', disabled: busy, onClick: () => setFocusRequest(v => v + 1) }, t('wbBlocked')),
|
|
2517
|
+
!control.status?.running && state?.status !== 'completed' && (state || !control.status) && !(blocked && inspecting) && button('start', singleRunKind(control) ? 'consoleWhole' : 'start', true, false, blocked ? 'outline' : 'primary'),
|
|
2518
|
+
(control.status?.running || state?.status === 'running' || locked || control.uncertainty) && button('stop', 'stop', true))),
|
|
2519
|
+
(control.error || control.uncertainty || control.status?.workspaceWarnings?.length || control.pending && !(control.pending.action === 'status' && control.status)) &&
|
|
2520
|
+
!(page === 'config' && (control.pending?.action === 'config-edit' || control.error?.action === 'config-edit') && !control.uncertainty) && h(Feedback, { control, selection, reconcile, t }),
|
|
2521
|
+
h('div', { className: css.workflowPage, hidden: page !== 'canvas' },
|
|
2522
|
+
state?.['shared-execution'] && h('section', { className: css.notice, role: 'status' }, h('strong', null, t('sharedBatch')), h('p', null, state['shared-execution'].packets.join(' · ')), state['shared-execution']['last-error'] && h('p', { role: 'alert' }, state['shared-execution']['last-error'])),
|
|
2523
|
+
hasTask && control.status && !state && !preparation && !busy && !control.uncertainty && h('div', { className: css.launchOptionsBody },
|
|
2524
|
+
h('label', { className: css.editorField }, t('launchMode'), h('select', { className: css.modeSelect, value: selection.mode, onChange: e => actions.setMode(e.target.value) }, ...['auto','quick','full'].map(mode => h('option', { key: mode, value: mode }, mode === 'auto' ? 'Auto' : mode === 'quick' ? 'ultraWork / Quick' : 'Full')))),
|
|
2525
|
+
button('launch', 'launch', true, false, 'primary')),
|
|
2526
|
+
preparation && h(InvestigationPanel, { control, selection, run, t }),
|
|
2527
|
+
state && h(RuntimeDecisionPanel, { ledger: state['runtime-decisions'], t }),
|
|
2528
|
+
state && h(ExecutionPolicyChanges, { packets: state.packets, t }),
|
|
2529
|
+
!state && !preparation && h('div', { className: css.workbenchEmpty }, h('h2', { className: css.title }, t(hasTask ? 'wbReadFirst' : 'taskNoSelection')), h('p', null, t(hasTask ? 'wbStartHelp' : 'taskEmptyHelp')), !hasTask && h('code', null, '/flywheel-task <goal>')),
|
|
2530
|
+
!preparation && h(ConsoleCanvas, { key: selectionKey(selectionOf(selection)), control, selection, run, t, focusRequest, onInspect: setInspecting })),
|
|
2531
|
+
h('div', { className: css.settingsPage, hidden: page !== 'config' },
|
|
2532
|
+
h(ConfigEditor, { key: selectionKey(selectionOf(selection)), contextKey: selectionKey(selectionOf(selection)), cache: configDrafts.current, control, selection, run, t, active: page === 'config', onClose: () => setPage('canvas'), diagnostics })),
|
|
2533
|
+
h('div', { className: css.historyPage, hidden: page !== 'runs' },
|
|
2534
|
+
h(RunExplorer, { control, selection, run, changeSelection, showCanvas: () => setPage('canvas'), t, active: page === 'runs' }),
|
|
2535
|
+
hasTask && !preparation && h('section', { className: css.legacySummary }, h(TaskStatus, { control, selection, t }))),
|
|
2536
|
+
page !== 'config' && h(TaskLocations, { control, selection, association, t }));
|
|
2537
|
+
}
|
|
2538
|
+
|
|
2539
|
+
Object.assign(zh, { consoleDiagnostics: '环境检查' });
|
|
2540
|
+
Object.assign(en, { consoleDiagnostics: 'Environment checks' });
|
|
2541
|
+
Object.assign(zh, { wbBackToNode: '← 返回节点', wbTakeover: '进入会话', wbRetry: '修复并重试', wbBlocked: '处理失败任务', taskThisSession: '当前会话', consoleRuns: '记录', status: '刷新', start: '继续流程', wbEvidence: '查看证据', wbNoSessions: '尚无会话记录' });
|
|
2542
|
+
Object.assign(en, { wbBackToNode: '← Back to node', wbTakeover: 'Open session', wbRetry: 'Repair and retry', wbBlocked: 'Resolve failure', taskThisSession: 'Current conversation', consoleRuns: 'History', status: 'Refresh', start: 'Continue workflow', wbEvidence: 'Inspect evidence', wbNoSessions: 'No session recorded yet' });
|
|
2543
|
+
|
|
2544
|
+
Object.assign(zh, { cfgWorktree: '自动管理任务 Worktree', cfgWorktreeHelp: '默认开启。关闭后不自动创建 Worktree;仍可并行,模型或用户可选择并登记执行目录。已有目录不会被删除' });
|
|
2545
|
+
Object.assign(en, { cfgWorktree: 'Automatically manage task Worktrees', cfgWorktreeHelp: 'On by default. Off keeps existing directories and still allows parallel tasks; callers may choose and register their own execution checkouts' });
|
|
2546
|
+
// The editor owns drafts only. File identity, validation, backup and publication stay in Go.
|
|
2547
|
+
Object.assign(zh, {
|
|
2548
|
+
consoleResource: 'Provider',
|
|
2549
|
+
cfgRouteDefault: '普通执行',
|
|
2550
|
+
cfgRouteFrontend: '前端执行',
|
|
2551
|
+
cfgRouteHard: '复杂执行',
|
|
2552
|
+
cfgRouteMultimodal: '多模态执行',
|
|
2553
|
+
cfgEffortDefault: '后端默认',
|
|
2554
|
+
cfgEffortOff: '关闭',
|
|
2555
|
+
cfgEffortMinimal: '最低',
|
|
2556
|
+
cfgEffortLow: '低',
|
|
2557
|
+
cfgEffortMedium: '中',
|
|
2558
|
+
cfgEffortHigh: '高',
|
|
2559
|
+
cfgEffortXhigh: '更高',
|
|
2560
|
+
cfgEffortMax: '最高',
|
|
2561
|
+
consoleInvalid: '请输入合法的完整配置 JSON,并保留已有顶层字段',
|
|
2562
|
+
cfgStrategy: '工作方式',
|
|
2563
|
+
cfgModels: '模型分配',
|
|
2564
|
+
cfgLimits: '预算与重试',
|
|
2565
|
+
cfgJev: 'JEV 决策',
|
|
2566
|
+
cfgMore: '更多配置操作',
|
|
2567
|
+
cfgSessionHelp: '同任务、同角色续接;执行与独立 Review 不共享历史',
|
|
2568
|
+
cfgScheduleHelp: '仅对契约声明独立、依赖已满足的任务并发执行',
|
|
2569
|
+
cfgDispatch: '并行任务派发方式',
|
|
2570
|
+
cfgDispatchHelp: '批次处理等待本批 Execute/L1 收尾;就绪队列在执行与验证完成后补充空位。评审和集成仍串行',
|
|
2571
|
+
cfgDispatchEligibility: '就绪队列仅适用于 Full 任务,且需要并行、并发数大于 1、自动管理独立 Worktree',
|
|
2572
|
+
cfgDispatchBatchState: '当前配置为批次处理',
|
|
2573
|
+
cfgDispatchReadyState: '配置条件已满足;运行时仍须核对 Full 任务的独立单元与依赖',
|
|
2574
|
+
cfgDispatchUnavailable: '当前不使用就绪队列:{reasons}。保留已选策略',
|
|
2575
|
+
cfgDispatchSerial: '调度策略为串行',
|
|
2576
|
+
cfgDispatchSingle: '并发上限为 1',
|
|
2577
|
+
cfgDispatchUnmanaged: '未开启自动 Worktree 管理',
|
|
2578
|
+
cfgDispatchInvalid: '并发数量需要修正',
|
|
2579
|
+
cfgReasonSeparator: ';',
|
|
2580
|
+
cfgDispatchBatch: '整批处理 · 默认',
|
|
2581
|
+
cfgDispatchReady: '就绪队列 · 空位立即补充',
|
|
2582
|
+
cfgPrewarm: '提前启动评审运行环境',
|
|
2583
|
+
cfgPrewarmHelp: '默认关闭。在串行任务验证期间初始化 DSH 评审 SDK;验证和调用准入通过后才创建或恢复会话、发送 Prompt。并行执行槽位和共享候选批次不预热,其他后端保持冷启动',
|
|
2584
|
+
cfgParallelHelp: 'Review 与集成仍串行推进',
|
|
2585
|
+
cfgPermissionHelp: '后端不支持的权限会明确拒绝',
|
|
2586
|
+
cfgScopeHelp: '优先级:任务 → 项目 → 全局 → 安装默认;选择完整配置文件,不逐字段叠加',
|
|
2587
|
+
cfgTemplateSource: '该范围尚无文件,将以安装版本默认配置创建',
|
|
2588
|
+
cfgFileSource: '正在编辑此范围的配置文件',
|
|
2589
|
+
cfgSaved: '配置已保存,下次启动生效',
|
|
2590
|
+
cfgClean: '配置已读取;修改后可保存',
|
|
2591
|
+
cfgDirty: '下次启动生效,当前运行保持原配置',
|
|
2592
|
+
cfgDiscard: '放弃修改',
|
|
2593
|
+
cfgSave: '保存修改',
|
|
2594
|
+
cfgReload: '重新读取配置',
|
|
2595
|
+
cfgReadError: '配置读取失败,可重新读取;不会创建或修改文件',
|
|
2596
|
+
cfgUnsaved: '当前有未保存的修改',
|
|
2597
|
+
cfgDiscardHelp: '继续将放弃此范围的草稿;本机文件不会被修改',
|
|
2598
|
+
cfgDiscardContinue: '放弃草稿并继续',
|
|
2599
|
+
cfgReset: '恢复安装版本默认配置',
|
|
2600
|
+
cfgResetHelp: '将完整默认配置载入草稿,保存时先备份原文件。当前任务的配置快照保持不变',
|
|
2601
|
+
cfgLoadDefaults: '载入待保存修改',
|
|
2602
|
+
cfgResetPending: '待保存:完整替换为安装版本默认配置',
|
|
2603
|
+
cfgJson: '高级配置 JSON',
|
|
2604
|
+
cfgInvalid: '有无效字段,请修正后保存',
|
|
2605
|
+
cfgNumber: '请输入有效的非负数;换算后须为整数',
|
|
2606
|
+
cfgNumberRange: '请输入 {min} 到 {max} 之间的数值',
|
|
2607
|
+
cfgToggleOn: '开启',
|
|
2608
|
+
cfgToggleOff: '关闭',
|
|
2609
|
+
cfgNextStart: '运行方式、模型、会话与预算修改在下次启动生效',
|
|
2610
|
+
cfgAdvancedTiming: '常规设置在下次启动生效;JEV 总开关的关闭也会阻止已有任务的后续请求与采用',
|
|
2611
|
+
cfgStructuredInvalid: '高级配置中有无效字段,请修正后保存',
|
|
2612
|
+
cfgFixJSON: '修正高级配置',
|
|
2613
|
+
cfgRequiredNumber: '此项需要填写数值',
|
|
2614
|
+
cfgMinutes: '分钟',
|
|
2615
|
+
cfgSeconds: '秒',
|
|
2616
|
+
cfgTimes: '次',
|
|
2617
|
+
cfgTasks: '个任务',
|
|
2618
|
+
cfgBudgetPolicy: '达到预算时',
|
|
2619
|
+
cfgBudgetOff: '不限制',
|
|
2620
|
+
cfgBudgetSoft: '仅告警',
|
|
2621
|
+
cfgBudgetHard: '阻止新的模型调用',
|
|
2622
|
+
cfgBudgetHelp: '缺失用量不当作 0;硬预算会限制并发并核对用量',
|
|
2623
|
+
cfgRetryHelp: '仅限传输故障;任务返修次数由任务契约控制',
|
|
2624
|
+
cfgTimeout: '单次运行超时',
|
|
2625
|
+
cfgTimeoutHelp: '达到时限后请求停止,等待子任务收尾',
|
|
2626
|
+
cfgBindingsHelp: '按真实配置编辑 Provider、模型、推理与权限;模型标识在运行时由后端核对',
|
|
2627
|
+
cfgEdit: '编辑',
|
|
2628
|
+
cfgBinding: '编辑阶段模型',
|
|
2629
|
+
cfgApplyBinding: '加入待保存修改',
|
|
2630
|
+
cfgBindingAdvanced: '后端与账号渠道',
|
|
2631
|
+
cfgModelHint: '使用此 Provider 支持的模型标识;预设不代表当前账号已授权',
|
|
2632
|
+
cfgJevHelp: '一个开关控制调查与执行中的 JEV 决策,包括取证、失败后下一步及批准的模型选择。关闭后停止后续 JEV 请求与采用,沿用原路径,也不采集影子快照',
|
|
2633
|
+
cfgJevSend: '保存开启配置后,符合条件的决策会发送允许范围内的任务与证据片段,并采用通过校验的选择。预算、验收与权限仍由 Go 执行;不需要逐任务再次开启',
|
|
2634
|
+
cfgJevEndpoint: 'MeRouter System One API 地址',
|
|
2635
|
+
cfgJevKeyHelp: '仅保存环境变量名,不填写 API Key。运行 Flywheel 的进程需要能读取该变量',
|
|
2636
|
+
cfgJevTimeout: '请求超时',
|
|
2637
|
+
cfgJevAdvanced: '模型版本与请求参数',
|
|
2638
|
+
cfgJevMaxCalls: '每项任务最多 JEV 请求次数',
|
|
2639
|
+
cfgJevApplicability: '最低适用概率',
|
|
2640
|
+
cfgJevChoice: '最低选择概率',
|
|
2641
|
+
cfgJevProbability: '请输入大于 0 且不超过 1 的概率',
|
|
2642
|
+
cfgJevData: '允许发送的数据类别',
|
|
2643
|
+
cfgJevDataHelp: '可在高级配置 JSON 中调整;类别不包含的数据不会获得发送许可',
|
|
2644
|
+
cfgJevNoData: '未授权任何数据类别',
|
|
2645
|
+
cfgJevModelInvalid: '请输入固定模型版本,例如 jev-1.14.0,最多 120 个字符',
|
|
2646
|
+
cfgJevKeyInvalid: '变量名须以英文字母或下划线开头,只能包含字母、数字和下划线,最多 120 个字符',
|
|
2647
|
+
cfgJevEndpointInvalid: '地址最多 2000 个字符',
|
|
2648
|
+
cfgJevDraftDiscarded: '已放弃无效的 JEV 输入,保留上次有效值',
|
|
2649
|
+
cfgJevChanges: '关闭也会禁止已有任务的后续 JEV 请求与采用;已记录的策略、回执和预算保留。调查创建时冻结策略,开启不会改写此前关闭时创建的调查',
|
|
2650
|
+
cfgLocation: '配置文件与来源',
|
|
2651
|
+
cfgRawDiff: '查看保存差异',
|
|
2652
|
+
cfgModelRole: '阶段',
|
|
2653
|
+
cfgProviderID: 'Provider ID',
|
|
2654
|
+
cfgConflict: '文件已被外部修改;草稿已保留。重新读取并核对后才能保存',
|
|
2655
|
+
});
|
|
2656
|
+
Object.assign(en, {
|
|
2657
|
+
consoleResource: 'Provider',
|
|
2658
|
+
cfgRouteDefault: 'General execution',
|
|
2659
|
+
cfgRouteFrontend: 'Frontend execution',
|
|
2660
|
+
cfgRouteHard: 'Complex execution',
|
|
2661
|
+
cfgRouteMultimodal: 'Multimodal execution',
|
|
2662
|
+
cfgEffortDefault: 'Backend default',
|
|
2663
|
+
cfgEffortOff: 'Off',
|
|
2664
|
+
cfgEffortMinimal: 'Minimal',
|
|
2665
|
+
cfgEffortLow: 'Low',
|
|
2666
|
+
cfgEffortMedium: 'Medium',
|
|
2667
|
+
cfgEffortHigh: 'High',
|
|
2668
|
+
cfgEffortXhigh: 'Extra high',
|
|
2669
|
+
cfgEffortMax: 'Maximum',
|
|
2670
|
+
consoleInvalid: 'Enter a valid complete configuration JSON object and retain existing top-level fields',
|
|
2671
|
+
cfgStrategy: 'Workflow policy',
|
|
2672
|
+
cfgModels: 'Model assignments',
|
|
2673
|
+
cfgLimits: 'Budget and retries',
|
|
2674
|
+
cfgJev: 'JEV decisions',
|
|
2675
|
+
cfgMore: 'More configuration actions',
|
|
2676
|
+
cfgSessionHelp:
|
|
2677
|
+
'Reuse within the same task and role; execution and independent Review do not share history',
|
|
2678
|
+
cfgScheduleHelp: 'Parallel execution requires declared independence and satisfied dependencies',
|
|
2679
|
+
cfgDispatch: 'Parallel dispatch',
|
|
2680
|
+
cfgDispatchHelp: 'Batch waits for every Execute/L1 in the group. Ready queue refills slots after execution and verification. Review and integration stay serial',
|
|
2681
|
+
cfgDispatchEligibility: 'Ready queue requires a Full task, parallel scheduling, a limit above 1 and automatically managed independent Worktrees',
|
|
2682
|
+
cfgDispatchBatchState: 'Configured for batch processing',
|
|
2683
|
+
cfgDispatchReadyState: 'Configuration conditions are met; the run must still have a Full task with eligible independent units and dependencies',
|
|
2684
|
+
cfgDispatchUnavailable: 'Ready queue is inactive: {reasons}. The selected policy is retained',
|
|
2685
|
+
cfgDispatchSerial: 'scheduling is serial',
|
|
2686
|
+
cfgDispatchSingle: 'the concurrency limit is 1',
|
|
2687
|
+
cfgDispatchUnmanaged: 'automatic Worktree management is off',
|
|
2688
|
+
cfgDispatchInvalid: 'correct the concurrency limit',
|
|
2689
|
+
cfgReasonSeparator: '; ',
|
|
2690
|
+
cfgDispatchBatch: 'Batch · default',
|
|
2691
|
+
cfgDispatchReady: 'Ready queue · refill available lanes',
|
|
2692
|
+
cfgPrewarm: 'Prepare the review runtime early',
|
|
2693
|
+
cfgPrewarmHelp: 'Off by default. Initialize the DSH review SDK during serial task checks. Create or resume a session and send the prompt only after validation and call admission. Parallel execution slots and shared-candidate batches do not prewarm; other backends start cold',
|
|
2694
|
+
cfgParallelHelp: 'Review and integration remain serial',
|
|
2695
|
+
cfgPermissionHelp: 'Unsupported backend permissions are rejected explicitly',
|
|
2696
|
+
cfgScopeHelp:
|
|
2697
|
+
'Priority: task → project → global → installed defaults. A complete file is selected, not a per-field overlay',
|
|
2698
|
+
cfgTemplateSource: 'No file exists for this scope; a new file starts from installed defaults',
|
|
2699
|
+
cfgFileSource: 'Editing the configuration file for this scope',
|
|
2700
|
+
cfgSaved: 'Saved; applies to the next start',
|
|
2701
|
+
cfgClean: 'Configuration loaded; edit to save changes',
|
|
2702
|
+
cfgDirty: 'Applies to the next start; the current run keeps its snapshot',
|
|
2703
|
+
cfgDiscard: 'Discard changes',
|
|
2704
|
+
cfgSave: 'Save changes',
|
|
2705
|
+
cfgReload: 'Reload configuration',
|
|
2706
|
+
cfgReadError: 'Could not read configuration. Retry without creating or changing a file',
|
|
2707
|
+
cfgUnsaved: 'There are unsaved changes',
|
|
2708
|
+
cfgDiscardHelp: 'Continuing discards this scope’s draft without changing the file',
|
|
2709
|
+
cfgDiscardContinue: 'Discard draft and continue',
|
|
2710
|
+
cfgReset: 'Restore installed version defaults',
|
|
2711
|
+
cfgResetHelp:
|
|
2712
|
+
'Load the complete defaults into the draft. Saving backs up the old file; the current run retains its snapshot',
|
|
2713
|
+
cfgLoadDefaults: 'Load pending changes',
|
|
2714
|
+
cfgResetPending: 'Pending: replace with the complete installed defaults',
|
|
2715
|
+
cfgJson: 'Advanced configuration JSON',
|
|
2716
|
+
cfgInvalid: 'Correct invalid fields before saving',
|
|
2717
|
+
cfgNumber: 'Enter a valid non-negative number that converts to an integer',
|
|
2718
|
+
cfgNumberRange: 'Enter a value from {min} to {max}',
|
|
2719
|
+
cfgToggleOn: 'On',
|
|
2720
|
+
cfgToggleOff: 'Off',
|
|
2721
|
+
cfgNextStart: 'Execution, model, session and budget changes apply to the next start',
|
|
2722
|
+
cfgAdvancedTiming: 'Regular settings apply to the next start. Turning JEV off also blocks subsequent requests and adoption in existing tasks',
|
|
2723
|
+
cfgStructuredInvalid: 'Advanced configuration contains invalid fields. Correct them before saving',
|
|
2724
|
+
cfgFixJSON: 'Fix advanced configuration',
|
|
2725
|
+
cfgRequiredNumber: 'A numeric value is required',
|
|
2726
|
+
cfgMinutes: 'minutes',
|
|
2727
|
+
cfgSeconds: 'seconds',
|
|
2728
|
+
cfgTimes: 'attempts',
|
|
2729
|
+
cfgTasks: 'tasks',
|
|
2730
|
+
cfgBudgetPolicy: 'When the budget is reached',
|
|
2731
|
+
cfgBudgetOff: 'No limit',
|
|
2732
|
+
cfgBudgetSoft: 'Warn only',
|
|
2733
|
+
cfgBudgetHard: 'Block new model calls',
|
|
2734
|
+
cfgBudgetHelp: 'Missing usage is not zero; hard budgets constrain concurrency and reconcile usage',
|
|
2735
|
+
cfgRetryHelp: 'Transport faults only. Repair limits belong to the task contract',
|
|
2736
|
+
cfgTimeout: 'Run timeout',
|
|
2737
|
+
cfgTimeoutHelp: 'Request cancellation at the limit and wait for child cleanup',
|
|
2738
|
+
cfgBindingsHelp:
|
|
2739
|
+
'Edit actual Provider, model, reasoning and permissions. The backend checks model identifiers at runtime',
|
|
2740
|
+
cfgEdit: 'Edit',
|
|
2741
|
+
cfgBinding: 'Edit stage model',
|
|
2742
|
+
cfgApplyBinding: 'Add to pending changes',
|
|
2743
|
+
cfgBindingAdvanced: 'Backend and account channel',
|
|
2744
|
+
cfgModelHint: 'Use a supported Provider model identifier. A preset does not prove account authorization',
|
|
2745
|
+
cfgJevHelp:
|
|
2746
|
+
'One switch controls JEV decisions during investigation and execution, including evidence collection, next steps after failure and approved model selection. Turning it off stops subsequent requests and adoption, keeps the original paths and disables shadow capture',
|
|
2747
|
+
cfgJevSend:
|
|
2748
|
+
'Saving an enabled configuration allows eligible decisions to send permitted task and evidence excerpts and adopt validated choices. Go retains budget, acceptance and permission checks; no per-task enable step is needed',
|
|
2749
|
+
cfgJevEndpoint: 'MeRouter System One API endpoint',
|
|
2750
|
+
cfgJevKeyHelp: 'Store the variable name, not the API key. The Flywheel process must inherit this variable',
|
|
2751
|
+
cfgJevTimeout: 'Request timeout',
|
|
2752
|
+
cfgJevAdvanced: 'Model version and request options',
|
|
2753
|
+
cfgJevMaxCalls: 'Maximum JEV requests per task',
|
|
2754
|
+
cfgJevApplicability: 'Minimum applicability probability',
|
|
2755
|
+
cfgJevChoice: 'Minimum choice probability',
|
|
2756
|
+
cfgJevProbability: 'Enter a probability greater than 0 and at most 1',
|
|
2757
|
+
cfgJevData: 'Permitted data categories',
|
|
2758
|
+
cfgJevDataHelp: 'Edit in Advanced configuration JSON. Other categories do not gain permission to be sent',
|
|
2759
|
+
cfgJevNoData: 'No data categories permitted',
|
|
2760
|
+
cfgJevModelInvalid: 'Enter a pinned model version such as jev-1.14.0, up to 120 characters',
|
|
2761
|
+
cfgJevKeyInvalid: 'Use up to 120 letters, digits or underscores, starting with a letter or underscore',
|
|
2762
|
+
cfgJevEndpointInvalid: 'The endpoint must be at most 2000 characters',
|
|
2763
|
+
cfgJevDraftDiscarded: 'Invalid JEV inputs were discarded; the last valid values remain',
|
|
2764
|
+
cfgJevChanges: 'Turning it off also blocks further JEV requests and adoption in existing tasks; recorded policies, receipts and budgets remain. Investigations freeze their policy at creation; enabling does not rewrite preparations created while off',
|
|
2765
|
+
cfgLocation: 'Configuration file and source',
|
|
2766
|
+
cfgRawDiff: 'Review changes',
|
|
2767
|
+
cfgModelRole: 'Stage',
|
|
2768
|
+
cfgProviderID: 'Provider ID',
|
|
2769
|
+
cfgConflict: 'The file changed externally. Your draft is retained; reload and review before saving',
|
|
2770
|
+
});
|
|
2771
|
+
|
|
2772
|
+
const cloneConfig = (value) => JSON.parse(JSON.stringify(value));
|
|
2773
|
+
// These bounds protect the visible controls; Go remains the save authority.
|
|
2774
|
+
const configNumberRules = {
|
|
2775
|
+
'scheduler.max-parallel': { min: 1, max: 8 },
|
|
2776
|
+
'jev.timeout-ms': { min: 1, max: 30000 },
|
|
2777
|
+
'jev.max-calls': { min: 1, max: 100 },
|
|
2778
|
+
'jev.min-applicability': { min: 0, max: 1, probability: true },
|
|
2779
|
+
'jev.min-choice': { min: 0, max: 1, probability: true },
|
|
2780
|
+
};
|
|
2781
|
+
function boundedConfigError(path, value) {
|
|
2782
|
+
const rule = configNumberRules[path];
|
|
2783
|
+
if (!rule) return null;
|
|
2784
|
+
if (rule.probability)
|
|
2785
|
+
return typeof value !== 'number' || !Number.isFinite(value) || value <= 0 || value > 1 ? 'cfgJevProbability' : null;
|
|
2786
|
+
if (!Number.isSafeInteger(value)) return 'cfgNumber';
|
|
2787
|
+
return value < rule.min || value > rule.max ? 'cfgNumberRange' : null;
|
|
2788
|
+
}
|
|
2789
|
+
function jevTextError(key, value) {
|
|
2790
|
+
if (key === 'endpoint') return typeof value !== 'string' || [...value].length > 2000 ? 'cfgJevEndpointInvalid' : null;
|
|
2791
|
+
if (key === 'model') return typeof value !== 'string' || value.length > 120 || !/^jev-\d+\.\d+\.\d+$/.test(value) ? 'cfgJevModelInvalid' : null;
|
|
2792
|
+
return typeof value !== 'string' || !/^[A-Za-z_][A-Za-z0-9_]{0,119}$/.test(value) ? 'cfgJevKeyInvalid' : null;
|
|
2793
|
+
}
|
|
2794
|
+
function configFieldErrors(values) {
|
|
2795
|
+
const errors = {};
|
|
2796
|
+
for (const path of Object.keys(configNumberRules)) {
|
|
2797
|
+
const [group, key] = path.split('.');
|
|
2798
|
+
if (record(values?.[group]) && Object.hasOwn(values[group], key)) {
|
|
2799
|
+
const error = boundedConfigError(path, values[group][key]);
|
|
2800
|
+
if (error) errors[path] = error;
|
|
2801
|
+
}
|
|
2802
|
+
}
|
|
2803
|
+
if (values?.scheduler !== undefined && !record(values.scheduler)) errors.scheduler = 'cfgInvalid';
|
|
2804
|
+
if (values?.jev !== undefined) {
|
|
2805
|
+
if (!record(values.jev)) errors.jev = 'cfgInvalid';
|
|
2806
|
+
else {
|
|
2807
|
+
for (const key of Object.keys(values.jev))
|
|
2808
|
+
if (key !== '_note' && !key.startsWith('_note_') &&
|
|
2809
|
+
!['enabled', 'endpoint', 'model', 'key-env', 'timeout-ms', 'max-calls', 'min-applicability', 'min-choice', 'allowed-data'].includes(key))
|
|
2810
|
+
errors['jev.' + key] = 'cfgInvalid';
|
|
2811
|
+
for (const key of ['endpoint', 'model', 'key-env']) {
|
|
2812
|
+
if (!Object.hasOwn(values.jev, key)) continue;
|
|
2813
|
+
const error = jevTextError(key, values.jev[key]);
|
|
2814
|
+
if (error) errors['jev.' + key] = error;
|
|
2815
|
+
}
|
|
2816
|
+
if (Object.hasOwn(values.jev, 'enabled') && typeof values.jev.enabled !== 'boolean') errors['jev.enabled'] = 'cfgInvalid';
|
|
2817
|
+
const data = values.jev['allowed-data'];
|
|
2818
|
+
if (data !== undefined && (!Array.isArray(data) || new Set(data).size !== data.length ||
|
|
2819
|
+
data.some(value => !['task-summary', 'diagnostic-facts', 'logs', 'source', 'diff'].includes(value))))
|
|
2820
|
+
errors['jev.allowed-data'] = 'cfgInvalid';
|
|
2821
|
+
}
|
|
2822
|
+
}
|
|
2823
|
+
return errors;
|
|
2824
|
+
}
|
|
2825
|
+
function ConfigEditor({ control, selection, run, t, active = false, cache, contextKey, onClose, diagnostics }) {
|
|
2826
|
+
// Drafts survive task navigation in this live Session binding only. Restoring
|
|
2827
|
+
// one always rereads the file; cached data never grants a revision to save.
|
|
2828
|
+
const cached = React.useRef(cache?.get(contextKey));
|
|
2829
|
+
const [scope, setScope] = React.useState(cached.current?.scope ?? 'global'),
|
|
2830
|
+
[section, setSection] = React.useState(cached.current?.section ?? 'strategy');
|
|
2831
|
+
const [draft, setDraft] = React.useState(cached.current?.draft ?? null),
|
|
2832
|
+
[base, setBase] = React.useState(cached.current?.base ?? null),
|
|
2833
|
+
[raw, setRaw] = React.useState(cached.current?.raw ?? '');
|
|
2834
|
+
const [invalid, setInvalid] = React.useState(cached.current?.invalid ?? false),
|
|
2835
|
+
[numbers, setNumbers] = React.useState(cached.current?.numbers ?? {}),
|
|
2836
|
+
[numberErrors, setNumberErrors] = React.useState(cached.current?.numberErrors ?? {});
|
|
2837
|
+
const [texts, setTexts] = React.useState(cached.current?.texts ?? {}),
|
|
2838
|
+
[textErrors, setTextErrors] = React.useState(cached.current?.textErrors ?? {}),
|
|
2839
|
+
[discardedJevInputs, setDiscardedJevInputs] = React.useState(cached.current?.discardedJevInputs ?? false);
|
|
2840
|
+
const [revision, setRevision] = React.useState(cached.current?.revision ?? ''),
|
|
2841
|
+
[conflict, setConflict] = React.useState(false);
|
|
2842
|
+
const [reset, setReset] = React.useState(cached.current?.reset ?? false),
|
|
2843
|
+
[saved, setSaved] = React.useState(false),
|
|
2844
|
+
[dialog, setDialog] = React.useState(null),
|
|
2845
|
+
[bindingEdit, setBindingEdit] = React.useState(null);
|
|
2846
|
+
const attempt = React.useRef(null),
|
|
2847
|
+
adopted = React.useRef(null),
|
|
2848
|
+
id = React.useId();
|
|
2849
|
+
const doc = control.editor,
|
|
2850
|
+
busy = control.pending !== null;
|
|
2851
|
+
const load = () => {
|
|
2852
|
+
cached.current = null;
|
|
2853
|
+
adopted.current = null;
|
|
2854
|
+
attempt.current = scope;
|
|
2855
|
+
void run('config-edit', selection, false, { editor: { scope } });
|
|
2856
|
+
};
|
|
2857
|
+
React.useEffect(() => {
|
|
2858
|
+
if (!active || busy || attempt.current === scope) return;
|
|
2859
|
+
attempt.current = scope;
|
|
2860
|
+
void run('config-edit', selection, false, { editor: { scope } });
|
|
2861
|
+
}, [active, busy, scope, run, selection.taskDir, selection.cwd]);
|
|
2862
|
+
React.useEffect(() => {
|
|
2863
|
+
if (!doc || doc.scope !== scope || adopted.current === doc) return;
|
|
2864
|
+
adopted.current = doc;
|
|
2865
|
+
if (cached.current?.dirty) {
|
|
2866
|
+
setConflict(cached.current.revision !== doc.revision);
|
|
2867
|
+
cached.current = null;
|
|
2868
|
+
return;
|
|
2869
|
+
}
|
|
2870
|
+
cached.current = null;
|
|
2871
|
+
setRevision(doc.revision);
|
|
2872
|
+
setConflict(false);
|
|
2873
|
+
setDraft(cloneConfig(doc.values));
|
|
2874
|
+
setBase(cloneConfig(doc.values));
|
|
2875
|
+
setRaw(JSON.stringify(doc.values, null, 2));
|
|
2876
|
+
setInvalid(false);
|
|
2877
|
+
setNumbers({});
|
|
2878
|
+
setNumberErrors({});
|
|
2879
|
+
setTexts({});
|
|
2880
|
+
setTextErrors({});
|
|
2881
|
+
setDiscardedJevInputs(false);
|
|
2882
|
+
setReset(false);
|
|
2883
|
+
setSaved(doc.applied === true);
|
|
2884
|
+
}, [doc, scope]);
|
|
2885
|
+
const replace = (values) => {
|
|
2886
|
+
setDraft(values);
|
|
2887
|
+
setRaw(JSON.stringify(values, null, 2));
|
|
2888
|
+
setInvalid(false);
|
|
2889
|
+
setSaved(false);
|
|
2890
|
+
};
|
|
2891
|
+
const changed = draft
|
|
2892
|
+
? Object.keys({ ...base, ...draft }).filter(
|
|
2893
|
+
(key) => JSON.stringify(draft[key]) !== JSON.stringify(base?.[key]),
|
|
2894
|
+
)
|
|
2895
|
+
: [];
|
|
2896
|
+
const fieldErrors = configFieldErrors(draft);
|
|
2897
|
+
const dirty = changed.length > 0 || reset || invalid || Object.values(numberErrors).some(Boolean) || Object.values(textErrors).some(Boolean);
|
|
2898
|
+
React.useEffect(() => {
|
|
2899
|
+
if (cache && draft)
|
|
2900
|
+
cache.set(contextKey, {
|
|
2901
|
+
scope,
|
|
2902
|
+
section,
|
|
2903
|
+
draft,
|
|
2904
|
+
base,
|
|
2905
|
+
raw,
|
|
2906
|
+
invalid,
|
|
2907
|
+
numbers,
|
|
2908
|
+
numberErrors,
|
|
2909
|
+
texts,
|
|
2910
|
+
textErrors,
|
|
2911
|
+
discardedJevInputs,
|
|
2912
|
+
reset,
|
|
2913
|
+
revision,
|
|
2914
|
+
dirty,
|
|
2915
|
+
});
|
|
2916
|
+
}, [
|
|
2917
|
+
scope,
|
|
2918
|
+
section,
|
|
2919
|
+
draft,
|
|
2920
|
+
base,
|
|
2921
|
+
raw,
|
|
2922
|
+
invalid,
|
|
2923
|
+
numbers,
|
|
2924
|
+
numberErrors,
|
|
2925
|
+
texts,
|
|
2926
|
+
textErrors,
|
|
2927
|
+
discardedJevInputs,
|
|
2928
|
+
reset,
|
|
2929
|
+
revision,
|
|
2930
|
+
dirty,
|
|
2931
|
+
cache,
|
|
2932
|
+
contextKey,
|
|
2933
|
+
]);
|
|
2934
|
+
const ready =
|
|
2935
|
+
!!draft &&
|
|
2936
|
+
doc?.scope === scope &&
|
|
2937
|
+
doc.revision === revision &&
|
|
2938
|
+
!conflict &&
|
|
2939
|
+
!invalid &&
|
|
2940
|
+
!Object.values(numberErrors).some(Boolean) &&
|
|
2941
|
+
!Object.values(textErrors).some(Boolean) &&
|
|
2942
|
+
Object.keys(fieldErrors).length === 0;
|
|
2943
|
+
const update = (group, key, value) => replace({ ...draft, [group]: { ...draft[group], [key]: value } });
|
|
2944
|
+
const discard = () => {
|
|
2945
|
+
const latest = conflict ? doc.values : base;
|
|
2946
|
+
replace(cloneConfig(latest));
|
|
2947
|
+
if (conflict) {
|
|
2948
|
+
setBase(cloneConfig(latest));
|
|
2949
|
+
setRevision(doc.revision);
|
|
2950
|
+
setConflict(false);
|
|
2951
|
+
}
|
|
2952
|
+
setNumbers({});
|
|
2953
|
+
setNumberErrors({});
|
|
2954
|
+
setTexts({});
|
|
2955
|
+
setTextErrors({});
|
|
2956
|
+
setDiscardedJevInputs(false);
|
|
2957
|
+
setReset(false);
|
|
2958
|
+
};
|
|
2959
|
+
const deferDiscard = (next) => {
|
|
2960
|
+
if (dirty) setDialog({ kind: 'discard', next });
|
|
2961
|
+
else next();
|
|
2962
|
+
};
|
|
2963
|
+
const switchScope = (value) => {
|
|
2964
|
+
if (scope === value) return;
|
|
2965
|
+
deferDiscard(() => {
|
|
2966
|
+
setScope(value);
|
|
2967
|
+
setDraft(null);
|
|
2968
|
+
setBase(null);
|
|
2969
|
+
setRaw('');
|
|
2970
|
+
setNumbers({});
|
|
2971
|
+
setNumberErrors({});
|
|
2972
|
+
setTexts({});
|
|
2973
|
+
setTextErrors({});
|
|
2974
|
+
setDiscardedJevInputs(false);
|
|
2975
|
+
setInvalid(false);
|
|
2976
|
+
setReset(false);
|
|
2977
|
+
setSaved(false);
|
|
2978
|
+
setConflict(false);
|
|
2979
|
+
setRevision('');
|
|
2980
|
+
cached.current = null;
|
|
2981
|
+
attempt.current = null;
|
|
2982
|
+
adopted.current = null;
|
|
2983
|
+
});
|
|
2984
|
+
};
|
|
2985
|
+
const save = () =>
|
|
2986
|
+
void run('config-save', selection, false, {
|
|
2987
|
+
editor: {
|
|
2988
|
+
scope,
|
|
2989
|
+
revision,
|
|
2990
|
+
values: reset ? draft : Object.fromEntries(changed.map((key) => [key, draft[key]])),
|
|
2991
|
+
reset,
|
|
2992
|
+
},
|
|
2993
|
+
});
|
|
2994
|
+
const clearNumberDrafts = (matches) => {
|
|
2995
|
+
const keep = ([path]) => !matches(path);
|
|
2996
|
+
setNumbers(old => Object.fromEntries(Object.entries(old).filter(keep)));
|
|
2997
|
+
setNumberErrors(old => Object.fromEntries(Object.entries(old).filter(keep)));
|
|
2998
|
+
};
|
|
2999
|
+
const changeJev = (enabled) => {
|
|
3000
|
+
const jevField = path => path.startsWith('jev.');
|
|
3001
|
+
const discarded = !enabled && Object.entries({ ...numberErrors, ...textErrors })
|
|
3002
|
+
.some(([path, error]) => jevField(path) && error);
|
|
3003
|
+
if (!enabled) {
|
|
3004
|
+
clearNumberDrafts(jevField);
|
|
3005
|
+
setTexts(old => Object.fromEntries(Object.entries(old).filter(([path]) => !jevField(path))));
|
|
3006
|
+
setTextErrors(old => Object.fromEntries(Object.entries(old).filter(([path]) => !jevField(path))));
|
|
3007
|
+
}
|
|
3008
|
+
setDiscardedJevInputs(discarded);
|
|
3009
|
+
update('jev', 'enabled', enabled);
|
|
3010
|
+
};
|
|
3011
|
+
const inputID = (key) => id + '-' + key;
|
|
3012
|
+
const field = (key, label, help, controlNode) =>
|
|
3013
|
+
h(
|
|
3014
|
+
'div',
|
|
3015
|
+
{ className: css.configField, key },
|
|
3016
|
+
h(
|
|
3017
|
+
'div',
|
|
3018
|
+
{ className: css.configFieldLabel },
|
|
3019
|
+
h('label', { htmlFor: inputID(key) }, label),
|
|
3020
|
+
h('p', { className: css.meta, id: inputID(key) + '-help', hidden: !help }, help || ''),
|
|
3021
|
+
),
|
|
3022
|
+
h('div', { className: css.configFieldControl }, controlNode),
|
|
3023
|
+
);
|
|
3024
|
+
const select = (key, value, choices, onChange) =>
|
|
3025
|
+
h(
|
|
3026
|
+
'select',
|
|
3027
|
+
{
|
|
3028
|
+
id: inputID(key),
|
|
3029
|
+
className: css.modeSelect,
|
|
3030
|
+
value,
|
|
3031
|
+
disabled: busy,
|
|
3032
|
+
'aria-describedby': inputID(key) + '-help',
|
|
3033
|
+
onChange: (e) => onChange(e.target.value),
|
|
3034
|
+
},
|
|
3035
|
+
...choices.map(([value, label]) => h('option', { key: value, value }, label)),
|
|
3036
|
+
);
|
|
3037
|
+
const toggle = (key, checked, onChange, unavailable = false) =>
|
|
3038
|
+
h('label', { className: css.configSwitch, htmlFor: inputID(key) },
|
|
3039
|
+
h('input', {
|
|
3040
|
+
id: inputID(key), type: 'checkbox', role: 'switch', checked, disabled: busy || unavailable,
|
|
3041
|
+
'aria-describedby': inputID(key) + '-help',
|
|
3042
|
+
onChange: event => onChange(event.target.checked),
|
|
3043
|
+
}),
|
|
3044
|
+
h('span', { className: css.configSwitchTrack, 'aria-hidden': true }),
|
|
3045
|
+
h('span', { className: css.configSwitchState, 'aria-hidden': true }, t(checked ? 'cfgToggleOn' : 'cfgToggleOff')));
|
|
3046
|
+
const input = (key, value, onChange, validate) => {
|
|
3047
|
+
const error = textErrors[key] || fieldErrors[key];
|
|
3048
|
+
return h(React.Fragment, null, h('input', {
|
|
3049
|
+
id: inputID(key),
|
|
3050
|
+
className: css.editorInput,
|
|
3051
|
+
value: texts[key] ?? value ?? '',
|
|
3052
|
+
disabled: busy,
|
|
3053
|
+
'aria-invalid': !!error,
|
|
3054
|
+
'aria-describedby': inputID(key) + '-help' + (error ? ' ' + inputID(key) + '-error' : ''),
|
|
3055
|
+
onChange: (e) => {
|
|
3056
|
+
const next = e.target.value;
|
|
3057
|
+
if (!validate) { onChange(next); return; }
|
|
3058
|
+
const invalidText = validate(next);
|
|
3059
|
+
setTexts(old => ({ ...old, [key]: next }));
|
|
3060
|
+
setTextErrors(old => ({ ...old, [key]: invalidText }));
|
|
3061
|
+
setSaved(false);
|
|
3062
|
+
setDiscardedJevInputs(false);
|
|
3063
|
+
if (!invalidText) onChange(next);
|
|
3064
|
+
},
|
|
3065
|
+
}), error && h('p', { id: inputID(key) + '-error', role: 'alert', className: css.errorText }, t(error)));
|
|
3066
|
+
};
|
|
3067
|
+
const number = (group, key, factor, unit, nullable = false, probability = false) => {
|
|
3068
|
+
const path = group + '.' + key,
|
|
3069
|
+
current = draft[group]?.[key],
|
|
3070
|
+
error = numberErrors[path] || fieldErrors[path],
|
|
3071
|
+
bounds = configNumberRules[path];
|
|
3072
|
+
return h(
|
|
3073
|
+
React.Fragment,
|
|
3074
|
+
null,
|
|
3075
|
+
h(
|
|
3076
|
+
'div',
|
|
3077
|
+
{ className: css.inputUnit },
|
|
3078
|
+
h('input', {
|
|
3079
|
+
id: inputID(path),
|
|
3080
|
+
inputMode: 'decimal',
|
|
3081
|
+
value: numbers[path] ?? (current == null ? '' : String(current / factor)),
|
|
3082
|
+
disabled: busy,
|
|
3083
|
+
'aria-invalid': !!error,
|
|
3084
|
+
'aria-describedby': inputID(path) + '-help' + (error ? ' ' + inputID(path) + '-error' : ''),
|
|
3085
|
+
onChange: (e) => {
|
|
3086
|
+
const value = e.target.value;
|
|
3087
|
+
setNumbers((old) => ({ ...old, [path]: value }));
|
|
3088
|
+
if (nullable && !value.trim()) {
|
|
3089
|
+
setNumberErrors((old) => ({ ...old, [path]: null }));
|
|
3090
|
+
update(group, key, null);
|
|
3091
|
+
return;
|
|
3092
|
+
}
|
|
3093
|
+
const scaled = Number(value) * factor,
|
|
3094
|
+
rounded = probability ? scaled : Math.round(scaled);
|
|
3095
|
+
const invalidNumber = !value.trim()
|
|
3096
|
+
? 'cfgRequiredNumber'
|
|
3097
|
+
: probability
|
|
3098
|
+
? !/^\d+(?:\.\d+)?$/.test(value.trim()) || !Number.isFinite(scaled) || scaled <= 0 || scaled > 1
|
|
3099
|
+
? 'cfgJevProbability'
|
|
3100
|
+
: null
|
|
3101
|
+
: !/^\d+(?:\.\d+)?$/.test(value.trim()) ||
|
|
3102
|
+
!Number.isSafeInteger(rounded) ||
|
|
3103
|
+
scaled < 0 ||
|
|
3104
|
+
Math.abs(scaled - rounded) > 0.000001
|
|
3105
|
+
? 'cfgNumber'
|
|
3106
|
+
: boundedConfigError(path, rounded);
|
|
3107
|
+
setNumberErrors((old) => ({ ...old, [path]: invalidNumber }));
|
|
3108
|
+
setSaved(false);
|
|
3109
|
+
if (!invalidNumber) update(group, key, rounded);
|
|
3110
|
+
},
|
|
3111
|
+
}),
|
|
3112
|
+
h('span', null, unit),
|
|
3113
|
+
),
|
|
3114
|
+
error && h('p', { id: inputID(path) + '-error', role: 'alert', className: css.errorText },
|
|
3115
|
+
t(error, bounds ? { min: bounds.min / factor, max: bounds.max / factor } : undefined)),
|
|
3116
|
+
);
|
|
3117
|
+
};
|
|
3118
|
+
const bindingOrder = [
|
|
3119
|
+
'loops.plan',
|
|
3120
|
+
'routes.default',
|
|
3121
|
+
'routes.frontend',
|
|
3122
|
+
'routes.hard',
|
|
3123
|
+
'routes.multimodal',
|
|
3124
|
+
'loops.review',
|
|
3125
|
+
'loops.judge',
|
|
3126
|
+
'loops.intake',
|
|
3127
|
+
'loops.garden',
|
|
3128
|
+
];
|
|
3129
|
+
const rows = ['loops', 'routes']
|
|
3130
|
+
.flatMap((group) =>
|
|
3131
|
+
Object.entries(draft?.[group] ?? {})
|
|
3132
|
+
.filter(([, value]) => record(value))
|
|
3133
|
+
.map(([key, value]) => [group, key, value]),
|
|
3134
|
+
)
|
|
3135
|
+
.sort(
|
|
3136
|
+
(a, b) =>
|
|
3137
|
+
(bindingOrder.indexOf(a[0] + '.' + a[1]) + 1 || 999) -
|
|
3138
|
+
(bindingOrder.indexOf(b[0] + '.' + b[1]) + 1 || 999),
|
|
3139
|
+
);
|
|
3140
|
+
if (record(draft?.['judge-panel'])) rows.push(['panel', 'judge-panel', draft['judge-panel']]);
|
|
3141
|
+
const bindingID = (group, key) => group === 'panel' ? 'judge-panel' : group + '.' + key;
|
|
3142
|
+
const resourceOf = (value) =>
|
|
3143
|
+
value.harness === 'dsh'
|
|
3144
|
+
? ['chatgpt', 'deepseek-account'].includes(value.provider || value.channel)
|
|
3145
|
+
? value.provider || value.channel
|
|
3146
|
+
: 'custom'
|
|
3147
|
+
: ['kimi', 'codex-pro', 'claude-code-pro'].includes(value.harness)
|
|
3148
|
+
? value.harness
|
|
3149
|
+
: 'custom';
|
|
3150
|
+
const resourceNames = {
|
|
3151
|
+
'deepseek-account': 'DeepSeek Account',
|
|
3152
|
+
chatgpt: 'ChatGPT · DSH',
|
|
3153
|
+
kimi: 'Kimi Code',
|
|
3154
|
+
'codex-pro': 'ChatGPT · Codex Pro',
|
|
3155
|
+
'claude-code-pro': 'Claude Code Pro',
|
|
3156
|
+
custom: t('consoleCustom'),
|
|
3157
|
+
};
|
|
3158
|
+
const preset = (resource) =>
|
|
3159
|
+
({
|
|
3160
|
+
'deepseek-account': doc.defaults.routes?.default,
|
|
3161
|
+
chatgpt: doc.defaults.loops?.[bindingEdit?.key === 'judge' ? 'judge' : 'review'],
|
|
3162
|
+
kimi: doc.defaults.loops?.plan,
|
|
3163
|
+
'codex-pro': doc.defaults.routes?.hard,
|
|
3164
|
+
'claude-code-pro': {
|
|
3165
|
+
harness: 'claude-code-pro',
|
|
3166
|
+
model: 'sonnet',
|
|
3167
|
+
channel: 'claude-subscription',
|
|
3168
|
+
thinking: 'high',
|
|
3169
|
+
},
|
|
3170
|
+
})[resource];
|
|
3171
|
+
const roleName = (group, key) =>
|
|
3172
|
+
group === 'routes'
|
|
3173
|
+
? {
|
|
3174
|
+
default: t('cfgRouteDefault'),
|
|
3175
|
+
frontend: t('cfgRouteFrontend'),
|
|
3176
|
+
hard: t('cfgRouteHard'),
|
|
3177
|
+
multimodal: t('cfgRouteMultimodal'),
|
|
3178
|
+
}[key] || t('executeRole') + ' · ' + key
|
|
3179
|
+
: t(roleLabels[key] || key);
|
|
3180
|
+
const efforts = {
|
|
3181
|
+
'': t('cfgEffortDefault'),
|
|
3182
|
+
off: t('cfgEffortOff'),
|
|
3183
|
+
minimal: t('cfgEffortMinimal'),
|
|
3184
|
+
low: t('cfgEffortLow'),
|
|
3185
|
+
medium: t('cfgEffortMedium'),
|
|
3186
|
+
high: t('cfgEffortHigh'),
|
|
3187
|
+
xhigh: t('cfgEffortXhigh'),
|
|
3188
|
+
max: t('cfgEffortMax'),
|
|
3189
|
+
};
|
|
3190
|
+
const openBinding = (group, key, value) => {
|
|
3191
|
+
const role = group === 'routes' ? 'execute' : key;
|
|
3192
|
+
setBindingEdit({
|
|
3193
|
+
group,
|
|
3194
|
+
key,
|
|
3195
|
+
role,
|
|
3196
|
+
value: cloneConfig(value),
|
|
3197
|
+
permission: draft.permissions?.bindings?.[bindingID(group, key)] ?? 'inherit',
|
|
3198
|
+
session: draft.sessions?.roles?.[role] ?? 'inherit',
|
|
3199
|
+
});
|
|
3200
|
+
};
|
|
3201
|
+
const applySessionRole = (next, role, session) => {
|
|
3202
|
+
const roles = { ...next.sessions?.roles };
|
|
3203
|
+
if (session === 'inherit') delete roles[role];
|
|
3204
|
+
else roles[role] = session;
|
|
3205
|
+
next.sessions = { ...next.sessions, roles };
|
|
3206
|
+
// Kimi's legacy Plan switch also controls history when mode=fresh.
|
|
3207
|
+
// Explicit role choices must not advertise a different actual behavior.
|
|
3208
|
+
if (role === 'plan' && session !== 'inherit' && next.loops?.plan) {
|
|
3209
|
+
const plan = next.loops.plan;
|
|
3210
|
+
if (plan.harness === 'kimi') plan['resident-session'] = session === 'scoped';
|
|
3211
|
+
else delete plan['resident-session'];
|
|
3212
|
+
}
|
|
3213
|
+
};
|
|
3214
|
+
const roleSession = (role) => {
|
|
3215
|
+
let mode = draft.sessions?.mode ?? 'fresh';
|
|
3216
|
+
if (role === 'plan') {
|
|
3217
|
+
const plan = draft.loops?.plan;
|
|
3218
|
+
if (plan?.['resident-session'] === false) mode = 'fresh';
|
|
3219
|
+
else if ((plan?.harness || draft.profiles?.[plan?.profile]?.harness) === 'kimi' && plan['resident-session'] === true) mode = 'scoped';
|
|
3220
|
+
}
|
|
3221
|
+
return t('cfgInheritMode', { mode: t(mode === 'scoped' ? 'cfgReuse' : 'cfgFresh') });
|
|
3222
|
+
};
|
|
3223
|
+
const inlineBinding = (group, key, field, value) => {
|
|
3224
|
+
const next = cloneConfig(draft);
|
|
3225
|
+
if (field === 'session') applySessionRole(next, group === 'routes' ? 'execute' : key, value);
|
|
3226
|
+
else if (group === 'panel') next['judge-panel'][field] = value;
|
|
3227
|
+
else next[group][key][field] = value;
|
|
3228
|
+
replace(next);
|
|
3229
|
+
};
|
|
3230
|
+
const saveBinding = () => {
|
|
3231
|
+
const { group, key, role, value, permission, session } = bindingEdit,
|
|
3232
|
+
next = cloneConfig(draft);
|
|
3233
|
+
if (group === 'panel') next['judge-panel'] = value;
|
|
3234
|
+
else next[group][key] = value;
|
|
3235
|
+
if (value.channel && !next.channels?.[value.channel])
|
|
3236
|
+
next.channels = {
|
|
3237
|
+
...next.channels,
|
|
3238
|
+
[value.channel]: doc.defaults.channels?.[value.channel] ?? { 'env-key': null },
|
|
3239
|
+
};
|
|
3240
|
+
if (permission !== (next.permissions?.bindings?.[bindingID(group, key)] ?? 'inherit')) {
|
|
3241
|
+
const bindings = { ...next.permissions?.bindings };
|
|
3242
|
+
if (permission === 'inherit') delete bindings[bindingID(group, key)];
|
|
3243
|
+
else bindings[bindingID(group, key)] = permission;
|
|
3244
|
+
next.permissions = { ...next.permissions, bindings };
|
|
3245
|
+
}
|
|
3246
|
+
if (session !== (next.sessions?.roles?.[role] ?? 'inherit')) applySessionRole(next, role, session);
|
|
3247
|
+
replace(next);
|
|
3248
|
+
setBindingEdit(null);
|
|
3249
|
+
};
|
|
3250
|
+
const patchBinding = (key, value) =>
|
|
3251
|
+
setBindingEdit((old) => ({ ...old, value: { ...old.value, [key]: value } }));
|
|
3252
|
+
const dispatchReasons = [];
|
|
3253
|
+
if ((draft?.scheduler?.mode ?? 'serial') !== 'parallel') dispatchReasons.push(t('cfgDispatchSerial'));
|
|
3254
|
+
if (numberErrors['scheduler.max-parallel'] || fieldErrors['scheduler.max-parallel']) dispatchReasons.push(t('cfgDispatchInvalid'));
|
|
3255
|
+
else if ((draft?.scheduler?.['max-parallel'] ?? 2) <= 1) dispatchReasons.push(t('cfgDispatchSingle'));
|
|
3256
|
+
if (draft?.worktree?.enabled === false) dispatchReasons.push(t('cfgDispatchUnmanaged'));
|
|
3257
|
+
const readyQueue = draft?.scheduler?.dispatch === 'ready-queue';
|
|
3258
|
+
const dispatchStatus = !readyQueue ? 'batch' : dispatchReasons.length ? 'unavailable' : 'conditional';
|
|
3259
|
+
let fields = null;
|
|
3260
|
+
if (draft && section === 'strategy')
|
|
3261
|
+
fields = h(
|
|
3262
|
+
React.Fragment,
|
|
3263
|
+
null,
|
|
3264
|
+
field(
|
|
3265
|
+
'sessions.mode',
|
|
3266
|
+
t('consoleSession'),
|
|
3267
|
+
t('cfgSessionHelp'),
|
|
3268
|
+
select(
|
|
3269
|
+
'sessions.mode',
|
|
3270
|
+
draft.sessions?.mode ?? 'fresh',
|
|
3271
|
+
[
|
|
3272
|
+
['scoped', t('consoleScoped')],
|
|
3273
|
+
['fresh', t('consoleFresh')],
|
|
3274
|
+
],
|
|
3275
|
+
(v) => replace({ ...draft, sessions: { ...draft.sessions, mode: v, 'policy-version': 1 } }),
|
|
3276
|
+
),
|
|
3277
|
+
),
|
|
3278
|
+
field('worktree.enabled', t('cfgWorktree'), t('cfgWorktreeHelp'),
|
|
3279
|
+
toggle('worktree.enabled', draft.worktree?.enabled !== false,
|
|
3280
|
+
(enabled) => update('worktree', 'enabled', enabled))),
|
|
3281
|
+
field(
|
|
3282
|
+
'scheduler.mode',
|
|
3283
|
+
t('consoleScheduling'),
|
|
3284
|
+
t('cfgScheduleHelp'),
|
|
3285
|
+
select(
|
|
3286
|
+
'scheduler.mode',
|
|
3287
|
+
draft.scheduler?.mode ?? 'serial',
|
|
3288
|
+
[
|
|
3289
|
+
['serial', t('consoleSerial')],
|
|
3290
|
+
['parallel', t('consoleParallel')],
|
|
3291
|
+
],
|
|
3292
|
+
(v) => {
|
|
3293
|
+
if (v === 'serial') clearNumberDrafts(path => path === 'scheduler.max-parallel');
|
|
3294
|
+
replace({ ...draft, scheduler: { 'max-parallel': 2, ...draft.scheduler, mode: v } });
|
|
3295
|
+
},
|
|
3296
|
+
),
|
|
3297
|
+
),
|
|
3298
|
+
draft.scheduler?.mode === 'parallel' &&
|
|
3299
|
+
field(
|
|
3300
|
+
'scheduler.max-parallel',
|
|
3301
|
+
t('consoleConcurrency'),
|
|
3302
|
+
t('cfgParallelHelp'),
|
|
3303
|
+
number('scheduler', 'max-parallel', 1, t('cfgTasks')),
|
|
3304
|
+
),
|
|
3305
|
+
field('scheduler.dispatch', t('cfgDispatch'),
|
|
3306
|
+
h(React.Fragment, null, t('cfgDispatchHelp'),
|
|
3307
|
+
h('span', { className: css.configHintLine }, t('cfgDispatchEligibility')),
|
|
3308
|
+
h('span', { className: css.configHintLine, role: 'status', 'data-dispatch-status': dispatchStatus },
|
|
3309
|
+
t(dispatchStatus === 'batch' ? 'cfgDispatchBatchState' : dispatchStatus === 'conditional' ? 'cfgDispatchReadyState' : 'cfgDispatchUnavailable',
|
|
3310
|
+
{ reasons: dispatchReasons.join(t('cfgReasonSeparator')) }))),
|
|
3311
|
+
select('scheduler.dispatch', draft.scheduler?.dispatch ?? 'batch',
|
|
3312
|
+
[['batch', t('cfgDispatchBatch')], ['ready-queue', t('cfgDispatchReady')]],
|
|
3313
|
+
(v) => update('scheduler', 'dispatch', v))),
|
|
3314
|
+
field('scheduler.prewarm', t('cfgPrewarm'), t('cfgPrewarmHelp'),
|
|
3315
|
+
toggle('scheduler.prewarm', draft.scheduler?.prewarm === true,
|
|
3316
|
+
(enabled) => update('scheduler', 'prewarm', enabled))),
|
|
3317
|
+
field(
|
|
3318
|
+
'permissions.mode',
|
|
3319
|
+
t('permission'),
|
|
3320
|
+
t('cfgPermissionHelp'),
|
|
3321
|
+
select(
|
|
3322
|
+
'permissions.mode',
|
|
3323
|
+
draft.permissions?.mode ?? 'role-based',
|
|
3324
|
+
[
|
|
3325
|
+
['role-based', t('consoleRolePermissions')],
|
|
3326
|
+
['full-access', t('permissionFull')],
|
|
3327
|
+
],
|
|
3328
|
+
(v) => update('permissions', 'mode', v),
|
|
3329
|
+
),
|
|
3330
|
+
),
|
|
3331
|
+
);
|
|
3332
|
+
if (draft && section === 'limits')
|
|
3333
|
+
fields = h(
|
|
3334
|
+
React.Fragment,
|
|
3335
|
+
null,
|
|
3336
|
+
field('budget.enforce', t('cfgBudgetPolicy'),
|
|
3337
|
+
t((draft.budget?.enforce ?? 'soft') === 'off' ? 'cfgBudgetOffHelp' : (draft.budget?.enforce ?? 'soft') === 'hard' ? 'cfgBudgetHardHelp' : 'cfgBudgetSoftHelp'),
|
|
3338
|
+
select('budget.enforce', draft.budget?.enforce ?? 'soft',
|
|
3339
|
+
[['off', t('cfgBudgetOff')], ['soft', t('cfgBudgetSoft')], ['hard', t('cfgBudgetHard')]],
|
|
3340
|
+
value => {
|
|
3341
|
+
if (value === 'off') {
|
|
3342
|
+
clearNumberDrafts(path => path === 'budget.daily-token-limit');
|
|
3343
|
+
}
|
|
3344
|
+
update('budget', 'enforce', value);
|
|
3345
|
+
})),
|
|
3346
|
+
draft.budget?.enforce !== 'off' && field('budget.daily-token-limit', t('consoleBudget'),
|
|
3347
|
+
t('cfgBudgetDay'), number('budget', 'daily-token-limit', 1, 'Token', true)),
|
|
3348
|
+
h('h4', { className: css.settingsSubheading }, t('cfgRunProtection')),
|
|
3349
|
+
field(
|
|
3350
|
+
'watchdog.wall-timeout-ms',
|
|
3351
|
+
t('cfgTimeout'),
|
|
3352
|
+
t('cfgTimeoutHelp'),
|
|
3353
|
+
number('watchdog', 'wall-timeout-ms', 60000, t('cfgMinutes')),
|
|
3354
|
+
),
|
|
3355
|
+
field(
|
|
3356
|
+
'retry.max-transport-retries',
|
|
3357
|
+
t('consoleRetries'),
|
|
3358
|
+
t('cfgRetryHelp'),
|
|
3359
|
+
number('retry', 'max-transport-retries', 1, t('cfgTimes')),
|
|
3360
|
+
),
|
|
3361
|
+
);
|
|
3362
|
+
if (draft && section === 'models')
|
|
3363
|
+
fields = h(React.Fragment, null,
|
|
3364
|
+
h('p', { className: css.meta }, t('cfgSessionDefault', { mode: t(draft.sessions?.mode === 'scoped' ? 'cfgReuse' : 'cfgFresh') })),
|
|
3365
|
+
h('div', { className: css.stageTableScroll }, h('table', { className: css.stageTable, 'aria-label': t('cfgModels') },
|
|
3366
|
+
h('thead', null, h('tr', null, ...['cfgModelRole', 'consoleResource', 'consoleModel', 'consoleThinking', 'consoleSession'].map(key => h('th', { key, scope: 'col' }, t(key))))),
|
|
3367
|
+
h('tbody', null, ...rows.map(([group, key, value]) => {
|
|
3368
|
+
const role = group === 'routes' ? 'execute' : key;
|
|
3369
|
+
const sessionChoices = [['inherit', roleSession(role)], ['scoped', t('cfgReuse')], ['fresh', t('cfgFresh')]];
|
|
3370
|
+
const policy = draft.sessions?.roles?.[role];
|
|
3371
|
+
const planConflict = role === 'plan' && policy && (policy === 'scoped' && value['resident-session'] === false || policy === 'fresh' && (value.harness || draft.profiles?.[value.profile]?.harness) === 'kimi' && value['resident-session'] === true);
|
|
3372
|
+
return h('tr', { key: bindingID(group, key), 'data-binding-id': bindingID(group, key) },
|
|
3373
|
+
h('th', { scope: 'row' }, roleName(group, key)),
|
|
3374
|
+
h('td', null, h(Button, { disabled: busy, onClick: () => openBinding(group, key, value), 'aria-label': t('cfgEdit') + ' ' + roleName(group, key) }, resourceNames[resourceOf(value)])),
|
|
3375
|
+
h('td', null, h('input', { className: css.editorInput, value: value.model ?? '', placeholder: value.profile || t('cfgEffortDefault'), disabled: busy, 'aria-label': roleName(group, key) + ' · ' + t('consoleModel'), onChange: e => inlineBinding(group, key, 'model', e.target.value) })),
|
|
3376
|
+
h('td', null, h('select', { className: css.modeSelect, value: value.thinking ?? '', disabled: busy, 'aria-label': roleName(group, key) + ' · ' + t('consoleThinking'), onChange: e => inlineBinding(group, key, 'thinking', e.target.value) }, ...[...new Set([...Object.keys(efforts), value.thinking ?? ''])].map(effort => h('option', { key: effort, value: effort }, efforts[effort] ?? effort)))),
|
|
3377
|
+
h('td', null, ['intake', 'garden'].includes(role) ? t('cfgIndependent') : h('select', { className: css.modeSelect, disabled: busy, value: draft.sessions?.roles?.[role] ?? 'inherit', 'aria-label': roleName(group, key) + ' · ' + t('consoleSession'), onChange: e => inlineBinding(group, key, 'session', e.target.value) }, ...sessionChoices.map(([v, label]) => h('option', { key: v, value: v }, label))), planConflict && h('div', { className: css.settingsHelp }, t('cfgPlanConflict'), h(Button, { disabled: busy, onClick: () => inlineBinding(group, key, 'session', policy) }, t('cfgApplyPolicy')))));
|
|
3378
|
+
})))),
|
|
3379
|
+
h('p', { className: css.settingsHelp }, t('consoleRouteSession')));
|
|
3380
|
+
if (draft && section === 'jev')
|
|
3381
|
+
fields = h(
|
|
3382
|
+
React.Fragment,
|
|
3383
|
+
null,
|
|
3384
|
+
h('p', { className: css.meta }, t('cfgJevHelp')),
|
|
3385
|
+
field(
|
|
3386
|
+
'jev.enabled',
|
|
3387
|
+
t('consoleJEV'),
|
|
3388
|
+
t('cfgJevChanges'),
|
|
3389
|
+
toggle('jev.enabled', draft.jev?.enabled === true, changeJev, draft.jev !== undefined && !record(draft.jev)),
|
|
3390
|
+
),
|
|
3391
|
+
discardedJevInputs && h('p', { className: css.configCallout, role: 'status' }, t('cfgJevDraftDiscarded')),
|
|
3392
|
+
draft.jev?.enabled &&
|
|
3393
|
+
h(
|
|
3394
|
+
React.Fragment,
|
|
3395
|
+
null,
|
|
3396
|
+
h('p', { className: css.configCallout }, t('cfgJevSend')),
|
|
3397
|
+
field(
|
|
3398
|
+
'jev.endpoint',
|
|
3399
|
+
t('cfgJevEndpoint'),
|
|
3400
|
+
null,
|
|
3401
|
+
input('jev.endpoint', draft.jev.endpoint, (v) => update('jev', 'endpoint', v), v => jevTextError('endpoint', v)),
|
|
3402
|
+
),
|
|
3403
|
+
field(
|
|
3404
|
+
'jev.model',
|
|
3405
|
+
t('consoleJEVVersion'),
|
|
3406
|
+
null,
|
|
3407
|
+
input('jev.model', draft.jev.model ?? 'jev-1.14.0', (v) => update('jev', 'model', v), v => jevTextError('model', v)),
|
|
3408
|
+
),
|
|
3409
|
+
field(
|
|
3410
|
+
'jev.key-env',
|
|
3411
|
+
t('consoleJEVEnv'),
|
|
3412
|
+
t('cfgJevKeyHelp'),
|
|
3413
|
+
input('jev.key-env', draft.jev['key-env'] ?? 'MEROUTER_API_KEY', (v) =>
|
|
3414
|
+
update('jev', 'key-env', v), v => jevTextError('key-env', v),
|
|
3415
|
+
),
|
|
3416
|
+
),
|
|
3417
|
+
h(
|
|
3418
|
+
'section',
|
|
3419
|
+
{ className: css.details },
|
|
3420
|
+
h('h4', null, t('cfgJevAdvanced')),
|
|
3421
|
+
field('jev.timeout-ms', t('cfgJevTimeout'), null, number('jev', 'timeout-ms', 1000, t('cfgSeconds'))),
|
|
3422
|
+
field('jev.max-calls', t('cfgJevMaxCalls'), null, number('jev', 'max-calls', 1, t('cfgTimes'))),
|
|
3423
|
+
field('jev.min-applicability', t('cfgJevApplicability'), null, number('jev', 'min-applicability', 1, '', false, true)),
|
|
3424
|
+
field('jev.min-choice', t('cfgJevChoice'), null, number('jev', 'min-choice', 1, '', false, true)),
|
|
3425
|
+
h('p', { className: css.meta }, t('cfgJevData') + ': ' + (Array.isArray(draft.jev['allowed-data']) ? draft.jev['allowed-data'].length ? draft.jev['allowed-data'].join(', ') : t('cfgJevNoData') : t('notConfigured'))),
|
|
3426
|
+
h('p', { className: css.meta }, t('cfgJevDataHelp')),
|
|
3427
|
+
),
|
|
3428
|
+
),
|
|
3429
|
+
);
|
|
3430
|
+
return h(
|
|
3431
|
+
'section',
|
|
3432
|
+
{ className: css.editor, 'aria-label': t('consoleConfig') },
|
|
3433
|
+
h(
|
|
3434
|
+
'div',
|
|
3435
|
+
{ className: css.configHeader },
|
|
3436
|
+
onClose && h(Button, { onClick: () => deferDiscard(() => { discard(); onClose(); }), disabled: busy }, t('cfgBack')),
|
|
3437
|
+
h('h2', { className: css.configTitle }, t('consoleConfig')),
|
|
3438
|
+
h(
|
|
3439
|
+
'div',
|
|
3440
|
+
{ className: css.segmented, 'aria-label': t('consoleScope') },
|
|
3441
|
+
...['global', 'project'].map((key) =>
|
|
3442
|
+
h(
|
|
3443
|
+
Button,
|
|
3444
|
+
{ key, disabled: busy, 'aria-pressed': scope === key, onClick: () => switchScope(key) },
|
|
3445
|
+
t(key === 'global' ? 'consoleGlobal' : 'consoleProject'),
|
|
3446
|
+
),
|
|
3447
|
+
),
|
|
3448
|
+
),
|
|
3449
|
+
),
|
|
3450
|
+
h(
|
|
3451
|
+
'nav',
|
|
3452
|
+
{ className: css.configNav, 'aria-label': t('consoleConfig') },
|
|
3453
|
+
...['strategy', 'models', 'limits', 'jev', 'advanced'].map((key) =>
|
|
3454
|
+
h(
|
|
3455
|
+
Button,
|
|
3456
|
+
{ key, 'aria-pressed': section === key, onClick: () => setSection(key) },
|
|
3457
|
+
t({ strategy: 'cfgStrategy', models: 'cfgModels', limits: 'cfgLimits', jev: 'cfgJev', advanced: 'cfgAdvanced' }[key]),
|
|
3458
|
+
),
|
|
3459
|
+
),
|
|
3460
|
+
),
|
|
3461
|
+
h(
|
|
3462
|
+
'div',
|
|
3463
|
+
{ className: css.configMain },
|
|
3464
|
+
h(
|
|
3465
|
+
'div',
|
|
3466
|
+
{ className: css.configContent, 'data-section': section },
|
|
3467
|
+
h(
|
|
3468
|
+
'h3',
|
|
3469
|
+
{ className: css.configTitle },
|
|
3470
|
+
t({ strategy: 'cfgStrategy', models: 'cfgModels', limits: 'cfgLimits', jev: 'cfgJev', advanced: 'cfgAdvanced' }[section]),
|
|
3471
|
+
),
|
|
3472
|
+
(!draft || doc?.scope !== scope) &&
|
|
3473
|
+
busy &&
|
|
3474
|
+
h(
|
|
3475
|
+
'div',
|
|
3476
|
+
{ className: css.configLoading, role: 'status', 'aria-label': t('wbLoading') },
|
|
3477
|
+
h(Spinner),
|
|
3478
|
+
),
|
|
3479
|
+
!busy &&
|
|
3480
|
+
(!draft || control.error?.action === 'config-edit') &&
|
|
3481
|
+
attempt.current === scope &&
|
|
3482
|
+
h(
|
|
3483
|
+
'div',
|
|
3484
|
+
{ role: 'alert', className: css.configCallout },
|
|
3485
|
+
h('p', null, t('cfgReadError')),
|
|
3486
|
+
h(Button, { onClick: () => deferDiscard(load) }, t('cfgReload')),
|
|
3487
|
+
control.error?.detail &&
|
|
3488
|
+
h(
|
|
3489
|
+
'section',
|
|
3490
|
+
{ className: css.details },
|
|
3491
|
+
h('h4', null, t('errorDetails')),
|
|
3492
|
+
h('pre', { className: css.raw }, control.error.detail),
|
|
3493
|
+
),
|
|
3494
|
+
),
|
|
3495
|
+
draft &&
|
|
3496
|
+
doc?.scope === scope &&
|
|
3497
|
+
h(
|
|
3498
|
+
React.Fragment,
|
|
3499
|
+
null,
|
|
3500
|
+
section !== 'jev' && h('p', { className: css.configTiming }, t(section === 'advanced' ? 'cfgAdvancedTiming' : 'cfgNextStart')),
|
|
3501
|
+
section !== 'advanced' && h('fieldset', { className: css.configFields, disabled: invalid }, fields),
|
|
3502
|
+
h(
|
|
3503
|
+
'section',
|
|
3504
|
+
{ className: css.configSource, hidden: section !== 'advanced' },
|
|
3505
|
+
h('h4', null, t('cfgLocation')),
|
|
3506
|
+
h('p', { className: css.meta }, t('cfgScopeHelp')),
|
|
3507
|
+
h('p', null, t(doc.source === doc.target ? 'cfgFileSource' : 'cfgTemplateSource')),
|
|
3508
|
+
h('code', { className: css.path }, doc.target),
|
|
3509
|
+
),
|
|
3510
|
+
h(
|
|
3511
|
+
'section',
|
|
3512
|
+
{ className: css.configSource, hidden: section !== 'advanced' },
|
|
3513
|
+
h('h4', null, t('cfgJson')),
|
|
3514
|
+
h('textarea', {
|
|
3515
|
+
className: css.editorJSON,
|
|
3516
|
+
value: raw,
|
|
3517
|
+
disabled: busy,
|
|
3518
|
+
'aria-label': t('consoleValues'),
|
|
3519
|
+
'aria-invalid': invalid || Object.keys(fieldErrors).length > 0,
|
|
3520
|
+
onChange: (e) => {
|
|
3521
|
+
setRaw(e.target.value);
|
|
3522
|
+
try {
|
|
3523
|
+
const value = JSON.parse(e.target.value);
|
|
3524
|
+
if (!record(value) || Object.keys(base ?? {}).some((key) => !Object.hasOwn(value, key)))
|
|
3525
|
+
throw new Error();
|
|
3526
|
+
setDraft(value);
|
|
3527
|
+
setInvalid(false);
|
|
3528
|
+
setNumbers({});
|
|
3529
|
+
setNumberErrors({});
|
|
3530
|
+
setTexts({});
|
|
3531
|
+
setTextErrors({});
|
|
3532
|
+
setDiscardedJevInputs(false);
|
|
3533
|
+
setSaved(false);
|
|
3534
|
+
} catch {
|
|
3535
|
+
setInvalid(true);
|
|
3536
|
+
}
|
|
3537
|
+
},
|
|
3538
|
+
}),
|
|
3539
|
+
),
|
|
3540
|
+
section === 'advanced' && (changed.length > 0 || reset) &&
|
|
3541
|
+
h(
|
|
3542
|
+
'section',
|
|
3543
|
+
{ className: css.configSource },
|
|
3544
|
+
h('h4', null, t('cfgRawDiff')),
|
|
3545
|
+
reset && h('p', { className: css.warningText }, t('cfgResetPending')),
|
|
3546
|
+
...changed.map((key) =>
|
|
3547
|
+
h(
|
|
3548
|
+
'div',
|
|
3549
|
+
{ key },
|
|
3550
|
+
h('h4', { className: css.heading }, key),
|
|
3551
|
+
h(
|
|
3552
|
+
'pre',
|
|
3553
|
+
{ className: css.raw },
|
|
3554
|
+
JSON.stringify({ before: base?.[key], after: draft[key] }, null, 2),
|
|
3555
|
+
),
|
|
3556
|
+
),
|
|
3557
|
+
),
|
|
3558
|
+
),
|
|
3559
|
+
),
|
|
3560
|
+
section === 'advanced' && h('div', { className: css.actions }, h(Button, { disabled: busy, onClick: () => deferDiscard(load) }, t('cfgReload'))),
|
|
3561
|
+
diagnostics && h('div', { hidden: section !== 'advanced' }, diagnostics),
|
|
3562
|
+
(Object.values(numberErrors).some(Boolean) || Object.values(textErrors).some(Boolean)) &&
|
|
3563
|
+
h('p', { role: 'alert', className: css.errorText }, t('cfgInvalid')),
|
|
3564
|
+
(invalid || Object.keys(fieldErrors).length > 0) && h('div', { className: css.configCallout, role: 'alert' },
|
|
3565
|
+
h('p', null, t(invalid ? 'consoleInvalid' : 'cfgStructuredInvalid')),
|
|
3566
|
+
Object.keys(fieldErrors).length > 0 && h('p', { className: css.meta }, Object.keys(fieldErrors).join(', ')),
|
|
3567
|
+
section !== 'advanced' && h(Button, { onClick: () => setSection('advanced') }, t('cfgFixJSON'))),
|
|
3568
|
+
(control.configUncertainty || conflict) &&
|
|
3569
|
+
h(
|
|
3570
|
+
'div',
|
|
3571
|
+
{ className: css.configCallout, role: 'alert' },
|
|
3572
|
+
h('p', null, t(conflict ? 'cfgConflict' : 'consoleReload')),
|
|
3573
|
+
h(Button, { disabled: busy, onClick: () => deferDiscard(load) }, t('cfgReload')),
|
|
3574
|
+
),
|
|
3575
|
+
),
|
|
3576
|
+
),
|
|
3577
|
+
h('footer', { className: css.configSaveBar, 'data-dirty': dirty },
|
|
3578
|
+
h('span', { className: css.meta, role: 'status' }, t(saved ? 'cfgSaved' : dirty ? 'cfgDirty' : 'cfgClean')),
|
|
3579
|
+
h('div', { className: css.actions },
|
|
3580
|
+
h(Button, { disabled: busy || !doc || doc.scope !== scope, onClick: () => setDialog({ kind: 'reset' }) }, t('cfgReset')),
|
|
3581
|
+
h(Button, { disabled: busy || !dirty || !base, onClick: discard }, t('cfgDiscard')),
|
|
3582
|
+
h(Button, { variant: 'primary', disabled: !ready || !dirty || busy || control.configUncertainty || !!control.uncertainty, onClick: save }, t('cfgSave')))),
|
|
3583
|
+
dialog &&
|
|
3584
|
+
h(
|
|
3585
|
+
WorkbenchDialog,
|
|
3586
|
+
{
|
|
3587
|
+
t,
|
|
3588
|
+
title: t(
|
|
3589
|
+
dialog.kind === 'reset' ? 'cfgReset' : 'cfgUnsaved',
|
|
3590
|
+
),
|
|
3591
|
+
onClose: () => setDialog(null),
|
|
3592
|
+
actions: h(
|
|
3593
|
+
React.Fragment,
|
|
3594
|
+
null,
|
|
3595
|
+
h(Button, { onClick: () => setDialog(null) }, t('wbCancel')),
|
|
3596
|
+
h(
|
|
3597
|
+
Button,
|
|
3598
|
+
{
|
|
3599
|
+
variant: 'primary',
|
|
3600
|
+
disabled: busy,
|
|
3601
|
+
onClick: () => {
|
|
3602
|
+
const next = dialog;
|
|
3603
|
+
setDialog(null);
|
|
3604
|
+
if (next.kind === 'reset') {
|
|
3605
|
+
replace(cloneConfig(doc.defaults));
|
|
3606
|
+
setNumbers({});
|
|
3607
|
+
setNumberErrors({});
|
|
3608
|
+
setTexts({});
|
|
3609
|
+
setTextErrors({});
|
|
3610
|
+
setDiscardedJevInputs(false);
|
|
3611
|
+
setReset(true);
|
|
3612
|
+
} else next.next();
|
|
3613
|
+
},
|
|
3614
|
+
},
|
|
3615
|
+
t(
|
|
3616
|
+
dialog.kind === 'reset' ? 'cfgLoadDefaults' : 'cfgDiscardContinue',
|
|
3617
|
+
),
|
|
3618
|
+
),
|
|
3619
|
+
),
|
|
3620
|
+
},
|
|
3621
|
+
h(
|
|
3622
|
+
'p',
|
|
3623
|
+
null,
|
|
3624
|
+
t(
|
|
3625
|
+
dialog.kind === 'reset' ? 'cfgResetHelp' : 'cfgDiscardHelp',
|
|
3626
|
+
),
|
|
3627
|
+
),
|
|
3628
|
+
dialog.kind === 'discard' && h('pre', { className: css.raw }, raw),
|
|
3629
|
+
),
|
|
3630
|
+
bindingEdit &&
|
|
3631
|
+
h(
|
|
3632
|
+
WorkbenchDialog,
|
|
3633
|
+
{
|
|
3634
|
+
t,
|
|
3635
|
+
title: t('cfgBinding') + ' · ' + roleName(bindingEdit.group, bindingEdit.key),
|
|
3636
|
+
onClose: () => setBindingEdit(null),
|
|
3637
|
+
actions: h(
|
|
3638
|
+
React.Fragment,
|
|
3639
|
+
null,
|
|
3640
|
+
h(Button, { onClick: () => setBindingEdit(null) }, t('wbCancel')),
|
|
3641
|
+
h(Button, { variant: 'primary', disabled: busy, onClick: saveBinding }, t('cfgApplyBinding')),
|
|
3642
|
+
),
|
|
3643
|
+
},
|
|
3644
|
+
field(
|
|
3645
|
+
'binding.resource',
|
|
3646
|
+
t('consoleResource'),
|
|
3647
|
+
null,
|
|
3648
|
+
select(
|
|
3649
|
+
'binding.resource',
|
|
3650
|
+
resourceOf(bindingEdit.value),
|
|
3651
|
+
Object.entries(resourceNames),
|
|
3652
|
+
(value) => {
|
|
3653
|
+
const next = preset(value);
|
|
3654
|
+
if (next)
|
|
3655
|
+
setBindingEdit((old) => {
|
|
3656
|
+
const value = cloneConfig(next);
|
|
3657
|
+
if (old.group !== 'loops' || old.key !== 'plan') delete value['resident-session'];
|
|
3658
|
+
return { ...old, value };
|
|
3659
|
+
});
|
|
3660
|
+
},
|
|
3661
|
+
),
|
|
3662
|
+
),
|
|
3663
|
+
field(
|
|
3664
|
+
'binding.model',
|
|
3665
|
+
t('consoleModel'),
|
|
3666
|
+
t('cfgModelHint'),
|
|
3667
|
+
input('binding.model', bindingEdit.value.model, (v) => patchBinding('model', v)),
|
|
3668
|
+
),
|
|
3669
|
+
field(
|
|
3670
|
+
'binding.thinking',
|
|
3671
|
+
t('consoleThinking'),
|
|
3672
|
+
null,
|
|
3673
|
+
select(
|
|
3674
|
+
'binding.thinking',
|
|
3675
|
+
bindingEdit.value.thinking ?? '',
|
|
3676
|
+
Object.entries(efforts).concat(
|
|
3677
|
+
bindingEdit.value.thinking && !Object.hasOwn(efforts, bindingEdit.value.thinking)
|
|
3678
|
+
? [[bindingEdit.value.thinking, bindingEdit.value.thinking]]
|
|
3679
|
+
: [],
|
|
3680
|
+
),
|
|
3681
|
+
(v) => {
|
|
3682
|
+
if (v) patchBinding('thinking', v);
|
|
3683
|
+
else
|
|
3684
|
+
setBindingEdit((old) => {
|
|
3685
|
+
const value = { ...old.value };
|
|
3686
|
+
delete value.thinking;
|
|
3687
|
+
return { ...old, value };
|
|
3688
|
+
});
|
|
3689
|
+
},
|
|
3690
|
+
),
|
|
3691
|
+
),
|
|
3692
|
+
field(
|
|
3693
|
+
'binding.permission',
|
|
3694
|
+
t('permission'),
|
|
3695
|
+
t('consolePermissionEffective'),
|
|
3696
|
+
select(
|
|
3697
|
+
'binding.permission',
|
|
3698
|
+
bindingEdit.permission,
|
|
3699
|
+
[
|
|
3700
|
+
['inherit', t('consoleInherit')],
|
|
3701
|
+
['read-only', t('permissionReadOnly')],
|
|
3702
|
+
['workspace-write', t('permissionWorkspace')],
|
|
3703
|
+
['danger-full-access', t('permissionFull')],
|
|
3704
|
+
],
|
|
3705
|
+
(v) => setBindingEdit((old) => ({ ...old, permission: v })),
|
|
3706
|
+
),
|
|
3707
|
+
),
|
|
3708
|
+
!['intake', 'garden'].includes(bindingEdit.key) &&
|
|
3709
|
+
field(
|
|
3710
|
+
'binding.session',
|
|
3711
|
+
t('consoleSession'),
|
|
3712
|
+
bindingEdit.group === 'routes' ? t('consoleRouteSession') : t('consoleScopeReason'),
|
|
3713
|
+
select(
|
|
3714
|
+
'binding.session',
|
|
3715
|
+
bindingEdit.session,
|
|
3716
|
+
[
|
|
3717
|
+
['inherit', t('consoleInherit')],
|
|
3718
|
+
['scoped', t('consoleScoped')],
|
|
3719
|
+
['fresh', t('consoleFresh')],
|
|
3720
|
+
],
|
|
3721
|
+
(v) => setBindingEdit((old) => ({ ...old, session: v })),
|
|
3722
|
+
),
|
|
3723
|
+
),
|
|
3724
|
+
h(
|
|
3725
|
+
'section',
|
|
3726
|
+
{ className: css.details },
|
|
3727
|
+
h('h4', null, t('cfgBindingAdvanced')),
|
|
3728
|
+
field(
|
|
3729
|
+
'binding.harness',
|
|
3730
|
+
t('consoleAdapter'),
|
|
3731
|
+
null,
|
|
3732
|
+
input('binding.harness', bindingEdit.value.harness, (v) => patchBinding('harness', v)),
|
|
3733
|
+
),
|
|
3734
|
+
field(
|
|
3735
|
+
'binding.provider',
|
|
3736
|
+
t('cfgProviderID'),
|
|
3737
|
+
null,
|
|
3738
|
+
input('binding.provider', bindingEdit.value.provider, (v) => patchBinding('provider', v)),
|
|
3739
|
+
),
|
|
3740
|
+
field(
|
|
3741
|
+
'binding.channel',
|
|
3742
|
+
t('consoleChannel'),
|
|
3743
|
+
null,
|
|
3744
|
+
input('binding.channel', bindingEdit.value.channel, (v) => patchBinding('channel', v)),
|
|
3745
|
+
),
|
|
3746
|
+
),
|
|
3747
|
+
),
|
|
3748
|
+
);
|
|
3749
|
+
}
|
|
3750
|
+
|
|
3751
|
+
Object.assign(zh, {
|
|
3752
|
+
consoleConfig: '设置', cfgStrategy: '运行方式', cfgModels: '模型与会话', cfgJev: 'JEV', cfgAdvanced: '高级与诊断', cfgBack: '← 返回流程',
|
|
3753
|
+
cfgSessionHelp: '各角色独立,可按阶段覆盖', consoleSession: '会话策略', cfgReuse: '按角色复用', cfgFresh: '每次新建', cfgInheritMode: '继承 · {mode}', cfgSessionDefault: '默认:{mode}', cfgIndependent: '独立调用',
|
|
3754
|
+
cfgScheduleHelp: '', cfgParallelHelp: '并发数为 1–8;Review 与集成仍串行', cfgPermissionHelp: '完全权限可访问工作区以外文件',
|
|
3755
|
+
cfgBudgetPolicy: '用量预算', cfgBudgetOff: '关闭', cfgBudgetSoft: '仅提醒', cfgBudgetHard: '达到后停止',
|
|
3756
|
+
cfgBudgetOffHelp: '不设 Token 门限,继续统计用量', cfgBudgetSoftHelp: '达到额度时提醒,任务继续运行', cfgBudgetHardHelp: '达到额度或用量无法确认时,暂停新调用', cfgBudgetDay: '按单个任务、UTC 日期统计;留空不设额度', consoleBudget: '单任务每日 Token 额度',
|
|
3757
|
+
cfgRunProtection: '运行保护', cfgRetryHelp: '传输故障重试,不影响任务返修次数', cfgTimeoutHelp: '', cfgTimeout: '单次运行时限',
|
|
3758
|
+
cfgClean: '没有待保存修改', cfgDirty: '有未保存修改', cfgSaved: '配置已保存', cfgReset: '恢复默认', cfgDiscard: '撤销',
|
|
3759
|
+
});
|
|
3760
|
+
Object.assign(en, {
|
|
3761
|
+
consoleConfig: 'Settings', cfgStrategy: 'Execution', cfgModels: 'Models and sessions', cfgJev: 'JEV', cfgAdvanced: 'Advanced and diagnostics', cfgBack: '← Back to workflow',
|
|
3762
|
+
cfgSessionHelp: 'Isolated roles; stages can override the default', cfgReuse: 'Reuse within role', cfgFresh: 'New each time', cfgInheritMode: 'Inherit · {mode}', cfgSessionDefault: 'Default: {mode}', cfgIndependent: 'Independent call',
|
|
3763
|
+
cfgScheduleHelp: '', cfgParallelHelp: 'Limit: 1–8. Review and integration remain serial', cfgPermissionHelp: 'Full access includes files outside the workspace',
|
|
3764
|
+
cfgBudgetPolicy: 'Usage budget', cfgBudgetOff: 'Off', cfgBudgetSoft: 'Warn only', cfgBudgetHard: 'Stop at limit', cfgBudgetOffHelp: 'No token limit; usage is still recorded', cfgBudgetSoftHelp: 'Warn at the limit and keep running', cfgBudgetHardHelp: 'Pause new calls at the limit or when usage is unknown', cfgBudgetDay: 'Per task and UTC day; blank means no limit', consoleBudget: 'Daily tokens per task',
|
|
3765
|
+
cfgRunProtection: 'Run protection', cfgRetryHelp: 'Transport faults only; task repair limits stay unchanged', cfgTimeoutHelp: '', cfgTimeout: 'Run time limit',
|
|
3766
|
+
cfgClean: 'No pending changes', cfgDirty: 'Unsaved changes', cfgSaved: 'Configuration saved', cfgReset: 'Restore defaults', cfgDiscard: 'Discard',
|
|
3767
|
+
});
|
|
3768
|
+
|
|
3769
|
+
Object.assign(zh, { cfgPlanConflict: 'Plan 旧历史开关与此策略不同', cfgApplyPolicy: '同步此策略' });
|
|
3770
|
+
Object.assign(en, { cfgPlanConflict: 'The legacy Plan history switch differs', cfgApplyPolicy: 'Align policy' });
|
|
3771
|
+
|
|
3772
|
+
Object.assign(zh, {
|
|
3773
|
+
runsDetails: '运行详情',
|
|
3774
|
+
runsNoReference: '不设置对照',
|
|
3775
|
+
runsMetricScope: '指标范围',
|
|
3776
|
+
runsSelected: '仅此运行',
|
|
3777
|
+
runsStart: '开始',
|
|
3778
|
+
runsFinish: '结束',
|
|
3779
|
+
runsSnapshot: '配置与契约条件',
|
|
3780
|
+
runsRead: '查看指标',
|
|
3781
|
+
runsNoSelection: '选择一条运行记录',
|
|
3782
|
+
runsFailure: '运行失败原因',
|
|
3783
|
+
runsCurrent: '当前任务',
|
|
3784
|
+
runsMissing: '未报告',
|
|
3785
|
+
runsMissingCount: '缺失 {count} 次',
|
|
3786
|
+
runsNoMetrics: '读取所选运行指标后显示用量与耗时;缺失费用不记作 0',
|
|
3787
|
+
runsTimeline: '阶段时间记录',
|
|
3788
|
+
runsTimelineWall: '已观测经过时间',
|
|
3789
|
+
runsTimelineSum: '阶段耗时累计',
|
|
3790
|
+
runsTimelineSpans: '已闭合 / 未知区间',
|
|
3791
|
+
runsTimelineUnmatched: '未关联时间区间的调用',
|
|
3792
|
+
runsTimelineHelp: '经过时间取已观测区间的并集;并行和嵌套阶段不能直接相加。未闭合区间保持未知,Provider 内部耗时尚不可见。',
|
|
3793
|
+
});
|
|
3794
|
+
Object.assign(en, {
|
|
3795
|
+
runsDetails: 'Run details',
|
|
3796
|
+
runsNoReference: 'No reference',
|
|
3797
|
+
runsMetricScope: 'Metrics scope',
|
|
3798
|
+
runsSelected: 'This run only',
|
|
3799
|
+
runsStart: 'Started',
|
|
3800
|
+
runsFinish: 'Finished',
|
|
3801
|
+
runsSnapshot: 'Configuration and contract conditions',
|
|
3802
|
+
runsRead: 'Inspect metrics',
|
|
3803
|
+
runsNoSelection: 'Select a run',
|
|
3804
|
+
runsFailure: 'Run failure',
|
|
3805
|
+
runsCurrent: 'Current task',
|
|
3806
|
+
runsMissing: 'Not reported',
|
|
3807
|
+
runsMissingCount: 'Missing {count}',
|
|
3808
|
+
runsNoMetrics: 'Read selected-run metrics to inspect usage and duration. Missing cost is not zero',
|
|
3809
|
+
runsTimeline: 'Phase timing',
|
|
3810
|
+
runsTimelineWall: 'Observed elapsed time',
|
|
3811
|
+
runsTimelineSum: 'Sum of phase durations',
|
|
3812
|
+
runsTimelineSpans: 'Closed / unknown intervals',
|
|
3813
|
+
runsTimelineUnmatched: 'Calls without a matched interval',
|
|
3814
|
+
runsTimelineHelp: 'Elapsed time is the union of observed intervals; parallel and nested phases cannot be added together. Open intervals remain unknown. Provider-internal timing is not observed.',
|
|
3815
|
+
});
|
|
3816
|
+
|
|
3817
|
+
function RunExplorer({ control, selection, run, changeSelection, showCanvas, t, active = false }) {
|
|
3818
|
+
const [selected, setSelected] = React.useState(''),
|
|
3819
|
+
[reference, setReference] = React.useState('');
|
|
3820
|
+
const [metricScope, setMetricScope] = React.useState('run');
|
|
3821
|
+
const runs = (control.runs ?? []).filter(item => item.taskDir === selection.taskDir && item.cwd === (control.status?.cwd || selection.cwd)),
|
|
3822
|
+
busy = control.pending !== null;
|
|
3823
|
+
const item =
|
|
3824
|
+
runs.find((r) => r.id === selected) ??
|
|
3825
|
+
runs.find((r) => r.id === control.status?.state?.run?.id) ??
|
|
3826
|
+
runs[0];
|
|
3827
|
+
const baseline = runs.find((r) => r.id === reference);
|
|
3828
|
+
const attempted = React.useRef(false);
|
|
3829
|
+
React.useEffect(() => {
|
|
3830
|
+
if (active && !busy && control.runs === null && !attempted.current) {
|
|
3831
|
+
attempted.current = true;
|
|
3832
|
+
void run('runs', selection);
|
|
3833
|
+
}
|
|
3834
|
+
}, [active, busy, control.runs, run]);
|
|
3835
|
+
const open = async () => {
|
|
3836
|
+
const next = { taskDir: item.taskDir, cwd: item.cwd };
|
|
3837
|
+
changeSelection(next);
|
|
3838
|
+
showCanvas();
|
|
3839
|
+
await run('status', next);
|
|
3840
|
+
};
|
|
3841
|
+
const metrics = () => {
|
|
3842
|
+
const source = item,
|
|
3843
|
+
next = source ? { taskDir: source.taskDir, cwd: source.cwd } : selection;
|
|
3844
|
+
if (source) changeSelection(next);
|
|
3845
|
+
return run('metrics', next, false, { runId: metricScope === 'task' ? undefined : source?.id });
|
|
3846
|
+
};
|
|
3847
|
+
const property = (label, value) =>
|
|
3848
|
+
h(
|
|
3849
|
+
React.Fragment,
|
|
3850
|
+
{ key: label },
|
|
3851
|
+
h('dt', null, label),
|
|
3852
|
+
h('dd', null, textValue(value, t('runsMissing'))),
|
|
3853
|
+
);
|
|
3854
|
+
const timeline = control.metrics?.timeline;
|
|
3855
|
+
const measuredTime = (value) => timeline?.completeSpans > 0 && Number.isFinite(value)
|
|
3856
|
+
? (value / 1000).toFixed(3) + ' s'
|
|
3857
|
+
: t('runsMissing');
|
|
3858
|
+
return h(
|
|
3859
|
+
'section',
|
|
3860
|
+
{ className: css.runExplorer, 'aria-label': t('consoleRuns') },
|
|
3861
|
+
h(
|
|
3862
|
+
'div',
|
|
3863
|
+
{ className: css.runToolbar },
|
|
3864
|
+
h(Button, { disabled: busy, onClick: () => run('runs', selection) }, t('runsRefresh')),
|
|
3865
|
+
h('p', { className: css.meta }, t('runsReadOnly')),
|
|
3866
|
+
),
|
|
3867
|
+
runs.length
|
|
3868
|
+
? h(
|
|
3869
|
+
'div',
|
|
3870
|
+
{ className: css.runSplit },
|
|
3871
|
+
h(
|
|
3872
|
+
'nav',
|
|
3873
|
+
{ className: css.runList, 'aria-label': t('consoleRuns') },
|
|
3874
|
+
...runs.slice(0, 100).map((row) =>
|
|
3875
|
+
h(
|
|
3876
|
+
'button',
|
|
3877
|
+
{
|
|
3878
|
+
key: row.id,
|
|
3879
|
+
type: 'button',
|
|
3880
|
+
className: css.runRow,
|
|
3881
|
+
'aria-pressed': item?.id === row.id,
|
|
3882
|
+
onClick: () => setSelected(row.id),
|
|
3883
|
+
},
|
|
3884
|
+
h('strong', null, row.summary?.slug ?? row.taskDir),
|
|
3885
|
+
h('span', { className: css.meta }, row.startedAt),
|
|
3886
|
+
h(
|
|
3887
|
+
'span',
|
|
3888
|
+
{ className: css.meta },
|
|
3889
|
+
[
|
|
3890
|
+
row.kind,
|
|
3891
|
+
row.finishedAt
|
|
3892
|
+
? row.error
|
|
3893
|
+
? t('fail')
|
|
3894
|
+
: canvasStatus(row.summary?.status, t)
|
|
3895
|
+
: t('runsLive'),
|
|
3896
|
+
]
|
|
3897
|
+
.filter(Boolean)
|
|
3898
|
+
.join(' · '),
|
|
3899
|
+
),
|
|
3900
|
+
),
|
|
3901
|
+
),
|
|
3902
|
+
),
|
|
3903
|
+
item &&
|
|
3904
|
+
h(
|
|
3905
|
+
'article',
|
|
3906
|
+
{ className: css.runDetail, 'aria-label': t('runsDetails') },
|
|
3907
|
+
h(
|
|
3908
|
+
'header',
|
|
3909
|
+
{ className: css.runTitle },
|
|
3910
|
+
h('h3', { className: css.configTitle }, item.summary?.slug ?? item.taskDir),
|
|
3911
|
+
h(
|
|
3912
|
+
'span',
|
|
3913
|
+
{ className: css.meta },
|
|
3914
|
+
[item.kind, item.branch, item.head?.slice(0, 12)].filter(Boolean).join(' · '),
|
|
3915
|
+
),
|
|
3916
|
+
),
|
|
3917
|
+
h(
|
|
3918
|
+
'div',
|
|
3919
|
+
{ className: css.actions },
|
|
3920
|
+
h(Button, { disabled: busy || !!control.uncertainty, onClick: open }, t('runsOpen')),
|
|
3921
|
+
h(Button, { disabled: busy, onClick: metrics }, t('runsMetrics')),
|
|
3922
|
+
h(
|
|
3923
|
+
'label',
|
|
3924
|
+
{ className: css.metricScope },
|
|
3925
|
+
t('runsMetricScope'),
|
|
3926
|
+
h(
|
|
3927
|
+
'select',
|
|
3928
|
+
{
|
|
3929
|
+
className: css.modeSelect,
|
|
3930
|
+
value: metricScope,
|
|
3931
|
+
disabled: busy,
|
|
3932
|
+
onChange: (e) => setMetricScope(e.target.value),
|
|
3933
|
+
},
|
|
3934
|
+
h('option', { value: 'run' }, t('runsSelected')),
|
|
3935
|
+
h('option', { value: 'task' }, t('runsAll')),
|
|
3936
|
+
),
|
|
3937
|
+
),
|
|
3938
|
+
),
|
|
3939
|
+
item.error &&
|
|
3940
|
+
h(
|
|
3941
|
+
'div',
|
|
3942
|
+
{ className: css.configCallout, role: 'note' },
|
|
3943
|
+
h('h4', { className: css.heading }, t('runsFailure')),
|
|
3944
|
+
h(
|
|
3945
|
+
'pre',
|
|
3946
|
+
{ className: css.raw },
|
|
3947
|
+
typeof item.error === 'string' ? item.error : JSON.stringify(item.error, null, 2),
|
|
3948
|
+
),
|
|
3949
|
+
),
|
|
3950
|
+
h(
|
|
3951
|
+
'dl',
|
|
3952
|
+
{ className: css.consoleProperties },
|
|
3953
|
+
property(t('runsStart'), item.startedAt),
|
|
3954
|
+
property(t('runsFinish'), item.finishedAt || t('runsLive')),
|
|
3955
|
+
property(t('consoleBranch'), item.branch),
|
|
3956
|
+
property(t('projectHome'), item.workspaceCwd),
|
|
3957
|
+
property(t('executionHome'), item.cwd),
|
|
3958
|
+
),
|
|
3959
|
+
item.quality &&
|
|
3960
|
+
item.finishedAt &&
|
|
3961
|
+
!item.error &&
|
|
3962
|
+
h(
|
|
3963
|
+
'p',
|
|
3964
|
+
null,
|
|
3965
|
+
t('runsObserved', {
|
|
3966
|
+
completed: item.quality.newCompletions,
|
|
3967
|
+
attempts: item.quality.newAttempts,
|
|
3968
|
+
failed: item.quality.newFailedAttempts,
|
|
3969
|
+
seconds: item.elapsedMs == null ? t('runsMissing') : (item.elapsedMs / 1000).toFixed(1),
|
|
3970
|
+
}),
|
|
3971
|
+
),
|
|
3972
|
+
h(
|
|
3973
|
+
'label',
|
|
3974
|
+
{ className: css.editorField },
|
|
3975
|
+
t('runsReference'),
|
|
3976
|
+
h(
|
|
3977
|
+
'select',
|
|
3978
|
+
{
|
|
3979
|
+
className: css.modeSelect,
|
|
3980
|
+
value: baseline?.id ?? '',
|
|
3981
|
+
onChange: (e) => setReference(e.target.value),
|
|
3982
|
+
},
|
|
3983
|
+
h('option', { value: '' }, t('runsNoReference')),
|
|
3984
|
+
...runs.map((row) =>
|
|
3985
|
+
h(
|
|
3986
|
+
'option',
|
|
3987
|
+
{ key: row.id, value: row.id },
|
|
3988
|
+
(row.summary?.slug ?? row.id) + ' · ' + row.startedAt,
|
|
3989
|
+
),
|
|
3990
|
+
),
|
|
3991
|
+
),
|
|
3992
|
+
),
|
|
3993
|
+
h(
|
|
3994
|
+
'p',
|
|
3995
|
+
{ className: css.meta },
|
|
3996
|
+
t('runsCompare') +
|
|
3997
|
+
' · ' +
|
|
3998
|
+
(baseline
|
|
3999
|
+
? !item.conditions || !baseline.conditions
|
|
4000
|
+
? t('runsMissing')
|
|
4001
|
+
: t(item.conditions === baseline.conditions ? 'runsEqual' : 'runsDifferent')
|
|
4002
|
+
: textValue(item.conditions).slice(0, 12)),
|
|
4003
|
+
),
|
|
4004
|
+
h(
|
|
4005
|
+
'section',
|
|
4006
|
+
{ className: css.details },
|
|
4007
|
+
h('h4', null, 'Run / ' + item.id),
|
|
4008
|
+
h('pre', { className: css.raw }, JSON.stringify(item, null, 2)),
|
|
4009
|
+
),
|
|
4010
|
+
),
|
|
4011
|
+
)
|
|
4012
|
+
: h('div', { className: css.workbenchEmpty }, h('p', null, t('runsEmpty'))),
|
|
4013
|
+
control.metrics
|
|
4014
|
+
? h(
|
|
4015
|
+
'section',
|
|
4016
|
+
{ className: css.runMetrics },
|
|
4017
|
+
h('p', { className: css.meta }, t('consoleCompareHelp')),
|
|
4018
|
+
h('p', { className: css.meta }, t('runsQuality')),
|
|
4019
|
+
timeline && h(
|
|
4020
|
+
'article',
|
|
4021
|
+
{ className: css.metricRow },
|
|
4022
|
+
h('h4', { className: css.heading }, t('runsTimeline')),
|
|
4023
|
+
h('p', { className: css.meta }, t('runsTimelineHelp')),
|
|
4024
|
+
h('dl', { className: css.consoleProperties },
|
|
4025
|
+
property(t('runsTimelineWall'), measuredTime(timeline.observedWallMs)),
|
|
4026
|
+
property(t('runsTimelineSum'), measuredTime(timeline.knownSpanMs)),
|
|
4027
|
+
property(t('runsTimelineSpans'), timeline.completeSpans + ' / ' + timeline.unknownSpans),
|
|
4028
|
+
property(t('runsTimelineUnmatched'), timeline.unmatchedCalls),
|
|
4029
|
+
),
|
|
4030
|
+
),
|
|
4031
|
+
!!(control.metrics.omittedGroups || control.metrics.omittedCalls) &&
|
|
4032
|
+
h('p', { className: css.meta }, t('runsBounded')),
|
|
4033
|
+
...(control.metrics.groups ?? []).map((group, index) =>
|
|
4034
|
+
h(
|
|
4035
|
+
'article',
|
|
4036
|
+
{ key: index, className: css.metricRow },
|
|
4037
|
+
h(
|
|
4038
|
+
'h4',
|
|
4039
|
+
{ className: css.heading },
|
|
4040
|
+
[group.role, group.harness, group.model].filter(Boolean).join(' · '),
|
|
4041
|
+
),
|
|
4042
|
+
h('p', { className: css.meta }, group.runId || t('runsLegacy')),
|
|
4043
|
+
h(
|
|
4044
|
+
'dl',
|
|
4045
|
+
{ className: css.consoleProperties },
|
|
4046
|
+
property(t('runsSamples'), group.calls + ' / ' + group.failedCalls),
|
|
4047
|
+
property(
|
|
4048
|
+
t('runsLatency'),
|
|
4049
|
+
(group.knownDurationMs / 1000).toFixed(1) +
|
|
4050
|
+
' s · ' +
|
|
4051
|
+
t('runsMissingCount', { count: group.missingDuration }),
|
|
4052
|
+
),
|
|
4053
|
+
property(
|
|
4054
|
+
t('runsUsage'),
|
|
4055
|
+
group.knownTokens +
|
|
4056
|
+
' / ' +
|
|
4057
|
+
group.missingUsage +
|
|
4058
|
+
' · ' +
|
|
4059
|
+
t('runsPartial', { count: group.partialUsage ?? 0 }),
|
|
4060
|
+
),
|
|
4061
|
+
property(
|
|
4062
|
+
t('runsCost'),
|
|
4063
|
+
(group.missingCost === group.calls
|
|
4064
|
+
? t('runsMissing')
|
|
4065
|
+
: '$' + group.knownCostUSD.toFixed(4)) +
|
|
4066
|
+
' / ' +
|
|
4067
|
+
group.missingCost,
|
|
4068
|
+
),
|
|
4069
|
+
),
|
|
4070
|
+
),
|
|
4071
|
+
),
|
|
4072
|
+
)
|
|
4073
|
+
: h('p', { className: css.runMetricsHint }, t('runsNoMetrics')),
|
|
4074
|
+
);
|
|
4075
|
+
}
|
|
4076
|
+
|
|
4077
|
+
return module.exports;
|
|
4078
|
+
}});
|