@xfe-repo/cli 2.0.9 → 2.0.12

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 CHANGED
@@ -1,5 +1,36 @@
1
1
  # @xfe-repo/cli
2
2
 
3
+ ## 2.0.12
4
+
5
+ ### Patch Changes
6
+
7
+ - 优化体验
8
+ - Updated dependencies
9
+ - @xfe-repo/cli-presets@2.0.7
10
+ - @xfe-repo/cli-core@2.0.7
11
+
12
+ ## 2.0.11
13
+
14
+ ### Patch Changes
15
+
16
+ - 去除server依赖
17
+
18
+ ## 2.0.10
19
+
20
+ ### Patch Changes
21
+
22
+ - 同步插件优化
23
+
24
+ ## 2.0.9
25
+
26
+ ### Patch Changes
27
+
28
+ - 优化插件
29
+ - Updated dependencies
30
+ - @xfe-repo/cli-presets@2.0.6
31
+ - @xfe-repo/cli-core@2.0.6
32
+ - @xfe-repo/server@0.1.1
33
+
3
34
  ## 2.0.9
4
35
 
5
36
  ### Patch Changes
package/dist/app.js CHANGED
@@ -10,7 +10,7 @@ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-run
10
10
  * 统一使用 useKeymap 注册全局快捷键,避免多 useInput 冲突
11
11
  */
12
12
  import { useState, useEffect, useCallback, useRef, useSyncExternalStore } from 'react';
13
- import { Box, Static, Text, useApp } from 'ink';
13
+ import { Box, Static, Text, useApp, useWindowSize } from 'ink';
14
14
  import { ConfirmInput, Spinner } from '@inkjs/ui';
15
15
  import { InkLogger } from './adapters/ink-adapter.js';
16
16
  import { useLogs } from './hooks/use-adapters.js';
@@ -18,6 +18,8 @@ import { KeymapProvider, useKeymap, KeymapLayer } from './hooks/use-keymap.js';
18
18
  import { useSessionManager } from './hooks/use-session-manager.js';
19
19
  import { MenuView } from './views/MenuView.js';
20
20
  import { RunnerView } from './views/RunnerView.js';
21
+ const BOTTOM_RESERVED_ROWS = 1;
22
+ const MIN_VIEW_ROWS = 1;
21
23
  // ============================================================
22
24
  // App
23
25
  // ============================================================
@@ -163,6 +165,8 @@ function AppInner({ runner, toast, scriptName }) {
163
165
  const emptyLoggerRef = useRef(new InkLogger());
164
166
  const activeLogger = activeSession?.logger ?? emptyLoggerRef.current;
165
167
  const { entries: logEntries } = useLogs(activeLogger);
168
+ const { rows: terminalRows } = useWindowSize();
169
+ const availableViewRows = resolveAvailableViewRows({ terminalRows, staticRows: logEntries.length });
166
170
  // 当 session 被移除但 view 仍为 session 时,自动回退到菜单(直接命令模式不回退,等待 doExit 完成)
167
171
  useEffect(() => {
168
172
  if (scriptName)
@@ -172,16 +176,16 @@ function AppInner({ runner, toast, scriptName }) {
172
176
  }
173
177
  }, [view, sessionManager.sessions, scriptName]);
174
178
  // ── 渲染 ──
175
- const renderView = () => {
179
+ const renderView = (availableRows) => {
176
180
  switch (view.type) {
177
181
  case 'loading':
178
182
  return _jsx(Spinner, { label: "\u521D\u59CB\u5316\u63D2\u4EF6..." });
179
183
  case 'menu':
180
- return (_jsx(MenuView, { projectName: projectDisplayName, commandsWithSource: runner.getCommandsWithSource(), activeSessions: sessionManager.activeSessions, runningCommandNames: sessionManager.runningScriptNames, badges: badges, storeSnapshot: storeSnapshot, toast: toast, onSelect: handleSelect, onFocusSession: handleFocusSession, onExit: handleExit, onUserInput: handleUserInput }));
184
+ return (_jsx(MenuView, { availableRows: availableRows, projectName: projectDisplayName, commandsWithSource: runner.getCommandsWithSource(), activeSessions: sessionManager.activeSessions, runningCommandNames: sessionManager.runningScriptNames, badges: badges, storeSnapshot: storeSnapshot, toast: toast, onSelect: handleSelect, onFocusSession: handleFocusSession, onExit: handleExit, onUserInput: handleUserInput }));
181
185
  case 'session': {
182
186
  if (!activeSession)
183
187
  return null;
184
- return (_jsx(RunnerView, { projectName: projectDisplayName, scriptName: activeSession.scriptName, startTime: activeSession.startTime, endTime: activeSession.endTime, prompt: activeSession.prompt, spinner: activeSession.spinner, badges: badges, storeSnapshot: storeSnapshot, hasError: activeSession.status === 'error', isCompleted: activeSession.status === 'completed', isSingleCommand: !!scriptName, isReadOnly: activeSession.isReadOnly }));
188
+ return (_jsx(RunnerView, { availableRows: availableRows, projectName: projectDisplayName, scriptName: activeSession.scriptName, startTime: activeSession.startTime, endTime: activeSession.endTime, prompt: activeSession.prompt, spinner: activeSession.spinner, badges: badges, storeSnapshot: storeSnapshot, hasError: activeSession.status === 'error', isCompleted: activeSession.status === 'completed', isSingleCommand: !!scriptName, isReadOnly: activeSession.isReadOnly }));
185
189
  }
186
190
  case 'exit-confirm':
187
191
  return (_jsxs(Box, { flexDirection: "column", gap: 1, borderStyle: "round", borderColor: "yellow", paddingX: 1, children: [_jsxs(Text, { color: "yellow", bold: true, children: ["\u5F53\u524D\u6709 ", sessionManager.runningSessions.length, " \u4E2A\u4EFB\u52A1\u6B63\u5728\u8FD0\u884C"] }), _jsx(Text, { children: "\u9000\u51FA\u5C06\u7EC8\u6B62\u6240\u6709\u8FD0\u884C\u4E2D\u7684\u4EFB\u52A1\uFF0C\u786E\u8BA4\u9000\u51FA\uFF1F(\u56DE\u8F66\u76F4\u63A5\u9000\u51FA)" }), _jsx(ConfirmInput, { onConfirm: doExit, onCancel: () => setView({ type: 'menu' }) })] }));
@@ -189,7 +193,7 @@ function AppInner({ runner, toast, scriptName }) {
189
193
  return (_jsx(Box, { children: _jsxs(Text, { color: "red", children: ["ERROR: ", view.message] }) }));
190
194
  }
191
195
  };
192
- return (_jsxs(_Fragment, { children: [_jsx(Static, { items: logEntries, children: (entry, i) => _jsx(LogLine, { entry: entry }, `${entry.timestamp}-${i}`) }), renderView()] }));
196
+ return (_jsxs(_Fragment, { children: [_jsx(Static, { items: logEntries, children: (entry, i) => _jsx(LogLine, { entry: entry }, `${entry.timestamp}-${i}`) }), _jsx(Box, { flexDirection: "column", height: availableViewRows, overflow: "hidden", children: renderView(availableViewRows) })] }));
193
197
  }
194
198
  // ─── Render Helpers ─────────────────────────────────────────
195
199
  function LogLine({ entry }) {
@@ -204,4 +208,7 @@ function LogLine({ entry }) {
204
208
  return _jsx(Text, { color: color, children: entry.message });
205
209
  return _jsx(Text, { children: entry.message });
206
210
  }
211
+ function resolveAvailableViewRows({ terminalRows, staticRows }) {
212
+ return Math.max(MIN_VIEW_ROWS, terminalRows - staticRows - BOTTOM_RESERVED_ROWS);
213
+ }
207
214
  //# sourceMappingURL=app.js.map
@@ -13,6 +13,8 @@ export interface CreateRemoteTaskOptions {
13
13
  readonly description: string;
14
14
  readonly documentUrl?: string;
15
15
  readonly baseBranch?: string;
16
+ readonly branchName?: string;
17
+ readonly workflowId?: string;
16
18
  readonly prepareWorkspace?: boolean;
17
19
  }
18
20
  export interface StartRemoteTaskOptions {
@@ -29,8 +31,8 @@ export interface ListClientRequestsOptions {
29
31
  readonly status?: string;
30
32
  }
31
33
  export declare class HttpBackendGateway {
32
- private baseUrl;
33
- private tenantId?;
34
+ private readonly baseUrl;
35
+ private readonly tenantId?;
34
36
  constructor(options: BackendGatewayOptions);
35
37
  health(): Promise<unknown>;
36
38
  createTask(options: CreateRemoteTaskOptions): Promise<unknown>;
@@ -3,12 +3,12 @@
3
3
  *
4
4
  * CLI 通过该适配器连接本地或远端 xfe server。
5
5
  */
6
- // ─── HttpBackendGateway ─────────────────────────────────────
6
+ // ─── Gateway ───────────────────────────────────────────────
7
7
  export class HttpBackendGateway {
8
8
  baseUrl;
9
9
  tenantId;
10
10
  constructor(options) {
11
- this.baseUrl = normalizeBaseUrl(options.baseUrl);
11
+ this.baseUrl = trimTrailingSlash(options.baseUrl);
12
12
  this.tenantId = options.tenantId;
13
13
  }
14
14
  async health() {
@@ -24,7 +24,7 @@ export class HttpBackendGateway {
24
24
  });
25
25
  }
26
26
  async listClientRequests(options = {}) {
27
- return this.get(createPathWithQuery('/client-requests', { status: options.status }));
27
+ return this.get('/client-requests', { status: options.status });
28
28
  }
29
29
  async respondClientRequest(options) {
30
30
  return this.post(`/client-requests/${encodeURIComponent(options.requestId)}/respond`, {
@@ -32,20 +32,25 @@ export class HttpBackendGateway {
32
32
  content: options.content,
33
33
  });
34
34
  }
35
- async get(pathname) {
36
- const response = await fetch(this.createUrl(pathname), { headers: this.createHeaders() });
35
+ async get(pathname, query) {
36
+ const response = await fetch(this.createUrl(pathname, query), { headers: this.createHeaders() });
37
37
  return readResponse(response);
38
38
  }
39
39
  async post(pathname, body) {
40
40
  const response = await fetch(this.createUrl(pathname), {
41
41
  method: 'POST',
42
- headers: { ...this.createHeaders(), 'content-type': 'application/json' },
42
+ headers: { ...this.createHeaders(), 'Content-Type': 'application/json' },
43
43
  body: JSON.stringify(body),
44
44
  });
45
45
  return readResponse(response);
46
46
  }
47
- createUrl(pathname) {
48
- return `${this.baseUrl}${pathname}`;
47
+ createUrl(pathname, query = {}) {
48
+ const url = new URL(`${this.baseUrl}${pathname}`);
49
+ for (const [key, value] of Object.entries(query)) {
50
+ if (value)
51
+ url.searchParams.set(key, value);
52
+ }
53
+ return url.toString();
49
54
  }
50
55
  createHeaders() {
51
56
  if (!this.tenantId)
@@ -53,17 +58,17 @@ export class HttpBackendGateway {
53
58
  return { 'x-xfe-tenant-id': this.tenantId };
54
59
  }
55
60
  }
56
- // ─── Helpers ────────────────────────────────────────────────
61
+ // ─── Helpers ───────────────────────────────────────────────
57
62
  async function readResponse(response) {
58
- const text = await response.text();
59
- const body = parseJson(text);
63
+ const body = await readJson(response);
60
64
  if (!response.ok) {
61
- const message = typeof body === 'object' && body && 'error' in body ? String(body.error) : text;
62
- throw new Error(message || `HTTP ${response.status}`);
65
+ const message = typeof body === 'object' && body && 'error' in body ? String(body.error) : response.statusText;
66
+ throw new Error(`HTTP ${response.status}: ${message}`);
63
67
  }
64
68
  return body;
65
69
  }
66
- function parseJson(text) {
70
+ async function readJson(response) {
71
+ const text = await response.text();
67
72
  if (!text)
68
73
  return undefined;
69
74
  try {
@@ -73,16 +78,7 @@ function parseJson(text) {
73
78
  return text;
74
79
  }
75
80
  }
76
- function normalizeBaseUrl(baseUrl) {
77
- return baseUrl.replace(/\/$/, '');
78
- }
79
- function createPathWithQuery(pathname, query) {
80
- const searchParams = new URLSearchParams();
81
- for (const [key, value] of Object.entries(query)) {
82
- if (value)
83
- searchParams.set(key, value);
84
- }
85
- const queryString = searchParams.toString();
86
- return queryString ? `${pathname}?${queryString}` : pathname;
81
+ function trimTrailingSlash(value) {
82
+ return value.replace(/\/+$/, '');
87
83
  }
88
84
  //# sourceMappingURL=http-backend.js.map
@@ -15,17 +15,19 @@ export function registerServerCommands(program) {
15
15
  .option('-p, --port <port>', `监听端口,默认 ${DEFAULT_SERVER_PORT}`)
16
16
  .option('-H, --host <host>', '监听地址')
17
17
  .action((options) => {
18
- const port = resolvePort(options.port);
19
- const server = startServer({ port, hostname: options.host });
20
- console.log(`XFE Server listening on ${options.host ?? '0.0.0.0'}:${server.port}`);
21
- console.log(`HTTP health: http://${options.host ?? 'localhost'}:${server.port}/health`);
22
- console.log(`MCP endpoint: http://${options.host ?? 'localhost'}:${server.port}/mcp`);
23
- process.once('SIGINT', () => void stopLocalServer(server.close));
24
- process.once('SIGTERM', () => void stopLocalServer(server.close));
18
+ const server = startServer({ port: resolvePort(options.port), hostname: options.host });
19
+ printServerStarted(server);
20
+ bindServerSignals(server);
25
21
  });
26
22
  }
27
23
  export function registerTaskCommands(program) {
28
24
  const taskCmd = program.command('task').helpCommand(false).description('远端/本地 XFE Server 任务');
25
+ registerTaskCreateCommand(taskCmd);
26
+ registerTaskStartCommand(taskCmd);
27
+ registerRequestCommands(taskCmd);
28
+ }
29
+ // ─── Task Commands ─────────────────────────────────────────
30
+ function registerTaskCreateCommand(taskCmd) {
29
31
  taskCmd
30
32
  .command('create')
31
33
  .description('通过 xfe server 创建远端 agent 任务')
@@ -36,41 +38,34 @@ export function registerTaskCommands(program) {
36
38
  .option('--tenant <tenantId>', '租户 ID,本地默认 local')
37
39
  .option('--document-url <url>', '需求文档链接')
38
40
  .option('--base-branch <branch>', '基准分支,默认 main')
41
+ .option('--branch <branchName>', '任务分支,默认 feat-${taskId}')
42
+ .option('--workflow <workflowId>', '任务流程 ID,默认 plan-code-review')
39
43
  .option('--prepare-workspace', '创建任务后立即 clone 仓库并创建任务分支')
40
44
  .option('--start', '创建任务后启动 agent workflow')
41
- .option('--executor <executor>', 'agent executor,mock | codex | copilot')
45
+ .option('--executor <executor>', 'agent executor,mock | codex | claude | copilot')
42
46
  .option('--auto-approve', '启动 agent workflow 后自动通过计划审批')
43
47
  .action(async (options) => {
44
48
  const gateway = createBackendGateway(options);
45
- const result = await gateway.createTask({
46
- repoUrl: options.repo,
47
- taskId: options.task,
48
- description: options.description,
49
- documentUrl: options.documentUrl,
50
- baseBranch: options.baseBranch,
51
- prepareWorkspace: options.prepareWorkspace,
52
- });
49
+ const result = await gateway.createTask(createRemoteTaskPayload(options));
53
50
  printJson(result);
54
51
  if (!options.start)
55
52
  return;
56
- const started = await gateway.startTask({ taskId: options.task, executor: options.executor, autoApprove: options.autoApprove });
57
- printJson(started);
53
+ printJson(await gateway.startTask({ taskId: options.task, executor: options.executor, autoApprove: options.autoApprove }));
58
54
  });
55
+ }
56
+ function registerTaskStartCommand(taskCmd) {
59
57
  taskCmd
60
58
  .command('start <taskId>')
61
59
  .description('启动已创建的 agent workflow')
62
- .option('--executor <executor>', 'agent executor,mock | codex | copilot')
60
+ .option('--executor <executor>', 'agent executor,mock | codex | claude | copilot')
63
61
  .option('--auto-approve', '自动通过计划审批')
64
62
  .option('--server <url>', `server 地址,默认 http://localhost:${DEFAULT_SERVER_PORT}`)
65
63
  .option('--tenant <tenantId>', '租户 ID,本地默认 local')
66
64
  .action(async (taskId, options) => {
67
65
  const gateway = createBackendGateway(options);
68
- const started = await gateway.startTask({ taskId, executor: options.executor, autoApprove: options.autoApprove });
69
- printJson(started);
66
+ printJson(await gateway.startTask({ taskId, executor: options.executor, autoApprove: options.autoApprove }));
70
67
  });
71
- registerRequestCommands(taskCmd);
72
68
  }
73
- // ─── Command Registration ───────────────────────────────────
74
69
  function registerRequestCommands(taskCmd) {
75
70
  taskCmd
76
71
  .command('requests')
@@ -79,9 +74,7 @@ function registerRequestCommands(taskCmd) {
79
74
  .option('--server <url>', `server 地址,默认 http://localhost:${DEFAULT_SERVER_PORT}`)
80
75
  .option('--tenant <tenantId>', '租户 ID,本地默认 local')
81
76
  .action(async (options) => {
82
- const gateway = createBackendGateway(options);
83
- const result = await gateway.listClientRequests({ status: options.status });
84
- printJson(result);
77
+ printJson(await createBackendGateway(options).listClientRequests({ status: options.status }));
85
78
  });
86
79
  taskCmd
87
80
  .command('watch')
@@ -91,11 +84,7 @@ function registerRequestCommands(taskCmd) {
91
84
  .option('--interval <ms>', '轮询间隔,默认 1000')
92
85
  .option('--once', '只处理当前 pending 请求后退出')
93
86
  .action(async (options) => {
94
- await watchClientRequests({
95
- gateway: createBackendGateway(options),
96
- intervalMs: resolvePollInterval(options.interval),
97
- once: Boolean(options.once),
98
- });
87
+ await watchClientRequests({ gateway: createBackendGateway(options), intervalMs: resolvePollInterval(options.interval), once: Boolean(options.once) });
99
88
  });
100
89
  taskCmd
101
90
  .command('respond <requestId>')
@@ -105,8 +94,7 @@ function registerRequestCommands(taskCmd) {
105
94
  .option('--server <url>', `server 地址,默认 http://localhost:${DEFAULT_SERVER_PORT}`)
106
95
  .option('--tenant <tenantId>', '租户 ID,本地默认 local')
107
96
  .action(async (requestId, options) => {
108
- const gateway = createBackendGateway(options);
109
- const result = await gateway.respondClientRequest({
97
+ const result = await createBackendGateway(options).respondClientRequest({
110
98
  requestId,
111
99
  action: parseResponseAction(options.action),
112
100
  content: parseJsonRecord(options.content),
@@ -114,18 +102,13 @@ function registerRequestCommands(taskCmd) {
114
102
  printJson(result);
115
103
  });
116
104
  }
117
- // ─── Watch Flow ─────────────────────────────────────────────
105
+ // ─── Watch Flow ────────────────────────────────────────────
118
106
  async function watchClientRequests(options) {
119
107
  const terminal = createInterface({ input: process.stdin, output: process.stdout });
120
108
  const handledRequestIds = new Set();
121
109
  try {
122
110
  while (true) {
123
- const requests = await listPendingClientRequests(options.gateway, handledRequestIds);
124
- for (const request of requests) {
125
- handledRequestIds.add(request.id);
126
- const response = await promptClientRequest(terminal, request);
127
- await options.gateway.respondClientRequest({ requestId: request.id, action: response.action, content: response.content });
128
- }
111
+ await handlePendingRequests(terminal, options.gateway, handledRequestIds);
129
112
  if (options.once)
130
113
  return;
131
114
  await wait(options.intervalMs);
@@ -135,6 +118,14 @@ async function watchClientRequests(options) {
135
118
  terminal.close();
136
119
  }
137
120
  }
121
+ async function handlePendingRequests(terminal, gateway, handledRequestIds) {
122
+ const requests = await listPendingClientRequests(gateway, handledRequestIds);
123
+ for (const request of requests) {
124
+ handledRequestIds.add(request.id);
125
+ const response = await promptClientRequest(terminal, request);
126
+ await gateway.respondClientRequest({ requestId: request.id, action: response.action, content: response.content });
127
+ }
128
+ }
138
129
  async function listPendingClientRequests(gateway, handledRequestIds) {
139
130
  const response = await gateway.listClientRequests({ status: 'pending' });
140
131
  return extractClientRequests(response).filter((request) => !handledRequestIds.has(request.id));
@@ -144,8 +135,7 @@ async function promptClientRequest(terminal, request) {
144
135
  console.log(request.message);
145
136
  if (isActionOnlyRequest(request.kind))
146
137
  return { action: await promptAction(terminal), content: undefined };
147
- const content = await promptSchemaContent(terminal, request.schema);
148
- return { action: 'accept', content };
138
+ return { action: 'accept', content: await promptSchemaContent(terminal, request.schema) };
149
139
  }
150
140
  async function promptAction(terminal) {
151
141
  const answer = await terminal.question('处理方式 accept/decline/cancel,默认 accept: ');
@@ -181,10 +171,33 @@ async function promptSchemaField(terminal, field) {
181
171
  return Number(value);
182
172
  return String(value ?? '');
183
173
  }
184
- // ─── Helpers ────────────────────────────────────────────────
174
+ function createRemoteTaskPayload(options) {
175
+ return {
176
+ repoUrl: options.repo,
177
+ taskId: options.task,
178
+ description: options.description,
179
+ documentUrl: options.documentUrl,
180
+ baseBranch: options.baseBranch,
181
+ branchName: options.branch,
182
+ workflowId: options.workflow,
183
+ prepareWorkspace: options.prepareWorkspace,
184
+ };
185
+ }
185
186
  function createBackendGateway(options) {
186
187
  return new HttpBackendGateway({ baseUrl: options.server ?? `http://localhost:${DEFAULT_SERVER_PORT}`, tenantId: options.tenant });
187
188
  }
189
+ function printServerStarted(server) {
190
+ console.log(`XFE Server listening on ${server.hostname}:${server.port}`);
191
+ console.log(`HTTP health: http://${server.hostname}:${server.port}/health`);
192
+ }
193
+ function bindServerSignals(server) {
194
+ process.once('SIGINT', () => void stopLocalServer(server));
195
+ process.once('SIGTERM', () => void stopLocalServer(server));
196
+ }
197
+ async function stopLocalServer(server) {
198
+ await server.close();
199
+ process.exit(0);
200
+ }
188
201
  function resolvePort(rawPort) {
189
202
  const port = Number(rawPort);
190
203
  return Number.isInteger(port) && port > 0 ? port : DEFAULT_SERVER_PORT;
@@ -193,10 +206,6 @@ function resolvePollInterval(rawInterval) {
193
206
  const intervalMs = Number(rawInterval);
194
207
  return Number.isInteger(intervalMs) && intervalMs > 0 ? intervalMs : 1000;
195
208
  }
196
- async function stopLocalServer(close) {
197
- await close();
198
- process.exit(0);
199
- }
200
209
  function parseResponseAction(action) {
201
210
  if (action === 'accept' || action === 'decline' || action === 'cancel')
202
211
  return action;
@@ -206,9 +215,8 @@ function parseJsonRecord(content) {
206
215
  if (!content)
207
216
  return undefined;
208
217
  const parsed = JSON.parse(content);
209
- if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) {
218
+ if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed))
210
219
  throw new Error('content 必须是 JSON object');
211
- }
212
220
  return parsed;
213
221
  }
214
222
  function extractClientRequests(response) {
@@ -8,9 +8,15 @@ import fs from 'fs';
8
8
  import path from 'path';
9
9
  import { useKeymap, KeymapLayer } from '../../hooks/use-keymap.js';
10
10
  import { useSelectableList } from './use-selectable-list.js';
11
- import { OptionList, SearchBar } from './OptionList.js';
12
- export const FuzzyPathPrompt = memo(function FuzzyPathPrompt({ question, onSubmit }) {
13
- const options = useMemo(() => collectPaths(question.rootPath || '.', question.itemType || 'any', question.excludePath).map((p) => ({ label: p, value: p })), [question.rootPath, question.itemType, question.excludePath]);
11
+ import { OptionList, SearchBar, resolveOptionPageSize } from './OptionList.js';
12
+ const SEARCH_PROMPT_RESERVED_ROWS = 3;
13
+ export const FuzzyPathPrompt = memo(function FuzzyPathPrompt({ question, onSubmit, maxContentRows }) {
14
+ const options = useMemo(() => collectPaths(question.rootPath || '.', question.itemType || 'any', question.excludePath).map((p) => ({ key: p, label: p, value: p })), [question.rootPath, question.itemType, question.excludePath]);
15
+ const pageSize = resolveOptionPageSize({
16
+ pageSize: question.pageSize,
17
+ maxRows: maxContentRows,
18
+ reservedRows: SEARCH_PROMPT_RESERVED_ROWS,
19
+ });
14
20
  const { searchText, filtered, selectedIndex, selectedItem } = useSelectableList({
15
21
  items: options,
16
22
  searchable: true,
@@ -23,7 +29,7 @@ export const FuzzyPathPrompt = memo(function FuzzyPathPrompt({ question, onSubmi
23
29
  },
24
30
  layer: KeymapLayer.Navigation,
25
31
  });
26
- return (_jsxs(Box, { flexDirection: "column", children: [_jsx(SearchBar, { text: searchText }), _jsx(OptionList, { options: filtered, selectedIndex: selectedIndex, pageSize: question.pageSize ?? 10 }), _jsx(Box, { marginTop: 1, children: _jsx(Text, { dimColor: true, children: "\u8F93\u5165\u641C\u7D22 \u2191\u2193 \u9009\u62E9 \u21B5 \u786E\u8BA4" }) })] }));
32
+ return (_jsxs(Box, { flexDirection: "column", children: [_jsx(SearchBar, { text: searchText }), _jsx(OptionList, { options: filtered, selectedIndex: selectedIndex, pageSize: pageSize }), _jsx(Box, { marginTop: 1, children: _jsx(Text, { dimColor: true, children: "\u8F93\u5165\u641C\u7D22 \u2191\u2193 \u9009\u62E9 \u21B5 \u786E\u8BA4" }) })] }));
27
33
  });
28
34
  // ─── Helpers ────────────────────────────────────────────────
29
35
  function collectPaths(rootPath, itemType, excludePath) {
@@ -6,11 +6,13 @@ import { memo } from 'react';
6
6
  import { Box, Text } from 'ink';
7
7
  import { useKeymap, KeymapLayer } from '../../hooks/use-keymap.js';
8
8
  import { useSelectableList } from './use-selectable-list.js';
9
- import { OptionList } from './OptionList.js';
9
+ import { OptionList, resolveOptionPageSize } from './OptionList.js';
10
10
  import { normalizeChoices } from './index.js';
11
- export const ListPrompt = memo(function ListPrompt({ question, onSubmit }) {
11
+ const LIST_PROMPT_RESERVED_ROWS = 2;
12
+ export const ListPrompt = memo(function ListPrompt({ question, onSubmit, maxContentRows }) {
12
13
  const options = normalizeChoices(question.choices);
13
14
  const defaultIndex = question.default != null ? options.findIndex((o) => o.value === question.default) : -1;
15
+ const pageSize = resolveOptionPageSize({ pageSize: question.pageSize, maxRows: maxContentRows, reservedRows: LIST_PROMPT_RESERVED_ROWS });
14
16
  const { filtered, selectedIndex, selectedItem } = useSelectableList({ items: options, defaultIndex });
15
17
  useKeymap({
16
18
  key: 'return',
@@ -20,6 +22,6 @@ export const ListPrompt = memo(function ListPrompt({ question, onSubmit }) {
20
22
  },
21
23
  layer: KeymapLayer.Navigation,
22
24
  });
23
- return (_jsxs(Box, { flexDirection: "column", children: [_jsx(OptionList, { options: filtered, selectedIndex: selectedIndex, pageSize: question.pageSize ?? 10 }), _jsx(Box, { marginTop: 1, children: _jsx(Text, { dimColor: true, children: "\u2191\u2193 \u9009\u62E9 \u21B5 \u786E\u8BA4" }) })] }));
25
+ return (_jsxs(Box, { flexDirection: "column", children: [_jsx(OptionList, { options: filtered, selectedIndex: selectedIndex, pageSize: pageSize }), _jsx(Box, { marginTop: 1, children: _jsx(Text, { dimColor: true, children: "\u2191\u2193 \u9009\u62E9 \u21B5 \u786E\u8BA4" }) })] }));
24
26
  });
25
27
  //# sourceMappingURL=ListPrompt.js.map
@@ -13,10 +13,16 @@ interface OptionListProps {
13
13
  readonly pageSize: number;
14
14
  readonly mode?: 'single' | 'checkbox';
15
15
  }
16
+ interface ResolveOptionPageSizeOptions {
17
+ readonly pageSize?: number;
18
+ readonly maxRows?: number;
19
+ readonly reservedRows?: number;
20
+ }
16
21
  export declare const OptionList: import("react").NamedExoticComponent<OptionListProps>;
17
22
  export declare const SearchBar: import("react").NamedExoticComponent<{
18
23
  readonly text: string;
19
24
  }>;
25
+ export declare function resolveOptionPageSize({ pageSize, maxRows, reservedRows }: ResolveOptionPageSizeOptions): number;
20
26
  export declare function computeWindow<T>(items: readonly T[], selectedIndex: number, pageSize: number): {
21
27
  visibleItems: T[];
22
28
  startIndex: number;
@@ -1,4 +1,4 @@
1
- import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
1
+ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
2
2
  /**
3
3
  * @xfe-repo/cli - 可滚动选项列表展示组件
4
4
  *
@@ -15,17 +15,43 @@ export const OptionList = memo(function OptionList({ options, selectedIndex, pag
15
15
  return (_jsxs(Box, { flexDirection: "column", marginTop: 1, children: [startIndex > 0 && _jsxs(Text, { dimColor: true, children: [" \u2191 \u8FD8\u6709 ", startIndex, " \u9879"] }), visibleItems.map((opt, i) => {
16
16
  const actualIndex = startIndex + i;
17
17
  const isFocused = actualIndex === selectedIndex;
18
- return (_jsxs(Box, { gap: 1, children: [_jsx(Text, { color: isFocused ? 'cyan' : undefined, children: isFocused ? '❯' : ' ' }), mode === 'checkbox' && _jsx(CheckboxIcon, { checked: opt.checked }), _jsx(Text, { color: isFocused ? 'cyan' : undefined, bold: isFocused, children: opt.label })] }, `${opt.label}-${actualIndex}`));
18
+ return (_jsxs(Box, { alignItems: "flex-start", children: [_jsx(OptionPrefix, { mode: mode, checked: opt.checked, isFocused: isFocused }), _jsx(OptionContent, { option: opt, isFocused: isFocused })] }, opt.key));
19
19
  }), startIndex + windowSize < totalItems && _jsxs(Text, { dimColor: true, children: [" \u2193 \u8FD8\u6709 ", totalItems - startIndex - windowSize, " \u9879"] })] }));
20
20
  });
21
21
  export const SearchBar = memo(function SearchBar({ text }) {
22
22
  return (_jsxs(Box, { children: [_jsx(Text, { dimColor: true, children: "\u641C\u7D22: " }), _jsxs(Text, { children: [text, _jsx(Text, { color: "gray", children: "\u258C" })] })] }));
23
23
  });
24
24
  // ─── Render Helpers ─────────────────────────────────────────
25
+ const OPTION_PREFIX_WIDTH = 2;
26
+ const CHECKBOX_OPTION_PREFIX_WIDTH = 4;
27
+ const FOCUSED_MARK = '❯';
28
+ const EMPTY_MARK = ' ';
29
+ const DESCRIPTION_COLOR = 'gray';
30
+ const DEFAULT_OPTION_PAGE_SIZE = 10;
31
+ const MIN_OPTION_PAGE_SIZE = 1;
32
+ const OPTION_LIST_TOP_MARGIN_ROWS = 1;
33
+ const OPTION_CONTENT_ROW_HEIGHT = 2;
34
+ const SCROLL_INDICATOR_ROWS = 2;
35
+ const OptionPrefix = memo(function OptionPrefix({ mode, checked, isFocused, }) {
36
+ const prefixWidth = mode === 'checkbox' ? CHECKBOX_OPTION_PREFIX_WIDTH : OPTION_PREFIX_WIDTH;
37
+ return (_jsxs(Box, { width: prefixWidth, flexShrink: 0, children: [_jsx(Text, { color: isFocused ? 'cyan' : undefined, children: isFocused ? FOCUSED_MARK : EMPTY_MARK }), mode === 'checkbox' && (_jsxs(_Fragment, { children: [_jsx(Text, { children: " " }), _jsx(CheckboxIcon, { checked: checked })] })), _jsx(Text, { children: " " })] }));
38
+ });
39
+ const OptionContent = memo(function OptionContent({ option, isFocused }) {
40
+ return (_jsxs(Box, { flexDirection: "column", flexShrink: 1, children: [_jsx(Text, { color: isFocused ? 'cyan' : undefined, bold: isFocused, children: option.label }), option.description && (_jsx(Text, { color: DESCRIPTION_COLOR, dimColor: true, children: option.description }))] }));
41
+ });
25
42
  const CheckboxIcon = memo(function CheckboxIcon({ checked }) {
26
43
  return _jsx(Text, { color: checked ? 'green' : 'gray', children: checked ? '◉' : '○' });
27
44
  });
28
45
  // ─── Helpers ────────────────────────────────────────────────
46
+ export function resolveOptionPageSize({ pageSize, maxRows, reservedRows = 0 }) {
47
+ const requestedPageSize = pageSize ?? DEFAULT_OPTION_PAGE_SIZE;
48
+ if (maxRows == null)
49
+ return requestedPageSize;
50
+ const availableOptionRows = maxRows - reservedRows - OPTION_LIST_TOP_MARGIN_ROWS - SCROLL_INDICATOR_ROWS;
51
+ const safeOptionRows = Math.max(OPTION_CONTENT_ROW_HEIGHT, availableOptionRows);
52
+ const heightLimitedPageSize = Math.floor(safeOptionRows / OPTION_CONTENT_ROW_HEIGHT);
53
+ return Math.max(MIN_OPTION_PAGE_SIZE, Math.min(requestedPageSize, heightLimitedPageSize));
54
+ }
29
55
  export function computeWindow(items, selectedIndex, pageSize) {
30
56
  const totalItems = items.length;
31
57
  const windowSize = Math.min(pageSize, totalItems);
@@ -6,9 +6,11 @@ import { memo, useState, useMemo } from 'react';
6
6
  import { Box, Text } from 'ink';
7
7
  import { useKeymap, KeymapLayer } from '../../hooks/use-keymap.js';
8
8
  import { useSelectableList } from './use-selectable-list.js';
9
- import { OptionList, SearchBar } from './OptionList.js';
9
+ import { OptionList, SearchBar, resolveOptionPageSize } from './OptionList.js';
10
10
  import { normalizeChoices } from './index.js';
11
- export const SearchCheckboxPrompt = memo(function SearchCheckboxPrompt({ question, onSubmit }) {
11
+ const SEARCH_PROMPT_RESERVED_ROWS = 3;
12
+ const ERROR_MESSAGE_ROWS = 2;
13
+ export const SearchCheckboxPrompt = memo(function SearchCheckboxPrompt({ question, onSubmit, maxContentRows }) {
12
14
  const defaultSet = useMemo(() => {
13
15
  if (question.default == null)
14
16
  return new Set();
@@ -19,6 +21,11 @@ export const SearchCheckboxPrompt = memo(function SearchCheckboxPrompt({ questio
19
21
  const defaultIndex = firstDefault != null ? allOptions.findIndex((o) => o.value === firstDefault) : -1;
20
22
  const [items, setItems] = useState(allOptions);
21
23
  const [error, setError] = useState('');
24
+ const pageSize = resolveOptionPageSize({
25
+ pageSize: question.pageSize,
26
+ maxRows: maxContentRows,
27
+ reservedRows: SEARCH_PROMPT_RESERVED_ROWS + (error ? ERROR_MESSAGE_ROWS : 0),
28
+ });
22
29
  const { searchText, filtered, selectedIndex } = useSelectableList({ items, defaultIndex, searchable: true });
23
30
  const toggleItem = () => {
24
31
  const target = filtered[selectedIndex];
@@ -45,6 +52,6 @@ export const SearchCheckboxPrompt = memo(function SearchCheckboxPrompt({ questio
45
52
  useKeymap({ key: ' ', handler: toggleItem, layer: KeymapLayer.Navigation });
46
53
  useKeymap({ key: 'ctrl+o', handler: toggleAll, layer: KeymapLayer.Navigation });
47
54
  useKeymap({ key: 'return', handler: submit, layer: KeymapLayer.Navigation });
48
- return (_jsxs(Box, { flexDirection: "column", children: [_jsx(SearchBar, { text: searchText }), _jsx(OptionList, { mode: "checkbox", options: filtered, selectedIndex: selectedIndex, pageSize: question.pageSize ?? 10 }), error && (_jsx(Box, { marginTop: 1, children: _jsx(Text, { color: "red", children: error }) })), _jsx(Box, { marginTop: 1, children: _jsx(Text, { dimColor: true, children: "\u8F93\u5165\u641C\u7D22 \u2191\u2193 \u9009\u62E9 \u7A7A\u683C \u5207\u6362 Ctrl+O \u5168\u9009 \u21B5 \u786E\u8BA4" }) })] }));
55
+ return (_jsxs(Box, { flexDirection: "column", children: [_jsx(SearchBar, { text: searchText }), _jsx(OptionList, { mode: "checkbox", options: filtered, selectedIndex: selectedIndex, pageSize: pageSize }), error && (_jsx(Box, { marginTop: 1, children: _jsx(Text, { color: "red", children: error }) })), _jsx(Box, { marginTop: 1, children: _jsx(Text, { dimColor: true, children: "\u8F93\u5165\u641C\u7D22 \u2191\u2193 \u9009\u62E9 \u7A7A\u683C \u5207\u6362 Ctrl+O \u5168\u9009 \u21B5 \u786E\u8BA4" }) })] }));
49
56
  });
50
57
  //# sourceMappingURL=SearchCheckboxPrompt.js.map
@@ -6,11 +6,17 @@ import { memo } from 'react';
6
6
  import { Box, Text } from 'ink';
7
7
  import { useKeymap, KeymapLayer } from '../../hooks/use-keymap.js';
8
8
  import { useSelectableList } from './use-selectable-list.js';
9
- import { OptionList, SearchBar } from './OptionList.js';
9
+ import { OptionList, SearchBar, resolveOptionPageSize } from './OptionList.js';
10
10
  import { normalizeChoices } from './index.js';
11
- export const SearchListPrompt = memo(function SearchListPrompt({ question, onSubmit }) {
11
+ const SEARCH_PROMPT_RESERVED_ROWS = 3;
12
+ export const SearchListPrompt = memo(function SearchListPrompt({ question, onSubmit, maxContentRows }) {
12
13
  const options = normalizeChoices(question.choices);
13
14
  const defaultIndex = question.default != null ? options.findIndex((o) => o.value === question.default) : -1;
15
+ const pageSize = resolveOptionPageSize({
16
+ pageSize: question.pageSize,
17
+ maxRows: maxContentRows,
18
+ reservedRows: SEARCH_PROMPT_RESERVED_ROWS,
19
+ });
14
20
  const { searchText, filtered, selectedIndex, selectedItem } = useSelectableList({
15
21
  items: options,
16
22
  defaultIndex,
@@ -24,6 +30,6 @@ export const SearchListPrompt = memo(function SearchListPrompt({ question, onSub
24
30
  },
25
31
  layer: KeymapLayer.Navigation,
26
32
  });
27
- return (_jsxs(Box, { flexDirection: "column", children: [_jsx(SearchBar, { text: searchText }), _jsx(OptionList, { options: filtered, selectedIndex: selectedIndex, pageSize: question.pageSize ?? 10 }), _jsx(Box, { marginTop: 1, children: _jsx(Text, { dimColor: true, children: "\u8F93\u5165\u641C\u7D22 \u2191\u2193 \u9009\u62E9 \u21B5 \u786E\u8BA4" }) })] }));
33
+ return (_jsxs(Box, { flexDirection: "column", children: [_jsx(SearchBar, { text: searchText }), _jsx(OptionList, { options: filtered, selectedIndex: selectedIndex, pageSize: pageSize }), _jsx(Box, { marginTop: 1, children: _jsx(Text, { dimColor: true, children: "\u8F93\u5165\u641C\u7D22 \u2191\u2193 \u9009\u62E9 \u21B5 \u786E\u8BA4" }) })] }));
28
34
  });
29
35
  //# sourceMappingURL=SearchListPrompt.js.map
@@ -1,8 +1,9 @@
1
- import type { PromptQuestion } from '@xfe-repo/cli-core';
1
+ import type { PromptChoice, PromptQuestion } from '@xfe-repo/cli-core';
2
2
  import type { SelectOption } from './use-selectable-list.js';
3
3
  export interface PromptViewProps {
4
4
  readonly question: PromptQuestion;
5
5
  readonly onSubmit: (value: any) => void;
6
+ readonly maxContentRows?: number;
6
7
  }
7
8
  export { InputPrompt } from './InputPrompt.js';
8
9
  export { ConfirmPrompt } from './ConfirmPrompt.js';
@@ -11,8 +12,5 @@ export { SearchListPrompt } from './SearchListPrompt.js';
11
12
  export { SearchCheckboxPrompt } from './SearchCheckboxPrompt.js';
12
13
  export { FuzzyPathPrompt } from './FuzzyPathPrompt.js';
13
14
  export declare const Prompt: import("react").NamedExoticComponent<PromptViewProps>;
14
- export declare function normalizeChoices(choices?: {
15
- name: string;
16
- value: any;
17
- }[] | string[]): SelectOption[];
15
+ export declare function normalizeChoices(choices?: PromptChoice[] | string[]): SelectOption[];
18
16
  //# sourceMappingURL=index.d.ts.map
@@ -16,19 +16,19 @@ export { ListPrompt } from './ListPrompt.js';
16
16
  export { SearchListPrompt } from './SearchListPrompt.js';
17
17
  export { SearchCheckboxPrompt } from './SearchCheckboxPrompt.js';
18
18
  export { FuzzyPathPrompt } from './FuzzyPathPrompt.js';
19
- export const Prompt = memo(function Prompt({ question, onSubmit }) {
19
+ export const Prompt = memo(function Prompt({ question, onSubmit, maxContentRows }) {
20
20
  switch (question.type) {
21
21
  case 'confirm':
22
22
  return _jsx(ConfirmPrompt, { question: question, onSubmit: onSubmit });
23
23
  case 'search-list':
24
- return _jsx(SearchListPrompt, { question: question, onSubmit: onSubmit });
24
+ return _jsx(SearchListPrompt, { question: question, onSubmit: onSubmit, maxContentRows: maxContentRows });
25
25
  case 'search-checkbox':
26
- return _jsx(SearchCheckboxPrompt, { question: question, onSubmit: onSubmit });
26
+ return _jsx(SearchCheckboxPrompt, { question: question, onSubmit: onSubmit, maxContentRows: maxContentRows });
27
27
  case 'fuzzy-path':
28
- return _jsx(FuzzyPathPrompt, { question: question, onSubmit: onSubmit });
28
+ return _jsx(FuzzyPathPrompt, { question: question, onSubmit: onSubmit, maxContentRows: maxContentRows });
29
29
  case 'list':
30
30
  case 'rawlist':
31
- return _jsx(ListPrompt, { question: question, onSubmit: onSubmit });
31
+ return _jsx(ListPrompt, { question: question, onSubmit: onSubmit, maxContentRows: maxContentRows });
32
32
  case 'input':
33
33
  default:
34
34
  return _jsx(InputPrompt, { question: question, onSubmit: onSubmit });
@@ -40,8 +40,19 @@ export function normalizeChoices(choices) {
40
40
  return [];
41
41
  return choices.map((choice) => {
42
42
  if (typeof choice === 'string')
43
- return { label: choice, value: choice };
44
- return { label: choice.name, value: choice.value };
43
+ return { key: choice, label: choice, value: choice };
44
+ return {
45
+ key: choice.key ?? getChoiceFallbackKey(choice),
46
+ label: choice.name,
47
+ value: choice.value,
48
+ description: choice.description,
49
+ };
45
50
  });
46
51
  }
52
+ function getChoiceFallbackKey(choice) {
53
+ if (typeof choice.value === 'string' || typeof choice.value === 'number' || typeof choice.value === 'boolean') {
54
+ return String(choice.value);
55
+ }
56
+ return choice.short ?? choice.name;
57
+ }
47
58
  //# sourceMappingURL=index.js.map
@@ -5,8 +5,10 @@
5
5
  * 供 ListPrompt / SearchListPrompt / FuzzyPathPrompt / SearchCheckboxPrompt 复用
6
6
  */
7
7
  export interface SelectOption {
8
+ readonly key: string;
8
9
  readonly label: string;
9
10
  readonly value: any;
11
+ readonly description?: string;
10
12
  }
11
13
  interface UseSelectableListOptions<T extends SelectOption> {
12
14
  readonly items: T[];
@@ -14,8 +14,8 @@ export function useSelectableList({ items, defaultIndex = 0, searchable = false,
14
14
  const filtered = useMemo(() => {
15
15
  if (!searchable || !searchText)
16
16
  return items;
17
- const q = searchText.toLowerCase();
18
- return items.filter((opt) => opt.label.toLowerCase().includes(q));
17
+ const searchKeyword = searchText.toLowerCase();
18
+ return items.filter((option) => getOptionSearchText(option).includes(searchKeyword));
19
19
  }, [items, searchText, searchable]);
20
20
  // 过滤结果变化时重置选中(跳过首次渲染,避免覆盖 defaultIndex)
21
21
  useUpdateEffect(() => {
@@ -54,4 +54,9 @@ export function useSelectableList({ items, defaultIndex = 0, searchable = false,
54
54
  selectedItem: filtered[selectedIndex],
55
55
  };
56
56
  }
57
+ // ─── Helpers ────────────────────────────────────────────────
58
+ function getOptionSearchText(option) {
59
+ const searchableText = [option.label, option.description ?? ''].join(' ');
60
+ return searchableText.toLowerCase();
61
+ }
57
62
  //# sourceMappingURL=use-selectable-list.js.map
@@ -17,20 +17,18 @@ import { useState, useEffect, useCallback, useRef } from 'react';
17
17
  */
18
18
  const FLUSH_DELAY_MS = 16;
19
19
  export function useLogs(logger) {
20
- const [entries, setEntries] = useState(() => logger.getUnrenderedEntries());
20
+ const [entries, setEntries] = useState([]);
21
21
  const bufferRef = useRef([]);
22
22
  const timerRef = useRef(null);
23
23
  const pendingRef = useRef([]);
24
24
  useEffect(() => {
25
25
  // 恢复上一个 logger 未刷新的缓冲条目(解决 session 关闭时日志丢失的竞态)
26
26
  const pending = pendingRef.current;
27
- if (pending.length > 0) {
28
- setEntries((prev) => [...prev, ...pending, ...logger.getUnrenderedEntries()]);
27
+ const unrendered = logger.getUnrenderedEntries();
28
+ if (pending.length > 0 || unrendered.length > 0) {
29
+ setEntries((prev) => [...prev, ...pending, ...unrendered]);
29
30
  pendingRef.current = [];
30
31
  }
31
- else {
32
- setEntries(logger.getUnrenderedEntries());
33
- }
34
32
  const flushBuffer = () => {
35
33
  timerRef.current = null;
36
34
  const batch = bufferRef.current;
@@ -8,6 +8,7 @@ import type { CommandWithSource, StatusBadge, PluginStoreSnapshot } from '@xfe-r
8
8
  import type { RunSession } from '../hooks/use-session-manager.js';
9
9
  import type { InkToast } from '../adapters/ink-adapter.js';
10
10
  interface MenuViewProps {
11
+ readonly availableRows: number;
11
12
  readonly projectName: string;
12
13
  readonly commandsWithSource: CommandWithSource[];
13
14
  readonly activeSessions: [string, RunSession][];
@@ -6,7 +6,7 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
6
6
  * 所有交互逻辑已下沉到子组件和 useKeymap 系统
7
7
  */
8
8
  import { useState, useMemo, useEffect, useCallback, memo } from 'react';
9
- import { Box, Text, useWindowSize } from 'ink';
9
+ import { Box, Text } from 'ink';
10
10
  import Link from 'ink-link';
11
11
  import { resolveBadgeProp } from '@xfe-repo/cli-core';
12
12
  import { buildCommandCatalog } from '../completion/catalog.js';
@@ -23,17 +23,14 @@ const LOGO_HEIGHT = 7;
23
23
  const BASE_CHROME_HEIGHT = 12;
24
24
  const TASK_GRID_CHROME_HEIGHT = 6;
25
25
  const MIN_SUGGESTIONS = 3;
26
- // 终端底部保留空行数,避免光标挤压界面(尤其是多行输入时)
27
- const BOTTOM_RESERVE = 0;
26
+ const MIN_VISIBLE_SUGGESTIONS = 1;
28
27
  // ─── Component ──────────────────────────────────────────────
29
- export const MenuView = memo(function MenuView({ projectName, commandsWithSource, activeSessions, runningCommandNames, badges, storeSnapshot, toast, onSelect, onFocusSession, onExit, onUserInput, }) {
28
+ export const MenuView = memo(function MenuView({ availableRows, projectName, commandsWithSource, activeSessions, runningCommandNames, badges, storeSnapshot, toast, onSelect, onFocusSession, onExit, onUserInput, }) {
30
29
  const hasActiveSessions = activeSessions.length > 0;
31
30
  const [focusArea, setFocusArea] = useState('commands');
32
- // ── 终端高度自适应 ──
33
- const { rows: terminalRows } = useWindowSize();
34
31
  const chromeHeight = BASE_CHROME_HEIGHT + (hasActiveSessions ? TASK_GRID_CHROME_HEIGHT : 0);
35
- const showLogo = terminalRows >= chromeHeight + LOGO_HEIGHT + MIN_SUGGESTIONS + BOTTOM_RESERVE;
36
- const maxSuggestions = Math.max(MIN_SUGGESTIONS, terminalRows - chromeHeight - (showLogo ? LOGO_HEIGHT : 0) - BOTTOM_RESERVE);
32
+ const showLogo = availableRows >= chromeHeight + LOGO_HEIGHT + MIN_SUGGESTIONS;
33
+ const maxSuggestions = Math.max(MIN_VISIBLE_SUGGESTIONS, availableRows - chromeHeight - (showLogo ? LOGO_HEIGHT : 0));
37
34
  const effectiveFocus = hasActiveSessions ? focusArea : 'commands';
38
35
  // 单一共享定时器驱动所有任务卡片的时间刷新
39
36
  useElapsedTime(hasActiveSessions ? 1 : null);
@@ -3,6 +3,12 @@
3
3
  *
4
4
  * 根据 PromptQuestion 类型路由到对应的 Prompt 组件
5
5
  */
6
- import type { PromptViewProps } from '../components/Prompts/index.js';
6
+ import type { PromptViewProps as PromptProps } from '../components/Prompts/index.js';
7
+ interface PromptViewProps {
8
+ readonly question: PromptProps['question'];
9
+ readonly onSubmit: PromptProps['onSubmit'];
10
+ readonly maxRows?: number;
11
+ }
7
12
  export declare const PromptView: import("react").NamedExoticComponent<PromptViewProps>;
13
+ export {};
8
14
  //# sourceMappingURL=PromptView.d.ts.map
@@ -7,8 +7,12 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
7
7
  import { memo } from 'react';
8
8
  import { Box, Text } from 'ink';
9
9
  import { Prompt } from '../components/Prompts/index.js';
10
+ // ─── Constants ──────────────────────────────────────────────
11
+ const PROMPT_VIEW_CHROME_ROWS = 3;
12
+ const MIN_PROMPT_CONTENT_ROWS = 1;
10
13
  // ─── Main Component ─────────────────────────────────────────
11
- export const PromptView = memo(function PromptView({ question, onSubmit }) {
12
- return (_jsxs(Box, { flexDirection: "column", borderStyle: "round", borderColor: "cyan", paddingX: 1, overflowX: "hidden", children: [_jsxs(Text, { bold: true, wrap: "truncate-end", children: [_jsx(Text, { color: "green", children: "?" }), " ", question.message, question.default != null && question.type !== 'confirm' && _jsxs(Text, { dimColor: true, children: [" (", String(question.default), ")"] })] }), _jsx(Prompt, { question: question, onSubmit: onSubmit })] }));
14
+ export const PromptView = memo(function PromptView({ question, onSubmit, maxRows }) {
15
+ const maxContentRows = maxRows == null ? undefined : Math.max(MIN_PROMPT_CONTENT_ROWS, maxRows - PROMPT_VIEW_CHROME_ROWS);
16
+ return (_jsxs(Box, { flexDirection: "column", borderStyle: "round", borderColor: "cyan", paddingX: 1, maxHeight: maxRows, overflow: "hidden", children: [_jsxs(Text, { bold: true, wrap: "truncate-end", children: [_jsx(Text, { color: "green", children: "?" }), " ", question.message, question.default != null && question.type !== 'confirm' && _jsxs(Text, { dimColor: true, children: [" (", String(question.default), ")"] })] }), _jsx(Prompt, { question: question, onSubmit: onSubmit, maxContentRows: maxContentRows })] }));
13
17
  });
14
18
  //# sourceMappingURL=PromptView.js.map
@@ -4,6 +4,7 @@
4
4
  import type { InkPrompt, InkSpinner } from '../adapters/ink-adapter.js';
5
5
  import type { StatusBadge, PluginStoreSnapshot } from '@xfe-repo/cli-core';
6
6
  interface RunnerViewProps {
7
+ readonly availableRows: number;
7
8
  readonly projectName: string;
8
9
  readonly scriptName: string;
9
10
  readonly startTime: number;
@@ -9,11 +9,48 @@ import { PromptView } from './PromptView.js';
9
9
  import { Logo } from '../components/Logo.js';
10
10
  import { SpinnerStatus } from '../components/SpinnerStatus.js';
11
11
  import { StatusBar } from '../components/StatusBar.js';
12
- export const RunnerView = memo(function RunnerView({ projectName, scriptName, startTime, endTime, prompt, spinner, badges, storeSnapshot, hasError, isCompleted, isSingleCommand, isReadOnly, }) {
12
+ // ─── Constants ──────────────────────────────────────────────
13
+ const LOGO_HEIGHT = 7;
14
+ const STATUS_BAR_HEIGHT = 3;
15
+ const SPINNER_STATUS_HEIGHT = 1;
16
+ const CONTENT_GAP_HEIGHT = 1;
17
+ const MIN_PROMPT_ROWS = 1;
18
+ const PROMPT_WITH_LOGO_MIN_ROWS = 8;
19
+ export const RunnerView = memo(function RunnerView({ availableRows, projectName, scriptName, startTime, endTime, prompt, spinner, badges, storeSnapshot, hasError, isCompleted, isSingleCommand, isReadOnly, }) {
13
20
  const { request, complete } = usePrompt(prompt);
14
21
  const spinnerState = useSpinnerState(spinner);
15
- return (_jsxs(_Fragment, { children: [_jsx(Logo, {}), _jsxs(Box, { flexDirection: "column", gap: 1, overflowX: "hidden", children: [_jsx(SpinnerStatus, { state: spinnerState }), !isReadOnly && request && _jsx(PromptView, { question: request.question, onSubmit: complete }, request.id)] }), !isSingleCommand && (_jsxs(Box, { gap: 1, borderStyle: "round", borderColor: isReadOnly ? 'blue' : 'cyan', paddingX: 1, overflowX: "hidden", children: [_jsx(StatusBar, { projectName: projectName, scriptName: scriptName, startTime: startTime, endTime: endTime, badges: badges, storeSnapshot: storeSnapshot }), _jsx(StatusHint, { isReadOnly: isReadOnly, hasError: hasError, isCompleted: isCompleted })] }))] }));
22
+ const hasPrompt = !isReadOnly && !!request;
23
+ const layout = resolveRunnerLayout({
24
+ availableRows,
25
+ hasPrompt,
26
+ hasSpinnerStatus: isSpinnerStatusVisible(spinnerState),
27
+ hasStatusBar: !isSingleCommand,
28
+ });
29
+ return (_jsxs(_Fragment, { children: [layout.showLogo && _jsx(Logo, {}), _jsxs(Box, { flexDirection: "column", gap: 1, overflowX: "hidden", children: [_jsx(SpinnerStatus, { state: spinnerState }), hasPrompt && _jsx(PromptView, { question: request.question, maxRows: layout.promptRows, onSubmit: complete }, request.id)] }), !isSingleCommand && (_jsxs(Box, { gap: 1, borderStyle: "round", borderColor: isReadOnly ? 'blue' : 'cyan', paddingX: 1, overflowX: "hidden", children: [_jsx(StatusBar, { projectName: projectName, scriptName: scriptName, startTime: startTime, endTime: endTime, badges: badges, storeSnapshot: storeSnapshot }), _jsx(StatusHint, { isReadOnly: isReadOnly, hasError: hasError, isCompleted: isCompleted })] }))] }));
16
30
  });
31
+ // ─── Layout Helpers ────────────────────────────────────────
32
+ function resolveRunnerLayout({ availableRows, hasPrompt, hasSpinnerStatus, hasStatusBar }) {
33
+ const fixedRows = resolveFixedRows({ hasSpinnerStatus, hasStatusBar });
34
+ if (!hasPrompt) {
35
+ return { showLogo: availableRows >= fixedRows + LOGO_HEIGHT };
36
+ }
37
+ const showLogo = availableRows >= fixedRows + LOGO_HEIGHT + PROMPT_WITH_LOGO_MIN_ROWS;
38
+ const logoRows = showLogo ? LOGO_HEIGHT : 0;
39
+ const availablePromptRows = availableRows - fixedRows - logoRows;
40
+ return {
41
+ showLogo,
42
+ promptRows: Math.max(MIN_PROMPT_ROWS, availablePromptRows),
43
+ };
44
+ }
45
+ function resolveFixedRows({ hasSpinnerStatus, hasStatusBar, }) {
46
+ const spinnerRows = hasSpinnerStatus ? SPINNER_STATUS_HEIGHT : 0;
47
+ const contentGapRows = hasSpinnerStatus ? CONTENT_GAP_HEIGHT : 0;
48
+ const statusBarRows = hasStatusBar ? STATUS_BAR_HEIGHT : 0;
49
+ return spinnerRows + contentGapRows + statusBarRows;
50
+ }
51
+ function isSpinnerStatusVisible(state) {
52
+ return state.active || !!state.variant;
53
+ }
17
54
  function StatusHint({ isReadOnly, hasError, isCompleted }) {
18
55
  if (isReadOnly) {
19
56
  const prefix = hasError ? '错误发生,' : isCompleted ? '任务完成,' : null;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@xfe-repo/cli",
3
- "version": "2.0.9",
3
+ "version": "2.0.12",
4
4
  "description": "XFE CLI - Ink-based terminal UI for project scaffolding",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -17,8 +17,8 @@
17
17
  "ink-link": "^5.0.0",
18
18
  "react": "^19.1.0",
19
19
  "zod": "^4.3.6",
20
- "@xfe-repo/cli-core": "2.0.5",
21
- "@xfe-repo/cli-presets": "2.0.5"
20
+ "@xfe-repo/cli-core": "2.0.7",
21
+ "@xfe-repo/cli-presets": "2.0.7"
22
22
  },
23
23
  "devDependencies": {
24
24
  "@types/node": "^24.3.0",