instar 0.28.58 → 0.28.59
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/dashboard/index.html
CHANGED
|
@@ -3221,6 +3221,7 @@
|
|
|
3221
3221
|
|
|
3222
3222
|
activeSession = tmuxSession;
|
|
3223
3223
|
userIsFollowing = true; // Reset scroll tracking on session switch
|
|
3224
|
+
hideResumeButton(); // Carry-over button from prior session would be misleading
|
|
3224
3225
|
historyLinesLoaded = 2000; // Reset history state for new session
|
|
3225
3226
|
historyLoading = false;
|
|
3226
3227
|
historyExhausted = false;
|
|
@@ -3334,15 +3335,20 @@
|
|
|
3334
3335
|
if (atBottom && !userIsFollowing) {
|
|
3335
3336
|
// User scrolled back to bottom — resume following and apply pending output
|
|
3336
3337
|
userIsFollowing = true;
|
|
3338
|
+
hideResumeButton();
|
|
3337
3339
|
if (pendingOutputData) {
|
|
3338
3340
|
const data = pendingOutputData;
|
|
3339
3341
|
pendingOutputData = null;
|
|
3340
3342
|
term.clear();
|
|
3341
|
-
term.write
|
|
3342
|
-
term.scrollToBottom();
|
|
3343
|
+
// term.write is async — scroll after write completes so the buffer is populated.
|
|
3344
|
+
term.write(data, () => { term.scrollToBottom(); });
|
|
3343
3345
|
}
|
|
3344
|
-
} else if (!atBottom) {
|
|
3346
|
+
} else if (!atBottom && userIsFollowing) {
|
|
3347
|
+
// User scrolled up — pause follow and surface the resume button
|
|
3348
|
+
// even if no new output is pending. The button is the always-available
|
|
3349
|
+
// "take me back to live" control.
|
|
3345
3350
|
userIsFollowing = false;
|
|
3351
|
+
showResumeButton();
|
|
3346
3352
|
}
|
|
3347
3353
|
|
|
3348
3354
|
// Infinite scroll: load more history when scrolled near the top
|
|
@@ -3370,12 +3376,13 @@
|
|
|
3370
3376
|
const atBottom = buf.baseY === 0 || (buf.baseY - buf.viewportY) <= 5;
|
|
3371
3377
|
if (atBottom) {
|
|
3372
3378
|
userIsFollowing = true;
|
|
3379
|
+
hideResumeButton();
|
|
3373
3380
|
if (pendingOutputData) {
|
|
3374
3381
|
const data = pendingOutputData;
|
|
3375
3382
|
pendingOutputData = null;
|
|
3376
3383
|
term.clear();
|
|
3377
|
-
term.write
|
|
3378
|
-
term.scrollToBottom();
|
|
3384
|
+
// term.write is async — scroll after write completes so the buffer is populated.
|
|
3385
|
+
term.write(data, () => { term.scrollToBottom(); });
|
|
3379
3386
|
}
|
|
3380
3387
|
}
|
|
3381
3388
|
}
|
|
@@ -3449,23 +3456,20 @@
|
|
|
3449
3456
|
btn.style.cssText = 'position:absolute;bottom:12px;left:50%;transform:translateX(-50%);z-index:10;background:var(--accent);color:#000;border:none;padding:6px 16px;border-radius:4px;font-size:12px;cursor:pointer;font-weight:600;opacity:0.95;box-shadow:0 2px 8px rgba(0,0,0,0.3);';
|
|
3450
3457
|
btn.onclick = () => {
|
|
3451
3458
|
userIsFollowing = true;
|
|
3459
|
+
const snapToBottom = () => {
|
|
3460
|
+
term.scrollToBottom();
|
|
3461
|
+
const container = document.getElementById('terminalContainer');
|
|
3462
|
+
if (container) container.scrollIntoView({ block: 'end', behavior: 'instant' });
|
|
3463
|
+
};
|
|
3452
3464
|
if (pendingOutputData) {
|
|
3453
3465
|
const data = pendingOutputData;
|
|
3454
3466
|
pendingOutputData = null;
|
|
3455
3467
|
term.clear();
|
|
3456
|
-
term.write
|
|
3457
|
-
//
|
|
3458
|
-
|
|
3459
|
-
term.scrollToBottom();
|
|
3460
|
-
// Also scroll the browser viewport to show the terminal bottom
|
|
3461
|
-
const container = document.getElementById('terminalContainer');
|
|
3462
|
-
if (container) container.scrollIntoView({ block: 'end', behavior: 'instant' });
|
|
3463
|
-
});
|
|
3468
|
+
// term.write is async — use its completion callback so scroll runs
|
|
3469
|
+
// after the buffer is populated, not against a stale viewport.
|
|
3470
|
+
term.write(data, snapToBottom);
|
|
3464
3471
|
} else {
|
|
3465
|
-
|
|
3466
|
-
term.scrollToBottom();
|
|
3467
|
-
const container = document.getElementById('terminalContainer');
|
|
3468
|
-
if (container) container.scrollIntoView({ block: 'end', behavior: 'instant' });
|
|
3472
|
+
snapToBottom();
|
|
3469
3473
|
}
|
|
3470
3474
|
hideResumeButton();
|
|
3471
3475
|
};
|
package/package.json
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
{
|
|
2
2
|
"$schema": "./builtin-manifest.schema.json",
|
|
3
3
|
"schemaVersion": 1,
|
|
4
|
-
"generatedAt": "2026-04-19T02:
|
|
5
|
-
"instarVersion": "0.28.
|
|
4
|
+
"generatedAt": "2026-04-19T02:56:02.572Z",
|
|
5
|
+
"instarVersion": "0.28.59",
|
|
6
6
|
"entryCount": 186,
|
|
7
7
|
"entries": {
|
|
8
8
|
"hook:session-start": {
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
# Upgrade Guide — Dashboard "Resume live output" reliability + always-visible control
|
|
2
|
+
|
|
3
|
+
## What Changed
|
|
4
|
+
|
|
5
|
+
Fixed two long-standing rough edges in the dashboard's terminal view for the
|
|
6
|
+
"▼ Resume live output" control.
|
|
7
|
+
|
|
8
|
+
### Scroll-to-bottom now actually lands at the bottom
|
|
9
|
+
|
|
10
|
+
Clicking "▼ Resume live output" after scrolling up in a session view used to
|
|
11
|
+
sometimes leave the viewport mid-history instead of snapping to the tail of
|
|
12
|
+
live output — defeating the purpose of the button. The same stale-scroll
|
|
13
|
+
happened on the two sibling auto-resume paths that fire when the user
|
|
14
|
+
scrolls back to the bottom or wheels down into it.
|
|
15
|
+
|
|
16
|
+
Root cause: `term.write(data)` in xterm is asynchronous. The old code called
|
|
17
|
+
`term.scrollToBottom()` immediately after the write, or inside a
|
|
18
|
+
`requestAnimationFrame` — neither guarantees the buffer has been populated,
|
|
19
|
+
so the scroll operated on stale viewport state and landed above the new
|
|
20
|
+
output.
|
|
21
|
+
|
|
22
|
+
Fix: all three resume paths now use xterm's write-completion callback —
|
|
23
|
+
`term.write(data, () => term.scrollToBottom())`. The scroll runs after the
|
|
24
|
+
buffer is populated, so the viewport lands at the new bottom.
|
|
25
|
+
|
|
26
|
+
### The button is always visible when paused
|
|
27
|
+
|
|
28
|
+
Previously the "▼ Resume live output" button only appeared when new output
|
|
29
|
+
happened to arrive while the user was scrolled up. In idle sessions —
|
|
30
|
+
nothing streaming, user just scrolled up to read history — the button never
|
|
31
|
+
surfaced, and there was no UI control to jump back to live.
|
|
32
|
+
|
|
33
|
+
Fix: button visibility now mirrors `!userIsFollowing` from every code path
|
|
34
|
+
that flips the flag. Scroll up in a session view (idle or streaming) and the
|
|
35
|
+
button appears. Scroll back to the bottom, click the button, wheel down into
|
|
36
|
+
the tail, or switch sessions, and the button hides.
|
|
37
|
+
|
|
38
|
+
## What to Tell Your User
|
|
39
|
+
|
|
40
|
+
The "Resume live output" button in your dashboard's session view now works
|
|
41
|
+
the way you'd expect: it shows up any time you've scrolled up (even in an
|
|
42
|
+
idle session where nothing is streaming), and clicking it reliably drops
|
|
43
|
+
you at the bottom of the live output instead of leaving you mid-history.
|
|
44
|
+
|
|
45
|
+
## Summary of New Capabilities
|
|
46
|
+
|
|
47
|
+
| Capability | How to Use |
|
|
48
|
+
|-----------|-----------|
|
|
49
|
+
| Reliable auto-follow resume in the dashboard terminal | Click "▼ Resume live output" after scrolling up in any session view; viewport snaps to the bottom |
|
|
50
|
+
| Button always available while paused | Scroll up in any session — the button surfaces automatically; scroll back down or click it to resume |
|
|
51
|
+
|
|
52
|
+
## Evidence
|
|
53
|
+
|
|
54
|
+
The fix was reproduced from Justin's report (topic 6309 on echo agent): the
|
|
55
|
+
button left him mid-history, and in idle sessions there was no button at
|
|
56
|
+
all.
|
|
57
|
+
|
|
58
|
+
Regression test (`tests/unit/dashboard-resumeLive.test.ts`, 10 tests) locks
|
|
59
|
+
in both invariants at the HTML-inspection layer:
|
|
60
|
+
|
|
61
|
+
- Scroll-after-write: all three resume paths use the xterm write callback;
|
|
62
|
+
the old `term.write(data); requestAnimationFrame(() => term.scrollToBottom())`
|
|
63
|
+
shape is explicitly banned.
|
|
64
|
+
- Button visibility: `term.onScroll` shows the button on scroll-up and
|
|
65
|
+
hides it on scroll-to-bottom; the wheel-down resume path hides the
|
|
66
|
+
button; session-switch reset hides any carry-over button.
|
|
67
|
+
|
|
68
|
+
Full dashboard test suite green across `dashboard-*.test.ts`.
|
|
69
|
+
|
|
70
|
+
Side-effects review:
|
|
71
|
+
`upgrades/side-effects/dashboard-resume-live-scroll.md` — concludes no
|
|
72
|
+
decision-point touched (pure UI timing + visibility fix), no over/under-block
|
|
73
|
+
risk, trivial rollback (git revert).
|
|
74
|
+
|
|
75
|
+
## Deployment Notes
|
|
76
|
+
|
|
77
|
+
No operator action required on update. The dashboard is static HTML served
|
|
78
|
+
from the installed instar package — clients pick up the fix on their next
|
|
79
|
+
page load after `instar` updates to 0.28.59.
|
|
80
|
+
|
|
81
|
+
## Rollback
|
|
82
|
+
|
|
83
|
+
Downgrade reverts the three resume paths to the prior (racy) code and
|
|
84
|
+
returns the button to its contingent-on-new-output visibility. No schema
|
|
85
|
+
changes, no state-file changes, no API changes.
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
# Side-Effects Review — Dashboard "Resume live output" reliability + always-visible while paused
|
|
2
|
+
|
|
3
|
+
**Version / slug:** `dashboard-resume-live-scroll`
|
|
4
|
+
**Date:** `2026-04-18`
|
|
5
|
+
**Author:** `echo`
|
|
6
|
+
**Second-pass reviewer:** `not required — pure UI timing fix, no gate/sentinel/watchdog/session-lifecycle surface`
|
|
7
|
+
|
|
8
|
+
## Summary of the change
|
|
9
|
+
|
|
10
|
+
The dashboard's "Resume live output" button (and the two sibling resume paths that
|
|
11
|
+
fire on scroll-to-bottom and wheel-down) did not reliably snap the xterm viewport
|
|
12
|
+
to the bottom after being clicked. The user still landed mid-history and had to
|
|
13
|
+
scroll manually to see live output — defeating the purpose of the button.
|
|
14
|
+
|
|
15
|
+
Root cause: `term.write(data)` is asynchronous — xterm parses and renders the
|
|
16
|
+
write on a microtask. The old code called `term.scrollToBottom()` either
|
|
17
|
+
immediately after the write or inside a `requestAnimationFrame`. Neither
|
|
18
|
+
guarantees the buffer has been populated, so `scrollToBottom()` snapped against
|
|
19
|
+
a stale `baseY` and landed above the new output.
|
|
20
|
+
|
|
21
|
+
Fix: use xterm's write-completion callback — `term.write(data, () => { term.scrollToBottom(); })`.
|
|
22
|
+
The callback fires after the write is flushed, so the scroll operates on the
|
|
23
|
+
final buffer state.
|
|
24
|
+
|
|
25
|
+
## Secondary change — button visibility mirrors follow state
|
|
26
|
+
|
|
27
|
+
The prior behavior only showed the "▼ Resume live output" button when new
|
|
28
|
+
output happened to arrive while the user was scrolled up — via `showResumeButton()`
|
|
29
|
+
inside `renderTerminalOutput`'s not-following branch. If the user scrolled up in
|
|
30
|
+
an idle session (no new output arriving), the button never appeared, and there
|
|
31
|
+
was no UI control to jump back to live.
|
|
32
|
+
|
|
33
|
+
Fix: `term.onScroll` now surfaces the button the moment the user scrolls up
|
|
34
|
+
(`!atBottom && userIsFollowing` → flip false + `showResumeButton()`), and hides
|
|
35
|
+
it the moment they return to the bottom by any means (`atBottom && !userIsFollowing`
|
|
36
|
+
→ flip true + `hideResumeButton()`). The wheel-down resume path hides the button
|
|
37
|
+
for the same reason. Session-switch reset calls `hideResumeButton()` so a
|
|
38
|
+
carry-over button from a prior session doesn't mislead.
|
|
39
|
+
|
|
40
|
+
The button is now the always-available "take me back to live" control, not a
|
|
41
|
+
contingent reaction to incoming output.
|
|
42
|
+
|
|
43
|
+
Files touched:
|
|
44
|
+
|
|
45
|
+
- `dashboard/index.html` — two overlapping changes:
|
|
46
|
+
- Three resume paths use xterm's write-completion callback:
|
|
47
|
+
1. The button `onclick` handler.
|
|
48
|
+
2. The `term.onScroll` handler's auto-resume branch.
|
|
49
|
+
3. The `xtermViewport` wheel handler's auto-resume branch.
|
|
50
|
+
The button handler retains `container.scrollIntoView({block:'end'})` in a
|
|
51
|
+
shared `snapToBottom` closure so both the pending-data and no-pending-data
|
|
52
|
+
branches do the same thing.
|
|
53
|
+
- Button visibility now mirrors `!userIsFollowing` from every path that
|
|
54
|
+
flips the flag: scroll-up in `onScroll`, scroll-to-bottom in `onScroll`,
|
|
55
|
+
wheel-down resume, and session-switch reset.
|
|
56
|
+
- `tests/unit/dashboard-resumeLive.test.ts` — new. HTML-inspection regression
|
|
57
|
+
test, consistent with the existing `dashboard-*.test.ts` pattern. Ten tests,
|
|
58
|
+
split across two describe blocks: (a) scroll-after-write invariants — locks
|
|
59
|
+
in the callback form on all three paths and bans the old
|
|
60
|
+
`term.write(data); requestAnimationFrame(() => term.scrollToBottom())` shape;
|
|
61
|
+
(b) button-visibility invariants — asserts show on scroll-up, hide on
|
|
62
|
+
scroll-to-bottom / wheel-resume / session switch.
|
|
63
|
+
|
|
64
|
+
## 1. Over-block
|
|
65
|
+
|
|
66
|
+
N/A — no block/allow decision exists in this change. The only behavior altered
|
|
67
|
+
is the timing of a DOM scroll call relative to an xterm write.
|
|
68
|
+
|
|
69
|
+
## 2. Under-block
|
|
70
|
+
|
|
71
|
+
N/A — see Over-block.
|
|
72
|
+
|
|
73
|
+
## 3. Level-of-abstraction fit
|
|
74
|
+
|
|
75
|
+
The fix lives at the exact layer where the bug lives: dashboard JavaScript
|
|
76
|
+
interacting with xterm.js. xterm's documented API for scheduling work after
|
|
77
|
+
a write is the write callback; using `requestAnimationFrame` was a workaround
|
|
78
|
+
that guessed at timing instead of using the library's native hook. The fix
|
|
79
|
+
moves us from guessing to the library-native pattern.
|
|
80
|
+
|
|
81
|
+
There is no higher-level authority that should own terminal-viewport scrolling
|
|
82
|
+
— this is a leaf UI behavior. There is no lower layer to delegate to — xterm
|
|
83
|
+
itself is the layer.
|
|
84
|
+
|
|
85
|
+
## 4. Signal-vs-authority compliance
|
|
86
|
+
|
|
87
|
+
No decision point touched. This is not a gate, filter, sentinel, watchdog, or
|
|
88
|
+
dispatcher. No brittle logic holds blocking authority. The principle does not
|
|
89
|
+
apply. Documented in Phase 1.
|
|
90
|
+
|
|
91
|
+
## 5. Interactions
|
|
92
|
+
|
|
93
|
+
- **`renderTerminalOutput` (the steady-state write path):** Still uses the
|
|
94
|
+
bare `term.write(data); term.scrollToBottom();` form. That path runs only
|
|
95
|
+
when `userIsFollowing === true` — xterm's default behavior is to auto-track
|
|
96
|
+
the bottom when the viewport is already there, so the follow-up
|
|
97
|
+
`scrollToBottom()` is essentially redundant and harmless. Not touched.
|
|
98
|
+
- **`term.onScroll` flag-flipping:** The scroll event fires during
|
|
99
|
+
`term.write()` as the buffer grows. If the write callback runs after
|
|
100
|
+
`scrollToBottom()`, the onScroll handler sees `atBottom === true` and
|
|
101
|
+
keeps `userIsFollowing === true`. No race with flag-flipping introduced.
|
|
102
|
+
- **Concurrent WS output:** If a new frame arrives via WebSocket during the
|
|
103
|
+
click handler, it calls `renderTerminalOutput` with `userIsFollowing === true`
|
|
104
|
+
(we just set it). That path does its own `term.clear()` + `term.write` +
|
|
105
|
+
`scrollToBottom()`, which will overwrite whatever our click handler was
|
|
106
|
+
mid-flight. Net effect: user ends up at bottom either way. No deadlock, no
|
|
107
|
+
torn state.
|
|
108
|
+
- **Infinite-scroll history loader:** Unaffected — uses `buf.viewportY <= 10`,
|
|
109
|
+
which is a separate concern from the bottom-snap logic.
|
|
110
|
+
|
|
111
|
+
## 6. External surfaces
|
|
112
|
+
|
|
113
|
+
None. This is client-side JavaScript in the dashboard HTML, served to the
|
|
114
|
+
user's browser. No other agents, no other users, no other systems observe the
|
|
115
|
+
change. No server API touched. No persisted state touched.
|
|
116
|
+
|
|
117
|
+
## 7. Rollback cost
|
|
118
|
+
|
|
119
|
+
Trivial: `git revert <commit>`. No data migration, no state repair, no release
|
|
120
|
+
coordination. Dashboard is static HTML served by the instar server; a revert
|
|
121
|
+
commit + server restart puts the old code back in front of the user
|
|
122
|
+
immediately.
|
|
123
|
+
|
|
124
|
+
## Verification
|
|
125
|
+
|
|
126
|
+
- HTML-inspection regression test passing (`tests/unit/dashboard-resumeLive.test.ts`, 6 tests).
|
|
127
|
+
- Full dashboard test suite passing (`dashboard-*.test.ts`, 20 tests total).
|
|
128
|
+
- Manual browser verification: deferred to user acceptance — reproducing the
|
|
129
|
+
bug end-to-end requires a live session with pending output and a user
|
|
130
|
+
scrolled up, which is Justin's original reporting path. The mechanical fix
|
|
131
|
+
is narrow enough (swap `term.write(data); scroll` → `term.write(data, scroll)`)
|
|
132
|
+
that the HTML regression is the load-bearing guarantee.
|