praxis-agent 0.43.0 → 0.43.1
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/README.md +24 -37
- package/dist/application/native-session-projection.d.ts +4 -0
- package/dist/application/native-session-projection.js +135 -0
- package/dist/application/session-service.d.ts +0 -1
- package/dist/application/session-service.js +9 -170
- package/dist/cli/interactive.js +112 -156
- package/dist/cli/tui/ansi-frame-renderer.d.ts +2 -0
- package/dist/cli/tui/ansi-frame-renderer.js +109 -21
- package/dist/cli/tui/ansi-surface.d.ts +6 -22
- package/dist/cli/tui/ansi-surface.js +6 -126
- package/dist/cli/tui/ansi-theme.d.ts +3 -2
- package/dist/cli/tui/ansi-theme.js +22 -1
- package/dist/cli/tui/quiet-choice-rows.d.ts +26 -0
- package/dist/cli/tui/quiet-choice-rows.js +302 -0
- package/dist/cli/tui/quiet-frame-adapter.d.ts +18 -0
- package/dist/cli/tui/quiet-frame-adapter.js +90 -0
- package/dist/cli/tui/quiet-frame.d.ts +43 -0
- package/dist/cli/tui/quiet-frame.js +346 -0
- package/dist/cli/tui/quiet-operational-rows.d.ts +17 -0
- package/dist/cli/tui/quiet-operational-rows.js +413 -0
- package/dist/cli/tui/quiet-screen-projector.d.ts +25 -0
- package/dist/cli/tui/quiet-screen-projector.js +110 -0
- package/dist/cli/tui/quiet-settings-rows.d.ts +17 -0
- package/dist/cli/tui/quiet-settings-rows.js +490 -0
- package/dist/cli/tui/status-line.d.ts +15 -9
- package/dist/cli/tui/status-line.js +15 -6
- package/dist/cli/tui/theme.js +20 -20
- package/dist/cli/tui/transcript-viewport.d.ts +5 -0
- package/dist/cli/tui/transcript-viewport.js +213 -78
- package/dist/cli/tui/tui-row-ir.js +4 -4
- package/dist/persistence/in-memory-transcript-store.js +1 -7
- package/dist/persistence/native-transcript-store.d.ts +0 -9
- package/dist/persistence/native-transcript-store.js +4 -38
- package/package.json +3 -2
|
@@ -1,26 +1,10 @@
|
|
|
1
1
|
import { type AnsiFrame } from './ansi-frame-renderer.js';
|
|
2
|
-
import type {
|
|
3
|
-
export interface
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
rows: number | undefined;
|
|
7
|
-
input: string;
|
|
8
|
-
busy: boolean;
|
|
9
|
-
status: string;
|
|
10
|
-
screenReader?: boolean;
|
|
11
|
-
display?: {
|
|
12
|
-
cwd: string;
|
|
13
|
-
model?: string;
|
|
14
|
-
effort?: string;
|
|
15
|
-
permissionMode?: string;
|
|
16
|
-
version?: string;
|
|
17
|
-
};
|
|
18
|
-
sessionLabel?: string;
|
|
19
|
-
cursor?: number;
|
|
20
|
-
onError: (error: unknown) => void;
|
|
2
|
+
import type { QuietFrame } from './quiet-frame.js';
|
|
3
|
+
export interface TuiAnsiSurfaceFrameProps {
|
|
4
|
+
readonly frame: QuietFrame;
|
|
5
|
+
readonly onError: (error: unknown) => void;
|
|
21
6
|
}
|
|
22
|
-
|
|
23
|
-
export declare function
|
|
24
|
-
export declare function projectAnsiSurfaceFrame(props: TuiAnsiSurfaceProps): AnsiFrame;
|
|
7
|
+
export type TuiAnsiSurfaceProps = TuiAnsiSurfaceFrameProps;
|
|
8
|
+
export declare function projectAnsiQuietFrame(frame: QuietFrame): AnsiFrame;
|
|
25
9
|
export declare function TuiAnsiSurface(props: TuiAnsiSurfaceProps): null;
|
|
26
10
|
//# sourceMappingURL=ansi-surface.d.ts.map
|
|
@@ -3,132 +3,12 @@ import { useStdout } from 'ink';
|
|
|
3
3
|
import { AnsiFullscreenRenderer, } from './ansi-frame-renderer.js';
|
|
4
4
|
import { resolveAnsiTextStyles } from './ansi-theme.js';
|
|
5
5
|
import { useTuiTheme } from './theme.js';
|
|
6
|
-
|
|
7
|
-
// every emitted row is still guaranteed to contain no line breaks.
|
|
8
|
-
function isUnsafeControl(character) {
|
|
9
|
-
const codePoint = character.codePointAt(0) ?? 0;
|
|
10
|
-
return ((codePoint >= 0 && codePoint <= 9) ||
|
|
11
|
-
codePoint === 11 ||
|
|
12
|
-
codePoint === 12 ||
|
|
13
|
-
(codePoint >= 14 && codePoint <= 31) ||
|
|
14
|
-
codePoint === 127);
|
|
15
|
-
}
|
|
16
|
-
function safeText(value) {
|
|
17
|
-
return typeof value === 'string'
|
|
18
|
-
? Array.from(value)
|
|
19
|
-
.filter((character) => !isUnsafeControl(character))
|
|
20
|
-
.join('')
|
|
21
|
-
: '';
|
|
22
|
-
}
|
|
23
|
-
function row(key, text, role = 'body') {
|
|
24
|
-
return {
|
|
25
|
-
key,
|
|
26
|
-
segments: [{ text: safeText(text) || ' ', role }],
|
|
27
|
-
height: 1,
|
|
28
|
-
source: key,
|
|
29
|
-
};
|
|
30
|
-
}
|
|
31
|
-
function activeRows(key, text, role = 'body') {
|
|
32
|
-
return safeText(text)
|
|
33
|
-
.replace(/\r\n?/gu, '\n')
|
|
34
|
-
.split('\n')
|
|
35
|
-
.map((line, index) => row(`${key}:${index}`, line, role));
|
|
36
|
-
}
|
|
37
|
-
function headerRows(props) {
|
|
38
|
-
const display = props.display;
|
|
39
|
-
const identity = `Praxis${display?.version ? ` ${safeText(display.version)}` : ''}${props.sessionLabel ? ` · session ${safeText(props.sessionLabel)}` : ''}`;
|
|
40
|
-
const metadata = [
|
|
41
|
-
display?.cwd ? `cwd ${safeText(display.cwd).slice(0, 80)}` : '',
|
|
42
|
-
display?.model ? `model ${safeText(display.model).slice(0, 40)}` : '',
|
|
43
|
-
display?.effort ? `effort ${safeText(display.effort).slice(0, 20)}` : '',
|
|
44
|
-
display?.permissionMode
|
|
45
|
-
? `permissions ${safeText(display.permissionMode).slice(0, 30)}`
|
|
46
|
-
: '',
|
|
47
|
-
]
|
|
48
|
-
.filter(Boolean)
|
|
49
|
-
.join(' · ');
|
|
50
|
-
return [
|
|
51
|
-
row('ansi:header:identity', identity, 'heading'),
|
|
52
|
-
row('ansi:header:context', metadata || 'Ready', 'muted'),
|
|
53
|
-
];
|
|
54
|
-
}
|
|
55
|
-
function summarize(value, depth = 0) {
|
|
56
|
-
if (depth > 1 || value === null || typeof value !== 'object')
|
|
57
|
-
return [];
|
|
58
|
-
const result = [];
|
|
59
|
-
for (const [key, item] of Object.entries(value)) {
|
|
60
|
-
if (typeof item === 'string' && item.length > 0) {
|
|
61
|
-
result.push(`${key}: ${safeText(item).slice(0, 120)}`);
|
|
62
|
-
}
|
|
63
|
-
else if (Array.isArray(item)) {
|
|
64
|
-
const strings = item.filter((entry) => typeof entry === 'string');
|
|
65
|
-
if (strings.length > 0)
|
|
66
|
-
result.push(`${key}: ${strings.slice(0, 4).map(safeText).join(', ')}`);
|
|
67
|
-
}
|
|
68
|
-
else if (typeof item === 'object' && item !== null) {
|
|
69
|
-
result.push(...summarize(item, depth + 1).slice(0, 4));
|
|
70
|
-
}
|
|
71
|
-
if (result.length >= 8)
|
|
72
|
-
break;
|
|
73
|
-
}
|
|
74
|
-
return result.slice(0, 8);
|
|
75
|
-
}
|
|
76
|
-
function surfaceRows(screen) {
|
|
77
|
-
const body = screen.body;
|
|
78
|
-
if (body.kind === 'session-picker') {
|
|
79
|
-
return [
|
|
80
|
-
row('ansi:session-picker', 'Sessions', 'heading'),
|
|
81
|
-
...summarize(body.surface).map((text, index) => row(`ansi:session:${index}`, text, 'muted')),
|
|
82
|
-
];
|
|
83
|
-
}
|
|
84
|
-
if (body.foreground.kind !== 'compose') {
|
|
85
|
-
const surface = body.foreground.surface;
|
|
86
|
-
return [
|
|
87
|
-
row(`ansi:foreground:${body.foreground.kind}`, body.foreground.kind, 'heading'),
|
|
88
|
-
...summarize(surface).map((text, index) => row(`ansi:foreground:${index}`, text, 'muted')),
|
|
89
|
-
];
|
|
90
|
-
}
|
|
91
|
-
return [];
|
|
92
|
-
}
|
|
93
|
-
/** ANSI mode is intentionally limited to the plain conversation surface. */
|
|
94
|
-
export function supportsAnsiSurface(screen) {
|
|
95
|
-
if (screen.body.kind !== 'conversation')
|
|
96
|
-
return false;
|
|
97
|
-
return (screen.body.intro === 'none' &&
|
|
98
|
-
screen.body.foreground.kind === 'compose' &&
|
|
99
|
-
screen.body.foreground.overlays.length === 0);
|
|
100
|
-
}
|
|
101
|
-
export function projectAnsiSurfaceFrame(props) {
|
|
102
|
-
const body = props.screen.body;
|
|
103
|
-
const lines = [...surfaceRows(props.screen)];
|
|
104
|
-
const headers = body.kind === 'conversation' ? headerRows(props) : [];
|
|
105
|
-
if (body.kind === 'conversation') {
|
|
106
|
-
lines.push(...body.transcript.rows);
|
|
107
|
-
if (body.transcript.active.visible) {
|
|
108
|
-
if (safeText(body.transcript.active.text))
|
|
109
|
-
lines.push(...activeRows('ansi:active:text', body.transcript.active.text));
|
|
110
|
-
if (safeText(body.transcript.active.thinking))
|
|
111
|
-
lines.push(...activeRows('ansi:active:thinking', body.transcript.active.thinking, 'muted'));
|
|
112
|
-
}
|
|
113
|
-
}
|
|
114
|
-
const cursor = Math.min(Math.max(0, props.cursor ?? props.input.length), props.input.length);
|
|
115
|
-
lines.push(row('composer', `❯ ${props.input.slice(0, cursor)}▌${props.input.slice(cursor)}`, 'input'));
|
|
116
|
-
lines.push(row('status', `● ${props.status}${props.busy ? ' · busy' : ''}`, 'muted'));
|
|
117
|
-
const limit = props.rows === undefined ? lines.length : Math.max(2, props.rows);
|
|
118
|
-
const chrome = lines.slice(-2);
|
|
119
|
-
const content = [...headers, ...lines.slice(0, -2)];
|
|
120
|
-
const headerCount = Math.min(headers.length, Math.max(0, limit - 2));
|
|
121
|
-
const visibleHeaders = headers.slice(0, headerCount);
|
|
122
|
-
const bodyRows = content.slice(headers.length);
|
|
123
|
-
const contentBudget = Math.max(0, limit - headerCount - 2);
|
|
6
|
+
export function projectAnsiQuietFrame(frame) {
|
|
124
7
|
return {
|
|
125
|
-
columns:
|
|
126
|
-
rows:
|
|
127
|
-
lines:
|
|
128
|
-
|
|
129
|
-
...bodyRows.slice(-contentBudget),
|
|
130
|
-
...chrome,
|
|
131
|
-
].slice(-limit),
|
|
8
|
+
columns: frame.columns,
|
|
9
|
+
rows: frame.rows,
|
|
10
|
+
lines: frame.lines,
|
|
11
|
+
...(frame.cursor === undefined ? {} : { cursor: frame.cursor }),
|
|
132
12
|
};
|
|
133
13
|
}
|
|
134
14
|
export function TuiAnsiSurface(props) {
|
|
@@ -175,7 +55,7 @@ export function TuiAnsiSurface(props) {
|
|
|
175
55
|
if (renderer === null)
|
|
176
56
|
return;
|
|
177
57
|
try {
|
|
178
|
-
renderer.draw(
|
|
58
|
+
renderer.draw(projectAnsiQuietFrame(props.frame));
|
|
179
59
|
}
|
|
180
60
|
catch (error) {
|
|
181
61
|
failedRef.current = true;
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
-
import type { TuiSemanticTheme
|
|
2
|
-
|
|
1
|
+
import type { TuiSemanticTheme } from './theme.js';
|
|
2
|
+
import type { TuiTextRole as RowIrTextRole } from './tui-row-ir.js';
|
|
3
|
+
export declare function resolveAnsiTextStyles(theme: Pick<TuiSemanticTheme, 'text' | 'noColor' | 'screenReader'>): Partial<Record<RowIrTextRole | 'link', string>>;
|
|
3
4
|
//# sourceMappingURL=ansi-theme.d.ts.map
|
|
@@ -73,11 +73,32 @@ export function resolveAnsiTextStyles(theme) {
|
|
|
73
73
|
if (theme.noColor || theme.screenReader)
|
|
74
74
|
return {};
|
|
75
75
|
const styles = {};
|
|
76
|
-
|
|
76
|
+
const roleMap = {
|
|
77
|
+
body: 'body',
|
|
78
|
+
heading: 'heading',
|
|
79
|
+
muted: 'muted',
|
|
80
|
+
accent: 'focusMarker',
|
|
81
|
+
success: 'success',
|
|
82
|
+
warning: 'warning',
|
|
83
|
+
error: 'error',
|
|
84
|
+
tool: 'info',
|
|
85
|
+
selection: 'focusMarker',
|
|
86
|
+
input: 'inputMarker',
|
|
87
|
+
diffAdded: 'diffAdded',
|
|
88
|
+
diffRemoved: 'diffRemoved',
|
|
89
|
+
};
|
|
90
|
+
for (const [role, semanticRole] of Object.entries(roleMap)) {
|
|
91
|
+
const style = theme.text[semanticRole] ??
|
|
92
|
+
theme.text[role];
|
|
93
|
+
if (style === undefined)
|
|
94
|
+
continue;
|
|
77
95
|
const sequence = styleSequence(style);
|
|
78
96
|
if (sequence !== undefined)
|
|
79
97
|
styles[role] = sequence;
|
|
80
98
|
}
|
|
99
|
+
const link = theme.text.link === undefined ? undefined : styleSequence(theme.text.link);
|
|
100
|
+
if (link !== undefined)
|
|
101
|
+
styles.link = link;
|
|
81
102
|
return styles;
|
|
82
103
|
}
|
|
83
104
|
//# sourceMappingURL=ansi-theme.js.map
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { type QuietFrameDensity, type QuietFrameRow } from './quiet-frame.js';
|
|
2
|
+
import type { TuiCommandPaletteModel } from './command-palette-model.js';
|
|
3
|
+
import type { TuiDecisionSurfaceModel } from './decision-surface-model.js';
|
|
4
|
+
import type { TuiElicitationSurfaceModel } from './mcp-elicitation-surface-model.js';
|
|
5
|
+
import type { TuiMentionPickerModel } from './mention-picker-model.js';
|
|
6
|
+
import type { TuiPermissionSurfaceModel } from './permission-surface-model.js';
|
|
7
|
+
import type { TuiSessionPickerModel } from './session-picker-model.js';
|
|
8
|
+
export type QuietPrioritySurface = {
|
|
9
|
+
readonly kind: 'editor-wait';
|
|
10
|
+
} | {
|
|
11
|
+
readonly kind: 'permission';
|
|
12
|
+
readonly surface: TuiPermissionSurfaceModel;
|
|
13
|
+
} | TuiDecisionSurfaceModel | {
|
|
14
|
+
readonly kind: 'elicitation';
|
|
15
|
+
readonly surface: TuiElicitationSurfaceModel;
|
|
16
|
+
};
|
|
17
|
+
export type QuietOverlaySurface = TuiCommandPaletteModel | TuiMentionPickerModel | {
|
|
18
|
+
readonly kind: 'exit-confirmation';
|
|
19
|
+
};
|
|
20
|
+
export type QuietChoiceSurface = QuietPrioritySurface | QuietOverlaySurface | TuiSessionPickerModel | TuiPermissionSurfaceModel;
|
|
21
|
+
export interface QuietChoiceProjectionOptions {
|
|
22
|
+
readonly density: QuietFrameDensity;
|
|
23
|
+
readonly screenReader?: boolean;
|
|
24
|
+
}
|
|
25
|
+
export declare function projectQuietChoiceRows(surface: QuietChoiceSurface, options: QuietChoiceProjectionOptions): readonly QuietFrameRow[];
|
|
26
|
+
//# sourceMappingURL=quiet-choice-rows.d.ts.map
|
|
@@ -0,0 +1,302 @@
|
|
|
1
|
+
import { createQuietFrameRow, } from './quiet-frame.js';
|
|
2
|
+
const excerpt = (value, max = 160) => typeof value === 'string'
|
|
3
|
+
? value.replace(/\s+/gu, ' ').trim().slice(0, max)
|
|
4
|
+
: '';
|
|
5
|
+
const detailAllowed = (density) => density === 'full' || density === 'standard';
|
|
6
|
+
const row = (key, text, role = 'body', sr) => createQuietFrameRow(key, text, 'focus', role, sr);
|
|
7
|
+
const footer = (screenReader, text = '↑/↓ select Enter confirm Esc cancel') => row('quiet:choice:footer', text, 'muted', screenReader
|
|
8
|
+
? 'Use up and down arrows to select. Press Enter to confirm. Press Escape to cancel.'
|
|
9
|
+
: undefined);
|
|
10
|
+
const explicitFooter = (key, visual, accessible) => row(key, visual, 'muted', accessible);
|
|
11
|
+
const marker = (selected, label, screenReader) => screenReader
|
|
12
|
+
? selected
|
|
13
|
+
? `Selected: ${label}`
|
|
14
|
+
: `Option: ${label}`
|
|
15
|
+
: `${selected ? '❯ ' : ' '}${label}`;
|
|
16
|
+
const excerptOption = (label, description, density) => detailAllowed(density) && description
|
|
17
|
+
? `${label} — ${excerpt(description)}`
|
|
18
|
+
: label;
|
|
19
|
+
const unsupported = (value) => {
|
|
20
|
+
throw new Error(`Unsupported quiet choice surface: ${String(value)}`);
|
|
21
|
+
};
|
|
22
|
+
function optionsRows(options, prefix, density, sr) {
|
|
23
|
+
return options.map((item, index) => row(`${prefix}:${item.id ?? item.index ?? index}`, marker(item.selected, excerptOption(item.label, item.description, density), sr), item.selected ? 'selection' : 'body', sr
|
|
24
|
+
? item.selected
|
|
25
|
+
? `Selected: ${item.label}`
|
|
26
|
+
: `Option: ${item.label}`
|
|
27
|
+
: undefined));
|
|
28
|
+
}
|
|
29
|
+
function permission(surface, density, sr) {
|
|
30
|
+
const out = [];
|
|
31
|
+
switch (surface.kind) {
|
|
32
|
+
case 'tool-request': {
|
|
33
|
+
out.push(row('quiet:permission:heading', excerpt(surface.tool.question || surface.heading), 'heading'));
|
|
34
|
+
if (detailAllowed(density)) {
|
|
35
|
+
for (const [key, value] of [
|
|
36
|
+
['subtitle', surface.tool.subtitle],
|
|
37
|
+
['explanation', surface.tool.explanation],
|
|
38
|
+
])
|
|
39
|
+
if (value)
|
|
40
|
+
out.push(row(`quiet:permission:${key}`, excerpt(value), 'muted'));
|
|
41
|
+
surface.tool.detail
|
|
42
|
+
.slice(0, 20)
|
|
43
|
+
.forEach((item, i) => out.push(row(`quiet:permission:detail:${i}`, `${item.prefix ?? ' '} ${excerpt(item.text)}`, item.prefix === '-'
|
|
44
|
+
? 'diffRemoved'
|
|
45
|
+
: item.prefix === '+'
|
|
46
|
+
? 'diffAdded'
|
|
47
|
+
: 'body')));
|
|
48
|
+
}
|
|
49
|
+
out.push(...optionsRows(surface.options.map((item, index) => ({
|
|
50
|
+
...item,
|
|
51
|
+
label: permissionLabel({
|
|
52
|
+
...item,
|
|
53
|
+
index: index + 1,
|
|
54
|
+
selected: index === surface.selectedIndex,
|
|
55
|
+
}),
|
|
56
|
+
selected: index === surface.selectedIndex,
|
|
57
|
+
})), 'quiet:permission:option', density, sr));
|
|
58
|
+
if (surface.feedbackMode)
|
|
59
|
+
out.push(row('quiet:permission:feedback', `> ${excerpt(surface.feedback) || 'Enter feedback…'}`, 'input'));
|
|
60
|
+
return [...out, footer(sr)];
|
|
61
|
+
}
|
|
62
|
+
case 'recovery-request':
|
|
63
|
+
out.push(row('quiet:recovery:heading', surface.heading, 'heading'), row('quiet:recovery:question', surface.question), row('quiet:recovery:display', excerpt(surface.display), 'muted'));
|
|
64
|
+
if (surface.feedbackMode)
|
|
65
|
+
out.push(row('quiet:recovery:feedback', `> ${excerpt(surface.feedback) || surface.feedbackPlaceholder}`, 'input'));
|
|
66
|
+
return [
|
|
67
|
+
...out,
|
|
68
|
+
...optionsRows(surface.options, 'quiet:recovery:option', density, sr),
|
|
69
|
+
footer(sr),
|
|
70
|
+
];
|
|
71
|
+
case 'permission-dashboard':
|
|
72
|
+
out.push(row('quiet:dashboard:heading', surface.heading, 'heading'), row('quiet:dashboard:tab', `Tab: ${surface.tabs.find((x) => x.current)?.label ?? 'Permissions'}`));
|
|
73
|
+
if (detailAllowed(density))
|
|
74
|
+
out.push(row('quiet:dashboard:description', excerpt(surface.description)));
|
|
75
|
+
if (surface.query)
|
|
76
|
+
out.push(row('quiet:dashboard:search', `Search: ${excerpt(surface.query)}`, 'input'));
|
|
77
|
+
if (surface.originalWorkspace)
|
|
78
|
+
out.push(row('quiet:dashboard:workspace:original', `Original workspace: ${surface.originalWorkspace.label}`, 'muted'));
|
|
79
|
+
out.push(...surface.rows
|
|
80
|
+
.slice(0, 20)
|
|
81
|
+
.map((item, i) => row(`quiet:dashboard:${item.id ?? item.index ?? i}`, marker(item.selected, `${item.label}${item.status === 'retrying' ? ' (retrying)' : item.status === 'denied' ? ' (denied)' : ''}`, sr), item.selected
|
|
82
|
+
? 'selection'
|
|
83
|
+
: item.status === 'denied'
|
|
84
|
+
? 'error'
|
|
85
|
+
: item.status === 'retrying'
|
|
86
|
+
? 'warning'
|
|
87
|
+
: 'body')));
|
|
88
|
+
if (surface.rows.length === 0 && surface.emptyState)
|
|
89
|
+
out.push(row('quiet:dashboard:empty', surface.emptyState, 'muted'));
|
|
90
|
+
return [
|
|
91
|
+
...out,
|
|
92
|
+
explicitFooter('quiet:dashboard:footer', surface.rows.length > 0
|
|
93
|
+
? '↑/↓ select Enter open Esc close'
|
|
94
|
+
: 'Esc close', surface.rows.length > 0
|
|
95
|
+
? 'Use left and right arrows to switch tabs. Use up and down arrows to select. Press Enter to open. Press Escape to close.'
|
|
96
|
+
: 'Use left and right arrows to switch tabs. Press Escape to close.'),
|
|
97
|
+
];
|
|
98
|
+
case 'permission-rule-input':
|
|
99
|
+
case 'workspace-directory-input':
|
|
100
|
+
out.push(row(`quiet:${surface.kind}:heading`, surface.heading, 'heading'), row(`quiet:${surface.kind}:description`, excerpt(surface.description)), row(`quiet:${surface.kind}:value`, `> ${excerpt(surface.value) || surface.placeholder}`, 'input'));
|
|
101
|
+
return [
|
|
102
|
+
...out,
|
|
103
|
+
explicitFooter(`quiet:${surface.kind}:footer`, 'Enter confirm Esc cancel', 'Press Enter to confirm. Press Escape to cancel.'),
|
|
104
|
+
];
|
|
105
|
+
case 'permission-scope':
|
|
106
|
+
out.push(row('quiet:scope:heading', surface.heading, 'heading'), row('quiet:scope:description', excerpt(surface.description)));
|
|
107
|
+
return [
|
|
108
|
+
...out,
|
|
109
|
+
...optionsRows(surface.options, 'quiet:scope:option', density, sr),
|
|
110
|
+
footer(sr),
|
|
111
|
+
];
|
|
112
|
+
case 'permission-delete':
|
|
113
|
+
out.push(row('quiet:delete:heading', surface.heading, 'heading'), row('quiet:delete:rule', `${surface.rule} · ${surface.scope}`), row('quiet:delete:question', surface.question, 'warning'));
|
|
114
|
+
if (surface.description)
|
|
115
|
+
out.push(row('quiet:delete:description', excerpt(surface.description), 'warning'));
|
|
116
|
+
return [
|
|
117
|
+
...out,
|
|
118
|
+
...optionsRows(surface.options, 'quiet:delete:option', density, sr).map((x, i) => i === 0
|
|
119
|
+
? {
|
|
120
|
+
...x,
|
|
121
|
+
segments: x.segments.map((s) => ({
|
|
122
|
+
...s,
|
|
123
|
+
role: 'error',
|
|
124
|
+
})),
|
|
125
|
+
}
|
|
126
|
+
: x),
|
|
127
|
+
footer(sr),
|
|
128
|
+
];
|
|
129
|
+
case 'workspace-directory-delete':
|
|
130
|
+
out.push(row('quiet:workspace-delete:heading', surface.heading, 'heading'), row('quiet:workspace-delete:path', surface.path), row('quiet:workspace-delete:description', excerpt(surface.description), 'warning'));
|
|
131
|
+
return [
|
|
132
|
+
...out,
|
|
133
|
+
...optionsRows(surface.options, 'quiet:workspace-delete:option', density, sr),
|
|
134
|
+
footer(sr),
|
|
135
|
+
];
|
|
136
|
+
default:
|
|
137
|
+
return unsupported(surface);
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
function permissionLabel(item) {
|
|
141
|
+
if (item.action === 'allow-once')
|
|
142
|
+
return 'Allow once';
|
|
143
|
+
if (item.action === 'deny')
|
|
144
|
+
return 'Deny';
|
|
145
|
+
const label = item.label.trim();
|
|
146
|
+
if (/^yes,\s*/iu.test(label)) {
|
|
147
|
+
const consequence = label.replace(/^yes,\s*/iu, '');
|
|
148
|
+
return /^allow\b/iu.test(consequence)
|
|
149
|
+
? `Allow${consequence.slice('allow'.length)}`
|
|
150
|
+
: `Allow ${consequence}`;
|
|
151
|
+
}
|
|
152
|
+
if (/^no,\s*/iu.test(label))
|
|
153
|
+
return `Deny ${label.replace(/^no,\s*/iu, '')}`;
|
|
154
|
+
if (/^yes$/iu.test(label))
|
|
155
|
+
return 'Allow for this session';
|
|
156
|
+
if (/^no$/iu.test(label))
|
|
157
|
+
return 'Deny';
|
|
158
|
+
return label;
|
|
159
|
+
}
|
|
160
|
+
function decision(surface, density, sr) {
|
|
161
|
+
const out = [
|
|
162
|
+
row(`quiet:decision:${surface.kind}:heading`, surface.heading, 'heading'),
|
|
163
|
+
];
|
|
164
|
+
if (surface.kind === 'plan-approval') {
|
|
165
|
+
if (detailAllowed(density)) {
|
|
166
|
+
out.push(row('quiet:plan:explanation', surface.explanation));
|
|
167
|
+
if (surface.plan)
|
|
168
|
+
out.push(row('quiet:plan:content', excerpt(surface.plan, 160), 'muted'));
|
|
169
|
+
out.push(row('quiet:plan:path', `Plan: ${excerpt(surface.planPath)}`, 'muted'));
|
|
170
|
+
}
|
|
171
|
+
out.push(...optionsRows(surface.options, 'quiet:plan:option', density, sr));
|
|
172
|
+
if (surface.feedbackMode)
|
|
173
|
+
out.push(row('quiet:plan:feedback', `> ${excerpt(surface.feedback) || surface.feedbackPlaceholder}`, 'input'));
|
|
174
|
+
return [
|
|
175
|
+
...out,
|
|
176
|
+
explicitFooter('quiet:plan:footer', '↑/↓ select Enter confirm Esc cancel', 'Use up and down arrows to select. Press Enter to confirm. Press Escape to cancel.'),
|
|
177
|
+
];
|
|
178
|
+
}
|
|
179
|
+
if (surface.progress)
|
|
180
|
+
out.push(row('quiet:question:progress', surface.progress, 'muted'));
|
|
181
|
+
if (surface.question)
|
|
182
|
+
out.push(row('quiet:question:text', surface.question));
|
|
183
|
+
out.push(...optionsRows(surface.options, 'quiet:question:option', density, sr));
|
|
184
|
+
out.push(row('quiet:question:answer', `Current answer: ${excerpt(surface.answer) || '(empty)'}`, 'input'));
|
|
185
|
+
if (surface.guidance)
|
|
186
|
+
out.push(row('quiet:question:guidance', surface.guidance, 'muted'));
|
|
187
|
+
if (surface.emptyState)
|
|
188
|
+
out.push(row('quiet:question:empty', surface.emptyState, 'muted'));
|
|
189
|
+
out.push(explicitFooter('quiet:question:footer', 'Enter answer Esc cancel', 'Press Enter to submit the answer. Press Escape to cancel.'));
|
|
190
|
+
return out;
|
|
191
|
+
}
|
|
192
|
+
function picker(surface, density, sr) {
|
|
193
|
+
const name = surface.kind === 'session-picker'
|
|
194
|
+
? 'Sessions'
|
|
195
|
+
: surface.kind === 'command-palette'
|
|
196
|
+
? 'Commands'
|
|
197
|
+
: 'Mentions';
|
|
198
|
+
const out = [
|
|
199
|
+
row(`quiet:${surface.kind}:heading`, `${name}${surface.query ? ` · ${excerpt(surface.query)}` : ''}`, 'heading'),
|
|
200
|
+
];
|
|
201
|
+
const { start, end } = surface.visibleRange;
|
|
202
|
+
surface.rows.slice(start, end).forEach((item) => {
|
|
203
|
+
const label = 'path' in item
|
|
204
|
+
? item.path
|
|
205
|
+
: 'invocation' in item
|
|
206
|
+
? detailAllowed(density)
|
|
207
|
+
? `${item.invocation} — ${item.description}`
|
|
208
|
+
: item.invocation
|
|
209
|
+
: 'name' in item
|
|
210
|
+
? detailAllowed(density)
|
|
211
|
+
? `${item.name} — ${item.description}`
|
|
212
|
+
: item.name
|
|
213
|
+
: detailAllowed(density) && item.detail
|
|
214
|
+
? `${item.label} — ${item.detail}`
|
|
215
|
+
: item.label;
|
|
216
|
+
out.push(row(`quiet:${surface.kind}:${item.id}`, marker(item.selected, excerptOption(label, undefined, density), sr), item.selected ? 'selection' : 'body'));
|
|
217
|
+
});
|
|
218
|
+
if (surface.rows.length === 0)
|
|
219
|
+
out.push(row(`quiet:${surface.kind}:empty`, 'No matching choices.', 'muted'));
|
|
220
|
+
return [
|
|
221
|
+
...out,
|
|
222
|
+
explicitFooter(`quiet:${surface.kind}:footer`, '↑/↓ select Enter confirm Esc cancel', 'Use up and down arrows to select. Press Enter to confirm. Press Escape to cancel.'),
|
|
223
|
+
];
|
|
224
|
+
}
|
|
225
|
+
function elicitation(surface, density, sr) {
|
|
226
|
+
const out = [
|
|
227
|
+
row(`quiet:elicitation:${surface.kind}:server`, `Server: ${excerpt(surface.serverName)}`, 'heading'),
|
|
228
|
+
row(`quiet:elicitation:${surface.kind}:message`, excerpt(surface.message)),
|
|
229
|
+
];
|
|
230
|
+
if (surface.kind === 'elicitation-url') {
|
|
231
|
+
out.push(row('quiet:elicitation:url', `URL: ${excerpt(surface.url)}`), row('quiet:elicitation:waiting', surface.waiting
|
|
232
|
+
? 'Waiting for confirmation…'
|
|
233
|
+
: marker(surface.selection === 0, surface.actionLabel, sr), surface.waiting
|
|
234
|
+
? 'warning'
|
|
235
|
+
: surface.selection === 0
|
|
236
|
+
? 'selection'
|
|
237
|
+
: 'body'), row('quiet:elicitation:cancel', marker(surface.selection === 1, 'Cancel', sr), surface.selection === 1 ? 'selection' : 'body'));
|
|
238
|
+
return [...out, footer(sr)];
|
|
239
|
+
}
|
|
240
|
+
const start = Math.max(0, Math.min(surface.state.focusIndex - 2, surface.state.fields.length - Math.max(1, surface.maxVisibleFields)));
|
|
241
|
+
surface.state.fields
|
|
242
|
+
.slice(start, start + Math.max(1, surface.maxVisibleFields))
|
|
243
|
+
.forEach((field) => {
|
|
244
|
+
const stored = surface.state.values[field.name];
|
|
245
|
+
const value = surface.state.fields.indexOf(field) === surface.state.focusIndex &&
|
|
246
|
+
['text', 'number', 'integer'].includes(field.kind) &&
|
|
247
|
+
surface.state.expandedField !== field.name
|
|
248
|
+
? surface.input
|
|
249
|
+
: stored;
|
|
250
|
+
const text = `${field.title}${field.required ? ' (required)' : ''}: ${value === undefined || value === '' ? '(empty)' : String(value)}${detailAllowed(density) && field.description ? ` — ${excerpt(field.description)}` : ''}${surface.state.errors[field.name] ? ` — ${excerpt(surface.state.errors[field.name])}` : ''}`;
|
|
251
|
+
out.push(row(`quiet:elicitation:field:${field.name}`, marker(surface.state.fields.indexOf(field) === surface.state.focusIndex, text, sr), surface.state.fields.indexOf(field) === surface.state.focusIndex
|
|
252
|
+
? 'selection'
|
|
253
|
+
: surface.state.errors[field.name]
|
|
254
|
+
? 'error'
|
|
255
|
+
: 'body'));
|
|
256
|
+
if (surface.state.expandedField === field.name) {
|
|
257
|
+
const optionStart = Math.max(0, Math.min(surface.state.optionIndex - 10, field.options.length - 20));
|
|
258
|
+
field.options
|
|
259
|
+
.slice(optionStart, optionStart + 20)
|
|
260
|
+
.forEach((option, offset) => {
|
|
261
|
+
const optionIndex = optionStart + offset;
|
|
262
|
+
const focused = surface.state.optionIndex === optionIndex;
|
|
263
|
+
const checked = field.kind === 'multi-enum' &&
|
|
264
|
+
Array.isArray(stored) &&
|
|
265
|
+
stored.includes(option.value);
|
|
266
|
+
const label = field.kind === 'multi-enum'
|
|
267
|
+
? `${checked ? '[x]' : '[ ]'} ${option.label}`
|
|
268
|
+
: option.label;
|
|
269
|
+
out.push(row(`quiet:elicitation:field:${field.name}:option:${option.value}`, marker(focused, label, sr), focused ? 'selection' : checked ? 'success' : 'body'));
|
|
270
|
+
});
|
|
271
|
+
}
|
|
272
|
+
});
|
|
273
|
+
return [
|
|
274
|
+
...out,
|
|
275
|
+
explicitFooter('quiet:elicitation:footer', '↑/↓ field Enter edit/select Esc cancel', 'Use up and down arrows to change fields. Press Enter to edit or select. Press Escape to cancel.'),
|
|
276
|
+
];
|
|
277
|
+
}
|
|
278
|
+
export function projectQuietChoiceRows(surface, options) {
|
|
279
|
+
const sr = options.screenReader === true;
|
|
280
|
+
if (surface.kind === 'permission')
|
|
281
|
+
return permission(surface.surface, options.density, sr);
|
|
282
|
+
if (surface.kind === 'elicitation')
|
|
283
|
+
return elicitation(surface.surface, options.density, sr);
|
|
284
|
+
if (surface.kind === 'editor-wait')
|
|
285
|
+
return [
|
|
286
|
+
row('quiet:editor-wait', 'Editor is open. Waiting for changes…', 'warning'),
|
|
287
|
+
explicitFooter('quiet:editor-wait:footer', 'Esc cancel', 'Press Escape to cancel.'),
|
|
288
|
+
];
|
|
289
|
+
if (surface.kind === 'exit-confirmation')
|
|
290
|
+
return [
|
|
291
|
+
row('quiet:exit:heading', 'Exit Praxis?', 'heading'),
|
|
292
|
+
explicitFooter('quiet:exit:footer', 'Enter confirm Esc cancel', 'Press Enter to confirm. Press Escape to cancel.'),
|
|
293
|
+
];
|
|
294
|
+
if (surface.kind === 'plan-approval' || surface.kind === 'question')
|
|
295
|
+
return decision(surface, options.density, sr);
|
|
296
|
+
if (surface.kind === 'session-picker' ||
|
|
297
|
+
surface.kind === 'command-palette' ||
|
|
298
|
+
surface.kind === 'mention-picker')
|
|
299
|
+
return picker(surface, options.density, sr);
|
|
300
|
+
return permission(surface, options.density, sr);
|
|
301
|
+
}
|
|
302
|
+
//# sourceMappingURL=quiet-choice-rows.js.map
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import type { QuietFrame } from './quiet-frame.js';
|
|
2
|
+
import { type TuiTextRole } from './theme.js';
|
|
3
|
+
export interface QuietInkLineSegment {
|
|
4
|
+
readonly text: string;
|
|
5
|
+
readonly role: TuiTextRole;
|
|
6
|
+
readonly cursor?: boolean;
|
|
7
|
+
}
|
|
8
|
+
export interface QuietInkFrameLine {
|
|
9
|
+
readonly key: string;
|
|
10
|
+
readonly segments: readonly QuietInkLineSegment[];
|
|
11
|
+
}
|
|
12
|
+
export declare function projectQuietInkFrameLines(frame: QuietFrame, screenReader?: boolean): readonly QuietInkFrameLine[];
|
|
13
|
+
export interface QuietInkFrameProps {
|
|
14
|
+
readonly frame: QuietFrame;
|
|
15
|
+
readonly screenReader?: boolean;
|
|
16
|
+
}
|
|
17
|
+
export declare function QuietInkFrame({ frame, screenReader, }: QuietInkFrameProps): import("react").JSX.Element;
|
|
18
|
+
//# sourceMappingURL=quiet-frame-adapter.d.ts.map
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
import { jsx as _jsx, Fragment as _Fragment } from "react/jsx-runtime";
|
|
2
|
+
import { Text } from 'ink';
|
|
3
|
+
import { useTuiTheme } from './theme.js';
|
|
4
|
+
import { terminalGraphemeWidth, terminalGraphemes, terminalTextWidth, } from './transcript-viewport.js';
|
|
5
|
+
const ROLE_MAP = {
|
|
6
|
+
body: 'body',
|
|
7
|
+
heading: 'heading',
|
|
8
|
+
muted: 'muted',
|
|
9
|
+
accent: 'focusMarker',
|
|
10
|
+
success: 'success',
|
|
11
|
+
warning: 'warning',
|
|
12
|
+
error: 'error',
|
|
13
|
+
tool: 'info',
|
|
14
|
+
selection: 'focusMarker',
|
|
15
|
+
input: 'inputMarker',
|
|
16
|
+
diffAdded: 'diffAdded',
|
|
17
|
+
diffRemoved: 'diffRemoved',
|
|
18
|
+
};
|
|
19
|
+
function plainText(row) {
|
|
20
|
+
return row.segments.map((segment) => segment.text).join('') || ' ';
|
|
21
|
+
}
|
|
22
|
+
export function projectQuietInkFrameLines(frame, screenReader = false) {
|
|
23
|
+
return frame.lines.map((row) => {
|
|
24
|
+
if (screenReader)
|
|
25
|
+
return {
|
|
26
|
+
key: row.key,
|
|
27
|
+
segments: [
|
|
28
|
+
{
|
|
29
|
+
text: row.accessibleText ?? plainText(row),
|
|
30
|
+
role: 'body',
|
|
31
|
+
},
|
|
32
|
+
],
|
|
33
|
+
};
|
|
34
|
+
const cursor = frame.cursor?.rowKey === row.key ? frame.cursor.column : undefined;
|
|
35
|
+
if (cursor === undefined)
|
|
36
|
+
return {
|
|
37
|
+
key: row.key,
|
|
38
|
+
segments: row.segments.map((segment) => ({
|
|
39
|
+
...segment,
|
|
40
|
+
role: ROLE_MAP[segment.role],
|
|
41
|
+
})),
|
|
42
|
+
};
|
|
43
|
+
const output = [];
|
|
44
|
+
let position = 0;
|
|
45
|
+
let inserted = false;
|
|
46
|
+
for (const segment of row.segments) {
|
|
47
|
+
const chars = terminalGraphemes(segment.text);
|
|
48
|
+
const mappedRole = ROLE_MAP[segment.role];
|
|
49
|
+
const segmentWidth = terminalTextWidth(segment.text);
|
|
50
|
+
if (!inserted && cursor >= position && cursor < position + segmentWidth) {
|
|
51
|
+
let offset = 0;
|
|
52
|
+
let cell = position;
|
|
53
|
+
for (const [index, grapheme] of chars.entries()) {
|
|
54
|
+
const width = terminalGraphemeWidth(grapheme);
|
|
55
|
+
if (cursor < cell + width) {
|
|
56
|
+
offset = index;
|
|
57
|
+
break;
|
|
58
|
+
}
|
|
59
|
+
cell += width;
|
|
60
|
+
}
|
|
61
|
+
const before = chars.slice(0, offset).join('');
|
|
62
|
+
const at = chars[offset] ?? ' ';
|
|
63
|
+
const after = chars
|
|
64
|
+
.slice(offset + (offset < chars.length ? 1 : 0))
|
|
65
|
+
.join('');
|
|
66
|
+
if (before)
|
|
67
|
+
output.push({ text: before, role: mappedRole });
|
|
68
|
+
output.push({ text: at, role: mappedRole, cursor: true });
|
|
69
|
+
if (after)
|
|
70
|
+
output.push({ text: after, role: mappedRole });
|
|
71
|
+
inserted = true;
|
|
72
|
+
}
|
|
73
|
+
else
|
|
74
|
+
output.push({ text: segment.text, role: mappedRole });
|
|
75
|
+
position += segmentWidth;
|
|
76
|
+
}
|
|
77
|
+
if (!inserted)
|
|
78
|
+
output.push({ text: ' ', role: 'body', cursor: true });
|
|
79
|
+
return { key: row.key, segments: output };
|
|
80
|
+
});
|
|
81
|
+
}
|
|
82
|
+
export function QuietInkFrame({ frame, screenReader = false, }) {
|
|
83
|
+
const theme = useTuiTheme();
|
|
84
|
+
const lines = projectQuietInkFrameLines(frame, screenReader);
|
|
85
|
+
return (_jsx(_Fragment, { children: lines.map((line) => (_jsx(Text, { children: line.segments.map((segment, index) => {
|
|
86
|
+
const style = theme.text[segment.role];
|
|
87
|
+
return (_jsx(Text, { ...style, ...(segment.cursor || style.inverse ? { inverse: true } : {}), children: segment.text }, `${line.key}:${index}`));
|
|
88
|
+
}) }, line.key))) }));
|
|
89
|
+
}
|
|
90
|
+
//# sourceMappingURL=quiet-frame-adapter.js.map
|