@steerable/agent-cli 0.6.63 → 0.6.65

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.
@@ -20,10 +20,21 @@ export declare class OpenTuiView {
20
20
  private bodyKey;
21
21
  private overlayKey;
22
22
  private picksKey;
23
+ private chatListOpen;
24
+ private transcriptPinned;
25
+ private transcriptScroll;
26
+ private revealId;
27
+ private restoreTranscript;
28
+ private readonly onFrame;
23
29
  constructor(renderer: CliRenderer);
24
30
  scrollPage(direction: -1 | 1): void;
25
31
  scrollEdge(edge: 'top' | 'bottom'): void;
26
32
  apply(screen: TuiScreen): void;
33
+ /** Transcript stays pinned to the bottom. The chat list follows the selected row instead. */
34
+ private followChats;
35
+ private atBottom;
36
+ private maxScrollTop;
37
+ private viewportRows;
27
38
  private paintOverlay;
28
39
  private paintPicks;
29
40
  private replaceBody;
package/dist/tui/view.js CHANGED
@@ -34,8 +34,31 @@ export class OpenTuiView {
34
34
  bodyKey = '';
35
35
  overlayKey = '';
36
36
  picksKey = '';
37
+ chatListOpen = false;
38
+ transcriptPinned = true;
39
+ transcriptScroll = 0;
40
+ revealId = null;
41
+ restoreTranscript = false;
42
+ onFrame = () => {
43
+ if (this.restoreTranscript) {
44
+ this.restoreTranscript = false;
45
+ this.body.stickyScroll = true;
46
+ const max = this.maxScrollTop();
47
+ this.body.scrollTop = this.transcriptPinned ? max : Math.min(this.transcriptScroll, max);
48
+ return;
49
+ }
50
+ if (!this.revealId)
51
+ return;
52
+ const id = this.revealId;
53
+ this.revealId = null;
54
+ this.body.scrollChildIntoView(id);
55
+ };
37
56
  constructor(renderer) {
38
57
  this.renderer = renderer;
58
+ renderer.on('frame', this.onFrame);
59
+ renderer.once('destroy', () => {
60
+ renderer.off('frame', this.onFrame);
61
+ });
39
62
  const shell = new BoxRenderable(renderer, {
40
63
  width: '100%',
41
64
  height: '100%',
@@ -173,12 +196,44 @@ export class OpenTuiView {
173
196
  this.body.scrollChildIntoView(`line-${screen.searchFocus}`);
174
197
  this.paintOverlay(screen);
175
198
  this.paintPicks(screen);
199
+ this.followChats(screen);
176
200
  const key = bodyKey(screen);
177
201
  if (key === this.bodyKey)
178
202
  return;
179
203
  this.bodyKey = key;
180
204
  this.replaceBody(screen);
181
205
  }
206
+ /** Transcript stays pinned to the bottom. The chat list follows the selected row instead. */
207
+ followChats(screen) {
208
+ const chats = screen.chats;
209
+ if (chats && !this.chatListOpen) {
210
+ this.transcriptPinned = this.atBottom();
211
+ this.transcriptScroll = this.body.scrollTop;
212
+ this.body.stickyScroll = false;
213
+ this.body.scrollTop = 0;
214
+ this.chatListOpen = true;
215
+ }
216
+ else if (!chats && this.chatListOpen) {
217
+ this.chatListOpen = false;
218
+ this.revealId = null;
219
+ this.restoreTranscript = true;
220
+ this.body.stickyScroll = false;
221
+ }
222
+ if (!chats)
223
+ return;
224
+ const index = chats.findIndex((chat) => chat.selected);
225
+ this.revealId = index < 0 ? null : index === 0 ? 'chat-heading' : `chat-${index}`;
226
+ }
227
+ atBottom() {
228
+ return this.body.scrollTop >= this.maxScrollTop() - 1;
229
+ }
230
+ maxScrollTop() {
231
+ return Math.max(0, this.body.scrollHeight - this.viewportRows());
232
+ }
233
+ viewportRows() {
234
+ const height = this.body.viewport.height;
235
+ return typeof height === 'number' ? height : 0;
236
+ }
182
237
  paintOverlay(screen) {
183
238
  const lines = screen.approval
184
239
  ? approvalLines(screen.approval)
@@ -239,9 +294,10 @@ export class OpenTuiView {
239
294
  return;
240
295
  }
241
296
  if (screen.chats) {
242
- this.body.add(text(renderer, dim, '会话'));
243
- for (const chat of screen.chats) {
297
+ this.body.add(text(renderer, dim, '会话', 'chat-heading'));
298
+ screen.chats.forEach((chat, index) => {
244
299
  const row = new BoxRenderable(renderer, {
300
+ id: `chat-${index}`,
245
301
  width: '100%',
246
302
  height: 1,
247
303
  backgroundColor: chat.selected ? selected : canvas,
@@ -249,7 +305,7 @@ export class OpenTuiView {
249
305
  });
250
306
  row.add(text(renderer, chat.selected ? ink : dim, chatText(chat)));
251
307
  this.body.add(row);
252
- }
308
+ });
253
309
  return;
254
310
  }
255
311
  screen.lines.forEach((line, index) => {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@steerable/agent-cli",
3
- "version": "0.6.63",
3
+ "version": "0.6.65",
4
4
  "description": "Product-neutral command line for a Steerable host. Products supply the bin name and brand.",
5
5
  "license": "Apache-2.0",
6
6
  "homepage": "https://steerableframework.com/",
@@ -38,9 +38,9 @@
38
38
  "dependencies": {
39
39
  "@earendil-works/pi-tui": "0.87.1",
40
40
  "@opentui/core": "0.5.12",
41
- "@steerable/agent-client": "0.6.63",
42
- "@steerable/agent-protocol": "0.6.63",
43
- "@steerable/agent-shell": "0.6.63"
41
+ "@steerable/agent-client": "0.6.65",
42
+ "@steerable/agent-protocol": "0.6.65",
43
+ "@steerable/agent-shell": "0.6.65"
44
44
  },
45
45
  "devDependencies": {
46
46
  "@types/node": "^22.10.5",