This commit is contained in:
2026-09-15 01:12:50 +08:00
parent 2db889ad89
commit 1a75503105
99 changed files with 4168 additions and 726 deletions
@@ -93,14 +93,16 @@
the project file, so the script travels with the project. <code>Run</code> evaluates the text against the
session shell as the globals object, and when the script returns a sequence of actions that sequence
is yielded into the run.</p>
<p>The title is what the mission row and the tree node show: the label is composed as <code>Script [title]</code>,
falling back to a bare <code>Script</code> while the title is empty. A newly added command starts empty on both
clients — no title, no text.</p>
<p>The panel renders on the command's own node in the Mission branch of the Execution page's Control
Tree, below the move / duplicate / delete control bar. A single-purpose kind like this one embeds its
whole editor on that node rather than growing section child nodes. Its tree checkbox only decides
whether the run plays the command; a disabled script is skipped during play and its editor stays
fully usable.</p>
<p>The title is what the command's tree row shows: the label is composed as <code>Script [title]</code>, falling
back to a bare <code>Script</code> while the title is empty. A newly added command starts empty — no title, no
text.</p>
<p>The panel renders on the command's own node under the Mission root of the Execution page's Control
Tree. The editor pane holds nothing but the editor: the command's move up / move down / duplicate /
delete buttons and its drag handle sit on its tree row, and the Add Command <code>+</code> sits on the row of
the list that holds it. A single-purpose kind like this one embeds its whole editor on that node
rather than growing section child nodes. Its tree checkbox only decides whether the run plays the
command; a disabled script is skipped during play — a caption above the title says so — and its
editor stays fully usable.</p>
<h2 id="layout">Layout</h2>
<ul>
<li>Head Line
@@ -163,9 +165,20 @@ save handed back. Each field is applied only when the body carries it. The respo
hash to chain into the next save.</p>
<p>Two prompts guard the edges, each with three buttons because neither is a yes/no question:</p>
<ul>
<li><strong>Unsaved Changes</strong>, raised when the Control Tree tries to move the selection off a command whose
save is still pending: Save &amp; switch flushes and then allows the switch, Discard drops the pending
write, Cancel keeps the selection where it is.</li>
<li><strong>Unsaved Changes</strong>, raised by the switch gate the panel hands the Control Tree host, which runs
it before the selection moves off the command and before every mission row op — move, duplicate,
delete, drag, Add Command. The op rebuilds the list it acted on, and that rebuild remounts the
panel whenever this command sits inside that list (a nested List included); when the panel
survives instead, the discard below has only put the autosave to sleep, and the next keystroke
resumes it on the same, still-valid path. The gate first lets a save already on the wire land,
and prompts only when edits are still staged after it: Save &amp; switch flushes and proceeds only
when that save landed — a conflict answered with Cancel leaves the edit staged and refuses the
switch; Discard reverts the editor to the last confirmed value and puts the autosave to sleep
until the next edit; Cancel keeps the selection where it is. A refused gate drops a row op and
says so in a notice (&quot;Cancelled — the open editor keeps its unsaved edits.&quot;). A row op cannot
reach the prompt while another is in flight — the host's busy gate refuses it first — but a
selection change holds no such gate, so a <code>?tree=</code> change arriving while a tree click's prompt is
up shares the pending answer instead of replacing it.</li>
<li><strong>Script changed elsewhere</strong>, raised when the server rejects the save because the hash no longer
matches — another tab, or a project reload, changed the command underneath. The rejection carries
the server's current text, title and hash, so the panel can offer Discard &amp; reload, Force
@@ -173,8 +186,10 @@ overwrite, or Cancel without a second round trip.</li>
</ul>
<p>The panel snapshots its command path at mount, and the autosave closes over that snapshot rather
than over the live selection, so a flush can never land on whichever command happens to be selected
when it fires. After each successful save the tree relabels the node in place instead of rebuilding
the branch, which would remount the editor mid-edit.</p>
when it fires. That path is an index-based entry path which every structural op re-points, which is
why the row ops run the gate above before their rebuild; an op on another row leaves the selection
on this editor's own row, re-indexed. After each successful save the tree relabels the node in
place instead of rebuilding the branch, which would remount the editor mid-edit.</p>
<h2 id="the-compile-gate">The Compile Gate</h2>
<p>A script that does not compile does nothing at run time: the command reports the compile failure
into the run's message stream and yields no work, so the run continues without it. Starting a run
@@ -190,8 +205,15 @@ the whole mission.</p>
<ul>
<li><code>wwwroot-src/src/components/mission/ScriptCommandPanel.vue</code> — this panel: the title row sharing
its line with the autosave indicator, the editor below it, the 500 ms autosave, the two
three-button prompts, the path snapshot taken at mount, and the change notice that relabels the
three-button prompts, the path snapshot taken at mount, the switch gate that waits for a save on
the wire and hands a second caller the pending prompt, and the change notice that relabels the
tree node.</li>
<li><code>wwwroot-src/src/components/controlTree/MissionCommandSlavePanel.vue</code> — the command node's
slave panel: mounts this editor on the entry path, forwards its switch gate to the host, and
shows the disabled caption above the title.</li>
<li><code>wwwroot-src/src/components/controlTree/ControlTreePanel.vue</code> — the tree rows: the drag handle
and the move / duplicate / delete buttons on the command's row, and the Add Command <code>+</code> on its
list's row.</li>
<li><code>wwwroot-src/src/components/widgets/TextEditor.vue</code> — the CodeMirror 6 wrapper: language and
read-only swapped through compartments, CRLF normalised on the way in so an unedited file cannot
dirty the autosave, an opt-in completion source that overrides the language's own, and a widened
@@ -208,14 +230,23 @@ kind union that maps straight onto CodeMirror's completion types.</li>
loader, the save that carries the expected hash, and the conflict error that carries the server's
current copy.</li>
<li><code>wwwroot-src/src/composables/useAutoSave.ts</code> — the debounce, the state machine and the conflict
recovery behind the panel's saving, saved and error states.</li>
recovery behind the panel's saving, saved and error states; the discard that reverts the editor
and sleeps the autosave until the next edit, and the wait for a save already on the wire.</li>
<li><code>wwwroot-src/src/components/widgets/AutoSaveIndicator.vue</code> — the status pill beside the title.</li>
<li><code>wwwroot-src/src/components/controlTree/useControlTreeHost.ts</code> — funnels every selection change
through the active panel's switch gate, which is how the Unsaved Changes prompt gets its say.</li>
and every mission row op through the active panel's switch gate, which is how the Unsaved
Changes prompt gets its say; selection is read-only while a row op is in flight, and a refused
gate drops the op with a notice.</li>
<li><code>wwwroot-src/src/components/controlTree/missionItemTypes.ts</code> — maps the <code>script</code> kind to this
panel and gives it no section children, so the whole editor lives on the command node.</li>
<li><code>wwwroot-src/src/i18n/en/mission.ts</code> — the panel's wording: both prompts, their buttons, and the
load and save error contexts.</li>
<li><code>wwwroot-src/src/i18n/en/mission.ts</code> — the panel's wording: both prompts' titles and messages,
the Save &amp; switch, Force overwrite and Discard &amp; reload buttons, and the load and save error
contexts.</li>
<li><code>wwwroot-src/src/i18n/en/common.ts</code> — the shared strings the panel borrows: the &ldquo;Title
(optional)&rdquo; label, the Cancel button both prompts carry, and the Unsaved Changes prompt's
Discard.</li>
<li><code>wwwroot-src/src/i18n/en/tree.ts</code> — the disabled caption and the notice a refused gate raises on
a row op.</li>
<li><code>Missions/ScriptCompletionController.cs</code> — <code>POST /api/script/completions</code>: a thin wrapper that
returns the items, 499 on cancellation and 500 with the message on failure.</li>
<li><code>Missions/ScriptCompletionService.cs</code> — the singleton holding one workspace, built from the same