@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.
- package/.package.json.release.bak +1 -1
- package/CHANGELOG.md +6 -0
- package/dist/src/docker-git/menu-render-select.js +14 -0
- package/dist/src/docker-git/menu-render.js +35 -7
- package/package.json +1 -1
- package/src/docker-git/menu-render-select.ts +24 -0
- package/src/docker-git/menu-render.ts +36 -4
- package/tests/docker-git/menu-select-order.test.ts +12 -1
package/CHANGELOG.md
CHANGED
|
@@ -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
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
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
|
@@ -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
|
|
173
|
-
|
|
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
|
-
...
|
|
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
|
})
|