@pellux/goodvibes-tui 0.19.23 → 0.19.24
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +16 -0
- package/README.md +1 -1
- package/package.json +1 -1
- package/src/core/conversation.ts +36 -13
- package/src/panels/panel-manager.ts +6 -2
- package/src/renderer/panel-composite.ts +42 -5
- package/src/renderer/panel-workspace-bar.ts +5 -1
- package/src/version.ts +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,22 @@ All notable changes to GoodVibes TUI.
|
|
|
4
4
|
|
|
5
5
|
---
|
|
6
6
|
|
|
7
|
+
## [0.19.24] — 2026-04-22
|
|
8
|
+
|
|
9
|
+
Four correctness and honesty fixes from an external architecture review. No SDK change (pinned at 0.23.2).
|
|
10
|
+
|
|
11
|
+
### Fixed
|
|
12
|
+
|
|
13
|
+
- **`/clear` now actually clears the display** (`src/core/conversation.ts`). The original `clearDisplay()` immediately rebuilt the display buffer from `this.messages`, so `getDisplayBlocks().length` was unchanged before and after the call — `/clear` was a no-op. The fix introduces a `_displayFromMessageIndex` watermark: `clearDisplay()` records the current message count and clears the buffer without re-rendering; `rebuildHistory()` now only renders messages added *after* the watermark, so the screen is blank until the next message arrives. The full LLM context (`getMessagesForLLM()`, `getMessageSnapshot()`) is untouched. `resetAll()` zeroes the watermark so a full reset continues to work. Regression tests added to `src/test/core/conversation.test.ts` (4 new tests).
|
|
14
|
+
|
|
15
|
+
- **Workspace tab bar shows active state on the unfocused pane** (`src/panels/panel-manager.ts`, `src/renderer/panel-workspace-bar.ts`). `getWorkspaceTabs()` previously set both `active` and `focused` from the single globally-focused panel, so the unfocused pane's selected tab lost its active marker entirely when focus moved to the other pane. The fix splits the two semantics: `active` is now true for the currently-selected tab in *its own pane* (derived from `pane.activeIndex`, independent of keyboard focus); `focused` is true only for the one tab in the globally-focused pane. The workspace bar renderer uses the `focused` flag to append a `▸` glyph to the focused tab's label, making keyboard-focus distinct from pane-level selection. Both panes now show their selected tab highlighted regardless of which has focus. Regression tests added to `src/test/panels/panel-manager.test.ts` (2 new tests).
|
|
16
|
+
|
|
17
|
+
- **Panel render cache race guard** (`src/renderer/panel-composite.ts`). The documented hazard in the R2 cache — where a mid-render `invalidate()` call would be silently clobbered by the trailing `markRendered()` — has been resolved. The fix wraps each panel's `invalidate()` the first time it enters `renderPanel()` to maintain a per-panel generation counter (stored in a module-level `WeakMap`). The generation is snapshotted before `render()` and compared after: `markRendered()` is only called when the generation is unchanged, meaning no concurrent invalidation fired. If the generation changed, `needsRender` stays `true` and the next frame re-renders with the fresh state. Backward-compatible: existing panels that don't invalidate during render are unaffected. New test file `src/test/renderer/panel-composite.test.ts` (4 tests).
|
|
18
|
+
|
|
19
|
+
- **Release gate filenames renamed to match what the tests actually verify** (`src/test/release-gates/`). `runtime-certification-gate.test.ts` was renamed to `runtime-contract-shape-gate.test.ts` and its `describe()` block updated to `'runtime contract shape gate'`. `foundation-surfaces-gate.test.ts` was renamed to `foundation-surface-stability-gate.test.ts` and its `describe()` block updated to `'foundation surface stability gate'`. The test content (assertions, helpers) is unchanged — this is an honesty rename only, removing the implication that the tests verify runtime behavior certification when they actually verify structural shape preservation.
|
|
20
|
+
|
|
21
|
+
---
|
|
22
|
+
|
|
7
23
|
## [0.19.23] — 2026-04-22
|
|
8
24
|
|
|
9
25
|
SDK 0.23.x constraint-propagation reconciliation. Surfaces the new per-chain constraint data across the WRFC panel, process modal, agent inspector, and agent detail modal; adds system-message notifications for constraint enumeration and violations; exposes the WRFC-injected `systemPromptAddendum` so operators can see the engineer addendum was applied.
|
package/README.md
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
[](https://github.com/mgd34msu/goodvibes-tui/actions/workflows/ci.yml)
|
|
4
4
|
[](https://opensource.org/licenses/MIT)
|
|
5
|
-
[](https://github.com/mgd34msu/goodvibes-tui)
|
|
6
6
|
|
|
7
7
|
A terminal-native AI coding, operations, automation, knowledge, and integration console with a typed runtime, omnichannel surfaces, structured memory/knowledge, and a raw ANSI renderer.
|
|
8
8
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@pellux/goodvibes-tui",
|
|
3
|
-
"version": "0.19.
|
|
3
|
+
"version": "0.19.24",
|
|
4
4
|
"description": "Terminal-native GoodVibes product for coding, operations, automation, knowledge, channels, and daemon-backed control-plane workflows.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "src/main.ts",
|
package/src/core/conversation.ts
CHANGED
|
@@ -74,6 +74,13 @@ export class ConversationManager extends SdkConversationManager {
|
|
|
74
74
|
private errorLineRegistry: number[] = [];
|
|
75
75
|
/** Streaming block start line in history buffer (for incremental streaming update). */
|
|
76
76
|
private streamingStartLine = -1;
|
|
77
|
+
/**
|
|
78
|
+
* Message index at the time of the last clearDisplay() call.
|
|
79
|
+
* rebuildHistory() renders only messages at or after this index, so the
|
|
80
|
+
* display stays blank for messages added before the clear while LLM history
|
|
81
|
+
* is fully preserved. Reset to 0 on resetAll() or rebuildHistory() width change.
|
|
82
|
+
*/
|
|
83
|
+
private _displayFromMessageIndex = 0;
|
|
77
84
|
|
|
78
85
|
public suppressSplash: boolean = false;
|
|
79
86
|
public splashOptions: SplashOptions = {};
|
|
@@ -215,6 +222,7 @@ export class ConversationManager extends SdkConversationManager {
|
|
|
215
222
|
this.messageLineRegistry = [];
|
|
216
223
|
this.errorLineRegistry = [];
|
|
217
224
|
this.streamingStartLine = -1;
|
|
225
|
+
this._displayFromMessageIndex = 0; // full reset — show everything on next render
|
|
218
226
|
}
|
|
219
227
|
|
|
220
228
|
/**
|
|
@@ -293,20 +301,27 @@ export class ConversationManager extends SdkConversationManager {
|
|
|
293
301
|
this.lastRenderedWidth = width;
|
|
294
302
|
this.dirty = false;
|
|
295
303
|
|
|
304
|
+
const snapshot = this.getMessageSnapshot();
|
|
305
|
+
// When _displayFromMessageIndex > 0, clearDisplay() was called. Only render
|
|
306
|
+
// messages added after the clear — the pre-clear history stays off-screen.
|
|
307
|
+
// On a full rebuild (e.g. width change), reset the display-start to 0 so the
|
|
308
|
+
// user can scroll back to the full history if needed.
|
|
309
|
+
const displayStart = this._displayFromMessageIndex;
|
|
310
|
+
const visibleSnapshot = displayStart > 0 ? snapshot.slice(displayStart) : snapshot;
|
|
311
|
+
|
|
296
312
|
// Tool messages ARE rendered (as collapsed blocks); this filter is only
|
|
297
313
|
// for determining whether to show the splash screen (tool-only messages
|
|
298
314
|
// don't count as visible conversation content for splash purposes).
|
|
299
|
-
const
|
|
300
|
-
const displayMessages = snapshot.filter(
|
|
315
|
+
const displayMessages = visibleSnapshot.filter(
|
|
301
316
|
(m) => m.role !== 'tool' && m.role !== 'system',
|
|
302
317
|
);
|
|
303
318
|
|
|
304
|
-
if (displayMessages.length === 0 && !this.suppressSplash) {
|
|
319
|
+
if (displayMessages.length === 0 && displayStart === 0 && !this.suppressSplash) {
|
|
305
320
|
this.addSplashScreen(width);
|
|
306
321
|
return;
|
|
307
322
|
}
|
|
308
323
|
|
|
309
|
-
this.appendMessages(
|
|
324
|
+
this.appendMessages(visibleSnapshot, width);
|
|
310
325
|
this.appendedUpTo = snapshot.length;
|
|
311
326
|
}
|
|
312
327
|
|
|
@@ -509,19 +524,27 @@ export class ConversationManager extends SdkConversationManager {
|
|
|
509
524
|
|
|
510
525
|
/**
|
|
511
526
|
* clearDisplay - Clear the visual history buffer without touching the LLM context messages.
|
|
512
|
-
* The next render will show a blank conversation area.
|
|
527
|
+
* The next render will show a blank conversation area. Subsequent message additions
|
|
528
|
+
* rebuild the display incrementally from that point forward.
|
|
529
|
+
*
|
|
530
|
+
* Contract:
|
|
531
|
+
* - getDisplayBlocks() returns an empty array immediately after this call.
|
|
532
|
+
* - getMessageSnapshot() is unaffected — full LLM history is preserved.
|
|
533
|
+
* - resetAll() (which clears both display and messages) continues to work.
|
|
534
|
+
* - rebuildHistory() can be called by callers that need a full display rebuild.
|
|
513
535
|
*/
|
|
514
536
|
public clearDisplay(): void {
|
|
515
537
|
this.history.clear();
|
|
516
|
-
this.
|
|
517
|
-
this.
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
this.
|
|
538
|
+
this.blockRegistry = [];
|
|
539
|
+
this.messageLineRegistry = [];
|
|
540
|
+
this.errorLineRegistry = [];
|
|
541
|
+
// Advance _displayFromMessageIndex to exclude all current messages from display.
|
|
542
|
+
// rebuildHistory() will only render messages added AFTER this point.
|
|
543
|
+
this._displayFromMessageIndex = this.getMessageSnapshot().length;
|
|
544
|
+
this.appendedUpTo = this._displayFromMessageIndex;
|
|
521
545
|
this.dirty = false;
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
this.appendedUpTo = snapshot.length;
|
|
546
|
+
// Do NOT re-render here — display stays blank until the next message is added.
|
|
547
|
+
// The lastRenderedWidth is kept so subsequent appends use the correct width.
|
|
525
548
|
}
|
|
526
549
|
}
|
|
527
550
|
|
|
@@ -347,13 +347,17 @@ export class PanelManager {
|
|
|
347
347
|
|
|
348
348
|
getWorkspaceTabs(): readonly WorkspaceTab[] {
|
|
349
349
|
if (this._cachedWorkspaceTabs !== null) return this._cachedWorkspaceTabs;
|
|
350
|
+
// `active` = the currently selected tab in its own pane (independent of focus).
|
|
351
|
+
// `focused` = true only for the one tab in the globally focused pane that is active.
|
|
350
352
|
const focusedPanelId = this.getActivePanel()?.id;
|
|
353
|
+
const topActivePanelId = this.topPane.panels[this.topPane.activeIndex]?.id;
|
|
354
|
+
const bottomActivePanelId = this.bottomPane.panels[this.bottomPane.activeIndex]?.id;
|
|
351
355
|
const topTabs = this.topPane.panels.map((panel) => ({
|
|
352
356
|
id: panel.id,
|
|
353
357
|
name: panel.name,
|
|
354
358
|
icon: panel.icon,
|
|
355
359
|
pane: 'top' as const,
|
|
356
|
-
active: panel.id ===
|
|
360
|
+
active: panel.id === topActivePanelId,
|
|
357
361
|
focused: panel.id === focusedPanelId,
|
|
358
362
|
}));
|
|
359
363
|
const bottomTabs = this.bottomPane.panels.map((panel) => ({
|
|
@@ -361,7 +365,7 @@ export class PanelManager {
|
|
|
361
365
|
name: panel.name,
|
|
362
366
|
icon: panel.icon,
|
|
363
367
|
pane: 'bottom' as const,
|
|
364
|
-
active: panel.id ===
|
|
368
|
+
active: panel.id === bottomActivePanelId,
|
|
365
369
|
focused: panel.id === focusedPanelId,
|
|
366
370
|
}));
|
|
367
371
|
const tabs = [...topTabs, ...bottomTabs] as WorkspaceTab[];
|
|
@@ -24,9 +24,9 @@ import { renderPanelWorkspaceBar } from './panel-workspace-bar.ts';
|
|
|
24
24
|
* path (e.g. deferred lazy-load) should call `this.invalidate()` AFTER the
|
|
25
25
|
* trailing work so the next frame picks it up.
|
|
26
26
|
*
|
|
27
|
-
*
|
|
28
|
-
*
|
|
29
|
-
*
|
|
27
|
+
* The fix (now applied): snapshot `needsRender` before calling `render()` and
|
|
28
|
+
* only call `markRendered()` when no concurrent invalidation occurred during
|
|
29
|
+
* the render pass — preserving any mid-render invalidation.
|
|
30
30
|
*/
|
|
31
31
|
interface PanelRenderCache {
|
|
32
32
|
lines: Line[];
|
|
@@ -34,15 +34,52 @@ interface PanelRenderCache {
|
|
|
34
34
|
height: number;
|
|
35
35
|
}
|
|
36
36
|
const panelRenderCache = new WeakMap<Panel, PanelRenderCache>();
|
|
37
|
+
/**
|
|
38
|
+
* Per-panel render-generation counter. Incremented whenever invalidate() fires
|
|
39
|
+
* on the panel (tracked here externally since Panel does not expose a version).
|
|
40
|
+
* We monkey-patch each panel the first time it enters renderPanel() by wrapping
|
|
41
|
+
* its invalidate() to bump the counter stored in this map.
|
|
42
|
+
*
|
|
43
|
+
* Race-guard: snapshot the generation before render(), compare after. If it
|
|
44
|
+
* changed, a mid-render invalidation occurred — leave needsRender=true.
|
|
45
|
+
*/
|
|
46
|
+
const panelRenderGen = new WeakMap<Panel, { gen: number }>();
|
|
47
|
+
|
|
48
|
+
function getRenderGenState(panel: Panel): { gen: number } {
|
|
49
|
+
let state = panelRenderGen.get(panel);
|
|
50
|
+
if (!state) {
|
|
51
|
+
state = { gen: 0 };
|
|
52
|
+
panelRenderGen.set(panel, state);
|
|
53
|
+
// Wrap invalidate() to bump generation counter.
|
|
54
|
+
const origInvalidate = panel.invalidate.bind(panel);
|
|
55
|
+
panel.invalidate = () => {
|
|
56
|
+
state!.gen++;
|
|
57
|
+
origInvalidate();
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
return state;
|
|
61
|
+
}
|
|
37
62
|
|
|
38
63
|
/** R2: Render a panel, skipping if nothing changed. Returns cached lines on a skip. */
|
|
39
|
-
function renderPanel(panel: Panel, width: number, height: number): Line[] {
|
|
64
|
+
export function renderPanel(panel: Panel, width: number, height: number): Line[] {
|
|
40
65
|
const cached = panelRenderCache.get(panel);
|
|
41
66
|
if (cached && !panel.needsRender && cached.width === width && cached.height === height) {
|
|
42
67
|
return cached.lines;
|
|
43
68
|
}
|
|
69
|
+
// Snapshot render-generation counter BEFORE calling render(). If an event
|
|
70
|
+
// listener fires during render() and calls panel.invalidate(), the generation
|
|
71
|
+
// counter will be bumped. We compare after render to detect mid-render races.
|
|
72
|
+
const genState = getRenderGenState(panel);
|
|
73
|
+
const genBefore = genState.gen;
|
|
44
74
|
const lines = panel.render(width, height);
|
|
45
|
-
|
|
75
|
+
// Only call markRendered() when no mid-render invalidation occurred.
|
|
76
|
+
// If the generation changed during render(), a concurrent invalidate() fired —
|
|
77
|
+
// leave needsRender=true so the next frame re-renders with the new state.
|
|
78
|
+
if (genState.gen === genBefore) {
|
|
79
|
+
panel.markRendered();
|
|
80
|
+
}
|
|
81
|
+
// If gen changed, needsRender is already true (invalidate() set it); do not
|
|
82
|
+
// call markRendered() — the next frame will pick it up.
|
|
46
83
|
panelRenderCache.set(panel, { lines, width, height });
|
|
47
84
|
return lines;
|
|
48
85
|
}
|
|
@@ -17,7 +17,11 @@ export function renderPanelWorkspaceBar(
|
|
|
17
17
|
return renderTabStrip({
|
|
18
18
|
width,
|
|
19
19
|
tabs: tabs.map((tab) => ({
|
|
20
|
-
|
|
20
|
+
// tab.active = selected in its own pane (drives highlighted background).
|
|
21
|
+
// tab.focused = has keyboard focus (drives brighter text / focus indicator).
|
|
22
|
+
// A tab can be active-but-not-focused (selected in the unfocused pane) or
|
|
23
|
+
// active-and-focused (selected in the focused pane).
|
|
24
|
+
label: `${tab.pane === 'bottom' ? 'v' : '^'} ${tab.icon} ${tab.name}${tab.focused ? ' ▸' : ''}`,
|
|
21
25
|
active: tab.active,
|
|
22
26
|
})),
|
|
23
27
|
prefixLabel: ' PANELS ',
|
package/src/version.ts
CHANGED
|
@@ -6,7 +6,7 @@ import { join } from 'node:path';
|
|
|
6
6
|
// The prebuild script updates the fallback value before compilation.
|
|
7
7
|
// Uses import.meta.dir (Bun) to locate package.json relative to this file,
|
|
8
8
|
// which is correct regardless of the process working directory.
|
|
9
|
-
let _version = '0.19.
|
|
9
|
+
let _version = '0.19.24';
|
|
10
10
|
try {
|
|
11
11
|
const pkg = JSON.parse(readFileSync(join(import.meta.dir, '..', 'package.json'), 'utf-8'));
|
|
12
12
|
_version = pkg.version ?? _version;
|