Skip to content

Add a clipboard write path and keyboard log navigation - #201

Merged
wallstop merged 7 commits into
masterfrom
session-092-console-read-and-write
Sep 29, 2026
Merged

wallstop merged 7 commits into
masterfrom
session-092-console-read-and-write

Conversation

@wallstop

@wallstop wallstop commented Sep 29, 2026 •

Copy link
Copy Markdown
Owner

DISCLOSURE: LLM-GENERATED TEXT

Why:

Nothing could be read out of the console or moved through with the keyboard. The log view is the only place an error ever appears, and it had no clipboard write path and no key that reached it (#194, #195).

What:

How we know:

EditMode 639/639. PlayMode matches master: the same 5 failures, measured on unmodified master.
The log-scroll suite skips on this host (no rendered Game view) and names the reason.


Note

Medium Risk
Touches built-in command behavior, clipboard I/O, and terminal input routing; regressions would show as wrong copied text, broken trace, or scroll/follow fighting new log output.

Overview
Adds copy-last and copy-log [n] so console output can land on the system clipboard (messages only, no stack traces), with TerminalClipboard centralizing read/write and reporting when a platform refuses the write. Log reads for copy and trace now use OutputLength to skip trailing command echoes instead of fixed “second newest” indices, which fixes copying your own command name and wrong trace targets (including programmatic dispatch).

While the command line keeps focus, Page Up/Down and Ctrl/Cmd+Home/End scroll the log via LogScrollKeys and TryScrollLog, detaching tail-follow so new output does not yank a parked view; LogTailFollower.Detach treats a page that lands on the end as following again.

time runs the timed command with the already-parsed CommandArg tail so quoted arguments survive. Docs (README, CHANGELOG) and a log-echo-contract agent skill document the echo contract and test helpers that echo before dispatch.

Reviewed by Cursor Bugbot for commit fafbc15. Bugbot is set up for automated code reviews on this repo. Configure here.

Nothing could be read out of the console. The log view is the only place a
message, a command echo, or a failing value ever appears, and none of it
could be selected, so a developer who saw an error in a device build had to
retype it or screenshot the game window.

copy-last and copy-log put the newest line, or the last N, on the system
clipboard and answer in the console like every other command. A write is a
request a platform can decline - tvOS has no clipboard, and a browser may
refuse without a user gesture - so the write is read back and compared, and
a copy that did not happen is reported rather than silently doing nothing.

time rejoined its arguments with single spaces, so a quoted argument arrived
as several: 'time set-variable greet "two words"' was rejected before the
timed command ran. It now dispatches the parsed arguments it already has
instead of a flattened string, and the log window read is shared with trace
rather than duplicated.
The log could not be moved at all from the keyboard. The command line owns
panel focus for as long as the console is open, so Page Up, Page Down, Home,
and End all arrived at the command field instead of the log view, leaving the
wheel and a 10px dragger as the only ways to reach an error in a full buffer.

Page Up and Page Down now page the log by one viewport, and the command
modifier with Home or End reaches its oldest and newest lines. Home and End
without it stay with the command line, where they move the caret as a text
field should. Both the command and control flags are accepted, for the reason
the paste path already checks both.

A page places the scroller and pins nothing, and that is load-bearing. The
follower detaches on a value below the pin it already holds, so a page reads
as the developer's own scroll; pinning to the new position instead would make
the position equal the pin, the tail would never detach, and the next pass
would scroll the view straight back to the end. LogTailFollowerTests now pins
that rule directly, since the terminal depends on it.

This host's editor lays out no UI Toolkit panel, so a log view has no geometry
here and its scroller has nothing to scroll. TerminalUILogScrollTests skips
with that reason rather than reporting a red the host produced; the decision
it drives is covered engine-independently in LogScrollKeysTests.
A panel left half-built by an earlier test can report a scroller range for a
frame or two and lose it again. Guarding on having seen a range once let a
test start on that flicker and assert into geometry that had gone, which read
as a red the change did not cause. The guard now requires the layout to
survive several consecutive frames, which is the difference between an
environment that can answer the suite and one that cannot.
The review's two blocking findings were measured rather than believed, and
both were refuted: restoring master's 'time' and running it still substitutes
a $name and still pushes the same single history entry, because substitution
happens when the outer line is parsed and the history funnel is the same one
either way. Those two behaviours are now pinned by tests, so the claim cannot
be re-made on the next review.

The real bug it found was the one neither of us had modelled. A page sets the
scroller and relies on the follower seeing a value below its pin - but running
a command calls Attach, which clears that pin, and the pin lands a frame
later. A page taken in that window has nothing to be below, so the next pass
reads it as output and snaps the view straight back to the end. LogTailFollower
gains an explicit Detach(position) for the one move Observe cannot infer, and
TerminalUI calls it. A heuristic inside Observe was tried first and broke three
existing contracts - a view nobody scrolled must still follow - which is why
the caller's knowledge is the signal and not a guess.

Also: the clipboard refusal message is no longer given for a log that holds no
text, which is a different failure from a platform declining a copy; Read() is
no longer dead code, since the paste path now goes through it; and the modifier
comment describes what the flags actually are rather than what a platform
usually sends.

Three of the seven PlayMode tests passed with the feature deleted - a negative
test that never parks the view first cannot fail. Each now proves the log moved
before asserting that a key did not move it, and two copy tests that asserted
a clipboard value they never set now assert the clipboard is unchanged.
…reeze

The second review executed the follower and found a freeze this branch would
have shipped. A page writes the scroller and then records a detach
unconditionally, so a key that moved nothing - Page Down or Ctrl+End on a view
already at its end - detached a view sitting exactly on the end. The next
frame's layout grows the end past it, and from there nothing moved the view
again: the log stopped scrolling, silently, with recovery only by running a
command. The trigger was the ordinary one, catching up on a log that is still
growing, where the last press of the sweep is the press that did it.

Detach now takes the extent and declines when the scroll landed on the end,
which puts the rule where the tolerance lives instead of restating it at the
call site. Pinned by a test that fails against the unconditional version, and
by a PlayMode test for the gesture itself.

Also from that round: whether the copied lines held text is answered per line
rather than on the joined string, because two empty lines still join to one
newline and were reported as a successful copy of a single character; the
Ctrl+End test no longer pages twice and now logs after the key so it can
distinguish "returned to the tail" from "never left it"; the history assertion
is one entry rather than at most one; the three new runtime metas carry the
MonoImporter their neighbours use instead of the one Unity would rewrite.

And the features are documented, which the previous commit shipped without:
README gains "Copying out of the console" and "Reading the log with the
keyboard" next to the paste section, and the changelog now says what a page
reaching the end does and that time substitutes a variable once rather than
twice.

@cursor cursor Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cursor Bugbot has reviewed your changes and found 1 potential issue.

Fix All in Cursor

❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.

Reviewed by Cursor Bugbot for commit a272b1a. Configure here.

Comment thread Runtime/CommandTerminal/Backend/BuiltinCommands.cs
The comment stated it twice: once as a behaviour kept, once as a behaviour
changed. A reader met two statements of one rule and could not tell which was
current. One block now says both halves - $name is still substituted, and a
stored value that is itself $name is no longer expanded a second time.
@wallstop

Copy link
Copy Markdown
Owner Author

DISCLOSURE: LLM-GENERATED TEXT

Correct, and it shipped broken. Fixed in a2... (see below).

EnterCommand writes the typed line as TerminalLogType.Input before
RunCommand, so the newest entry when copy-last runs is the word
copy-last. From the console it copied itself instead of the message.

It survived two review rounds because the test helper called
shell.RunCommand(line) directly, which never writes the echo, so the whole
suite exercised a path the product never takes. Run now mirrors the console:

Terminal.Log(TerminalLogType.Input, line);
_shell.RunCommand(line);

That alone turned 7 of the suite green-red. The fix is OutputLength, which
walks back over trailing Input entries; copy-last and copy-log bound
their window with it.

Swept the class, not the symptom. trace read a fixed logCount - 2, which
has the same defect twice: it printed a command line whenever the command
above trace said nothing, and skipped the newest message when dispatched
programmatically (no echo). It now uses OutputLength(...) - 1. Both changes
are in the changelog.

Two consumers confirmed already correct, and left alone:
CommandPaletteUI.CollectOutput captures its Version delta before its own
echo and filters the whole window, which is right because that window cannot
contain another submit's echo; TerminalUI.RefreshLogs renders echoes, which
is right for display. The asymmetry is deliberate and now documented.

Recorded as a skill so it does not recur: .llm/skills/log-echo-contract/.

Cursor Bugbot found it on the PR, and it was right: EnterCommand writes the
typed line into the log as TerminalLogType.Input before the handler runs, so
copy-last copied the word "copy-last" instead of the message the developer was
reaching for. The second review had already found this and it was dropped.

The test suite could not see it because the file's Run helper called
shell.RunCommand directly, which never writes the echo, so the whole suite
exercised a path the product never takes. Run now mirrors the console, and that
alone turned 7 of 19 tests red before any production change.

OutputLength walks back over the trailing command echoes and bounds the copy
window. Only the newest end is walked: an echo further back is a command the
developer ran, and a transcript wants those as much as the output.

Swept rather than patched. trace read a fixed logCount - 2, which has the same
defect twice - it printed a command line whenever the command above it said
nothing, and it skipped the newest message when dispatched programmatically,
where there is no echo at all. It uses the same rule now, and both changes are
in the changelog.

CommandPaletteUI.CollectOutput and TerminalUI.RefreshLogs were checked and are
already correct: the palette filters a whole window that cannot contain
another submit's echo, and the view renders echoes because they belong on
screen. The asymmetry is deliberate and is now written down.

New skill .llm/skills/log-echo-contract/ carries the contract, the two valid
filter shapes, and the test-helper trap that hid all of it.
@wallstop
wallstop merged commit 9de9b58 into master Sep 29, 2026
12 checks passed
@wallstop
wallstop deleted the session-092-console-read-and-write branch September 29, 2026 19:52
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant