mirror of
https://github.com/Kaelio/ktx.git
synced 2026-07-13 11:22:11 +02:00
Improve connector credential setup UX
This commit is contained in:
parent
d89be2390f
commit
1b5a9fe120
6 changed files with 882 additions and 115 deletions
|
|
@ -30,6 +30,8 @@ export interface ContextBuildViewState {
|
|||
primarySources: ContextBuildTargetState[];
|
||||
contextSources: ContextBuildTargetState[];
|
||||
frame: number;
|
||||
startedAt: number | null;
|
||||
totalElapsedMs: number;
|
||||
}
|
||||
|
||||
export interface ContextBuildArgs {
|
||||
|
|
@ -79,7 +81,7 @@ function statusIcon(status: ContextBuildTargetState['status'], frame: number, st
|
|||
case 'running':
|
||||
return SPINNER_FRAMES[frame % SPINNER_FRAMES.length] ?? '⠋';
|
||||
default:
|
||||
return '·';
|
||||
return '○';
|
||||
}
|
||||
}
|
||||
switch (status) {
|
||||
|
|
@ -90,10 +92,27 @@ function statusIcon(status: ContextBuildTargetState['status'], frame: number, st
|
|||
case 'running':
|
||||
return cyan(SPINNER_FRAMES[frame % SPINNER_FRAMES.length] ?? '⠋');
|
||||
default:
|
||||
return dim('·');
|
||||
return dim('○');
|
||||
}
|
||||
}
|
||||
|
||||
function extractPercent(detailLine: string | null): number | null {
|
||||
if (!detailLine) return null;
|
||||
const match = detailLine.match(/^\[(\d+)%\]/);
|
||||
return match ? Number(match[1]) : null;
|
||||
}
|
||||
|
||||
const BAR_WIDTH = 12;
|
||||
const BAR_FILLED = '█';
|
||||
const BAR_EMPTY = '░';
|
||||
|
||||
function renderProgressBar(percent: number, styled: boolean): string {
|
||||
const filled = Math.round((percent / 100) * BAR_WIDTH);
|
||||
const empty = BAR_WIDTH - filled;
|
||||
const bar = `${BAR_FILLED.repeat(filled)}${BAR_EMPTY.repeat(empty)}`;
|
||||
return styled ? cyan(bar) : bar;
|
||||
}
|
||||
|
||||
function targetDetail(target: ContextBuildTargetState, styled: boolean): string {
|
||||
if (target.status === 'done') {
|
||||
const parts: string[] = [];
|
||||
|
|
@ -105,7 +124,17 @@ function targetDetail(target: ContextBuildTargetState, styled: boolean): string
|
|||
return styled ? red('failed') : 'failed';
|
||||
}
|
||||
if (target.status === 'running') {
|
||||
return target.detailLine ?? (target.target.operation === 'scan' ? 'scanning...' : 'ingesting...');
|
||||
const percent = extractPercent(target.detailLine);
|
||||
const progressText = target.detailLine?.replace(/^\[\d+%\]\s*/, '')
|
||||
?? (target.target.operation === 'scan' ? 'scanning...' : 'ingesting...');
|
||||
const elapsed = target.elapsedMs > 0 ? formatDuration(target.elapsedMs) : null;
|
||||
const parts: string[] = [];
|
||||
if (percent !== null) {
|
||||
parts.push(`${renderProgressBar(percent, styled)} ${percent}%`);
|
||||
}
|
||||
parts.push(progressText);
|
||||
if (elapsed) parts.push(styled ? dim(elapsed) : elapsed);
|
||||
return parts.join(' ');
|
||||
}
|
||||
return styled ? dim('queued') : 'queued';
|
||||
}
|
||||
|
|
@ -140,17 +169,39 @@ export function renderContextBuildView(
|
|||
): string {
|
||||
const styled = options.styled ?? true;
|
||||
const width = columnWidth(state);
|
||||
const allTargets = [...state.primarySources, ...state.contextSources];
|
||||
const doneCount = allTargets.filter((t) => t.status === 'done' || t.status === 'failed').length;
|
||||
const totalCount = allTargets.length;
|
||||
const hasActive = allTargets.some((t) => t.status === 'running' || t.status === 'queued');
|
||||
const allDone = totalCount > 0 && !hasActive;
|
||||
|
||||
const headerParts = ['Building KTX context'];
|
||||
if (totalCount > 0) {
|
||||
const progressParts: string[] = [`${doneCount}/${totalCount}`];
|
||||
if (state.totalElapsedMs > 0) progressParts.push(formatDuration(state.totalElapsedMs));
|
||||
const progress = `(${progressParts.join(' · ')})`;
|
||||
headerParts.push(styled ? dim(progress) : progress);
|
||||
}
|
||||
const header = headerParts.join(' ');
|
||||
const headerPlainLength = header.replace(/\x1b\[[0-9;]*m/g, '').length;
|
||||
const separator = '─'.repeat(Math.max(21, headerPlainLength));
|
||||
|
||||
const lines: string[] = [
|
||||
'',
|
||||
'Building KTX context',
|
||||
'─────────────────────',
|
||||
header,
|
||||
separator,
|
||||
...renderTargetGroup('Primary sources', state.primarySources, state.frame, styled, width),
|
||||
...renderTargetGroup('Context sources', state.contextSources, state.frame, styled, width),
|
||||
'',
|
||||
];
|
||||
const hasActive = [...state.primarySources, ...state.contextSources].some(
|
||||
(t) => t.status === 'running' || t.status === 'queued',
|
||||
);
|
||||
|
||||
if (allDone && state.totalElapsedMs > 0) {
|
||||
const sourcesLabel = totalCount === 1 ? '1 source' : `${totalCount} sources`;
|
||||
const summary = ` Done in ${formatDuration(state.totalElapsedMs)} · ${sourcesLabel} processed`;
|
||||
lines.push(styled ? green(summary) : summary);
|
||||
lines.push('');
|
||||
}
|
||||
|
||||
if (options.showHint && hasActive) {
|
||||
const hint = ` d to detach · ${resumeCommand(options.projectDir)} to resume`;
|
||||
lines.push(styled ? dim(hint) : hint);
|
||||
|
|
@ -177,7 +228,7 @@ export function parseScanSummary(output: string): string | null {
|
|||
export function parseIngestSummary(output: string): string | null {
|
||||
const parts: string[] = [];
|
||||
const workUnits = output.match(/Work units: (\d+)/);
|
||||
if (workUnits) parts.push(`${workUnits[1]} work units`);
|
||||
if (workUnits) parts.push(`${workUnits[1]} items indexed`);
|
||||
const savedMemory = output.match(/Saved memory: (.+)/);
|
||||
if (savedMemory) parts.push(savedMemory[1]);
|
||||
return parts.length > 0 ? parts.join(' · ') : null;
|
||||
|
|
@ -289,6 +340,8 @@ export function initViewState(targets: KtxPublicIngestPlanTarget[]): ContextBuil
|
|||
primarySources: targets.filter((t) => t.operation === 'scan').map(makeTargetState),
|
||||
contextSources: targets.filter((t) => t.operation === 'source-ingest').map(makeTargetState),
|
||||
frame: 0,
|
||||
startedAt: null,
|
||||
totalElapsedMs: 0,
|
||||
};
|
||||
}
|
||||
|
||||
|
|
@ -303,6 +356,8 @@ export async function runContextBuild(
|
|||
const isTTY = io.stdout.isTTY === true;
|
||||
const nowFn = deps.now ?? (() => Date.now());
|
||||
|
||||
state.startedAt = nowFn();
|
||||
|
||||
const repainter = isTTY ? createRepainter(io) : null;
|
||||
const viewOpts = { styled: true, projectDir: args.projectDir };
|
||||
const paint = (hint: boolean) => repainter?.paint(renderContextBuildView(state, { ...viewOpts, showHint: hint }));
|
||||
|
|
@ -312,6 +367,9 @@ export async function runContextBuild(
|
|||
if (repainter) {
|
||||
spinnerInterval = setInterval(() => {
|
||||
state.frame++;
|
||||
if (state.startedAt !== null) {
|
||||
state.totalElapsedMs = nowFn() - state.startedAt;
|
||||
}
|
||||
for (const t of [...state.primarySources, ...state.contextSources]) {
|
||||
if (t.status === 'running' && t.startedAt !== null) {
|
||||
t.elapsedMs = nowFn() - t.startedAt;
|
||||
|
|
@ -400,6 +458,10 @@ export async function runContextBuild(
|
|||
cleanupKeystroke?.();
|
||||
}
|
||||
|
||||
if (state.startedAt !== null) {
|
||||
state.totalElapsedMs = nowFn() - state.startedAt;
|
||||
}
|
||||
|
||||
if (detached) {
|
||||
return { exitCode: 0, detached: true };
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue