mirror of
https://github.com/Kaelio/ktx.git
synced 2026-07-07 11:02:11 +02:00
feat(cli): redesign Notion page picker UI and add skip-empty flow (#78)
Rework the inline picker to use a cleaner visual style (filled/empty square glyphs, bordered layout, clack-style header) and streamlined keybindings (Enter to confirm, Escape to quit, Right Arrow to expand). Replace the transient "select at least one" hint with a skip-empty confirmation prompt that exits cleanly via quit-without-save. Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
9ecb8cb119
commit
f219ba22a6
4 changed files with 126 additions and 86 deletions
|
|
@ -11,7 +11,6 @@ import {
|
||||||
selectAllVisible,
|
selectAllVisible,
|
||||||
selectNone,
|
selectNone,
|
||||||
toggleChecked,
|
toggleChecked,
|
||||||
TRANSIENT_HINT_DURATION_MS,
|
|
||||||
visibleNodeIds,
|
visibleNodeIds,
|
||||||
type NotionPickerPageInput,
|
type NotionPickerPageInput,
|
||||||
} from './notion-page-picker-tree.js';
|
} from './notion-page-picker-tree.js';
|
||||||
|
|
@ -223,22 +222,24 @@ describe('bulk actions and reducer effects', () => {
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it('blocks empty saves, updates search state, and quits without saving', () => {
|
it('prompts skip-empty confirmation on empty save, updates search state, and quits without saving', () => {
|
||||||
const state = buildInitialState({
|
const state = buildInitialState({
|
||||||
tree: buildPickerTree(pages()),
|
tree: buildPickerTree(pages()),
|
||||||
existingRootPageIds: [],
|
existingRootPageIds: [],
|
||||||
currentCrawlMode: 'selected_roots',
|
currentCrawlMode: 'selected_roots',
|
||||||
});
|
});
|
||||||
|
|
||||||
const blockedSave = reducer(state, 'save-request', 9000);
|
const emptySave = reducer(state, 'save-request');
|
||||||
expect(blockedSave).toEqual({
|
expect(emptySave).toEqual({
|
||||||
next: {
|
next: { ...state, pendingConfirm: 'skip-empty' },
|
||||||
...state,
|
effect: null,
|
||||||
transientHint: {
|
});
|
||||||
text: 'Select at least one page or press q to quit',
|
expect(reducer(emptySave.next, 'save-confirm')).toEqual({
|
||||||
expiresAt: 9000 + TRANSIENT_HINT_DURATION_MS,
|
next: { ...state, pendingConfirm: null },
|
||||||
},
|
effect: 'quit-without-save',
|
||||||
},
|
});
|
||||||
|
expect(reducer(emptySave.next, 'save-cancel')).toEqual({
|
||||||
|
next: { ...state, pendingConfirm: null },
|
||||||
effect: null,
|
effect: null,
|
||||||
});
|
});
|
||||||
expect(
|
expect(
|
||||||
|
|
@ -262,7 +263,7 @@ describe('bulk actions and reducer effects', () => {
|
||||||
const withHint = {
|
const withHint = {
|
||||||
...state,
|
...state,
|
||||||
transientHint: {
|
transientHint: {
|
||||||
text: 'Select at least one page or press q to quit',
|
text: 'Select at least one page or press esc to cancel',
|
||||||
expiresAt: 11500,
|
expiresAt: 11500,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -22,7 +22,7 @@ export interface PickerState {
|
||||||
checked: Set<string>;
|
checked: Set<string>;
|
||||||
cursorId: string;
|
cursorId: string;
|
||||||
search: { editing: boolean; query: string };
|
search: { editing: boolean; query: string };
|
||||||
pendingConfirm: 'mode-switch' | null;
|
pendingConfirm: 'mode-switch' | 'skip-empty' | null;
|
||||||
preLoadWarnings: string[];
|
preLoadWarnings: string[];
|
||||||
transientHint: { text: string; expiresAt: number } | null;
|
transientHint: { text: string; expiresAt: number } | null;
|
||||||
currentCrawlMode: 'all_accessible' | 'selected_roots';
|
currentCrawlMode: 'all_accessible' | 'selected_roots';
|
||||||
|
|
@ -61,7 +61,7 @@ interface MutableNode {
|
||||||
childIds: string[];
|
childIds: string[];
|
||||||
}
|
}
|
||||||
|
|
||||||
export const TRANSIENT_HINT_DURATION_MS = 2500;
|
const TRANSIENT_HINT_DURATION_MS = 2500;
|
||||||
|
|
||||||
const collator = new Intl.Collator('en', { sensitivity: 'base', numeric: true });
|
const collator = new Intl.Collator('en', { sensitivity: 'base', numeric: true });
|
||||||
|
|
||||||
|
|
@ -444,7 +444,8 @@ export function buildInitialState(args: {
|
||||||
export function reducer(state: PickerState, cmd: PickerCommand, now = Date.now()): { next: PickerState; effect: PickerEffect } {
|
export function reducer(state: PickerState, cmd: PickerCommand, now = Date.now()): { next: PickerState; effect: PickerEffect } {
|
||||||
if (state.pendingConfirm) {
|
if (state.pendingConfirm) {
|
||||||
if (cmd === 'save-confirm') {
|
if (cmd === 'save-confirm') {
|
||||||
return { next: cloneState(state, { pendingConfirm: null }), effect: 'save' };
|
const effect: PickerEffect = state.pendingConfirm === 'skip-empty' ? 'quit-without-save' : 'save';
|
||||||
|
return { next: cloneState(state, { pendingConfirm: null }), effect };
|
||||||
}
|
}
|
||||||
if (cmd === 'save-cancel') {
|
if (cmd === 'save-cancel') {
|
||||||
return { next: cloneState(state, { pendingConfirm: null }), effect: null };
|
return { next: cloneState(state, { pendingConfirm: null }), effect: null };
|
||||||
|
|
@ -498,19 +499,13 @@ export function reducer(state: PickerState, cmd: PickerCommand, now = Date.now()
|
||||||
};
|
};
|
||||||
case 'save-request':
|
case 'save-request':
|
||||||
if (state.checked.size === 0) {
|
if (state.checked.size === 0) {
|
||||||
return {
|
return { next: cloneState(state, { pendingConfirm: 'skip-empty' }), effect: null };
|
||||||
next: cloneState(state, {
|
|
||||||
transientHint: transientHint('Select at least one page or press q to quit', now),
|
|
||||||
}),
|
|
||||||
effect: null,
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
if (state.currentCrawlMode === 'all_accessible') {
|
if (state.currentCrawlMode === 'all_accessible') {
|
||||||
return { next: cloneState(state, { pendingConfirm: 'mode-switch' }), effect: null };
|
return { next: cloneState(state, { pendingConfirm: 'mode-switch' }), effect: null };
|
||||||
}
|
}
|
||||||
return { next: state, effect: 'save' };
|
return { next: state, effect: 'save' };
|
||||||
case 'save-confirm':
|
case 'save-confirm':
|
||||||
return { next: state, effect: 'save' };
|
|
||||||
case 'save-cancel':
|
case 'save-cancel':
|
||||||
return { next: state, effect: null };
|
return { next: state, effect: null };
|
||||||
case 'quit':
|
case 'quit':
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
/* @jsxImportSource react */
|
/* @jsxImportSource react */
|
||||||
import { render as renderInkTest } from 'ink-testing-library';
|
import { render as renderInkTest } from 'ink-testing-library';
|
||||||
import { act, type ReactNode } from 'react';
|
import { type ReactNode } from 'react';
|
||||||
import { afterEach, describe, expect, it, vi } from 'vitest';
|
import { describe, expect, it, vi } from 'vitest';
|
||||||
import { buildInitialState, buildPickerTree, type NotionPickerPageInput } from './notion-page-picker-tree.js';
|
import { buildInitialState, buildPickerTree, type NotionPickerPageInput } from './notion-page-picker-tree.js';
|
||||||
import {
|
import {
|
||||||
NotionPickerApp,
|
NotionPickerApp,
|
||||||
|
|
@ -70,13 +70,9 @@ function fakeInkInstance(): NotionPickerInkInstance {
|
||||||
}
|
}
|
||||||
|
|
||||||
function normalizeFrameWrap(frame: string | undefined): string {
|
function normalizeFrameWrap(frame: string | undefined): string {
|
||||||
return frame?.replace(/\n/g, ' ') ?? '';
|
return frame?.replace(/\n/g, ' ').replace(/│ /g, '').replace(/ +/g, ' ') ?? '';
|
||||||
}
|
}
|
||||||
|
|
||||||
afterEach(() => {
|
|
||||||
vi.useRealTimers();
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('notionPickerCommandForInkInput', () => {
|
describe('notionPickerCommandForInkInput', () => {
|
||||||
it('maps browse, search, and confirm input to reducer commands', () => {
|
it('maps browse, search, and confirm input to reducer commands', () => {
|
||||||
expect(notionPickerCommandForInkInput('', { downArrow: true }, state().search, null)).toBe('cursor-down');
|
expect(notionPickerCommandForInkInput('', { downArrow: true }, state().search, null)).toBe('cursor-down');
|
||||||
|
|
@ -87,9 +83,11 @@ describe('notionPickerCommandForInkInput', () => {
|
||||||
expect(notionPickerCommandForInkInput('/', {}, state().search, null)).toBe('search-start');
|
expect(notionPickerCommandForInkInput('/', {}, state().search, null)).toBe('search-start');
|
||||||
expect(notionPickerCommandForInkInput('a', {}, state().search, null)).toBe('select-all-visible');
|
expect(notionPickerCommandForInkInput('a', {}, state().search, null)).toBe('select-all-visible');
|
||||||
expect(notionPickerCommandForInkInput('n', {}, state().search, null)).toBe('select-none');
|
expect(notionPickerCommandForInkInput('n', {}, state().search, null)).toBe('select-none');
|
||||||
expect(notionPickerCommandForInkInput('s', {}, state().search, null)).toBe('save-request');
|
expect(notionPickerCommandForInkInput('', { return: true }, state().search, null)).toBe('save-request');
|
||||||
expect(notionPickerCommandForInkInput('q', {}, state().search, null)).toBe('quit');
|
expect(notionPickerCommandForInkInput('', { escape: true }, state().search, null)).toBe('quit');
|
||||||
expect(notionPickerCommandForInkInput('c', { ctrl: true }, state().search, null)).toBe('quit');
|
expect(notionPickerCommandForInkInput('c', { ctrl: true }, state().search, null)).toBe('quit');
|
||||||
|
expect(notionPickerCommandForInkInput('s', {}, state().search, null)).toBeNull();
|
||||||
|
expect(notionPickerCommandForInkInput('q', {}, state().search, null)).toBeNull();
|
||||||
|
|
||||||
expect(notionPickerCommandForInkInput('x', {}, { editing: true, query: '' }, null)).toEqual({
|
expect(notionPickerCommandForInkInput('x', {}, { editing: true, query: '' }, null)).toEqual({
|
||||||
type: 'search-input',
|
type: 'search-input',
|
||||||
|
|
@ -145,13 +143,16 @@ describe('NotionPickerApp', () => {
|
||||||
);
|
);
|
||||||
|
|
||||||
const frame = lastFrame() ?? '';
|
const frame = lastFrame() ?? '';
|
||||||
expect(frame).toContain('Notion pages visible to integration "Design Workspace"');
|
expect(frame).toContain('Select Notion pages to ingest');
|
||||||
|
expect(frame).toContain('Workspace: Design Workspace');
|
||||||
expect(frame).toContain('5000-page cap reached - some pages not shown');
|
expect(frame).toContain('5000-page cap reached - some pages not shown');
|
||||||
expect(frame).toContain('1 stored root_page_ids no longer visible - they will be removed if you save');
|
expect(frame).toContain('1 stored root_page_ids no longer visible - they will be removed if you save');
|
||||||
expect(frame).toContain('▸ [ ] Engineering Docs ▸ (1)');
|
expect(frame).toContain('◻ Engineering Docs ▸ (1)');
|
||||||
expect(frame).toContain(' [ ] Marketing');
|
expect(frame).toContain('◻ Marketing');
|
||||||
expect(frame).not.toContain('Search ready: -');
|
expect(frame).not.toContain('Search ready: -');
|
||||||
expect(frame).toContain('space toggle · enter expand · / search · a all · n none · s save & exit · q quit');
|
expect(normalizeFrameWrap(frame)).toContain(
|
||||||
|
'Right Arrow to expand, Up/Down to move, Space to select or unselect, Slash to filter, Enter to confirm, Escape to go back, or Ctrl+C to exit.',
|
||||||
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('renders partial discovery warnings without stale-root save suffix', () => {
|
it('renders partial discovery warnings without stale-root save suffix', () => {
|
||||||
|
|
@ -199,8 +200,8 @@ describe('NotionPickerApp', () => {
|
||||||
);
|
);
|
||||||
|
|
||||||
const frame = lastFrame() ?? '';
|
const frame = lastFrame() ?? '';
|
||||||
expect(frame).toContain('▸ [×] Engineering Docs ▾');
|
expect(frame).toContain('◼ Engineering Docs ▾');
|
||||||
expect(frame).toContain(' [~] Architecture');
|
expect(frame).toContain(' ◼ Architecture');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('supports keyboard selection, all_accessible confirmation, and save callback', async () => {
|
it('supports keyboard selection, all_accessible confirmation, and save callback', async () => {
|
||||||
|
|
@ -220,12 +221,12 @@ describe('NotionPickerApp', () => {
|
||||||
|
|
||||||
stdin.write(' ');
|
stdin.write(' ');
|
||||||
await waitForInkInput();
|
await waitForInkInput();
|
||||||
expect(lastFrame()).toContain('[×] Engineering Docs');
|
expect(lastFrame()).toContain('◼ Engineering Docs');
|
||||||
|
|
||||||
stdin.write('s');
|
stdin.write('\r');
|
||||||
await waitForInkInput();
|
await waitForInkInput();
|
||||||
expect(normalizeFrameWrap(lastFrame())).toContain(
|
expect(normalizeFrameWrap(lastFrame())).toContain(
|
||||||
'Save will switch crawl_mode all_accessible -> selected_roots and limit ingest to 1 selected page. [y] confirm [esc] back',
|
'Switch crawl_mode from all_accessible to selected_roots? Will limit ingest to 1 selected page. Press Enter to confirm or Escape to go back.',
|
||||||
);
|
);
|
||||||
|
|
||||||
stdin.write('y');
|
stdin.write('y');
|
||||||
|
|
@ -233,8 +234,7 @@ describe('NotionPickerApp', () => {
|
||||||
expect(onExit).toHaveBeenCalledWith({ kind: 'save', rootPageIds: [IDS.engineering] });
|
expect(onExit).toHaveBeenCalledWith({ kind: 'save', rootPageIds: [IDS.engineering] });
|
||||||
});
|
});
|
||||||
|
|
||||||
it('removes transient hints after their expiry time', async () => {
|
it('prompts skip-empty confirmation on empty submit and dismisses on cancel', async () => {
|
||||||
vi.useFakeTimers();
|
|
||||||
const onExit = vi.fn();
|
const onExit = vi.fn();
|
||||||
const { stdin, lastFrame } = renderInkTest(
|
const { stdin, lastFrame } = renderInkTest(
|
||||||
<NotionPickerApp
|
<NotionPickerApp
|
||||||
|
|
@ -249,17 +249,25 @@ describe('NotionPickerApp', () => {
|
||||||
/>,
|
/>,
|
||||||
);
|
);
|
||||||
|
|
||||||
await act(async () => {
|
stdin.write('\r');
|
||||||
stdin.write('s');
|
await waitForInkInput();
|
||||||
await vi.advanceTimersByTimeAsync(10);
|
expect(normalizeFrameWrap(lastFrame())).toContain(
|
||||||
});
|
'Nothing selected. Skip this step? Press Enter to skip or Escape to go back.',
|
||||||
expect(lastFrame()).toContain('Select at least one page or press q to quit');
|
);
|
||||||
|
|
||||||
await act(async () => {
|
|
||||||
await vi.advanceTimersByTimeAsync(2500);
|
|
||||||
});
|
|
||||||
expect(lastFrame()).not.toContain('Select at least one page or press q to quit');
|
|
||||||
expect(onExit).not.toHaveBeenCalled();
|
expect(onExit).not.toHaveBeenCalled();
|
||||||
|
|
||||||
|
stdin.write('n');
|
||||||
|
await waitForInkInput();
|
||||||
|
expect(lastFrame()).not.toContain('Nothing selected. Skip this step?');
|
||||||
|
expect(onExit).not.toHaveBeenCalled();
|
||||||
|
|
||||||
|
stdin.write('\r');
|
||||||
|
await waitForInkInput();
|
||||||
|
expect(lastFrame()).toContain('Nothing selected. Skip this step?');
|
||||||
|
|
||||||
|
stdin.write('\r');
|
||||||
|
await waitForInkInput();
|
||||||
|
expect(onExit).toHaveBeenCalledWith({ kind: 'quit' });
|
||||||
});
|
});
|
||||||
|
|
||||||
it('renders row-window overflow indicators when the visible list is clipped', async () => {
|
it('renders row-window overflow indicators when the visible list is clipped', async () => {
|
||||||
|
|
@ -312,7 +320,7 @@ describe('NotionPickerApp', () => {
|
||||||
/>,
|
/>,
|
||||||
);
|
);
|
||||||
|
|
||||||
stdin.write('q');
|
stdin.write('\u0003');
|
||||||
await waitForInkInput();
|
await waitForInkInput();
|
||||||
expect(onExit).toHaveBeenCalledWith({ kind: 'quit' });
|
expect(onExit).toHaveBeenCalledWith({ kind: 'quit' });
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -16,6 +16,7 @@ const COLOR_THEME = {
|
||||||
text: 'white',
|
text: 'white',
|
||||||
muted: 'gray',
|
muted: 'gray',
|
||||||
active: 'cyan',
|
active: 'cyan',
|
||||||
|
selected: 'green',
|
||||||
warning: 'yellow',
|
warning: 'yellow',
|
||||||
} as const;
|
} as const;
|
||||||
|
|
||||||
|
|
@ -23,6 +24,7 @@ const NO_COLOR_THEME = {
|
||||||
text: 'white',
|
text: 'white',
|
||||||
muted: 'white',
|
muted: 'white',
|
||||||
active: 'white',
|
active: 'white',
|
||||||
|
selected: 'white',
|
||||||
warning: 'white',
|
warning: 'white',
|
||||||
} as const;
|
} as const;
|
||||||
|
|
||||||
|
|
@ -158,13 +160,12 @@ export function notionPickerCommandForInkInput(
|
||||||
if (key.downArrow) return 'cursor-down';
|
if (key.downArrow) return 'cursor-down';
|
||||||
if (key.leftArrow) return 'cursor-left';
|
if (key.leftArrow) return 'cursor-left';
|
||||||
if (key.rightArrow) return 'cursor-right';
|
if (key.rightArrow) return 'cursor-right';
|
||||||
if (key.return) return 'expand';
|
if (key.return) return 'save-request';
|
||||||
if (input === ' ') return 'toggle-check';
|
if (input === ' ') return 'toggle-check';
|
||||||
if (input === '/') return 'search-start';
|
if (input === '/') return 'search-start';
|
||||||
if (input === 'a') return 'select-all-visible';
|
if (input === 'a') return 'select-all-visible';
|
||||||
if (input === 'n') return 'select-none';
|
if (input === 'n') return 'select-none';
|
||||||
if (input === 's') return 'save-request';
|
if (key.escape) return 'quit';
|
||||||
if (input === 'q' || key.escape) return 'quit';
|
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -174,18 +175,27 @@ function PickerRow(props: { state: PickerState; nodeId: string; width: number; t
|
||||||
const focused = props.state.cursorId === node.id;
|
const focused = props.state.cursorId === node.id;
|
||||||
const locked = isAncestorChecked(node.id, props.state.checked, props.state.byId);
|
const locked = isAncestorChecked(node.id, props.state.checked, props.state.byId);
|
||||||
const checked = props.state.checked.has(node.id);
|
const checked = props.state.checked.has(node.id);
|
||||||
const glyph = locked ? '[~]' : checked ? '[×]' : '[ ]';
|
const isSelected = checked || locked;
|
||||||
const children =
|
const glyph = isSelected ? '◼' : '◻';
|
||||||
|
const glyphColor = locked ? props.theme.muted : checked ? props.theme.selected : props.theme.muted;
|
||||||
|
const childAffordance =
|
||||||
node.childIds.length > 0 ? (props.state.expanded.has(node.id) ? ' ▾' : ` ▸ (${node.childIds.length})`) : '';
|
node.childIds.length > 0 ? (props.state.expanded.has(node.id) ? ' ▾' : ` ▸ (${node.childIds.length})`) : '';
|
||||||
const prefix = `${focused ? '▸' : ' '} ${glyph} ${' '.repeat(node.depth * 2)}`;
|
const indent = ' '.repeat(node.depth * 2);
|
||||||
const color = focused ? props.theme.active : locked || node.archived ? props.theme.muted : props.theme.text;
|
const titleColor = focused ? props.theme.text : props.theme.muted;
|
||||||
const title = truncateText(`${node.title}${children}`, Math.max(10, props.width - prefix.length));
|
|
||||||
const inverse = rowMatchesSearch(props.state, node.id);
|
const inverse = rowMatchesSearch(props.state, node.id);
|
||||||
|
const prefixWidth = indent.length + 2;
|
||||||
|
const title = truncateText(`${node.title}${childAffordance}`, Math.max(10, props.width - prefixWidth));
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Text color={color} strikethrough={node.archived}>
|
<Text>
|
||||||
{prefix}
|
<Text color={glyphColor}>
|
||||||
<Text inverse={inverse}>{title}</Text>
|
{indent}
|
||||||
|
{glyph}
|
||||||
|
</Text>
|
||||||
|
<Text color={titleColor} strikethrough={node.archived}>
|
||||||
|
{' '}
|
||||||
|
<Text inverse={inverse}>{title}</Text>
|
||||||
|
</Text>
|
||||||
</Text>
|
</Text>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
@ -198,7 +208,7 @@ export function NotionPickerApp(props: NotionPickerAppProps): ReactNode {
|
||||||
const visibleIds = visibleNodeIds(state);
|
const visibleIds = visibleNodeIds(state);
|
||||||
const selectedIndex = Math.max(0, visibleIds.indexOf(state.cursorId));
|
const selectedIndex = Math.max(0, visibleIds.indexOf(state.cursorId));
|
||||||
const reservedRows = state.pendingConfirm === 'mode-switch' ? 9 : 8;
|
const reservedRows = state.pendingConfirm === 'mode-switch' ? 9 : 8;
|
||||||
const visibleRows = Math.max(5, Math.min(20, (props.terminalRows ?? 24) - reservedRows));
|
const visibleRows = Math.max(5, Math.min(12, (props.terminalRows ?? 24) - reservedRows));
|
||||||
const rows = windowItems(visibleIds, selectedIndex, visibleRows);
|
const rows = windowItems(visibleIds, selectedIndex, visibleRows);
|
||||||
const hiddenAbove = rows.offset;
|
const hiddenAbove = rows.offset;
|
||||||
const hiddenBelow = Math.max(0, visibleIds.length - rows.offset - rows.items.length);
|
const hiddenBelow = Math.max(0, visibleIds.length - rows.offset - rows.items.length);
|
||||||
|
|
@ -254,34 +264,60 @@ export function NotionPickerApp(props: NotionPickerAppProps): ReactNode {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Box flexDirection="column">
|
<Box flexDirection="column">
|
||||||
<Text color={theme.active}>Notion pages visible to integration "{props.workspaceLabel}"</Text>
|
<Text>
|
||||||
{props.cappedAtCount ? <Text color={theme.warning}>{props.cappedAtCount}-page cap reached - some pages not shown</Text> : null}
|
<Text color={theme.active}>◆</Text>
|
||||||
{state.preLoadWarnings.map((warning) => (
|
<Text bold> Select Notion pages to ingest</Text>
|
||||||
<Text key={warning} color={theme.warning}>
|
</Text>
|
||||||
{staleWarningText(warning)}
|
<Box
|
||||||
</Text>
|
flexDirection="column"
|
||||||
))}
|
borderStyle="single"
|
||||||
{showSearch ? (
|
borderTop={false}
|
||||||
|
borderRight={false}
|
||||||
|
borderBottom={false}
|
||||||
|
borderColor={theme.active}
|
||||||
|
paddingLeft={1}
|
||||||
|
>
|
||||||
<Text color={theme.muted}>
|
<Text color={theme.muted}>
|
||||||
/ {state.search.query}
|
Right Arrow to expand, Up/Down to move, Space to select or unselect, Slash to filter, Enter to confirm, Escape
|
||||||
{state.search.editing ? '█' : ''} ({searchMatchCount} matches)
|
to go back, or Ctrl+C to exit.
|
||||||
</Text>
|
</Text>
|
||||||
) : null}
|
<Text> </Text>
|
||||||
<Box flexDirection="column">
|
<Text color={theme.muted}>Workspace: {props.workspaceLabel}</Text>
|
||||||
|
{props.cappedAtCount ? (
|
||||||
|
<Text color={theme.warning}>{props.cappedAtCount}-page cap reached - some pages not shown</Text>
|
||||||
|
) : null}
|
||||||
|
{state.preLoadWarnings.map((warning) => (
|
||||||
|
<Text key={warning} color={theme.warning}>
|
||||||
|
{staleWarningText(warning)}
|
||||||
|
</Text>
|
||||||
|
))}
|
||||||
|
{showSearch ? (
|
||||||
|
<Text>
|
||||||
|
<Text color={theme.muted}>/ </Text>
|
||||||
|
<Text>
|
||||||
|
{state.search.query}
|
||||||
|
{state.search.editing ? '█' : ''}
|
||||||
|
</Text>
|
||||||
|
<Text color={theme.muted}> ({searchMatchCount} matches)</Text>
|
||||||
|
</Text>
|
||||||
|
) : null}
|
||||||
{hiddenAbove > 0 ? <Text color={theme.muted}>↑ {hiddenAbove} more</Text> : null}
|
{hiddenAbove > 0 ? <Text color={theme.muted}>↑ {hiddenAbove} more</Text> : null}
|
||||||
{rows.items.map((nodeId) => (
|
{rows.items.map((nodeId) => (
|
||||||
<PickerRow key={nodeId} state={state} nodeId={nodeId} width={width} theme={theme} />
|
<PickerRow key={nodeId} state={state} nodeId={nodeId} width={width} theme={theme} />
|
||||||
))}
|
))}
|
||||||
{hiddenBelow > 0 ? <Text color={theme.muted}>↓ {hiddenBelow} more</Text> : null}
|
{hiddenBelow > 0 ? <Text color={theme.muted}>↓ {hiddenBelow} more</Text> : null}
|
||||||
|
{state.pendingConfirm === 'mode-switch' ? (
|
||||||
|
<Text color={theme.warning}>
|
||||||
|
Switch crawl_mode from all_accessible to selected_roots? Will limit ingest to{' '}
|
||||||
|
{selectedPageCountText(selectedCount)}. Press Enter to confirm or Escape to go back.
|
||||||
|
</Text>
|
||||||
|
) : null}
|
||||||
|
{state.pendingConfirm === 'skip-empty' ? (
|
||||||
|
<Text color={theme.warning}>Nothing selected. Skip this step? Press Enter to skip or Escape to go back.</Text>
|
||||||
|
) : null}
|
||||||
|
{state.transientHint ? <Text color={theme.warning}>{state.transientHint.text}</Text> : null}
|
||||||
</Box>
|
</Box>
|
||||||
{state.pendingConfirm === 'mode-switch' ? (
|
<Text color={theme.active}>└</Text>
|
||||||
<Text color={theme.warning}>
|
|
||||||
Save will switch crawl_mode all_accessible -> selected_roots and limit ingest to{' '}
|
|
||||||
{selectedPageCountText(selectedCount)}. [y] confirm [esc] back
|
|
||||||
</Text>
|
|
||||||
) : null}
|
|
||||||
{state.transientHint ? <Text color={theme.warning}>{state.transientHint.text}</Text> : null}
|
|
||||||
<Text color={theme.muted}>space toggle · enter expand · / search · a all · n none · s save & exit · q quit</Text>
|
|
||||||
</Box>
|
</Box>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
@ -323,7 +359,7 @@ export async function renderNotionPickerTui(
|
||||||
exitOnCtrlC: false,
|
exitOnCtrlC: false,
|
||||||
patchConsole: false,
|
patchConsole: false,
|
||||||
maxFps: 30,
|
maxFps: 30,
|
||||||
alternateScreen: true,
|
alternateScreen: false,
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
await instance.waitUntilExit();
|
await instance.waitUntilExit();
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue