@ckanner/dsh-plugin-flywheel 0.28.0 → 0.28.2
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/PROTOCOL.md +33 -6
- package/README.en.md +134 -13
- package/README.md +38 -10
- package/THIRD_PARTY_NOTICES.md +38 -0
- package/dist/client.js +721 -172
- package/dist/codex-pro-bridge.js +4 -2
- package/dist/control.js +36 -10
- package/dist/index.js +17 -3
- package/dist/policy.js +10 -8
- package/dist/protocol.js +6 -4
- package/package.json +10 -4
package/dist/client.js
CHANGED
|
@@ -1,205 +1,754 @@
|
|
|
1
1
|
window.__ModuleLoader__.load({id:"@ckanner/dsh-plugin-flywheel",factory:(require)=>{
|
|
2
2
|
const module={exports:{}};const exports=module.exports;
|
|
3
|
-
// The build wraps this file in
|
|
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).
|
|
4
6
|
const React = require('react');
|
|
5
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","check":"flywheel_check_K2PPkW","checkBody":"flywheel_checkBody_K2PPkW","checkState":"flywheel_checkState_K2PPkW","checks":"flywheel_checks_K2PPkW","details":"flywheel_details_K2PPkW","empty":"flywheel_empty_K2PPkW","error":"flywheel_error_K2PPkW","errorText":"flywheel_errorText_K2PPkW","feedback":"flywheel_feedback_K2PPkW","field":"flywheel_field_K2PPkW","fieldLabel":"flywheel_fieldLabel_K2PPkW","fields":"flywheel_fields_K2PPkW","flywheel-dash":"flywheel_flywheel-dash_K2PPkW","flywheel-spin":"flywheel_flywheel-spin_K2PPkW","header":"flywheel_header_K2PPkW","heading":"flywheel_heading_K2PPkW","idleFeedback":"flywheel_idleFeedback_K2PPkW","input":"flywheel_input_K2PPkW","inputShell":"flywheel_inputShell_K2PPkW","loading":"flywheel_loading_K2PPkW","loadingCompact":"flywheel_loadingCompact_K2PPkW","meta":"flywheel_meta_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","section":"flywheel_section_K2PPkW","sectionHeader":"flywheel_sectionHeader_K2PPkW","selection":"flywheel_selection_K2PPkW","spinner":"flywheel_spinner_K2PPkW","spinnerArc":"flywheel_spinnerArc_K2PPkW","spinnerMotion":"flywheel_spinnerMotion_K2PPkW","spinnerTrack":"flywheel_spinnerTrack_K2PPkW","textInput":"flywheel_textInput_K2PPkW","title":"flywheel_title_K2PPkW","warningText":"flywheel_warningText_K2PPkW"};
|
|
11
|
+
const stylesheet = ".flywheel_page_K2PPkW {\n box-sizing: border-box;\n width: 100%;\n min-width: 0;\n max-width: 1120px;\n color: var(--dsw-alias-label-primary);\n overflow-wrap: anywhere;\n margin: 0 auto;\n padding: 24px clamp(16px, 3vw, 32px) 32px;\n font-family: inherit;\n font-size: 13px;\n line-height: 20px;\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_fields_K2PPkW {\n grid-template-columns: repeat(auto-fit, minmax(min(100%, 320px), 1fr));\n gap: 16px;\n margin-bottom: 2px;\n display: grid;\n}\n\n.flywheel_field_K2PPkW {\n align-content: start;\n gap: 6px;\n min-width: 0;\n display: grid;\n}\n\n.flywheel_fieldLabel_K2PPkW {\n font-weight: 500;\n}\n\n.flywheel_button_K2PPkW {\n box-sizing: border-box;\n min-height: 36px;\n color: var(--dsw-alias-label-primary);\n cursor: pointer;\n background: none;\n border: none;\n border-radius: 12px;\n justify-content: center;\n align-items: center;\n gap: 4px;\n padding: 0 14px;\n font-family: inherit;\n font-size: 14px;\n font-weight: 400;\n line-height: 22px;\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: 28px;\n padding: 0 10px;\n font-size: 12px;\n line-height: 18px;\n}\n\n.flywheel_buttonPrimary_K2PPkW {\n background: var(--dsw-alias-button-primary-fill);\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: var(--dsw-alias-button-primary-hover);\n }\n\n .flywheel_buttonOutline_K2PPkW:hover:not(:disabled) {\n background: var(--dsw-alias-interactive-bg-hover);\n }\n}\n\n.flywheel_inputShell_K2PPkW {\n border: .5px solid var(--dsw-alias-border-l4);\n background: var(--dsw-alias-bg-layer-1);\n border-radius: 12px;\n align-items: center;\n gap: 6px;\n height: 32px;\n padding: 0 8px;\n display: inline-flex;\n}\n\n.flywheel_inputShell_K2PPkW:focus-within {\n border-color: var(--dsw-alias-state-business-primary);\n}\n\n.flywheel_inputShell_K2PPkW[data-disabled=\"true\"] {\n opacity: .4;\n cursor: not-allowed;\n}\n\n.flywheel_textInput_K2PPkW {\n min-width: 0;\n color: var(--dsw-alias-label-primary);\n background: none;\n border: none;\n outline: none;\n flex: 1;\n font-family: inherit;\n font-size: 14px;\n line-height: 22px;\n}\n\n.flywheel_textInput_K2PPkW::placeholder {\n color: var(--dsw-alias-label-secondary);\n}\n\n.flywheel_textInput_K2PPkW:disabled {\n cursor: not-allowed;\n}\n\n.flywheel_input_K2PPkW {\n box-sizing: border-box;\n width: 100%;\n min-width: 0;\n}\n\n.flywheel_input_K2PPkW input {\n font-size: 13px;\n line-height: 20px;\n}\n\n@media (pointer: coarse) {\n .flywheel_input_K2PPkW input {\n font-size: 16px;\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 min-height: 28px;\n padding-top: 4px;\n padding-bottom: 4px;\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: 176px;\n margin-top: 12px;\n padding: 12px;\n scroll-padding: 12px;\n overflow: auto;\n}\n\n@media (width <= 600px) {\n .flywheel_feedback_K2PPkW {\n height: 224px;\n }\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-l4);\n margin: 28px 0 20px;\n padding-top: 20px;\n}\n\n.flywheel_advanced_K2PPkW > summary, .flywheel_details_K2PPkW > summary {\n cursor: pointer;\n width: fit-content;\n max-width: 100%;\n}\n\n.flywheel_advanced_K2PPkW > summary:focus-visible, .flywheel_details_K2PPkW > summary:focus-visible {\n outline: 2px solid var(--dsw-alias-state-business-primary);\n outline-offset: 3px;\n border-radius: 8px;\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 > summary {\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";
|
|
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 two controls we use. Markup and native-prop/ref forwarding follow
|
|
17
|
+
// DSH b830ec950faba0a46dda564be3e2d854afed94f1:
|
|
18
|
+
// packages/client/ui-primitives/src/{Button,Input}.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
|
+
const Input = React.forwardRef(function FlywheelInput({ className, ...rest }, ref) {
|
|
30
|
+
return h('span', { className: [css.inputShell, className].filter(Boolean).join(' '),
|
|
31
|
+
'data-disabled': rest.disabled ? 'true' : undefined,
|
|
32
|
+
}, h('input', { ...rest, ref, className: css.textInput }));
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
// The ongoing glyph and reduced-motion behavior are copied from the same
|
|
36
|
+
// pinned host's ui-primitives StateDot; this plugin owns the SVG and CSS.
|
|
37
|
+
function Spinner() {
|
|
38
|
+
const synchronize = element => {
|
|
39
|
+
if (!element) return;
|
|
40
|
+
for (const animation of element.getAnimations?.({ subtree: true }) ?? []) animation.startTime = 0;
|
|
41
|
+
};
|
|
42
|
+
return h('svg', { className: css.spinner, width: 20, height: 20,
|
|
43
|
+
viewBox: '0 0 24 24', 'aria-hidden': true, ref: synchronize,
|
|
44
|
+
onAnimationStart: event => synchronize(event.currentTarget) },
|
|
45
|
+
h('g', { className: css.spinnerMotion },
|
|
46
|
+
h('circle', { className: css.spinnerTrack, cx: 12, cy: 12, r: 9.5 }),
|
|
47
|
+
h('circle', { className: css.spinnerArc, cx: 12, cy: 12, r: 9.5 })));
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// A small immutable snapshot source for this plugin's flat write sets. This is
|
|
51
|
+
// not a generic deep-draft engine: all updates replace root fields, and nested
|
|
52
|
+
// engine reports are retained as read-only values. Only the host subscribes.
|
|
53
|
+
function createSnapshot(initial) {
|
|
54
|
+
let snapshot = Object.freeze({ ...initial });
|
|
55
|
+
const listeners = new Set();
|
|
56
|
+
return {
|
|
57
|
+
getSnapshot: () => snapshot,
|
|
58
|
+
subscribe: listener => {
|
|
59
|
+
listeners.add(listener);
|
|
60
|
+
return () => { listeners.delete(listener); };
|
|
61
|
+
},
|
|
62
|
+
update: mutate => {
|
|
63
|
+
const next = { ...snapshot };
|
|
64
|
+
mutate(next);
|
|
65
|
+
if (Object.keys(next).every(key => Object.is(next[key], snapshot[key]))) return;
|
|
66
|
+
snapshot = Object.freeze(next);
|
|
67
|
+
for (const listener of [...listeners]) listener();
|
|
68
|
+
},
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// Public structural StoreHandle / StoreInstance contract, verified at pinned
|
|
73
|
+
// packages/client/store/src/contract.ts. The renderer creates and caches each
|
|
74
|
+
// instance by session binding, then provides useStore/actions to the component.
|
|
75
|
+
// We do not import its engine, subscribe from React, or persist live state.
|
|
76
|
+
function createViewStore() {
|
|
77
|
+
const spec = {
|
|
78
|
+
init: () => ({ taskDir: '', cwd: '', advancedOpen: false }),
|
|
79
|
+
actions: {
|
|
80
|
+
setSelection: (draft, next) => { draft.taskDir = next.taskDir; draft.cwd = next.cwd; },
|
|
81
|
+
setAdvancedOpen: (draft, open) => { draft.advancedOpen = open; },
|
|
82
|
+
},
|
|
83
|
+
};
|
|
84
|
+
return {
|
|
85
|
+
spec,
|
|
86
|
+
create: () => {
|
|
87
|
+
const source = createSnapshot(spec.init());
|
|
88
|
+
const actions = Object.fromEntries(Object.entries(spec.actions).map(([name, action]) =>
|
|
89
|
+
[name, (...args) => source.update(draft => action(draft, ...args))]));
|
|
90
|
+
return { actions, getSnapshot: source.getSnapshot, subscribe: source.subscribe,
|
|
91
|
+
clearPersisted: () => {},
|
|
92
|
+
};
|
|
93
|
+
},
|
|
94
|
+
};
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
const zh = {
|
|
98
|
+
view: 'Flywheel',
|
|
99
|
+
title: 'Flywheel 任务',
|
|
100
|
+
description: '准备任务后,在这里查看进度、启动、继续或停止',
|
|
101
|
+
lifecycle: '任务在后台独立运行。切换视图或会话不会停止任务;需要停止时请使用这里的“停止”',
|
|
102
|
+
selection: '打开已有任务',
|
|
103
|
+
taskDir: '任务资料目录',
|
|
104
|
+
taskHint: '填写助手准备任务时给出的完整路径,里面保存任务说明和验收清单;只查看配置或做检查时可留空',
|
|
105
|
+
cwd: '项目目录(可选)',
|
|
106
|
+
cwdHint: '默认使用当前 DSH 会话的项目目录,可留空。任务在其他项目或工作区时,填写对应的完整路径',
|
|
107
|
+
pathInvalid: '请填写以 / 开头的完整路径',
|
|
108
|
+
pathControl: '路径不能包含换行或空字符',
|
|
109
|
+
taskRequired: '填写任务资料目录后,可刷新状态、启动或停止任务',
|
|
110
|
+
status: '刷新状态',
|
|
111
|
+
start: '启动',
|
|
112
|
+
stop: '停止',
|
|
113
|
+
config: '查看模型配置',
|
|
114
|
+
doctor: '检查安装与配置',
|
|
115
|
+
probe: '检查 DSH 接入',
|
|
116
|
+
doctorHelp: '检查安装与配置:核对本机工具、模型配置和所选任务文件,不会启动任务',
|
|
117
|
+
probeHelp: '检查 DSH 接入:临时启动独立的 DSH 运行环境,检查配置、项目目录和权限后退出;可能联网读取模型目录',
|
|
118
|
+
busy: '正在等待“{action}”确认…',
|
|
119
|
+
busyOther: '正在等待上一个目录的“{action}”结束;其结果不会覆盖当前选择',
|
|
120
|
+
mutationLock: '启动或停止确认前,暂时不能切换任务资料目录或项目目录',
|
|
121
|
+
unknownOutcome: '“{action}”结果未确认',
|
|
122
|
+
unknownHelp: '请求可能已经送达引擎。先刷新这个任务的状态,再决定下一步操作',
|
|
123
|
+
reconcile: '刷新待确认任务',
|
|
124
|
+
errorTitle: '“{action}”未完成',
|
|
125
|
+
errorHelp: '检查连接和目录后,重新执行这项只读检查',
|
|
126
|
+
notDispatchedTitle: '“{action}”未发送到引擎',
|
|
127
|
+
notDispatchedHelp: '检查错误详情,修正目录或引擎配置后重试',
|
|
128
|
+
errorDetails: '错误详情',
|
|
129
|
+
feedback: '操作反馈',
|
|
130
|
+
statusHeading: '任务状态',
|
|
131
|
+
emptyHeading: '还没有任务状态',
|
|
132
|
+
emptyBody: '先在“对话”里让助手按 flywheel-new 准备任务,审阅任务说明和验收标准,再填写助手给出的任务资料目录。已有任务可直接使用原目录',
|
|
133
|
+
emptySelected: '点击“刷新状态”查看这个任务;打开本页不会自动启动任务',
|
|
134
|
+
lastChecked: '上次确认:{time}',
|
|
135
|
+
stale: '本次状态未确认,下方保留上次结果',
|
|
136
|
+
stateUnconfirmed: '状态待确认',
|
|
137
|
+
previousState: '上次确认状态:{status}',
|
|
138
|
+
task: '任务',
|
|
139
|
+
running: '运行中',
|
|
140
|
+
uninitialized: '尚未初始化',
|
|
141
|
+
engineMissing: '引擎未运行,状态待核对',
|
|
142
|
+
unknownState: '未知状态({status})',
|
|
143
|
+
unknown: '未知',
|
|
144
|
+
unreported: '未报告',
|
|
145
|
+
pending: '待开始',
|
|
146
|
+
paused: '已暂停',
|
|
147
|
+
stopped: '已停止',
|
|
148
|
+
completed: '已完成',
|
|
149
|
+
escalated: '需要人工处理',
|
|
150
|
+
budgetStopped: '预算已停止',
|
|
151
|
+
milestone: '当前里程碑:{name}',
|
|
152
|
+
packets: '工作单元',
|
|
153
|
+
packetsCount: '工作单元(共 {count} 个)',
|
|
154
|
+
packet: '工作单元',
|
|
155
|
+
state: '状态',
|
|
156
|
+
route: '路由',
|
|
157
|
+
packetPending: '待执行',
|
|
158
|
+
awaitingCredentials: '等待凭证',
|
|
159
|
+
executing: '执行中',
|
|
160
|
+
executed: '待验收',
|
|
161
|
+
l1Passed: '验收已通过',
|
|
162
|
+
l1Failed: '验收未通过',
|
|
163
|
+
reviewFailed: '评审退回',
|
|
164
|
+
interrupted: '已中断',
|
|
165
|
+
inbox: '等待处理',
|
|
166
|
+
inboxCount: '待人工处理记录:{count} 条',
|
|
167
|
+
budget: '用量与成本',
|
|
168
|
+
cost: '已报告成本合计:$' + '{amount}',
|
|
169
|
+
costUnknown: '成本:未知',
|
|
170
|
+
costPartial: '已知成本合计:$' + '{amount};另有 {count} 次调用成本未知,合计不完整',
|
|
171
|
+
costMissing: '成本:未知({count} 次调用未报告结算成本)',
|
|
172
|
+
tokens: '已报告 Token:{tokens}',
|
|
173
|
+
tokensMissing: ';{count} 次调用用量未知,合计不完整',
|
|
174
|
+
tokenLimit: '每日 Token 限额:{limit} · 预算策略:{policy}',
|
|
175
|
+
budgetWarning: '预算提示:{message}',
|
|
176
|
+
advanced: '配置与排查(按需)',
|
|
177
|
+
advancedHelp: '首次安装、升级或运行失败时可用,平时不必每次点击;两项检查都不向模型发送消息,也不能验证账号、额度或实际回答能力',
|
|
178
|
+
configuration: '实际模型配置',
|
|
179
|
+
configurationHelp: '这里显示 Flywheel 从下方来源读取的实际生效配置;DSH 对话输入框当前选择的模型不会替代它',
|
|
180
|
+
source: '配置来源:{source}',
|
|
181
|
+
embedded: '内嵌模板',
|
|
182
|
+
scope: '配置层级:{scope}',
|
|
183
|
+
outOfScope: '未用于本次检查',
|
|
184
|
+
model: '模型',
|
|
185
|
+
profile: 'Profile',
|
|
186
|
+
harness: 'Harness',
|
|
187
|
+
provider: 'Provider',
|
|
188
|
+
channel: '渠道',
|
|
189
|
+
thinking: '推理',
|
|
190
|
+
permission: '阶段权限要求',
|
|
191
|
+
permissionFull: '完全权限(无需逐步确认)',
|
|
192
|
+
permissionWorkspace: '项目内写入',
|
|
193
|
+
permissionReadOnly: '只读',
|
|
194
|
+
session: '会话恢复',
|
|
195
|
+
enabled: '启用',
|
|
196
|
+
disabled: '关闭',
|
|
197
|
+
notConfigured: '未配置',
|
|
198
|
+
nativeProvider: '由原生运行时管理',
|
|
199
|
+
default: '默认',
|
|
200
|
+
undeclared: '未声明',
|
|
201
|
+
planRole: '规划',
|
|
202
|
+
executeRole: '执行',
|
|
203
|
+
reviewRole: '评审',
|
|
204
|
+
judgeRole: '终验',
|
|
205
|
+
intakeRole: '需求收集',
|
|
206
|
+
gardenRole: '知识维护',
|
|
207
|
+
panelRole: '第二意见',
|
|
208
|
+
diagnostic: '环境检查结果',
|
|
209
|
+
doctorPassed: '检查结束:未发现阻断项',
|
|
210
|
+
doctorFailed: '检查结束:存在阻断项',
|
|
211
|
+
localOnly: '本次检查本机工具、配置和所选任务文件,未启动 DSH 接入检查',
|
|
212
|
+
probeRequested: '已请求 DSH 接入检查(不调用模型)',
|
|
213
|
+
inferenceBoundary: '没有向模型发送消息;账号授权、可用额度和实际回答能力仍未验证',
|
|
214
|
+
pass: '通过',
|
|
215
|
+
fail: '失败',
|
|
216
|
+
warn: '注意',
|
|
217
|
+
unverified: '未验证',
|
|
218
|
+
skipped: '跳过',
|
|
219
|
+
errorCode: '错误码:{code}',
|
|
220
|
+
remedy: '处理建议:{remedy}',
|
|
221
|
+
raw: '引擎原始结果(JSON)',
|
|
222
|
+
};
|
|
223
|
+
const en = {
|
|
224
|
+
view: 'Flywheel',
|
|
225
|
+
title: 'Flywheel tasks',
|
|
226
|
+
description: 'After preparing a task, inspect its progress, start, resume or stop it here',
|
|
227
|
+
lifecycle: 'Tasks run independently in the background. Switching views or sessions does not stop a task; use Stop here when needed.',
|
|
228
|
+
selection: 'Open an existing task',
|
|
229
|
+
taskDir: 'Task files directory',
|
|
230
|
+
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',
|
|
231
|
+
cwd: 'Project directory (optional)',
|
|
232
|
+
cwdHint: 'Leave blank to use the project directory of this DSH session. If the task belongs to another project or worktree, enter that full path.',
|
|
233
|
+
pathInvalid: 'Enter a full path beginning with /',
|
|
234
|
+
pathControl: 'Paths cannot contain line breaks or null characters',
|
|
235
|
+
taskRequired: 'Enter the task files directory to refresh status, start or stop the task',
|
|
236
|
+
status: 'Refresh status',
|
|
237
|
+
start: 'Start',
|
|
238
|
+
stop: 'Stop',
|
|
239
|
+
config: 'Inspect model configuration',
|
|
240
|
+
doctor: 'Check installation and configuration',
|
|
241
|
+
probe: 'Check DSH integration',
|
|
242
|
+
doctorHelp: 'Installation and configuration: check local tools, model settings and selected task files without starting the task.',
|
|
243
|
+
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.',
|
|
244
|
+
busy: 'Waiting for “{action}” confirmation…',
|
|
245
|
+
busyOther: 'Waiting for “{action}” for the previous directory; its result will not replace the current selection',
|
|
246
|
+
mutationLock: 'Task and project directories are locked until start or stop is confirmed',
|
|
247
|
+
unknownOutcome: '“{action}” outcome is unconfirmed',
|
|
248
|
+
unknownHelp: 'The request may have reached the engine. Refresh this task before choosing the next action.',
|
|
249
|
+
reconcile: 'Refresh unconfirmed task',
|
|
250
|
+
errorTitle: '“{action}” did not complete',
|
|
251
|
+
errorHelp: 'Check the connection and directories, then retry this read-only check.',
|
|
252
|
+
notDispatchedTitle: '“{action}” was not dispatched to the engine',
|
|
253
|
+
notDispatchedHelp: 'Check the error details, correct the directories or engine configuration, then retry.',
|
|
254
|
+
errorDetails: 'Error details',
|
|
255
|
+
feedback: 'Operation feedback',
|
|
256
|
+
statusHeading: 'Task status',
|
|
257
|
+
emptyHeading: 'No task status yet',
|
|
258
|
+
emptyBody: 'In Chat, ask the assistant to prepare a task with flywheel-new. Review its instructions and acceptance checks, then enter the task files directory it provides. For an existing task, use its original directory.',
|
|
259
|
+
emptySelected: 'Refresh status to inspect this task. Opening this view does not start it.',
|
|
260
|
+
lastChecked: 'Last confirmed: {time}',
|
|
261
|
+
stale: 'Current status is unconfirmed; the previous result is retained below',
|
|
262
|
+
stateUnconfirmed: 'Status unconfirmed',
|
|
263
|
+
previousState: 'Previously confirmed state: {status}',
|
|
264
|
+
task: 'Task',
|
|
265
|
+
running: 'Running',
|
|
266
|
+
uninitialized: 'Not initialized',
|
|
267
|
+
engineMissing: 'Engine is not running; state needs verification',
|
|
268
|
+
unknownState: 'Unknown state ({status})',
|
|
269
|
+
unknown: 'Unknown',
|
|
270
|
+
unreported: 'Not reported',
|
|
271
|
+
pending: 'Pending',
|
|
272
|
+
paused: 'Paused',
|
|
273
|
+
stopped: 'Stopped',
|
|
274
|
+
completed: 'Completed',
|
|
275
|
+
escalated: 'Needs human attention',
|
|
276
|
+
budgetStopped: 'Stopped by budget',
|
|
277
|
+
milestone: 'Current milestone: {name}',
|
|
278
|
+
packets: 'Work packets',
|
|
279
|
+
packetsCount: 'Work packets ({count})',
|
|
280
|
+
packet: 'Work packet',
|
|
281
|
+
state: 'State',
|
|
282
|
+
route: 'Route',
|
|
283
|
+
packetPending: 'Pending',
|
|
284
|
+
awaitingCredentials: 'Waiting for credentials',
|
|
285
|
+
executing: 'Executing',
|
|
286
|
+
executed: 'Awaiting verification',
|
|
287
|
+
l1Passed: 'Verification passed',
|
|
288
|
+
l1Failed: 'Verification failed',
|
|
289
|
+
reviewFailed: 'Changes requested',
|
|
290
|
+
interrupted: 'Interrupted',
|
|
291
|
+
inbox: 'Waiting for attention',
|
|
292
|
+
inboxCount: 'Human attention records: {count}',
|
|
293
|
+
budget: 'Usage and cost',
|
|
294
|
+
cost: 'Reported cost total: $' + '{amount}',
|
|
295
|
+
costUnknown: 'Cost: unknown',
|
|
296
|
+
costPartial: 'Known cost: $' + '{amount}; {count} other calls have unknown cost, so this total is incomplete',
|
|
297
|
+
costMissing: 'Cost: unknown ({count} calls did not report billed cost)',
|
|
298
|
+
tokens: 'Reported tokens: {tokens}',
|
|
299
|
+
tokensMissing: '; usage is unknown for {count} calls, so this total is incomplete',
|
|
300
|
+
tokenLimit: 'Daily token limit: {limit} · Budget policy: {policy}',
|
|
301
|
+
budgetWarning: 'Budget notice: {message}',
|
|
302
|
+
advanced: 'Configuration and troubleshooting (optional)',
|
|
303
|
+
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.',
|
|
304
|
+
configuration: 'Effective model configuration',
|
|
305
|
+
configurationHelp: 'These are the effective Flywheel settings loaded from the source below. The model selected in the DSH chat composer does not replace them.',
|
|
306
|
+
source: 'Configuration source: {source}',
|
|
307
|
+
embedded: 'Embedded template',
|
|
308
|
+
scope: 'Configuration level: {scope}',
|
|
309
|
+
outOfScope: 'Not used for this check',
|
|
310
|
+
model: 'Model',
|
|
311
|
+
profile: 'Profile',
|
|
312
|
+
harness: 'Harness',
|
|
313
|
+
provider: 'Provider',
|
|
314
|
+
channel: 'Channel',
|
|
315
|
+
thinking: 'Reasoning',
|
|
316
|
+
permission: 'Required permission',
|
|
317
|
+
permissionFull: 'Full access (no step-by-step approval)',
|
|
318
|
+
permissionWorkspace: 'Write within the project',
|
|
319
|
+
permissionReadOnly: 'Read only',
|
|
320
|
+
session: 'Session resume',
|
|
321
|
+
enabled: 'Enabled',
|
|
322
|
+
disabled: 'Disabled',
|
|
323
|
+
notConfigured: 'Not configured',
|
|
324
|
+
nativeProvider: 'Managed by the native runtime',
|
|
325
|
+
default: 'Default',
|
|
326
|
+
undeclared: 'Not declared',
|
|
327
|
+
planRole: 'Plan',
|
|
328
|
+
executeRole: 'Execute',
|
|
329
|
+
reviewRole: 'Review',
|
|
330
|
+
judgeRole: 'Judge',
|
|
331
|
+
intakeRole: 'Intake',
|
|
332
|
+
gardenRole: 'Knowledge maintenance',
|
|
333
|
+
panelRole: 'Second opinion',
|
|
334
|
+
diagnostic: 'Environment check results',
|
|
335
|
+
doctorPassed: 'Check complete: no blockers found',
|
|
336
|
+
doctorFailed: 'Check complete: blockers found',
|
|
337
|
+
localOnly: 'Local tools, configuration and selected task files were checked; no DSH integration check ran.',
|
|
338
|
+
probeRequested: 'A DSH integration check was requested (no model call)',
|
|
339
|
+
inferenceBoundary: 'No message was sent to a model. Account access, available quota and real model responses remain unverified.',
|
|
340
|
+
pass: 'Passed',
|
|
341
|
+
fail: 'Failed',
|
|
342
|
+
warn: 'Notice',
|
|
343
|
+
unverified: 'Unverified',
|
|
344
|
+
skipped: 'Skipped',
|
|
345
|
+
errorCode: 'Error code: {code}',
|
|
346
|
+
remedy: 'Next step: {remedy}',
|
|
347
|
+
raw: 'Raw engine response (JSON)',
|
|
348
|
+
};
|
|
6
349
|
|
|
7
350
|
const taskLabels = {
|
|
8
|
-
pending: '
|
|
9
|
-
escalated: '
|
|
351
|
+
pending: 'pending', paused: 'paused', stopped: 'stopped', completed: 'completed',
|
|
352
|
+
escalated: 'escalated', 'budget-stopped': 'budgetStopped',
|
|
10
353
|
};
|
|
11
354
|
const packetLabels = {
|
|
12
|
-
pending: '
|
|
13
|
-
|
|
14
|
-
|
|
355
|
+
pending: 'packetPending', 'awaiting-credentials': 'awaitingCredentials',
|
|
356
|
+
executing: 'executing', executed: 'executed', 'l1-passed': 'l1Passed',
|
|
357
|
+
'l1-failed': 'l1Failed', 'review-failed': 'reviewFailed', completed: 'completed',
|
|
358
|
+
interrupted: 'interrupted', escalated: 'escalated', inbox: 'inbox',
|
|
359
|
+
};
|
|
360
|
+
const roleLabels = { plan: 'planRole', execute: 'executeRole', review: 'reviewRole', judge: 'judgeRole',
|
|
361
|
+
intake: 'intakeRole', garden: 'gardenRole', 'judge-panel': 'panelRole' };
|
|
362
|
+
const checkLabels = { pass: 'pass', fail: 'fail', warn: 'warn', unknown: 'unverified', skipped: 'skipped' };
|
|
363
|
+
const permissionLabels = {
|
|
364
|
+
'danger-full-access': 'permissionFull',
|
|
365
|
+
'workspace-write': 'permissionWorkspace',
|
|
366
|
+
'read-only': 'permissionReadOnly',
|
|
15
367
|
};
|
|
16
|
-
const
|
|
17
|
-
const
|
|
18
|
-
|
|
19
|
-
const
|
|
20
|
-
const
|
|
21
|
-
const
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
function
|
|
25
|
-
if (value
|
|
26
|
-
if (!value.
|
|
27
|
-
|
|
28
|
-
return taskLabels[value.state.status] ?? `未知状态(${value.state.status ?? '未报告'})`;
|
|
368
|
+
const record = value => value !== null && typeof value === 'object' && !Array.isArray(value);
|
|
369
|
+
const textValue = (value, fallback = '—') => typeof value === 'string' || typeof value === 'number' ? String(value) : fallback;
|
|
370
|
+
const selectionOf = value => ({ taskDir: value.taskDir.trim(), cwd: value.cwd.trim() });
|
|
371
|
+
const selectionKey = value => JSON.stringify([value.taskDir, value.cwd]);
|
|
372
|
+
const mutates = action => action === 'start' || action === 'stop';
|
|
373
|
+
const actionKey = value => value.probe ? 'probe' : value.action;
|
|
374
|
+
class NotDispatchedControlReply extends Error {}
|
|
375
|
+
|
|
376
|
+
function pathProblem(value) {
|
|
377
|
+
if (/[\0\r\n]/.test(value)) return 'pathControl';
|
|
378
|
+
if (value.trim() && !value.trim().startsWith('/')) return 'pathInvalid';
|
|
379
|
+
return null;
|
|
29
380
|
}
|
|
30
381
|
|
|
31
|
-
function
|
|
382
|
+
function taskLabel(value, t) {
|
|
383
|
+
if (value.running) return t('running');
|
|
384
|
+
if (!value.state) return t('uninitialized');
|
|
385
|
+
if (value.state.status === 'running') return t('engineMissing');
|
|
386
|
+
const key = taskLabels[value.state.status];
|
|
387
|
+
return key ? t(key) : t('unknownState', { status: textValue(value.state.status, t('unreported')) });
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
function readReply(reply, request) {
|
|
391
|
+
if (!reply?.ok) throw new Error(reply?.error?.message ?? 'DSH command transport failed');
|
|
392
|
+
const result = reply.value?.result;
|
|
393
|
+
if (result?.kind !== 'success') {
|
|
394
|
+
let failure;
|
|
395
|
+
// This is a versioned command envelope, not a heuristic over error prose.
|
|
396
|
+
// Older hosts and malformed/ambiguous replies keep the conservative outcome.
|
|
397
|
+
if (result?.kind === 'error' && typeof result.text === 'string') {
|
|
398
|
+
try { failure = JSON.parse(result.text); } catch { /* legacy unstructured error */ }
|
|
399
|
+
}
|
|
400
|
+
const typed = record(failure) && failure.protocol === 1 &&
|
|
401
|
+
failure.type === 'flywheel-control-error' && failure.action === request.action &&
|
|
402
|
+
['not-dispatched', 'unknown'].includes(failure.dispatch) && typeof failure.message === 'string';
|
|
403
|
+
const message = typed ? failure.message : result?.text ?? 'Flywheel command unavailable';
|
|
404
|
+
if (typed && failure.dispatch === 'not-dispatched') throw new NotDispatchedControlReply(message);
|
|
405
|
+
throw new Error(message);
|
|
406
|
+
}
|
|
407
|
+
const value = JSON.parse(result.text);
|
|
408
|
+
// runControl has already matched the engine's task identity against the
|
|
409
|
+
// host's realpath-normalized request. A browser cannot compare that identity
|
|
410
|
+
// lexically with a user path (/var, symlinks, trailing slash or "..").
|
|
411
|
+
if (!record(value) || value.protocol !== 1 || typeof value.ok !== 'boolean' ||
|
|
412
|
+
typeof value.taskDir !== 'string' ||
|
|
413
|
+
(request.taskDir ? !value.taskDir.startsWith('/') : value.taskDir !== '')) {
|
|
414
|
+
throw new Error('Flywheel control protocol or task identity mismatch');
|
|
415
|
+
}
|
|
416
|
+
if (!value.ok && !(request.action === 'doctor' && value.diagnostic?.ok === false)) {
|
|
417
|
+
throw new Error(typeof value.error === 'string' ? value.error : 'Flywheel control failed');
|
|
418
|
+
}
|
|
419
|
+
const state = value.state;
|
|
420
|
+
const configurationValid = report => record(report) && Array.isArray(report.bindings) &&
|
|
421
|
+
report.bindings.every(record);
|
|
422
|
+
if (['status', 'start', 'stop'].includes(request.action) &&
|
|
423
|
+
(typeof value.running !== 'boolean' || (state !== null && !record(state)) ||
|
|
424
|
+
(state?.packets != null && !record(state.packets)) ||
|
|
425
|
+
(state?.inbox != null && !Array.isArray(state.inbox)) ||
|
|
426
|
+
(state?.budget != null && (!record(state.budget) ||
|
|
427
|
+
(state.budget.warnings != null && !Array.isArray(state.budget.warnings)))))) {
|
|
428
|
+
throw new Error('Flywheel control returned malformed task status');
|
|
429
|
+
}
|
|
430
|
+
if (request.action === 'config' && !configurationValid(value.configuration)) {
|
|
431
|
+
throw new Error('Flywheel control returned malformed model configuration');
|
|
432
|
+
}
|
|
433
|
+
if (request.action === 'doctor' && (!record(value.diagnostic) ||
|
|
434
|
+
value.diagnostic.ok !== value.ok || !Array.isArray(value.diagnostic.checks) ||
|
|
435
|
+
!value.diagnostic.checks.every(record) ||
|
|
436
|
+
(value.diagnostic.configuration !== undefined && !configurationValid(value.diagnostic.configuration)))) {
|
|
437
|
+
throw new Error('Flywheel control returned malformed environment report');
|
|
438
|
+
}
|
|
439
|
+
return value;
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
function initialControl() {
|
|
443
|
+
return { pending: null, status: null, checkedAt: null, stale: false, configuration: null,
|
|
444
|
+
diagnostic: null, raw: null, error: null, uncertainty: null };
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
// Transport state belongs to this apply-owned controller, not a component or
|
|
448
|
+
// the draft store. Its observable reaches React only through slot injection.
|
|
449
|
+
function createControl(execute, pendingOperations, isDisposed) {
|
|
450
|
+
const source = createSnapshot(initialControl());
|
|
451
|
+
let selected = null;
|
|
452
|
+
let epoch = 0;
|
|
453
|
+
let active = null;
|
|
454
|
+
const select = selection => {
|
|
455
|
+
const normalized = selectionOf(selection);
|
|
456
|
+
const key = selectionKey(normalized);
|
|
457
|
+
if (selected === key) return;
|
|
458
|
+
selected = key;
|
|
459
|
+
epoch += 1;
|
|
460
|
+
source.update(draft => {
|
|
461
|
+
Object.assign(draft, initialControl(), { pending: draft.pending, uncertainty: draft.uncertainty });
|
|
462
|
+
});
|
|
463
|
+
};
|
|
464
|
+
const run = async (action, selection, probe = false) => {
|
|
465
|
+
if (isDisposed() || active || (mutates(action) && source.getSnapshot().uncertainty)) return;
|
|
466
|
+
if (!['status', 'start', 'stop', 'config', 'doctor'].includes(action)) return;
|
|
467
|
+
if (pathProblem(selection.taskDir) || pathProblem(selection.cwd)) return;
|
|
468
|
+
const target = selectionOf(selection);
|
|
469
|
+
if (['status', 'start', 'stop'].includes(action) && !target.taskDir) return;
|
|
470
|
+
select(selection);
|
|
471
|
+
const requestEpoch = epoch;
|
|
472
|
+
const requestKey = selected;
|
|
473
|
+
const request = { action, ...(target.taskDir ? { taskDir: target.taskDir } : {}),
|
|
474
|
+
...(target.cwd ? { cwd: target.cwd } : {}), ...(probe ? { probe: true } : {}) };
|
|
475
|
+
const operation = { action, probe, ...target, key: requestKey };
|
|
476
|
+
active = operation;
|
|
477
|
+
source.update(draft => { draft.pending = operation; draft.error = null; });
|
|
478
|
+
let timer;
|
|
479
|
+
let disposeWait;
|
|
480
|
+
try {
|
|
481
|
+
const deadline = new Promise((_, reject) => {
|
|
482
|
+
timer = setTimeout(() => reject(new Error('Flywheel control confirmation timed out')), CONTROL_WAIT_MS);
|
|
483
|
+
disposeWait = () => reject(new Error('Flywheel UI was unloaded before confirmation'));
|
|
484
|
+
pendingOperations.add(disposeWait);
|
|
485
|
+
});
|
|
486
|
+
const value = await Promise.race([execute(request), deadline]);
|
|
487
|
+
if (isDisposed() || requestEpoch !== epoch) return;
|
|
488
|
+
source.update(draft => {
|
|
489
|
+
if (['status', 'start', 'stop'].includes(action)) {
|
|
490
|
+
draft.status = value;
|
|
491
|
+
draft.checkedAt = Date.now();
|
|
492
|
+
draft.stale = false;
|
|
493
|
+
if (draft.uncertainty?.key === requestKey) draft.uncertainty = null;
|
|
494
|
+
}
|
|
495
|
+
if (['config', 'doctor'].includes(action)) {
|
|
496
|
+
draft.configuration = value.configuration ?? value.diagnostic?.configuration ?? null;
|
|
497
|
+
draft.diagnostic = value.diagnostic ?? null;
|
|
498
|
+
}
|
|
499
|
+
draft.raw = value;
|
|
500
|
+
});
|
|
501
|
+
} catch (error) {
|
|
502
|
+
if (isDisposed() || requestEpoch !== epoch) return;
|
|
503
|
+
source.update(draft => {
|
|
504
|
+
const detail = error instanceof Error ? error.message : String(error);
|
|
505
|
+
const dispatch = error instanceof NotDispatchedControlReply ? 'not-dispatched' : 'unknown';
|
|
506
|
+
draft.error = { action, probe, detail, dispatch };
|
|
507
|
+
if (action === 'status' || (mutates(action) && dispatch === 'unknown')) draft.stale = true;
|
|
508
|
+
if (mutates(action) && dispatch === 'unknown') draft.uncertainty = { ...operation, detail };
|
|
509
|
+
});
|
|
510
|
+
} finally {
|
|
511
|
+
clearTimeout(timer);
|
|
512
|
+
pendingOperations.delete(disposeWait);
|
|
513
|
+
active = null;
|
|
514
|
+
if (!isDisposed()) source.update(draft => { draft.pending = null; });
|
|
515
|
+
}
|
|
516
|
+
};
|
|
517
|
+
return { source, select, run, isMutating: () => active !== null && mutates(active.action) };
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
function Configuration({ value, t }) {
|
|
32
521
|
if (!value) return null;
|
|
33
|
-
|
|
34
|
-
h('
|
|
35
|
-
|
|
36
|
-
h('
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
h('
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
522
|
+
const field = (name, value, code = false) => h(React.Fragment, { key: name },
|
|
523
|
+
h('dt', null, t(name)), h('dd', null, code ? h('code', null, value) : value));
|
|
524
|
+
return h('section', { className: css.section, 'aria-label': t('configuration') },
|
|
525
|
+
h('h3', { className: css.heading }, t('configuration')),
|
|
526
|
+
h('p', { className: css.meta }, t('configurationHelp')),
|
|
527
|
+
h('p', { className: css.meta }, t('source', { source: textValue(value.source, '') || t('embedded') })),
|
|
528
|
+
h('p', { className: css.meta }, t('scope', { scope: textValue(value.scope) })),
|
|
529
|
+
h('div', { className: css.bindings }, ...(value.bindings ?? []).map((binding, index) =>
|
|
530
|
+
h('article', { className: css.binding, key: textValue(binding?.id, String(index)) },
|
|
531
|
+
h('div', { className: css.bindingHeader },
|
|
532
|
+
h('h4', { className: css.bindingTitle }, roleLabels[binding?.role] ? t(roleLabels[binding.role]) : textValue(binding?.role)),
|
|
533
|
+
h('code', { className: css.meta }, textValue(binding?.route || binding?.id))),
|
|
534
|
+
binding?.inScope === false && h('p', { className: css.meta }, t('outOfScope')),
|
|
535
|
+
h('dl', { className: css.properties },
|
|
536
|
+
field('model', textValue(binding?.model, '') || t('notConfigured'), true),
|
|
537
|
+
binding?.profile && field('profile', textValue(binding.profile)),
|
|
538
|
+
field('harness', textValue(binding?.harness), true),
|
|
539
|
+
field('provider', textValue(binding?.provider, '') || (binding?.harness === 'dsh' ? t('notConfigured') : t('nativeProvider'))),
|
|
540
|
+
field('channel', textValue(binding?.channel, '') || t('notConfigured')),
|
|
541
|
+
field('thinking', textValue(binding?.thinking, '') || t('default')),
|
|
542
|
+
field('permission', Object.hasOwn(permissionLabels, binding?.permissionMode) ?
|
|
543
|
+
t(permissionLabels[binding.permissionMode]) : textValue(binding?.permissionMode, '') || t('undeclared')),
|
|
544
|
+
field('session', t(binding?.residentSession ? 'enabled' : 'disabled')))))));
|
|
51
545
|
}
|
|
52
546
|
|
|
53
|
-
function Diagnostic({ value }) {
|
|
547
|
+
function Diagnostic({ value, t }) {
|
|
54
548
|
if (!value) return null;
|
|
55
|
-
return h('section', { 'aria-label': '
|
|
56
|
-
h('
|
|
57
|
-
h('p',
|
|
58
|
-
h('p',
|
|
59
|
-
h('ul', {
|
|
60
|
-
h('li', {
|
|
61
|
-
h('
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
549
|
+
return h('section', { className: css.section, 'aria-label': t('diagnostic') },
|
|
550
|
+
h('h3', { className: css.heading }, t(value.ok ? 'doctorPassed' : 'doctorFailed')),
|
|
551
|
+
h('p', { className: css.meta }, t(value.probeRequested ? 'probeRequested' : 'localOnly')),
|
|
552
|
+
h('p', { className: css.meta }, t('inferenceBoundary')),
|
|
553
|
+
h('ul', { className: css.checks }, ...(value.checks ?? []).map((check, index) =>
|
|
554
|
+
h('li', { className: css.check, key: textValue(check?.id, '') + ':' + index },
|
|
555
|
+
h('span', { className: css.checkState, 'data-state': textValue(check?.status, 'unknown') },
|
|
556
|
+
t(checkLabels[check?.status] ?? 'unverified')),
|
|
557
|
+
h('div', { className: css.checkBody },
|
|
558
|
+
h('h4', { className: css.bindingTitle }, textValue(check?.id)),
|
|
559
|
+
h('p', null, textValue(check?.summary)),
|
|
560
|
+
check?.code && h('p', { className: css.meta }, t('errorCode', { code: textValue(check.code) })),
|
|
561
|
+
check?.remedy && h('p', null, t('remedy', { remedy: textValue(check.remedy) })))))));
|
|
65
562
|
}
|
|
66
563
|
|
|
67
|
-
function Budget({ value }) {
|
|
564
|
+
function Budget({ value, t }) {
|
|
68
565
|
if (!value) return null;
|
|
69
566
|
const cost = value.totals?.['cost-usd'];
|
|
70
567
|
const unknownCost = value['unknown-cost-calls'] ?? 0;
|
|
71
568
|
const unknownUsage = value['unknown-usage-calls'] ?? 0;
|
|
72
|
-
let costText = Number.isFinite(cost) ?
|
|
569
|
+
let costText = Number.isFinite(cost) ? t('cost', { amount: cost.toFixed(4) }) : t('costUnknown');
|
|
73
570
|
if (unknownCost > 0) {
|
|
74
571
|
costText = Number.isFinite(cost) && cost > 0 ?
|
|
75
|
-
|
|
76
|
-
`成本:未知(${unknownCost} 次调用未报告结算成本)`;
|
|
572
|
+
t('costPartial', { amount: cost.toFixed(4), count: unknownCost }) : t('costMissing', { count: unknownCost });
|
|
77
573
|
}
|
|
78
|
-
return h('section', { 'aria-label': '
|
|
574
|
+
return h('section', { className: css.budget, 'aria-label': t('budget') },
|
|
79
575
|
h('p', null, costText),
|
|
80
|
-
h('p', null,
|
|
81
|
-
(unknownUsage > 0 ?
|
|
82
|
-
value['daily-token-limit'] != null && h('p',
|
|
83
|
-
|
|
84
|
-
...(value.warnings ?? []).map((warning, index) => h('p', { key: index },
|
|
576
|
+
h('p', null, t('tokens', { tokens: value.totals?.tokens ?? t('unknown') }) +
|
|
577
|
+
(unknownUsage > 0 ? t('tokensMissing', { count: unknownUsage }) : '')),
|
|
578
|
+
value['daily-token-limit'] != null && h('p', { className: css.meta },
|
|
579
|
+
t('tokenLimit', { limit: value['daily-token-limit'], policy: textValue(value.enforce) })),
|
|
580
|
+
...(value.warnings ?? []).map((warning, index) => h('p', { key: index },
|
|
581
|
+
t('budgetWarning', { message: textValue(warning) }))));
|
|
85
582
|
}
|
|
86
583
|
|
|
87
|
-
function
|
|
88
|
-
const
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
const
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
placeholder: '/path/to/repo/docs/task',
|
|
153
|
-
style: { width: '100%', boxSizing: 'border-box', padding: 6, border: '1px solid #8888', borderRadius: 4, margin: '4px 0 8px' } })),
|
|
154
|
-
h('label', { style: { display: 'block' } }, '仓库目录(留空使用当前 DSH 会话目录)',
|
|
155
|
-
h('input', { value: cwd, onChange: e => changePath(setCwd, e.target.value), placeholder: '/path/to/repo',
|
|
156
|
-
style: { width: '100%', boxSizing: 'border-box', padding: 6, border: '1px solid #8888', borderRadius: 4, margin: '4px 0 8px' } })),
|
|
157
|
-
h('div', { style: { display: 'flex', flexWrap: 'wrap', gap: 8, marginBottom: 8 } },
|
|
158
|
-
button('status', '刷新状态', true), button('start', '启动', true), button('stop', '停止', true)),
|
|
159
|
-
h('div', { style: { display: 'flex', flexWrap: 'wrap', gap: 8, marginBottom: 8 } },
|
|
160
|
-
button('config', '查看模型配置', false), button('doctor', '检查环境', false),
|
|
161
|
-
button('doctor', 'SDK 无推理检查', false, true)),
|
|
162
|
-
busy && h('p', { role: 'status' }, '正在等待引擎确认…'),
|
|
163
|
-
error && h('p', { role: 'alert', style: { color: '#d34b4b', whiteSpace: 'pre-wrap' } }, error),
|
|
164
|
-
status && h('section', { 'aria-label': '任务状态' },
|
|
165
|
-
h('h4', { style: { marginBottom: 6 } }, '任务状态(上次刷新)'),
|
|
166
|
-
h('p', null, `${status.state?.slug ?? '任务'} · ${taskLabel(status)}` + (status.running && status.pid > 0 ? ` · PID ${status.pid}` : '')),
|
|
167
|
-
status.state?.['current-milestone'] && h('p', null, `当前里程碑:${status.state['current-milestone']}`),
|
|
168
|
-
packets.length > 0 && h('div', { style: { maxHeight: 260, overflow: 'auto' } },
|
|
169
|
-
h('table', { 'aria-label': '工作单元', style: tableStyle },
|
|
170
|
-
h('caption', { style: { textAlign: 'left' } }, `工作单元(共 ${packets.length} 个)`),
|
|
171
|
-
h('thead', null, h('tr', null, ...['工作单元', '状态', '路由'].map(title => h('th', { key: title, scope: 'col', style: cellStyle }, title)))),
|
|
172
|
-
h('tbody', null, ...packets.map(([id, packet]) => h('tr', { key: id },
|
|
173
|
-
h('td', { style: cellStyle }, id),
|
|
174
|
-
h('td', { style: cellStyle }, packetLabels[packet?.status] ?? packet?.status ?? '未知'),
|
|
175
|
-
h('td', { style: cellStyle }, packet?.route ?? '—')))))),
|
|
176
|
-
h(Budget, { value: status.state?.budget }),
|
|
177
|
-
status.state?.inbox?.length > 0 && h('details', null,
|
|
178
|
-
h('summary', null, `待人工处理记录:${status.state.inbox.length} 条`),
|
|
179
|
-
h('pre', { style: rawStyle }, JSON.stringify(status.state.inbox, null, 2)))),
|
|
180
|
-
h(Diagnostic, { value: diagnostic }),
|
|
181
|
-
h(Configuration, { value: configuration }),
|
|
182
|
-
raw && h('details', null, h('summary', null, '引擎原始结果(JSON)'),
|
|
183
|
-
h('pre', { style: rawStyle }, JSON.stringify(raw, null, 2))),
|
|
184
|
-
));
|
|
584
|
+
function TaskStatus({ control, selection, t }) {
|
|
585
|
+
const { status, checkedAt, stale } = control;
|
|
586
|
+
if (!status) return h('section', { className: css.empty, 'aria-label': t('statusHeading') },
|
|
587
|
+
h('h3', { className: css.heading }, t('emptyHeading')),
|
|
588
|
+
h('p', { className: css.meta }, t(selection.taskDir.trim() ? 'emptySelected' : 'emptyBody')));
|
|
589
|
+
const packets = Object.entries(status.state?.packets ?? {});
|
|
590
|
+
return h('section', { className: css.section, 'aria-label': t('statusHeading') },
|
|
591
|
+
h('div', { className: css.sectionHeader },
|
|
592
|
+
h('h3', { className: css.heading }, textValue(status.state?.slug, t('task'))),
|
|
593
|
+
h('span', { className: css.badge }, stale ? t('stateUnconfirmed') : taskLabel(status, t))),
|
|
594
|
+
h('p', { className: css.meta },
|
|
595
|
+
t('lastChecked', { time: new Date(checkedAt).toLocaleString() }) +
|
|
596
|
+
(status.running && status.pid > 0 ? ' · PID ' + status.pid : '')),
|
|
597
|
+
stale && h('p', { className: css.warningText },
|
|
598
|
+
t('stale') + ' · ' + t('previousState', { status: taskLabel(status, t) })),
|
|
599
|
+
status.state?.['current-milestone'] && h('p', null,
|
|
600
|
+
t('milestone', { name: textValue(status.state['current-milestone']) })),
|
|
601
|
+
packets.length > 0 && h('table', { className: css.packets, 'aria-label': t('packets') },
|
|
602
|
+
h('caption', null, t('packetsCount', { count: packets.length })),
|
|
603
|
+
h('thead', null, h('tr', null, ...['packet', 'state', 'route'].map(key => h('th', { key, scope: 'col' }, t(key))))),
|
|
604
|
+
h('tbody', null, ...packets.map(([id, packet]) => h('tr', { key: id },
|
|
605
|
+
h('td', null, id),
|
|
606
|
+
h('td', null, packetLabels[packet?.status] ? t(packetLabels[packet.status]) : textValue(packet?.status, t('unknown'))),
|
|
607
|
+
h('td', null, textValue(packet?.route)))))),
|
|
608
|
+
h(Budget, { value: status.state?.budget, t }),
|
|
609
|
+
status.state?.inbox?.length > 0 && h('details', { className: css.details },
|
|
610
|
+
h('summary', null, t('inboxCount', { count: status.state.inbox.length })),
|
|
611
|
+
h('pre', { className: css.raw }, JSON.stringify(status.state.inbox, null, 2))));
|
|
612
|
+
}
|
|
613
|
+
|
|
614
|
+
function Feedback({ control, selection, reconcile, t }) {
|
|
615
|
+
const busy = control.pending !== null;
|
|
616
|
+
const error = control.error &&
|
|
617
|
+
(!mutates(control.error.action) || control.error.dispatch === 'not-dispatched') ? control.error : null;
|
|
618
|
+
const mode = control.uncertainty ? 'uncertain' : error ? 'error' : busy ? 'loading' : 'idle';
|
|
619
|
+
const otherSelection = busy && control.pending.key !== selectionKey(selectionOf(selection));
|
|
620
|
+
const locked = busy && mutates(control.pending.action);
|
|
621
|
+
const pendingLabel = busy ? t(otherSelection ? 'busyOther' : 'busy', {
|
|
622
|
+
action: t(actionKey(control.pending)),
|
|
623
|
+
}) : '';
|
|
624
|
+
return h('div', { className: css.feedback, 'data-state': mode,
|
|
625
|
+
role: 'region', 'aria-label': t('feedback'), tabIndex: mode === 'uncertain' || mode === 'error' ? 0 : undefined },
|
|
626
|
+
busy && h('div', { className: control.uncertainty ? css.loadingCompact : css.loading,
|
|
627
|
+
role: 'status', 'aria-label': pendingLabel },
|
|
628
|
+
h(Spinner),
|
|
629
|
+
!control.uncertainty && (locked || otherSelection) && h('p', null, pendingLabel),
|
|
630
|
+
locked && h('p', { className: css.meta }, t('mutationLock'))),
|
|
631
|
+
control.uncertainty && h('section', { className: css.notice, role: 'alert' },
|
|
632
|
+
h('h3', { className: css.heading }, t('unknownOutcome', { action: t(control.uncertainty.action) })),
|
|
633
|
+
h('p', null, t('unknownHelp')),
|
|
634
|
+
h('code', { className: css.path }, control.uncertainty.taskDir),
|
|
635
|
+
h(Button, { variant: 'outline', size: 'sm', disabled: busy, onClick: reconcile }, t('reconcile')),
|
|
636
|
+
h('details', { className: css.details },
|
|
637
|
+
h('summary', null, t('errorDetails')),
|
|
638
|
+
h('pre', { className: css.raw }, control.uncertainty.detail))),
|
|
639
|
+
error && h('section', { className: css.error, role: 'alert' },
|
|
640
|
+
h('h3', { className: css.heading }, t(error.dispatch === 'not-dispatched' ? 'notDispatchedTitle' : 'errorTitle',
|
|
641
|
+
{ action: t(actionKey(error)) })),
|
|
642
|
+
h('p', null, t(error.dispatch === 'not-dispatched' ? 'notDispatchedHelp' : 'errorHelp')),
|
|
643
|
+
h('details', { className: css.details },
|
|
644
|
+
h('summary', null, t('errorDetails')),
|
|
645
|
+
h('pre', { className: css.raw }, error.detail))),
|
|
646
|
+
mode === 'idle' && h('div', { className: css.idleFeedback },
|
|
647
|
+
h('p', null, t('lifecycle')),
|
|
648
|
+
!selection.taskDir.trim() && h('p', null, t('taskRequired'))));
|
|
185
649
|
}
|
|
186
650
|
|
|
187
|
-
|
|
651
|
+
function TaskView({ useStore, actions, useControl, changeSelection, run, reconcile, t }) {
|
|
652
|
+
const selection = useStore(value => value);
|
|
653
|
+
const control = useControl(value => value);
|
|
654
|
+
const formId = React.useId();
|
|
655
|
+
const busy = control.pending !== null;
|
|
656
|
+
const locked = busy && mutates(control.pending.action);
|
|
657
|
+
const invalidTask = pathProblem(selection.taskDir);
|
|
658
|
+
const invalidCwd = pathProblem(selection.cwd);
|
|
659
|
+
const button = (action, labelKey, needsTask, probe = false, variant = 'outline') => h(Button, {
|
|
660
|
+
key: labelKey, variant, size: 'sm',
|
|
661
|
+
'aria-describedby': ['doctor', 'probe'].includes(labelKey) ? formId + '-' + labelKey + '-help' : undefined,
|
|
662
|
+
disabled: busy || !!invalidTask || !!invalidCwd || (needsTask && !selection.taskDir.trim()) ||
|
|
663
|
+
(mutates(action) && control.uncertainty !== null),
|
|
664
|
+
onClick: () => run(action, selection, probe),
|
|
665
|
+
}, t(labelKey));
|
|
666
|
+
const field = (name, hint, placeholder, problem) => h('div', { className: css.field },
|
|
667
|
+
h('label', { htmlFor: formId + '-' + name, className: css.fieldLabel }, t(name)),
|
|
668
|
+
h(Input, { id: formId + '-' + name, className: css.input,
|
|
669
|
+
value: selection[name], disabled: locked, placeholder, spellCheck: false,
|
|
670
|
+
'aria-invalid': !!problem, 'aria-describedby': formId + '-' + name + '-help',
|
|
671
|
+
onChange: event => changeSelection({ taskDir: selection.taskDir, cwd: selection.cwd, [name]: event.target.value }) }),
|
|
672
|
+
h('p', { id: formId + '-' + name + '-help', className: problem ? css.errorText : css.meta }, t(problem ?? hint)));
|
|
673
|
+
return h('section', { className: css.page, 'aria-label': t('title') },
|
|
674
|
+
h('style', { 'data-plugin': '@ckanner/dsh-plugin-flywheel',
|
|
675
|
+
'data-plugin-css': '@ckanner/dsh-plugin-flywheel/client.module.css',
|
|
676
|
+
}, stylesheet),
|
|
677
|
+
h('header', { className: css.header },
|
|
678
|
+
h('h2', { className: css.title }, t('title')),
|
|
679
|
+
h('p', { className: css.meta }, t('description'))),
|
|
680
|
+
h('section', { className: css.selection, 'aria-label': t('selection') },
|
|
681
|
+
h('div', { className: css.fields },
|
|
682
|
+
field('taskDir', 'taskHint', '/Users/me/project/docs/fix-login', invalidTask),
|
|
683
|
+
field('cwd', 'cwdHint', '/Users/me/project', invalidCwd)),
|
|
684
|
+
h('div', { className: css.actions },
|
|
685
|
+
button('status', 'status', true, false, 'primary'),
|
|
686
|
+
button('start', 'start', true), button('stop', 'stop', true))),
|
|
687
|
+
h(Feedback, { control, selection, reconcile, t }),
|
|
688
|
+
h(TaskStatus, { control, selection, t }),
|
|
689
|
+
h('details', { className: css.advanced, open: selection.advancedOpen,
|
|
690
|
+
onToggle: event => actions.setAdvancedOpen(event.currentTarget.open) },
|
|
691
|
+
h('summary', { className: css.heading }, t('advanced')),
|
|
692
|
+
h('div', { className: css.advancedBody },
|
|
693
|
+
h('p', { className: css.meta }, t('advancedHelp')),
|
|
694
|
+
h('div', { className: css.actions },
|
|
695
|
+
button('config', 'config', false), button('doctor', 'doctor', false),
|
|
696
|
+
button('doctor', 'probe', false, true)),
|
|
697
|
+
h('p', { className: css.meta, id: formId + '-doctor-help' }, t('doctorHelp')),
|
|
698
|
+
h('p', { className: css.meta, id: formId + '-probe-help' }, t('probeHelp')),
|
|
699
|
+
h(Diagnostic, { value: control.diagnostic, t }),
|
|
700
|
+
h(Configuration, { value: control.configuration, t }))),
|
|
701
|
+
control.raw && h('details', { className: css.details },
|
|
702
|
+
h('summary', null, t('raw')),
|
|
703
|
+
h('pre', { className: css.raw }, JSON.stringify(control.raw, null, 2))));
|
|
704
|
+
}
|
|
705
|
+
|
|
706
|
+
exports.inject = ['slots', 'remote', 'remote.commands', 'locale'];
|
|
188
707
|
exports.apply = function apply(ctx) {
|
|
189
|
-
ctx.
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
708
|
+
ctx.effect(() => ctx.locale.register(NS, { zh, en }), 'flywheel: locales');
|
|
709
|
+
const t = ctx.locale.bind(NS);
|
|
710
|
+
// Drafts are slot-owned view state. No busy/status value is persisted or
|
|
711
|
+
// restored as a live fact after a new Session binding or a plugin reload.
|
|
712
|
+
const viewStore = createViewStore();
|
|
713
|
+
const controls = new WeakMap();
|
|
714
|
+
const pendingOperations = new Set();
|
|
715
|
+
let disposed = false;
|
|
716
|
+
ctx.on('dispose', () => {
|
|
717
|
+
disposed = true;
|
|
718
|
+
for (const release of pendingOperations) release();
|
|
719
|
+
pendingOperations.clear();
|
|
720
|
+
});
|
|
721
|
+
ctx.slots.inject('conversation.view', () => ctx.slots.register({
|
|
722
|
+
name: 'conversation.view', id: 'flywheel', order: 20, label: () => t('view'),
|
|
723
|
+
locale: NS, store: viewStore,
|
|
724
|
+
inject: (sessionId, actions) => {
|
|
725
|
+
// Bound actions identify one retained Session binding. A recycled
|
|
726
|
+
// session id with a fresh binding must not adopt an old async result.
|
|
727
|
+
let control = controls.get(actions);
|
|
728
|
+
if (!control) {
|
|
729
|
+
control = createControl(async request => readReply(
|
|
730
|
+
await ctx.remote.commands.execute(sessionId, '/flywheel ' + JSON.stringify(request), []), request),
|
|
731
|
+
pendingOperations, () => disposed);
|
|
732
|
+
controls.set(actions, control);
|
|
733
|
+
}
|
|
734
|
+
return {
|
|
735
|
+
hooks: { control: control.source },
|
|
736
|
+
changeSelection: next => {
|
|
737
|
+
if (disposed || control.isMutating()) return;
|
|
738
|
+
actions.setSelection(next);
|
|
739
|
+
control.select(next);
|
|
740
|
+
},
|
|
741
|
+
run: control.run,
|
|
742
|
+
reconcile: () => {
|
|
743
|
+
const outcome = control.source.getSnapshot().uncertainty;
|
|
744
|
+
if (!outcome || control.source.getSnapshot().pending) return;
|
|
745
|
+
const next = { taskDir: outcome.taskDir, cwd: outcome.cwd };
|
|
746
|
+
actions.setSelection(next);
|
|
747
|
+
return control.run('status', next);
|
|
748
|
+
},
|
|
749
|
+
};
|
|
750
|
+
},
|
|
751
|
+
}, TaskView));
|
|
203
752
|
};
|
|
204
753
|
|
|
205
754
|
return module.exports;
|