@prover-coder-ai/docker-git 1.0.18 → 1.0.19

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.
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@prover-coder-ai/docker-git",
3
- "version": "1.0.18",
3
+ "version": "1.0.19",
4
4
  "description": "Minimal Vite-powered TypeScript console starter using Effect",
5
5
  "main": "dist/src/docker-git/main.js",
6
6
  "bin": {
package/CHANGELOG.md CHANGED
@@ -1,5 +1,11 @@
1
1
  # @prover-coder-ai/docker-git
2
2
 
3
+ ## 1.0.19
4
+
5
+ ### Patch Changes
6
+
7
+ - chore: automated version bump
8
+
3
9
  ## 1.0.18
4
10
 
5
11
  ### Patch Changes
@@ -37,6 +37,20 @@ export const buildSelectLabels = (items, selected, purpose, runtimeByProject) =>
37
37
  : ` [started=${renderStartedAtCompact(runtime)}]`;
38
38
  return `${prefix} ${index + 1}. ${item.displayName} (${refLabel})${runtimeSuffix}`;
39
39
  });
40
+ export const buildSelectListWindow = (total, selected, maxVisible) => {
41
+ if (total <= 0) {
42
+ return { start: 0, end: 0 };
43
+ }
44
+ const visible = Math.max(1, maxVisible);
45
+ if (total <= visible) {
46
+ return { start: 0, end: total };
47
+ }
48
+ const boundedSelected = Math.min(Math.max(selected, 0), total - 1);
49
+ const half = Math.floor(visible / 2);
50
+ const maxStart = total - visible;
51
+ const start = Math.min(Math.max(boundedSelected - half, 0), maxStart);
52
+ return { start, end: start + visible };
53
+ };
40
54
  const buildDetailsContext = (item, runtimeByProject) => {
41
55
  const runtime = runtimeForProject(runtimeByProject, item);
42
56
  return {
@@ -2,7 +2,7 @@ import { Match } from "effect";
2
2
  import { Box, Text } from "ink";
3
3
  import React from "react";
4
4
  import { renderLayout } from "./menu-render-layout.js";
5
- import { buildSelectLabels, renderSelectDetails, selectHint, selectTitle } from "./menu-render-select.js";
5
+ import { buildSelectLabels, buildSelectListWindow, renderSelectDetails, selectHint, selectTitle } from "./menu-render-select.js";
6
6
  import { createSteps, menuItems } from "./menu-types.js";
7
7
  // CHANGE: render menu views with Ink without JSX
8
8
  // WHY: keep UI logic separate from input/state reducers
@@ -66,13 +66,41 @@ const computeListWidth = (labels) => {
66
66
  const maxLabelWidth = labels.length > 0 ? Math.max(...labels.map((label) => label.length)) : 24;
67
67
  return Math.min(Math.max(maxLabelWidth + 2, 28), 54);
68
68
  };
69
+ const readStdoutRows = () => {
70
+ const rows = process.stdout.rows;
71
+ if (typeof rows !== "number" || !Number.isFinite(rows) || rows <= 0) {
72
+ return null;
73
+ }
74
+ return rows;
75
+ };
76
+ const computeSelectListMaxRows = () => {
77
+ const rows = readStdoutRows();
78
+ if (rows === null) {
79
+ return 12;
80
+ }
81
+ return Math.max(6, rows - 14);
82
+ };
69
83
  const renderSelectListBox = (el, items, selected, labels, width) => {
70
- const list = labels.map((label, index) => el(Text, {
71
- key: items[index]?.projectDir ?? String(index),
72
- color: index === selected ? "green" : "white",
73
- wrap: "truncate"
74
- }, label));
75
- return el(Box, { flexDirection: "column", width }, ...(list.length > 0 ? list : [el(Text, { color: "gray" }, "No projects found.")]));
84
+ const window = buildSelectListWindow(labels.length, selected, computeSelectListMaxRows());
85
+ const hiddenAbove = window.start;
86
+ const hiddenBelow = labels.length - window.end;
87
+ const visibleLabels = labels.slice(window.start, window.end);
88
+ const list = visibleLabels.map((label, offset) => {
89
+ const index = window.start + offset;
90
+ return el(Text, {
91
+ key: items[index]?.projectDir ?? String(index),
92
+ color: index === selected ? "green" : "white",
93
+ wrap: "truncate"
94
+ }, label);
95
+ });
96
+ const before = hiddenAbove > 0
97
+ ? [el(Text, { color: "gray", wrap: "truncate" }, `[scroll] ${hiddenAbove} more above`)]
98
+ : [];
99
+ const after = hiddenBelow > 0
100
+ ? [el(Text, { color: "gray", wrap: "truncate" }, `[scroll] ${hiddenBelow} more below`)]
101
+ : [];
102
+ const listBody = list.length > 0 ? list : [el(Text, { color: "gray" }, "No projects found.")];
103
+ return el(Box, { flexDirection: "column", width }, ...before, ...listBody, ...after);
76
104
  };
77
105
  const renderSelectDetailsBox = (el, input) => {
78
106
  const details = renderSelectDetails(el, input.purpose, input.items[input.selected], input.runtimeByProject, input.connectEnableMcpPlaywright);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@prover-coder-ai/docker-git",
3
- "version": "1.0.18",
3
+ "version": "1.0.19",
4
4
  "description": "Minimal Vite-powered TypeScript console starter using Effect",
5
5
  "main": "dist/src/docker-git/main.js",
6
6
  "bin": {
@@ -97,6 +97,30 @@ export const buildSelectLabels = (
97
97
  return `${prefix} ${index + 1}. ${item.displayName} (${refLabel})${runtimeSuffix}`
98
98
  })
99
99
 
100
+ export type SelectListWindow = {
101
+ readonly start: number
102
+ readonly end: number
103
+ }
104
+
105
+ export const buildSelectListWindow = (
106
+ total: number,
107
+ selected: number,
108
+ maxVisible: number
109
+ ): SelectListWindow => {
110
+ if (total <= 0) {
111
+ return { start: 0, end: 0 }
112
+ }
113
+ const visible = Math.max(1, maxVisible)
114
+ if (total <= visible) {
115
+ return { start: 0, end: total }
116
+ }
117
+ const boundedSelected = Math.min(Math.max(selected, 0), total - 1)
118
+ const half = Math.floor(visible / 2)
119
+ const maxStart = total - visible
120
+ const start = Math.min(Math.max(boundedSelected - half, 0), maxStart)
121
+ return { start, end: start + visible }
122
+ }
123
+
100
124
  type SelectDetailsContext = {
101
125
  readonly item: ProjectItem
102
126
  readonly refLabel: string
@@ -6,6 +6,7 @@ import type { ProjectItem } from "@effect-template/lib/usecases/projects"
6
6
  import { renderLayout } from "./menu-render-layout.js"
7
7
  import {
8
8
  buildSelectLabels,
9
+ buildSelectListWindow,
9
10
  renderSelectDetails,
10
11
  selectHint,
11
12
  type SelectPurpose,
@@ -162,6 +163,22 @@ const computeListWidth = (labels: ReadonlyArray<string>): number => {
162
163
  return Math.min(Math.max(maxLabelWidth + 2, 28), 54)
163
164
  }
164
165
 
166
+ const readStdoutRows = (): number | null => {
167
+ const rows = process.stdout.rows
168
+ if (typeof rows !== "number" || !Number.isFinite(rows) || rows <= 0) {
169
+ return null
170
+ }
171
+ return rows
172
+ }
173
+
174
+ const computeSelectListMaxRows = (): number => {
175
+ const rows = readStdoutRows()
176
+ if (rows === null) {
177
+ return 12
178
+ }
179
+ return Math.max(6, rows - 14)
180
+ }
181
+
165
182
  const renderSelectListBox = (
166
183
  el: typeof React.createElement,
167
184
  items: ReadonlyArray<ProjectItem>,
@@ -169,8 +186,13 @@ const renderSelectListBox = (
169
186
  labels: ReadonlyArray<string>,
170
187
  width: number
171
188
  ): React.ReactElement => {
172
- const list = labels.map((label, index) =>
173
- el(
189
+ const window = buildSelectListWindow(labels.length, selected, computeSelectListMaxRows())
190
+ const hiddenAbove = window.start
191
+ const hiddenBelow = labels.length - window.end
192
+ const visibleLabels = labels.slice(window.start, window.end)
193
+ const list = visibleLabels.map((label, offset) => {
194
+ const index = window.start + offset
195
+ return el(
174
196
  Text,
175
197
  {
176
198
  key: items[index]?.projectDir ?? String(index),
@@ -179,12 +201,22 @@ const renderSelectListBox = (
179
201
  },
180
202
  label
181
203
  )
182
- )
204
+ })
205
+
206
+ const before = hiddenAbove > 0
207
+ ? [el(Text, { color: "gray", wrap: "truncate" }, `[scroll] ${hiddenAbove} more above`)]
208
+ : []
209
+ const after = hiddenBelow > 0
210
+ ? [el(Text, { color: "gray", wrap: "truncate" }, `[scroll] ${hiddenBelow} more below`)]
211
+ : []
212
+ const listBody = list.length > 0 ? list : [el(Text, { color: "gray" }, "No projects found.")]
183
213
 
184
214
  return el(
185
215
  Box,
186
216
  { flexDirection: "column", width },
187
- ...(list.length > 0 ? list : [el(Text, { color: "gray" }, "No projects found.")])
217
+ ...before,
218
+ ...listBody,
219
+ ...after
188
220
  )
189
221
  }
190
222
 
@@ -1,6 +1,6 @@
1
1
  import { describe, expect, it } from "vitest"
2
2
 
3
- import { buildSelectLabels } from "../../src/docker-git/menu-render-select.js"
3
+ import { buildSelectLabels, buildSelectListWindow } from "../../src/docker-git/menu-render-select.js"
4
4
  import { sortItemsByLaunchTime } from "../../src/docker-git/menu-select-order.js"
5
5
  import type { SelectProjectRuntime } from "../../src/docker-git/menu-types.js"
6
6
  import { makeProjectItem } from "./fixtures/project-item.js"
@@ -70,4 +70,15 @@ describe("menu-select order", () => {
70
70
  expect(downLabel).toContain("running, ssh=2, started=2026-02-17 09:45 UTC")
71
71
  emitProof("UI labels show container start timestamp in Connect and Down views")
72
72
  })
73
+
74
+ it("keeps full list visible when projects fit into viewport", () => {
75
+ const window = buildSelectListWindow(8, 3, 12)
76
+ expect(window).toEqual({ start: 0, end: 8 })
77
+ })
78
+
79
+ it("computes a scrolling window around selected project", () => {
80
+ expect(buildSelectListWindow(30, 0, 10)).toEqual({ start: 0, end: 10 })
81
+ expect(buildSelectListWindow(30, 15, 10)).toEqual({ start: 10, end: 20 })
82
+ expect(buildSelectListWindow(30, 29, 10)).toEqual({ start: 20, end: 30 })
83
+ })
73
84
  })